前言
音视频封装指的是将编码后的数据放入具有一定规则的容器文件中,比如MP4文件,MOV文件,MP3文件等等。容器文件和编码方法是两个不同的概念,需要区分,不过MP3即是编码方法,也是一种容器文件。音视频封装是一种很常见的应用场景,比如封装成MP4文件,便于存储和传播。MP4既可以只包含音频或者视频,也可以同时包含多个音频和视频。本文以MP4为例,将音视频数据封装到MP4容器文件中
封装相关流程
音视频的封装是基于AVFormatContext来实现的
封装相关函数
1、int avformat_alloc_output_context(AVFormatContext **outfmtCtx,AVOutputFormat *oformat,const char *format_name,const char *filename) 创建封装用的上下文,一般第二个和第三个参数设置为NULL即可,第四个参数为输出的文件路径
2、AVStream* avformat_new_stream(AVFormatContext *ouCtx,AVCodec *codec) 为封装上下文添加一路音/视频流,第二个参数一般设置为NULL;没添加一个新的流,AVFormatContext的nb_streams值加1,然后当调用av_write_frame或者av_interleave_write_frame()函数写入AVPacket数据时,将根据AVPacket中的stream_index写入到对应的AVStream中
3、int avcodec_parameters_copy(AVCodecParameters *dst,AVCodecParameters *src) 从src中拷贝编码相关参数到dst中
4、int avio_open(AVIOContext* io,const char* io_url, int flags) 初始化AVIOContext缓冲区,io_url为最终输出文件的路径,flags为AVIO_FLAG_WRITE代表创建封装用的IO缓冲区
5、int avformat_write_header(AVFormatContext *fmt,AVDictionary **options) 写入封装用的头文件信息。这一步之后AVFormatContext中的一些相关参数也会被初始化
6、int av_write_frame(AVFormatContext *fmt,AVPacket *packet) 写入数据。packet代表着压缩的音频或视频数据,它的stream_index一定要设置正确,他的pts,dts,duration也一定要是基于AVStream的time_base,因为封装文件音视频时长帧率数据是根据这三个值计算出来的。
备注:调用此方法时,dts的值一定要线性增加,不然出错
7、int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt); 和上面方法一样,区别就是它会内部维护一个缓冲区,自己根据dts的值排序后再写入,即它不需要dts的值线性增加
8、int av_write_trailer(AVFormatContext *fmt) 写入封装尾信息,一个完整的封装流程必须包含此函数的调用
实现代码
本文实现了两个例子,例子1即直接读取文件中的数据然后原封不动的再封装;例子2是将一个文件中的音频流和一个文件中的视频流合并成一个音视频文件(这里实现了MP4或MOV文件)
分别对应下面 void doReMuxer();和void doMuxerTwoFile();函数
-
例子1
#ifndef muxer_hpp #define muxer_hpp #include <stdio.h> #include <string> extern "C"{ #include <libavformat/avformat.h> #include <libavutil/timestamp.h> #include "CLog.h" } using namespace std; class Muxer { public: /** ffmpeg编译完成后支持的封装器位于libavformat目录下的muxer_list.c文件中,具体的配置在.configure文件中,如下: * print_enabled_components libavformat/muxer_list.c AVOutputFormat muxer_list $MUXER_LIST * xxxx.mp4对应的封装器为ff_mov_muxer */ Muxer(); ~Muxer(); // 解析文件并原封不动在封装 void doReMuxer(); // 将两个文件中音频和视频合并,如果两个文件时间不一致,则将较长的进行截断 void doMuxerTwoFile(); }; 公共实现代码 include "muxer.hpp" Muxer::Muxer() { } Muxer::~Muxer() { }
/** 遇到问题:avformat_close_input奔溃
-
分析原因:最开始是这样定义releaseResource(AVFormatContext *in_fmt1,AVFormatContext *in_fmt2,AVFormatContext *ou_fmt3)函数的;in_fmt1是一个指针变量,当外部调用此函数时
-
如果传递的实参如果是一个临时变量,那么在此函数内部就算在avformat_close_input(in_fmt1);后面执行in_fmt1=NULL;它也不能将外部传进来的实参置为NULL,如果多次调用
-
releaseResource()函数并且传递的实参in_fmt1还是同一个,就会造成avformat_close_input释放多次,所以造成奔溃。
-
解决方案:将函数定义成如下方式static void releaseResource(AVFormatContext in_fmt1,AVFormatContext in_fmt2,AVFormatContext **ou_fmt3);事实上ffmpeg的很多释放函数也
-
是采用的指针的指针作为形参的定义方式。
*/ static void releaseResource(AVFormatContext **in_fmt1,AVFormatContext **in_fmt2,AVFormatContext **ou_fmt3) { if (in_fmt1 && *in_fmt1) { avformat_close_input(in_fmt1); *in_fmt1 = NULL; } if (in_fmt2 && *in_fmt2) { avformat_close_input(in_fmt2); *in_fmt2 = NULL; } if (ou_fmt3 && *ou_fmt3 != NULL) { avformat_free_context(*ou_fmt3); *ou_fmt3 = NULL; } } #endif /* muxer_hpp */
例子1实现代码
void Muxer::doReMuxer() { string curFile(__FILE__); unsigned long pos = curFile.find("1-video_encode_decode"); if (pos == string::npos) { LOGD("can not find file"); return; } string srcDir = curFile.substr(0,pos) + "filesources/"; string srcPath = srcDir + "test_1280x720_1.mp4"; string dstPath = "1-test_1280_720_1.MP4"; AVFormatContext *in_fmtCtx = NU