上个月做流媒体视频转码,现抽个时间对它进行总结。
【前提】
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<String> commend = new ArrayList<String>(); //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<String> commend = new ArrayList<String>(); 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<TaskEntity> 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】
这里简单提一下,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命令管理器两个方向,继续分享上个月的流媒体之旅。