java封装FFmpeg命令,支持原生ffmpeg全部命令,实现FFmpeg多进程处理与多线程输出控制(开启、关闭、查询),rtsp/rtmp推流、拉流

前言:

之前已经对FFmpeg命令进行了封装http://blog.csdn.net/eguid_1/article/details/51787646,但是当时没有考虑到扩展性,所以总体设计不是太好,需要改动的地方也比较多,也不支持原生ffmpeg命令,所以本次版本推翻了前面的版本重新设计接口和实现,全面支持各个流程注入自己的实现,并且在原有命令组装基础上增加一个接口用来支持全部原生FFmpeg命令。

本篇是使用FFmpeg命令行方式进行转码,推流等操作,由于该种方式使用的是进程,资源消耗过大,介意的可以使用JavaCV方式进行转码推流等操作:

javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG、javaCV-openCV)

javaCV开发详解之3:收流器实现,录制流媒体服务器的rtsp/rtmp视频文件(基于javaCV-FFMPEG)

javaCV开发详解之4:转流器实现(也可作为本地收流器、推流器,新增添加图片及文字水印,视频图像帧保存),实现rtsp/rtmp/本地文件转发到rtmp流媒体服务器(基于javaCV-FFMPEG)

javaCV开发详解之6:本地音频(话筒设备)和视频(摄像头)抓取、混合并推送(录制)到服务器(本地)

javaCV开发详解之8:转封装在rtsp转rtmp流中的应用(无须转码,更低的资源消耗,更好的性能,更低延迟)

概述:

提供一个管理器用于方便管理FFmpeg命令的执行、停止和执行信息持久化。

可以方便的使用ffmpeg来进行推流,拉流,转流等任务

新版本更新:增加了一个保活线程来保证所有命令行执行的任务稳定可靠运行,如果遇到任务异常中断,保活线程会立即重启这个任务,保证任务的及时有效运行。

实现的功能:

①开启一个进程+一个输出线程来执行原生ffmpeg命令②开启一个进程+一个输出线程来执行组装命令③查询执行任务信息④查询全部正在执行的任务⑤停止进程和输出线程⑥停止全部正在执行的任务

源码包下载:http://download.csdn.net/detail/eguid_1/9668143

本项目已在github上进行维护,如需查看或下载源码请至github进行查看:

github项目地址:https://github.com/eguid/FFCH4J

1、接口设计

1.1、发布任务接口
通过原生ffmpeg命令发布处理任务
通过map组装成ffmpeg命令来处理任务
1.2、终止任务接口
结束任务
结束全部任务
1.3、任务查询接口
查询
查询全部

1.4、接口实现注入

执行处理器注入

命令组装器注入

持久化实现注入

2、内部实现

2.1、任务处理器
开启一个进程用于执行ffmpeg命令
开启一个子线程用于输出ffmpeg执行过程
停止进程
停止输出线程(需要在进程关闭前停止输出线程)
按照正确顺序停止进程和线程2.2、输出线程处理器
用于输出ffmpeg执行过程

 

package cc.eguid.FFmpegCommandManager.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import cc.eguid.FFmpegCommandManager.entity.TaskEntity;
/**
 * 任务处理实现
 * @author eguid
 * @since jdk1.7
 * @version 2016年10月29日
 */
public class TaskHandlerImpl implements TaskHandler {
	private Runtime runtime = null;

	@Override
	public TaskEntity process(String id, String command) {
		Process process = null;
		OutHandler outHandler = null;
		TaskEntity tasker = null;
		try {
			if (runtime == null) {
				runtime = Runtime.getRuntime();
			}
			process = runtime.exec(command);// 执行本地命令获取任务主进程
			outHandler = new OutHandler(process.getErrorStream(), id);
			outHandler.start();
			tasker = new TaskEntity(id, process, outHandler);
		} catch (IOException e) {
			stop(outHandler);
			stop(process);
			// 出现异常说明开启失败,返回null
			return null;
		}
		return tasker;
	}

	@Override
	public boolean stop(Process process) {
		if (process != null && process.isAlive()) {
			process.destroy();
			return true;
		}
		return false;
	}

	@Override
	public boolean stop(Thread outHandler) {
		if (outHandler != null && outHandler.isAlive()) {
			outHandler.stop();
			outHandler.destroy();
			return true;
		}
		return false;
	}

	@Override
	public boolean stop(Process process, Thread thread) {
		boolean ret;
		ret=stop(thread);
		ret=stop(process);
		return ret;
	}
}

/**
 * 
 * @author eguid
 *
 */
class OutHandler extends Thread {
	/**
	 * 控制状态
	 */
	private volatile boolean desstatus = true;

	/**
	 * 读取输出流
	 */
	private BufferedReader br = null;

	/**
	 * 输出类型
	 */
	private String type = null;

	public OutHandler(InputStream is, String type) {
		br = new BufferedReader(new InputStreamReader(is));
		this.type = type;
	}

	/**
	 * 重写线程销毁方法,安全的关闭线程
	 */
	@Override
	public void destroy() {
		setDesStatus(false);
	}

	public void setDesStatus(boolean desStatus) {
		this.desstatus = desStatus;
	}

	/**
	 * 执行输出线程
	 */
	@Override
	public void run() {
		String msg = null;
		int index = 0;
		int errorIndex = 0;
		int status = 10;
		try {
			System.out.println(type + "开始推流!");
			while (desstatus && (msg = br.readLine()) != null) {
				if (msg.indexOf("[rtsp") != -1) {
					System.out.println("接收" + status + "个数据包" + msg);
					System.out.println(type + ",网络异常丢包,丢包次数:" + errorIndex++ + ",消息体:" + msg);
					status = 10;
					index = 0;
				}

				if (index % status == 0) {
					System.out.println("接收" + status + "个数据包" + msg);
					status *= 2;
				}
				index++;
			}
		} catch (IOException e) {
			System.out.println("发生内部异常错误,自动关闭[" + this.getId() + "]线程");
			destroy();
		} finally {
			if (this.isAlive()) {
				destroy();
			}
		}
	}
}

 

 


2.3、持久化服务
增加任务信息
删除任务信息
删除全部任务信息
查询任务信息
查询全部任务信息

 

 

 

        任务是否存在

 

package cc.eguid.FFmpegCommandManager.dao;

import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import cc.eguid.FFmpegCommandManager.entity.TaskEntity;

/**
 * 任务信息持久层实现
 * 
 * @author eguid
 * @since jdk1.7
 * @version 2016年10月29日
 */
public class TaskDaoImpl implements TaskDao {
	// 存放任务信息
	private ConcurrentMap<String, TaskEntity> map = null;

	public TaskDaoImpl(int size) {
		map = new ConcurrentHashMap<>(size);
	}

	@Override
	public TaskEntity get(String id) {
		return map.get(id);
	}

	@Override
	public Collection<TaskEntity> getAll() {
		return map.values();
	}

	@Override
	public int add(TaskEntity taskEntity) {
		String id = taskEntity.getId();
		if (id != null && !map.containsKey(id)) {
			if (map.put(taskEntity.getId(), taskEntity) != null) {
				return 1;
			}
		}
		return 0;
	}

	@Override
	public int remove(String id) {
		if(map.remove(id) != null){
			return 1;
		};
		return 0;
	}

	@Override
	public int removeAll() {
		int size = map.size();
		try {
			map.clear();
		} catch (Exception e) {
			return 0;
		}
		return size;
	}

	@Override
	public boolean isHave(String id) {
		return map.containsKey(id);
	}

}

 


2.3命令组装器
用于将参数组装成对应的ffmpeg命令

 

 

 

package cc.eguid.FFmpegCommandManager.util;

import java.util.Map;

public class CommandAssemblyUtil {
	/**
	 * 
	 * @param map
	 *            -要组装的map
	 * @param id
	 *            -返回参数:id
	 * @param id
	 *            -返回参数:组装好的命令
	 * @return
	 */
	public static String assembly(Map<String, String> paramMap) {
		try {
			if (paramMap.containsKey("ffmpegPath")) {
				String ffmpegPath = (String) paramMap.get("ffmpegPath");
				// -i:输入流地址或者文件绝对地址
				StringBuilder comm = new StringBuilder(ffmpegPath + " -i ");
				// 是否有必输项:输入地址,输出地址,应用名,twoPart:0-推一个元码流;1-推一个自定义推流;2-推两个流(一个是自定义,一个是元码)
				if (paramMap.containsKey("input") && paramMap.containsKey("output") && paramMap.containsKey("appName")
						&& paramMap.containsKey("twoPart")) {
					String input = (String) paramMap.get("input");
					String output = (String) paramMap.get("output");
					String appName = (String) paramMap.get("appName");
					String twoPart = (String) paramMap.get("twoPart");
					String codec = (String) paramMap.get("codec");
					// 默认h264解码
					codec = (codec == null ? "h264" : (String) paramMap.get("codec"));
					// 输入地址
					comm.append(input);
					// 当twoPart为0时,只推一个元码流
					if ("0".equals(twoPart)) {
						comm.append(" -vcodec " + codec + " -f flv -an " + output + appName);
					} else {
						// -f :转换格式,默认flv
						if (paramMap.containsKey("fmt")) {
							String fmt = (String) paramMap.get("fmt");
							comm.append(" -f " + fmt);
						}
						// -r :帧率,默认25;-g :帧间隔
						if (paramMap.containsKey("fps")) {
							String fps = (String) paramMap.get("fps");
							comm.append(" -r " + fps);
							comm.append(" -g " + fps);
						}
						// -s 分辨率 默认是原分辨率
						if (paramMap.containsKey("rs")) {
							String rs = (String) paramMap.get("rs");
							comm.append(" -s " + rs);
						}
						// 输出地址+发布的应用名
						comm.append(" -an " + output + appName);
						// 当twoPart为2时推两个流,一个自定义流,一个元码流
						if ("2".equals(twoPart)) {
							// 一个视频源,可以有多个输出,第二个输出为拷贝源视频输出,不改变视频的各项参数并且命名为应用名+HD
							comm.append(" -vcodec copy  -f flv -an ").append(output + appName + "HD");
						}
					}
					return comm.toString();
				}
			}
		} catch (Exception e) {
			return null;
		}
		return null;
	}
}

 

 

 

 

 

2.4、配置文件读取器
读取配置文件中的ffmpeg路径配置
读取默认位置的ffmpeg执行文件

 

package cc.eguid.FFmpegCommandManager.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * 读取配置文件并加载FFmpeg
 * 
 * @author eguid
 * @since jdk1.7
 * @version 2016年10月29日
 */
public class LoadConfig {
	private volatile static boolean isHave = false;
	private volatile static String ffmpegPath = null;

	public LoadConfig() {
		super();
		if (readConfig()) {
			System.out.println("读取FFmpeg执行路径成功!");
			isHave = true;
		} else if (initConfInfo()) {
			// 配置文件读取失败,自动从项目路径加载ffmpeg
			isHave = true;
		} else {
			isHave = false;
		}

	}

	protected boolean readConfig() {
		String path = null;
		File confFile = new File(getClass().getResource("/").getPath() + "loadFFmpeg.properties");
		System.out.println("读取FFMPEG配置文件:" + confFile.getPath());
		if (confFile != null && confFile.exists() && confFile.isFile() && confFile.canRead()) {
			Properties prop = new Properties();
			try {
				prop.load(new FileInputStream(confFile));
				path = prop.getProperty("path");
				if (path != null) {
					System.out.println("读取配置文件中的ffmpeg路径:" + path);
					ffmpegPath = path;
					return true;
				}
			} catch (IOException e) {
				System.err.println("读取配置文件失败!");
				return false;
			}
		}
		System.err.println("读取配置文件失败!");
		return false;
	}

	/**
	 * 从配置文件中初始化参数
	 */
	protected boolean initConfInfo() {
		String path = getClass().getResource("../").getPath() + "ffmpeg/ffmpeg.exe";
		System.out.print("预加载默认项目路径FFMPEG配置:" + path);
		File ffmpeg = new File(path);
		ffmpegPath = ffmpeg.getPath();
		if (isHave = ffmpeg.isFile()) {
			return true;
		}
		System.out.println("加载ffmpeg失败!");
		return false;
	}

	/**
	 * 判断ffmpeg环境配置
	 * 
	 * @return true:配置成功;false:配置失败
	 */
	public boolean isHave() {
		return isHave;
	}

	/**
	 * 获取ffmpeg环境调用地址 添加方法功能描述
	 * 
	 * @return
	 */
	public String getPath() {
		return ffmpegPath;
	}

	public static void main(String[] args) {
		LoadConfig conf = new LoadConfig();
	}
}

 

 

 

 

 

 

 

 

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__0809 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值