【流媒体】ffmpeg小结

     上个月做流媒体视频转码,现抽个时间对它进行总结。

    【前提】

      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命令管理器两个方向,继续分享上个月的流媒体之旅。

 
 
     


    




猜你喜欢

转载自blog.csdn.net/u013047584/article/details/78994925