ffmpeg.c(4.3.1)源码剖析(一)https://developer.aliyun.com/article/1473997
三、main 函数主要流程分析
main 函数如下:
int main_ffmpeg431(int argc, char **argv) { int i, ret; BenchmarkTimeStamps ti; init_dynload(); register_exit(ffmpeg_cleanup); setvbuf(stderr,NULL,_IONBF,0); /* win32 runtime needs this */ av_log_set_flags(AV_LOG_SKIP_REPEATED); parse_loglevel(argc, argv, options); if(argc>1 && !strcmp(argv[1], "-d")){ run_as_daemon=1; av_log_set_callback(log_callback_null); argc--; argv++; } #if CONFIG_AVDEVICE avdevice_register_all(); #endif avformat_network_init(); //show_banner(argc, argv, options); /* parse options and open all input/output files */ ret = ffmpeg_parse_options(argc, argv); if (ret < 0) exit_program(1); if (nb_output_files <= 0 && nb_input_files == 0) { show_usage(); av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name); exit_program(1); } /* file converter / grab */ if (nb_output_files <= 0) { av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n"); exit_program(1); } for (i = 0; i < nb_output_files; i++) { if (strcmp(output_files[i]->ctx->oformat->name, "rtp")) want_sdp = 0; } current_time = ti = get_benchmark_time_stamps(); if (transcode() < 0) exit_program(1); if (do_benchmark) { int64_t utime, stime, rtime; current_time = get_benchmark_time_stamps(); utime = current_time.user_usec - ti.user_usec; stime = current_time.sys_usec - ti.sys_usec; rtime = current_time.real_usec - ti.real_usec; av_log(NULL, AV_LOG_INFO, "bench: utime=%0.3fs stime=%0.3fs rtime=%0.3fs\n", utime / 1000000.0, stime / 1000000.0, rtime / 1000000.0); } av_log(NULL, AV_LOG_DEBUG, "%"PRIu64" frames successfully decoded, %"PRIu64" decoding errors\n", decode_error_stat[0], decode_error_stat[1]); if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1]) exit_program(69); exit_program(received_nb_signals ? 255 : main_return_code); return main_return_code; }
总结起来,分为以下几个步骤:
- 1、初始化工作
- 2、解析命令行参数
- 3、编码
- 4、收尾
四、ffmpeg_parse_options
下面是 ffmpeg_parse_options 的调用关系
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 的选项。
- 全局选项的位置不需要限定, 因为选项是以选项名字查找的。
- 可以有多组输入参数和多组输出参数。
①、解析命令行 split_commandline()
split_commandline() 负责解析命令行。
/** * Split the commandline into an intermediate form convenient for further * processing. * * The commandline is assumed to be composed of options which either belong to a * group (those with OPT_SPEC, OPT_OFFSET or OPT_PERFILE) or are global * (everything else). * * A group (defined by an OptionGroupDef struct) is a sequence of options * terminated by either a group separator option (e.g. -i) or a parameter that * is not an option (doesn't start with -). A group without a separator option * must always be first in the supplied groups list. * * All options within the same group are stored in one OptionGroup struct in an * OptionGroupList, all groups with the same group definition are stored in one * OptionGroupList in OptionParseContext.groups. The order of group lists is the * same as the order of group definitions. */ 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。
split_commandline() 在一个循环中解析命令行,主要涉及如下函数。
函数 | 功能 |
find_option() | 查询支持的 option 列表, 检查当前元素是否一个option |
add_option() | 将 option 加入一个临时组。(因为 option 先于 group 出现,还不知道应该加入到哪个组。) |
match_group_separator() | 查询支持的 group 列表,检查当前元素是否是一个 Group |
finish_group() | 设置临时组的参数,并用它填充 OptionParseContext.groups(现在知道应该加入哪个组了) |
②、parse_optgroup()
parse_optgroup() 负责将 OptionGroup 转换成 OptionsContext。
/** * Parse an options group and write results into optctx. * * @param optctx an app-specific options context. NULL for global options group */ 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()函数设置它。
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() 设置日志输出等级。
③、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 并读出。
#define MATCH_PER_STREAM_OPT(name, type, outvar, fmtctx, st)\ {\ int i, ret, matches = 0;\ SpecifierOpt *so = NULL;\ for (i = 0; i < o->nb_ ## name; i++) {\ char *spec = o->name[i].specifier;\ if ((ret = check_stream_specifier(fmtctx, st, spec)) > 0) {\ outvar = o->name[i].u.type;\ so = &o->name[i];\ matches++;\ } else if (ret < 0)\ exit_program(1);\ }\ if (matches > 1)\ WARN_MULTIPLE_OPT_USAGE(name, type, so, st);\ } #define MATCH_PER_TYPE_OPT(name, type, outvar, fmtctx, mediatype)\ {\ int i;\ for (i = 0; i < o->nb_ ## name; i++) {\ char *spec = o->name[i].specifier;\ if (!strcmp(spec, mediatype))\ outvar = o->name[i].u.type;\ }\ }
2、vf 选项解析
下图是 avfilter_graph_parse2() 的函数调用关系。
①、filters
如下是 filters 的一个例子。 它来自 ffmpeg 的文档:https://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 中。
②、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。
③、avfilter_graph_parse2()
avfilter_graph_parse2() 负责解析 vf 选项内容。
/** * Add a graph described by a string to a graph. * * @param[in] graph the filter graph where to link the parsed graph context * @param[in] filters string to be parsed * @param[out] inputs a linked list of all free (unlinked) inputs of the * parsed graph will be returned here. It is to be freed * by the caller using avfilter_inout_free(). * @param[out] outputs a linked list of all free (unlinked) outputs of the * parsed graph will be returned here. It is to be freed by the * caller using avfilter_inout_free(). * @return zero on success, a negative AVERROR code on error * * @note This function returns the inputs and outputs that are left * unlinked after parsing the graph and the caller then deals with * them. * @note This function makes no reference whatsoever to already * existing parts of the graph and the inputs parameter will on return * contain inputs of the newly parsed part of the graph. Analogously * the outputs parameter will contain outputs of the newly created * filters. */ int avfilter_graph_parse2(AVFilterGraph *graph, const char *filters, AVFilterInOut **inputs, AVFilterInOut **outputs);
输入参数 filters 是 vf 选项内容。输出参数 Inputs 是导出的输入接口,outputs 是 filters 导出的输出接口。
avfilter_graph_parse2() 主要调用四个函数进行解析。
函数 | 功能 |
parse_input() | 选取若干 open_outputs,以更新 curr_inputs |
parse_filter() | 解析过滤器 |
link_filter_inouts() | 将新的过滤器连入当前的 curr_inputs,并更新 curr_inputs |
parse_output() | 结束当前的 curr_inputs,加入 open_outputs。 |
④、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。
ffmpeg.c(4.3.1)源码剖析(三)https://developer.aliyun.com/article/1473999