ffmpeg.c(4.3.1)源码剖析


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

FFmpeg源码结构图

(感谢雷博士:雷神)

函数背景色

函数在图中以方框的形式表现出来。不同的背景色标志了该函数不同的作用:

粉红色背景函数:FFmpeg的API函数。

白色背景的函数:FFmpeg的内部函数。

黄色背景的函数:URLProtocol结构体中的函数,包含了读写各种协议的功能。

绿色背景的函数:AVOutputFormat结构体中的函数,包含了读写各种封装格式的功能。

蓝色背景的函数:AVCodec结构体中的函数,包含了编解码的功能。

ffmpeg整体流程.jpg

区域

整个关系图可以分为以下几个区域:

左边区域——架构函数区域:这些函数并不针对某一特定的视频格式。

右上方黄色区域——协议处理函数区域:不同的协议(RTP,RTMP,FILE)会调用不同的协议处理函数。

右边中间绿色区域——封装格式处理函数区域:不同的封装格式(MKV,FLV,MPEG2TS,AVI)会调用不同的封装格式处理函数。

右边下方蓝色区域——编解码函数区域:不同的编码标准(HEVC,H.264,MPEG2)会调用不同的编解码函数。



箭头线

为了把调用关系表示的更明显,图中的箭头线也使用了不同的颜色:

红色的箭头线:标志了编码的流程。

其他颜色的箭头线:标志了函数之间的调用关系。其中:

    调用URLProtocol结构体中的函数用黄色箭头线标识;

    调用AVOutputFormat结构体中的函数用绿色箭头线标识;

    调用AVCodec结构体中的函数用蓝色箭头线标识。




函数所在的文件

每个函数标识了它所在的文件路径。

左边区域(架构函数) 


右上区域(URLProtocol协议处理函数)

URLProtocol结构体包含如下协议处理函数指针:

url_open():打开

url_read():读取

url_write():写入

url_seek():调整进度

url_close():关闭



【例子】不同的协议对应着上述接口有不同的实现函数,举几个例子:

File协议(即文件)对应的URLProtocol结构体ff_file_protocol:

url_open() -> file_open() -> open()

url_read() -> file_read() -> read()

url_write() -> file_write() -> write()

url_seek() -> file_seek() -> lseek()


url_close() -> file_close() -> close()


RTMP协议(libRTMP)对应的URLProtocol结构体ff_librtmp_protocol:

url_open() -> rtmp_open() -> RTMP_Init(), RTMP_SetupURL(), RTMP_Connect(), RTMP_ConnectStream()

url_read() -> rtmp_read() -> RTMP_Read()

url_write() -> rtmp_write() -> RTMP_Write()

url_seek() -> rtmp_read_seek() -> RTMP_SendSeek()


url_close() -> rtmp_close() -> RTMP_Close()

UDP协议对应的URLProtocol结构体ff_udp_protocol:

url_open() -> udp_open()

url_read() -> udp_read()

url_write() -> udp_write()

url_seek() -> udp_close()

url_close() -> udp_close()






右中区域(AVOutputFormat封装格式处理函数)

AVOutputFormat包含如下封装格式处理函数指针:

write_header():写文件头

write_packet():写一帧数据

write_trailer():写文件尾


【例子】不同的封装格式对应着上述接口有不同的实现函数,举几个例子:

FLV封装格式对应的AVOutputFormat结构体ff_flv_muxer:

write_header() -> flv_write_header()

write_packet() –> flv_write_packet()

write_trailer() -> flv_write_trailer()


MKV封装格式对应的AVOutputFormat结构体ff_matroska_muxer:

write_header() -> mkv_write_header()

write_packet() –> mkv_write_flush_packet()

write_trailer() -> mkv_write_trailer()


MPEG2TS封装格式对应的AVOutputFormat结构体ff_mpegts_muxer:

write_header() -> mpegts_write_header()

write_packet() –> mpegts_write_packet()

write_trailer() -> mpegts_write_end()


AVI封装格式对应的AVOutputFormat结构体ff_avi_muxer:

write_header() -> avi_write_header()

write_packet() –> avi_write_packet()

write_trailer() -> avi_write_trailer()


右下区域(AVCodec编解码函数)

AVCodec包含如下编解码函数指针:

init():初始化

encode2():编码一帧数据

close():关闭


【例子】不同的编解码器对应着上述接口有不同的实现函数,举几个例子:

HEVC编码器对应的AVCodec结构体ff_libx265_encoder:

init() -> libx265_encode_init() -> x265_param_alloc(), x265_param_default_preset(), x265_encoder_open()

encode2() -> libx265_encode_frame() -> x265_encoder_encode()

close() -> libx265_encode_close() -> x265_param_free(), x265_encoder_close()


H.264编码器对应的AVCodec结构体ff_libx264_encoder:

init() -> X264_init() -> x264_param_default(), x264_encoder_open(), x264_encoder_headers()

encode2() -> X264_frame() -> x264_encoder_encode()

close() -> X264_close() -> x264_encoder_close()


VP8编码器(libVPX)对应的AVCodec结构体ff_libvpx_vp8_encoder:

init() -> vpx_init() -> vpx_codec_enc_config_default()

encode2() -> vp8_encode() -> vpx_codec_enc_init(), vpx_codec_encode()

close() -> vp8_free() -> vpx_codec_destroy()


MPEG2编码器对应的AVCodec结构体ff_mpeg2video_encoder:

init() -> encode_init()

encode2() -> ff_mpv_encode_picture()

close() -> ff_mpv_encode_end()








ffmpeg.h头文件详解

:全局变量与结构体


InputStream **input_streams = NULL;

int        nb_input_streams = 0;

InputFile   **input_files   = NULL;

int        nb_input_files   = 0;


OutputStream **output_streams = NULL;

int         nb_output_streams = 0;

OutputFile   **output_files   = NULL;

int         nb_output_files   = 0;



其中: input_streams是输入流的数组,nb_input_streams是输入流的个数。 input_files是输入文件(也可能是设备)的数组,nb_input_files是输入文件的个数。 下面的输出相关的变量们就不用解释了。


可以看出,文件和流是分别保存的。于是,可以想象,结构InputStream中应有其所属的文件在InputFile中的序号(file_index)。输入流数组应是这样填充的:每当在输入文件中找到一个流时,就把它添加到input_streams中,所以一个输入文件对应的输入流在input_streams中是紧靠着的,于是InputFile结构中应有其第一个流在input_streams中的开始序号(ist_index)和被放在input_streams中的流的总个数(nb_streams)。


在输出流output_streams中,除了要保存其所在的输出文件在output_files中的序号(index),还应保存其对应的输入流在input_streams中的序号( source_index),也应保存其在所属输出文件中的流序号(file_index)。而输出文件中呢,只需保存它的第一个流在output_streams中的序号(ost_index)



流和文件都准备好了,下面就是转换,那么转换过程是怎样的呢?

还是我来猜一猜吧:

首先打开输入文件们,然后跟据输入流们准备并打开解码器们,然后跟据输出流们准备并打开编码器们,然后创建输出文件们,然后为所有输出文件们写好头部,然后就在循环中把输入流转换到输出流并写入输出文件中,转换完后跳出循环,然后写入文件尾,最后关闭所有的输出文件.



main函数主要流程分析

1.初始化工作

2.解析命令行参数

3.编码

4.收尾




ffmpeg_parse_options

image.png

1.1 命令行例子

ffmpeg -i abc.mp4 -i bbb.avi   -vcodec libx264  -acodec aac  -vf scale=640:480  -f flv -y abc.flv

命令行包括三个部分:输入参数,输出参数,和全局选项。

-i /home/ron/music/avm.mp4是输入参数,a.mp4是输出参数。输入/输出参数可以有专属的选项,这些选项应该紧挨着放在输入输出参数前面。如-vf “split [main][tmp]...[main][flip]”就是输出参数a.mp4的选项。

全局选项的位置不需要限定,因为选项是以选项名字查找的。

可以有多组输入参数和多组输出参数。

1.2 解析命令行

split_commandline()负责解析命令行。

int split_commandline(OptionParseContext *octx, int argc, char *argv[],const OptionDef *options,const OptionGroupDef *groups, int nb_groups);

解析的结果保存在OptionParseContext中。解析时需要参考OptionDef和OptionGroupDef。OptonDef[]是支持ffmpeg的选项列表,OptionGroupDef[]是支持的组列表,包括输入类和输出类,前者以-i开头,加上设备名。后者只有文件名。

下面的类图显示了涉及的类:


OptionGroup保存一个输入(或输出)和它的选项列表。Option表示一个选项。

OptionParseContext中包括多个OptionGroup。全局选项保存在global_opts中。所有输入设备的选项保存在一个OptionGroupList实例中,所有输出设备的选项保存在另一个实例中。两者合起来组成数组groups.

1.3 split_commandline()

split_commandline()在一个循环中解析命令行,主要涉及如下函数。

函数 功能

find_option() 查询支持的option列表,检查当前元素是否一个option

add_option() 将option加入一个临时组。(因为option先于group出现,还不知道应该加入到哪个组。)

match_group_separator() 查询支持的group列表,检查当前元素是否是一个Group

finish_group() 设置临时组的参数,并用它填充OptionParseContext.groups(现在知道应该加入哪个组了)

1.4 parse_optgroup()

parse_optgroup()负责将OptionGroup转换成OptionsContext。

int parse_optgroup(void *optctx, OptionGroup *g);

OptionGroup保存的选项值是字符串,而OptionsContext保存的值是由OptionDef定义的实际类型。parse_optgroup()的第一个参数optctx实际上是OptonsContext。

下面的类图显示了涉及的类:


SpecifierOpt保存实际类型的选项。OptionsContext有若干个SpecifierOpt数组的成员。每个specfier数组保存一类选项。如filters保存”filter”选项。但filter可以是”filter:v”,属于video,也可以是“filter:a”,属于audio。SpecifierOpt.specifier成员就是用来标记这个选项应该属于谁的。对于”filter:v”,SpecifierOpt.specifier就是”v”。

这里顺便提一下AVDictionary。解析过程没有用到它。用户设置的选项可能不成功,而选项的最终值会保存在这里。用av_dict_set()函数设置它。

1.5 parse_optgroup()

parse_optgroup()函数遍历OptonGroup中的Option,调用write_option()将其写入OptionsContext。

对于基本的选项,它的OptionDef中定义了它在OptionsContext的偏移,所以将字符串转化后,直接写入就好了。比如”filter:v”。

有的选项可能是其他选项的别名。这时它的OptionDef指定了一个回调函数。这个函数会重定向到所指向的选项上去。如”vf”就是”filter:v”的别名,它的OptionDef指定了回调函数opt_video_filter()。这个函数会调用parse_option()和find_option()查找”filter:v”对应的OptionDef,并再次调用write_option()。

全局选项。它的OptionDef也定义了一个回调函数。这个函数直接设置全局变量。如loglevel,它的OptionDef定义了opt_loglevel()。这个函数调用av_log_set_level()设置日志输出等级。

1.6 MATCH_PER_XXX_OPT()

宏MATCH_PER_TYPE_OPT()和MATCH_PER_STREAM_OPT()用于从OptionsContext读值。

前者指定参数mediatype,用它跟OptionsContext.spcifier比较,找出option并读出。后者指定参数AVStream,调用check_stream_specifier(),用AVStream的属性与OptionContext.specifier匹配,找出option并读出。


2 vf选项解析

2.1 avfilter_graph_parse2()

avfilter_graph_parse2()负责解析vf选项内容。

int avfilter_graph_parse2 (AVFilterGraph *graph, const char *filters,

AVFilterInOut **inputs,

AVFilterInOut **outputs);

输入参数filters是vf选项内容。输出参数Inputs是导出的输入接口,outputs是filters导出的输出接口。

2.2 filters

如下是filters的一个例子。它来自ffmpeg的文档:

http://ffmpeg.org/ffmpeg-filters.html#Filtergraph-description

ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

对应FilterGraph的结构示意图如下。 矩形框内是vf的内容对应的部分。其中split应该导出到inputs中,overlay应该导出到outputs中。


2.3 vf术语

描述vf的解析过程需要使用一些术语。其中一部分是关于vf语法的,另外一部分是关于生成的FilterGraph结构的。


上图标出了vf语法的术语。

过滤器。过滤器用红色标出,包括它的名字和参数。如”split”,只有名字。又如”overlay=0:H/2”,overlay是名字,”0:H/2”是参数。名字和参数用 = 连接。

位置点。有两类位置点,有名的和无名的。有名位置点用绿色标出,名字用 [] 包住,如main, flip, tmp。无名位置点不必标出。

路径。路径是一条从位置点开始,中间过滤器和位置点交错,在位置点结束的处理流程。多条路径组成整个filtergraph。中间的位置点都是无名的,开始和结束的位置点应该是有名的,除非这条路径在filtergraph的开始和结束位置。路径之间用 ; 隔开。如 [tmp] crop=iw:ih/2:0:0, vflip [flip]。以tmp开始,中间包括crop和vflip和一个无名位置点,在flip结束。有名位置点是该路径与其他路径的连接点,所以需要有一个名字来标记,而无名位置点只存在该路径内部的两个过滤器之间,是隐含的,所以不需要名字。

下图是FilterGraph的结构图。


FilterGraph是由一系列的过滤器,Pad和Pad Link构成的。

过滤器来自FilterGraph语法中的过滤器,它有一组In Pad和一组OutPad, Pad与语法中的位置点对应。过滤器之间通过Pad联系,Pad Link用来将一个In Pad连接到一个OutPad。Pad Link没有对应的语法元素。

Input/Output用于解析过程,也用于保存整个解析的结果,以返回给调用者。open_inputs标记当前还没有解析(与其他OutPad连接)的InPad,open_outputs标记当前还没有解析的OutPad,curr_inputs标记当前将要解析的InPad。

2.4 avfilter_graph_parse2()

avfilter_graph_parse2()主要调用四个函数进行解析。

函数 功能

parse_input() 选取若干open_outputs,以更新curr_inputs

parse_filter() 解析过滤器

link_filter_inouts() 将新的过滤器连入当前的curr_inputs,并更新curr_inputs

parse_output() 结束当前的curr_inputs,加入open_outputs。

2.5 解析过程

下面的图描述了上述语法的解析过程。图上部的xxx()是当前步骤调用的函数,下面的字符串是语法,当前变色的部分是正在解析的部分。

2.6 FilterGraph类

下面的类图显示了FilterGraph各元素对应的类。


AVFilterContext表示过滤器。AVFilter是它的属性类。

AVFilterPad是Pad类。一个AVFilterContext实例包括AVFilterPad的一组In Pad实例和一组Out Pad实例。AVFilterLink是Pad Link类,它连接两个AVFilterPad实例。

AVFilterLink有一个FFFrameQueue,用于保存过滤的中间结果。这时一个frame的数据通道。

AVFilterContext有一个空间,用于保存该特定类型Filter的私有信息,可以是CropContext,SplitContext或其他filter的一种。

AVFilterInOut用于解析过程标记open_iputs, open_ouputs和curr_inputs。它没有直接引用AVFilterPad,而是引用AVFilterContext,和用序号间接指向AVFilterPad。

AVFilterGraph和FilterGraph是代表整个FilterGraph的容器类。

2.7 avfilter_graph_parse2()

下图是avfilter_graph_parse2()的函数调用关系。



transcode函数

transcode_init()

transcode_step



transcode_init函数

初始化工作:

    AVFormatContext *oc;//输出流的编解码器结构

    OutputStream *ost; //输出流

    InputStream *ist; //输入流

init_input_stream

init_output_stream




transcode_step函数





好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***