上个月做流媒体视频转码,现抽个时间对它进行总结。
【前提】
java本身没有自己的流媒体架构,而且没有公司和人在为java开发一套流媒体架构,就连nginx-rtmp和srs这种主流级别的流媒体服务器都在使用ffmpeg做插件,可见ffmpeg在流媒体架构这块的重要性。
【宏观】
官方地址:https://ffmpeg.org/
画了2幅图,简单对ffmpeg这个流媒体架构做个简单的说明:
图 1-0 ffmpeg基本信息
图1-1 ffmpeg四大功能、库文件
两幅图,大致可以了解到,ffmpeg是一套处理流媒体的开源、免费架构,对于java后台开发来讲,如若遇到流媒体处理需求,无论使用何种架构,其实本质上是绕不开ffmpeg的,常用的“暴风影音”、“qq影音”、“迅雷影音”等,打开他们的安装目录,其实都会发现ffmpeg的身影,可见其强大之处。
目前,ffmpeg的几大功能点:1.编解码、2.转码、3.复用、4.流处理、5.过滤、6.播放,我主要接触“编解码”和“转码”,具体可以参考我的上一篇博客:【视频】“异步+准实时”解决主流H5播放器格式兼容问题 , 主要用的是ffmpeg的命令行功能,将其封装为java后台接口,实现转码操作,接下来主要从“ffmpeg命令行”使用、java后台封装2个角度进行ffmpeg的介绍,之后有需求,会对“ffserver”、“ffplayer”、“ffprobe”进行介绍。
【命令行工具】
ffmpeg [ global_options ] {[ input_file_options ] -iinput_url} ... {[ output_file_options ]output_url} ...
如上所述,这是ffmpeg命令行的基本格式,快速的视频和音频转换器,也可以从现场音频/视频源获取。它还可以在任意采样率之间转换,并通过高质量的多相滤波器实时调整视频大小。
举几个简单的例子:
(1)该命令要将输出文件的视频比特率设置为64 kbit / s
ffmpeg -i input.avi -b:v 64k -bufsize 64k output.avi
(2)要强制输出文件的帧频为24 fps
ffmpeg -i input.avi -r 24 output.avi
(3)要强制输入文件的帧速率(仅适用于原始格式)为1 fps,输出文件的帧速率为24 fps:
ffmpeg -r 1 -i input.m2v -r 24 output.avi
(4)将avi格式视频A转化为flv格式视频
ffmpeg -i video_origine.avi -acodec libmp3lame -ab 56K -ar 44100 -b 200K -r 15 -s 320x240 -f flv video_finale.flv
(5)分离视频中的音频流
ffmpeg -i input_file -vcodec copy -an output_file_video
如上所述,是直接利用ffmpeg命令行来操作视频的过程,其中1.2.3是改变原有视频的帧率、码率等视频原有特性,4是将视频的编码格式进行转码(pay attention:我们说的格式,在上一篇文章中也提到了,文件格式、视频格式、视频编码格式,这里讨论的是最后一种),5则是将视频进行提取、分离。
在windows或者Linux不同的环境下使用ffmpeg,需要下载不同的安装包:https://ffmpeg.org/download.html,比如在转码过程,会是酱紫:
基本上转码过程和“格式工厂”效率差不多,转后的清晰度,则是由我们设置的帧率和码率来控制,ffmpeg做视频转码,有一点不足,就是比较消耗CPU资源,本机8GB内存,如图:
性能会直接飙升至90%,不过好在问题是在之后的jave架构中被解决(jave本质也是ffmpeg架构,封装的比较好)。
每个输出的转码过程,可以参考官网中给出的这个原理图:
_______ ______________
| | | |
| 输入| 分路器| 编码数据| 解码器
| 文件| ---------> | 数据包| ----- +
| _______ | | ______________ | |
v
_________
| |
| 解码|
| 框架|
| _________ |
________ ______________ |
| | | | |
| 输出| <-------- | 编码数据| <---- +
| 文件| muxer | 数据包| 编码器
| ________ | | ______________ |
ffmpeg调用libavformat库(包含demuxers)来读取输入文件并获取包含编码数据的数据包。当有多个输入文件时,ffmpeg通过跟踪任何活动输入流上的最低时间戳,尝试使其保持同步。然后将编码的数据包传送给解码器(除非为数据流选择了流拷贝,请参阅进一步的描述)。解码器产生未压缩的帧(原始视频/ PCM音频/ ...),可以通过滤波进一步处理(见下一节)。在过滤之后,帧被传递给编码器,编码器对其进行编码并输出编码的数据包。最后,这些传递给复用器,将编码的数据包写入输出文件。
【java接口封装】
上面介绍了ffmpeg命令行的直接使用,在java程序中如果使用ffmpeg则需将命令封装为接口使用,一种简单的写法如下:
package video;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
public class ConvertH264 {
private final static String PATH = "";
//转码后的输出路径设置
private final static String OUTPATH = "D:\\FfmpegFile\\output";
public static void main(String[] args) {
if(!checkfile(PATH)){
System.out.println("源文件不是一个完整的文件,请检查");
return;
}
process();
}
/**
* 执行转码,期间对源视频是否被ffmpeg支持进行验证
* @return 状态
*/
private static int process(){
int status = checkContentType();
if(status == 0){
processH264(PATH);
}else if(status == 1){
String tempPath = processAVI(PATH);
if(!"ERROR".equals(tempPath)){
processH264(tempPath);
}
}
return status;
}
/**
* 检查是否为一个完整的文件
* @param path
* @return
*/
private static boolean checkfile(String path) {
File file = new File(path);
if (!file.isFile()) {
return false;
}
return true;
}
/**
* check contentType
* @return int 0-能解析; 1-不能解析
*/
private static int checkContentType() {
String type = PATH.substring(PATH.lastIndexOf(".") + 1, PATH.length())
.toLowerCase();
//ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等)
if (type.equals("avi")) {
return 0;
} else if (type.equals("mpg")) {
return 0;
} else if (type.equals("wmv")) {
return 0;
} else if (type.equals("3gp")) {
return 0;
} else if (type.equals("mov")) {
return 0;
} else if (type.equals("mp4")) {
return 0;
} else if (type.equals("asf")) {
return 0;
} else if (type.equals("asx")) {
return 0;
} else if (type.equals("flv")) {
return 0;
}
//对ffmpeg无法解析的文件格式(wmv9,rm,rmvb等),策略是先转换成avi格式
else if (type.equals("wmv9")) {
return 1;
} else if (type.equals("rm")) {
return 1;
} else if (type.equals("rmvb")) {
return 1;
}
return 2;
}
//暂时按照时间生成文件名(之后和史宏再商榷)
private static String generateFileName(){
Calendar c = Calendar.getInstance();
return String.valueOf(c.getTimeInMillis())+ Math.round(Math.random() * 100000);
}
//注意ffmpeg一定要提前编译h264编码格式
private static boolean processH264(String oldpath){
//码率 -- 尺寸 -- 432*240 源帧率 -- 29 位率(继续调试,获得相对最清楚的版本)
String savename = generateFileName();
List commend = new ArrayList();
//ffmpeg.exe的路径地址,下个版本,和程序地址同步,resource文件中
commend.add("D:\\ffmpeg\\ffmpeg");
commend.add("-i");
commend.add(oldpath);
commend.add("-ab");
commend.add("56");
commend.add("-ar");
commend.add("22050");
commend.add("-vcodec");
commend.add("h264");
commend.add("-qscale");
commend.add("8");
commend.add("-r");
commend.add("15");
commend.add("-s");
commend.add("600*500");
commend.add("D:\\" + savename + ".mp4");
try {
//调用线程命令进行转码
ProcessBuilder builder = new ProcessBuilder(commend);
builder.command(commend);
builder.start();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private static String processAVI(String oldpath){
String saveName = generateFileName();
List commend = new ArrayList();
commend.add("D\\ffmpeg\\mencoder");
commend.add("-oac");
commend.add("lavc");
commend.add("-lavcopts");
commend.add("acodec=mp3:abitrate=64");
commend.add("-ovc");
commend.add("xvid");
commend.add("-xvidencopts");
commend.add("bitrate=600");
commend.add("-of");
commend.add("avi");
commend.add("-o");
commend.add("D:\\FfmpegFile\\output" + saveName + ".avi");
try{
return OUTPATH + saveName + ".avi";
}catch(Exception e){
e.printStackTrace();
return "ERROR";
}
}
}
其中,核心代码段:
ProcessBuilder builder = new ProcessBuilder(commend);
builder.command(commend);
builder.start();
通过启动一个新的进程,来让他进行转码,而这个进程,其实就是dos命令的方式去调用ffmpeg。最初使用这种方式实现转码功能,发现问题在于转码过程中进程死掉了,转码一半的时候失败了,程序中也不能发现,进程异步执行,不可控。
之后,在github上,发现流媒体大神的ffmpeg命令接口化工具:
https://github.com/eguid/FFmpegCommandHandler4java
这个开源项目会在以后单独介绍分享,把ffmpeg在java中接口的封装做到了可控、命令执行、停止、查询的功能。基本功能使用:
FFmpegManager manager=new FFmpegManagerImpl(10);
//当然也可以这样:FFmpegManager manager=new FFmpegManagerImpl();//这样会从配置文件中读取size的值作为初始化参数
//组装命令
Map map = new HashMap();
map.put("appName", "test123");
map.put("input","rtsp://admin:[email protected]:37779/cam/realmonitor?channel=1&subtype=0");
map.put("output", "rtmp://192.168.30.21/live/");
map.put("codec","h264");
map.put("fmt", "flv");
map.put("fps", "25");
map.put("rs", "640x360");
map.put("twoPart","2");
//执行任务,id就是appName,如果执行失败返回为null
String id=manager.start(map);
System.out.println(id);
//通过id查询
TaskEntity info=manager.query(id);
System.out.println(info);
//查询全部
Collection infoList=manager.queryAll();
System.out.println(infoList);
//停止id对应的任务
manager.stop(id);
//执行原生ffmpeg命令(不包含ffmpeg的执行路径,该路径会从配置文件中自动读取)
manager.start("test1", "ffmpeg -i input_file -vcodec copy -an output_file_video");
//包含完整ffmpeg执行路径的命令
manager.start("test2,","d:/ffmpeg/ffmpeg -i input_file -vcodec copy -an output_file_video",true);
//停止全部任务
manager.stopAll();
这里简单提一下,jave是利用ffmpeg封装的一个java控制的流媒体工具包,地址:
http://www.sauronsoftware.it/projects/jave/
比较遗憾的一点是,jave在09年之后,就停止了更新,利用jave实现视频编码格式转换的核心代码如下:
private String process(String oldpath){
String newPath = "";
try {
newPath = TEMP_FILE_PATH + File.separator + System.currentTimeMillis() + ".mp4";
File source = new File(oldpath);
File target = new File(newPath);
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
VideoAttributes video = new VideoAttributes();
video.setCodec("flv");
video.setBitRate(new Integer(360000));
video.setFrameRate(new Integer(30));
video.setSize(new VideoSize(400, 300));
EncodingAttributes attrs = new EncodingAttributes();
attrs.setFormat("flv");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
Encoder encoder = new Encoder();
//获取编码信息
MultimediaInfo beforeStatus = encoder.getInfo(source);
encoder.encode(source, target, attrs);
//通过转后的状态判断
if(!target.exists() || target.length() == 0){
return "";
}
} catch (IllegalArgumentException e) {
logger.error("转码encode过程中出现异常",e);
return "";
} catch (InputFormatException e) {
logger.error("转码encode过程中出现异常",e);
return "";
} catch (EncoderException e) {
logger.error("转码encode过程中出现异常",e);
return "";
}
return newPath;
}
处理的完整过程,请参考我的github下的VideoEncoder项目:
https://github.com/zhangzhenhua92/VideoEncoder
之后两篇博客,将分别从jave和ffmpeg命令管理器两个方向,继续分享上个月的流媒体之旅。