本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8
ffmpeg 源码分析系列以一条简单的命令开始,ffmpeg -i a.mp4 b.flv
,分析其内部逻辑。
a.mp4下载链接:百度网盘,提取码:nl0s 。
本文主要分析 parse_optgroup()
的内部逻辑,函数调用用流程图如下。
parse_optgroup(void *optctx, OptionGroup *g)
这个函数的 optgroup
其实是 OptionGroup
的缩写。看函数名字跟形参,就知道这个函数干的活就是把OptionGroup
解析给 optctx
。这里的 optctx
是一个新的结构体 OptionsContext
,之前没有介绍过。
那 OptionGroup *g
是从哪里来的呢?如下图所示,全部都在OptionParseContext octx
这个变量。
如前面一章所说,split_commandline()
已经把 cli 命令行参数全局解析 到 OptionParseContext octx
这个变量了。
OptionGroup
其实是有多个的,分别存放在 octx
的3个地方。
octx.global_opts
,存放全局参数的OptionGroup
。octx.groups[1]->groups[0]
, 这里也是OptionGroup
,存放的是输入文件的OptionGroup
,octx.groups[1]->nb_groups
代表有多少个OptionGroup
,之前说过,一个输入或输出文件,对应一个OptionGroup
。octx.groups[0]->groups[0]
,输出文件OptionGroup
,与输入同理。
接下来分析 parse_optgroup()
是如何把 octx
里面那么多 OptionGroup
一个一个解析给 OptionsContext
的。
- ffmpeg_opt.c:3309行,解析全局变量,
parse_optgroup(NULL, &octx.global_opts)
这里是个例外,没有传递OptionsContext
,所以会把octx.global_opt
解析到全局变量,看parse_optgroup
的代码实现就知道了,具体操作是在write_option()
,这个稍后分析。 - ffmpeg_opt.c:3319行,利用
open_files()
函数解析输入文件的OptionGroup
,open_files()
的第一个参数是OptionGroupList
,是输入文件的OptionGroup
集合。如果有3个输入文件,OptionGroupList->nb_groups
就是3,通过遍历OptionGroupList->nb_groups
,不断调用parse_optgroup()
把各个输入文件的OptionGroup
解析到OptionsContext
。 - fmpeg_opt.c:3333行,也是调
open_files()
,不过这里处理的是输出文件的OptionGroup
,跟第二点原理一样。
由此可见 parse_optgroup()
主要做两件事。
- 解析
OptionGroup
结构到全局变量,这个不用说了,自己在代码里搜一下全局变量,就知道某些全局变量在哪里用到。 - 转换
OptionGroup
结构成OptionsContext
,这个OptionsContext
也是对应一个输入文件或者输出文件。一个输入文件的参数,命令行-i
前面的这些-vcodec
,-itsoffset
等等参数,最后都会存到OptionsContext
里。
要理解 parse_optgroup()
的内部逻辑 就需要理解 OptionsContext
这个结构,还有内部的 write_option()
函数。
我画了个图,OptionsContext
这个结构 太大了,会省略某一部分成员参数,原理是一样的,请看
据本人统计,OptionsContext
里面的成员参数有50个以上,非常庞大。但其实,他的成员参数就只有3种类型。
OptionGroup *g
,这里也有OptionGroup
,OptionsContext
的OptionGroup
是直接拷贝传递进来的OptionGroup
,代码在ffmpeg_opt.c:3269
行,o.g = g;
- 常规类型,
int
,int64_t
,cha*
等等C语言的类型。 SpecifierOpt
,这个类型特别重要,不过只有两个成员参数。char *specifier 通常是 'a','v','',代表这是一个视频或音频,或者不区分音视频的参数。后面的union str
是一个联合体,可以是 字符串,int,浮点数。注意,每一个SpecifierOpt xxx
后面都有一个SpecifierOpt nb_xxx
,代表有多少个SpecifierOpt
。例如 nb_codec_names 等于2,就代表有两个 codec,分别是codec_names[0]
和codec_names[1]
。虽然一个输入文件通常只会指定一个解码器,但ffmpeg
为了通用性,每个SpecifierOpt
后面都会有一个数量参数nb_xxx
。
OptionsContext
已经分析完了,接下来讲讲 write_option()
这个函数的内部逻辑。
write_option()
的调用在 cmdutils.c:437行,write_option(optctx, o->opt, o->key, o->val)
。
write_option()
是 parse_optgroup()
的内部函数,从 cmdutils.c:437行 的传参可以看出,parse_optgroup()
不断遍历 OptionGroup::opts
,
把 Option
传递给 write_option()
,写到 optctx
里面。
write_option()
的内部逻辑相对复杂,请结合源码看本文章。
如上图所示,第一行代码就是初步确定 dst
,为什么说是初步,因为 dst
在后面有可能会变化。理解 write_option()
需要先解释一下 OptionDef
这个结构的一些常见值。
在 ffmpeg_opt.c:3368
行定义了一个 OptionDef
的数组。如图:
OptionDef
有5个成员 char *name
,int flags
, union:u
,char *help
,char *argname
。
char *name
对应命令行中的-xxx
的xxx
。int flags
,flag值的介绍如下:- HAS_ARG :代表这个
Option
必须有value,就是-xxx
后面必须还有一个字符串参数 例如-xxx yyy
如果没有yyy
就会报错,具体代码在cmd_utils.c:809
行。 - OPT_INT :代表
dst
是一个 INT,会把 传递进write_option()
的Option::val
转换成int
赋值给dst
。这个时候dst
指向一个全局变量,或者OptionsContext
里面的某个int
成员。 - OPT_BOOL :代表
dst
是一个BOOL,C语言没有布尔,等值于 OPT_INT,dst
被设置成0或1。 - OPT_FLOAT :代表
dst
是一个 浮点数,跟OPT_INT 原理一样 - OPT_STRING :代表
dst
是一个 char * , 跟OPT_INT 原理一样。 - OPT_INT64 :代表
dst
是一个 INT64 , 跟OPT_INT 原理一样。 - OPT_DOUBLE:代表
dst
是一个 Double , 跟OPT_INT 原理一样。 - OPT_EXIT :
write_option()
里面处理完这个标志的Option
就退出程序,具体代码在cmd_utils.c:344
行 - OPT_OFFSET :代表
dst
是OptionsContext
里面的 int float 等普通成员,具体是哪个成员,通过u.off
偏移取得地址,(uint8_t *)optctx + po->u.off
- OPT_SPEC :代表
dst
是OptionsContext
里面的SpecifierOpt
成员。注意,这里是一个数组,dst 会指向数组的某个下标地址。 - OPT_TIME :代表传递进
write_option()
的Option::val
是一个时间字符串 "01:00" 之类的。会调用parse_time_or_die()
转换这个字符串成Int64,然后 赋值给dst
。OPT_INPUT - OPT_INPUT :代表此命令行参数是输入参数,只能在
-i
前面出现,不能在-i
后面出现。如果出现会报错,cmd_utils.c:424
行 - OPT_OUTPUT :代表此命令行参数是输出参数,只能在输出文件名前面出现,不能在后面出现。如果出现会报错,
cmd_utils.c:424
行 - OPT_VIDEO :视频标示,只是一个标示,可以在 show_help 的时候指定 flags 为 OPT_VIDEO ,只显示视频相关的命令行参数。
- OPT_AUDIO :音频标示,同OPT_VIDEO 。
- OPT_SUBTITLE :字幕标示,同OPT_VIDEO 。
- OPT_DATA :数据标示,同OPT_VIDEO 。
- OPT_EXPERT :专家标示,同OPT_VIDEO 。
- OPT_PERFILE :不知道干嘛的。
- HAS_ARG :代表这个
union:u
,u 是一个联合体,分别有void *dst_ptr
,int (*func_arg){..}
,size_t off
void *dst_ptr
:设置了就代表 u 指向一个全局变量。int (*func_arg){..}
:设置了就代表 u 指向 一个函数指针,write_option()
里会调用指向的那个函数size_t off
:代表OptionsContext
的OFFSET,用来确定dst
要指向OptionsContext
的哪个成员参数,(uint8_t *)optctx + po->u.off
。
char *help
,命令行参数help。char *argname
,也是help的时候用的。
至此,write_option()
也已经分析完毕。FFmpeg
命令行解析流程分析已经完成一大半,最后做下小总结。
从 main()
入口开始,到 split_command()
把 argv
所有命令行参数转换成 OptionParseContext
,再到 parse_optgroup(NULL, &octx.global_opts)
把OptionParseContext::global_opts
解析给 全局变量。再到 open_file()
打开输入/输出文件,然后把每个输入/输出文件的专属 OptionGroup
转换成 OptionsContext
,然后再把 OptionsContext
丢给 open_input_file()
或 open_output_file()
。
下一章 会介绍 open_input_file()
跟 open_output_file()
的内部实现。本文介绍的 OptionGroup
如何转成全局变量跟 OptionsContext
已经结束了。
©版权所属:知识星球:弦外之音,QQ:2338195090。
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。