ffmpeg 代码阅读笔记1/2
============================================================
0. ffmpeg 主流程
============================================================
ret = ffmpeg_parse_options(argc, argv); //一招完成所有初始化.
if (transcode() < 0) // 一招完成循环处理所有数据.
transcode()退出就结束了. 有点一分为2的思想.
============================================================
甲: ffmpeg_parse_options 分析
============================================================
大致流程为5步:
ret = split_commandline(&octx, argc, argv, options, groups,FF_ARRAY_ELEMS(groups)); //分隔命令行
ret = parse_optgroup(NULL, &octx.global_opts); //分析全局选项
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file); // 打开输入文件
ret = init_complex_filters(); // 初始化复杂过滤器
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file); //打开输出文件
然后退出.
前言:
ffmpeg 有众多的选项,成千上万, ffmpeg -h full 有上万条输出!
这些选项被分成了几个表. 大致可分为6个表,
全局选项1个表, AVoption选项5个表下辖若干个表,构成树表.
============================================================
一. split_commandline 分析,
============================================================
split_commandline 都干了些什么?
ret = split_commandline(&octx, argc, argv, options, groups, FF_ARRAY_ELEMS(groups));
octx: option context, 输出参数, 全局选项都放到这里了.
argc,argv: 输入参数
options: 输入参数, 全局选项表入口, 最后一项为空项表示结束
groups: 输入参数,组表入口
FF_ARRAY_ELEMS(groups): 组表项个数, 实际上就是2,一个输出组,一个输入组
其实用不着split, 选项,参数都已经按空格被分割放在不同的argv[index]中了,问题是进一步把这些选项,参数放到哪里?
该函数把命令行分割为它的内部实现!
它依次扫描命令行的每一个选项, 首先判断是不是输入文件或者输出文件
ret = match_group_separator(groups, nb_groups, opt);
如果不是那就先查是不是正常选项normal option. 从选项中去找它.
po = find_option(options, opt); //options 是选项表, opt 是当前选项
会返回一个选项指针po
若po-name不为空表示找到了,把结果保存到octx中, 调用下面函数来执行
add_opt(octx, po, opt, arg); //opt 是选项, arg 是选项参数
这里的octx是OptionParseContext, 见后面结构描述. octx用来收集用户的选项输入.
根据opt->flag,可能会保存到octx的全局group,或cur_group中
po->name为空表示在options这个表中没找到.
如果没找到,再查AVOptions, 把选项及参数都传人函数来查找.
opt_default(NULL, opt, argv[optindex])
这一次它要大面积的查,从codec,fromat,resample,sws,swr 5个类中查
const AVClass *cc = avcodec_get_class();
const AVClass *fc = avformat_get_class();
const AVClass *rc = avresample_get_class();
const AVClass *sc = sws_get_class();
const AVClass *swr_class = swr_get_class();
const AVOption *o = opt_find(void *obj, char *name, char *unit, int opt_flags, int search_flags);
直到查到为止consume=1或者找不到.
例如: 从format_class 类中查,obj为fc,name 为输入项,unit为NULL, opt_flags 为0, search_flags 查子表及伪目标.
o = opt_find(&fc, opt, NULL, 0, AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ);
opt_find 是对 av_opt_find 的简单封装.
查到了,例如从format 表中找到的,则加入到format_opts字典中.
av_dict_set(&format_opts, opt, arg, FLAGS); //opt 选项,arg参数,FLAGS(追加或覆盖之意)
其中format_opts 是全局变量.是一个AVDictionary 指针, 结果存到这里去.
字典的概念后面有描述.
如果是输入文件,
finish_group(octx, 1, arg); //arg->输入文件名
如果是输出文件,会调用
finish_group(octx, 0, opt); //opt->输出文件名
把当前分析的所有选项cur_group都归结为输入文件(第二参数为1)或者输出文件(第二参数为0).
这里介绍几个感念:
A组: option 相关
1.OptionDef 结构, (选项标识定义)
比较简单, 有名称,标志,offset或函数指针联合体,帮助信息,参数名称,成千上万个选项实例都是这样定义的.
2.Option 结构. (选项定义), 命令行上每个选项会对应一个Option 变量
由一个OptionDef指针,一个key指针,一个value指针构成,key,value都是char 指针
3. OptionGroupDef 结构.(选项组标识定义)
这是个简单结构,包含一个group名, 分隔符(可为空),及标志
4. OptionGroup 结构: (选项组定义) 每个文件会对应一个选项组
包含一个Option 表, 实现为Option *opts, int nb_opts
包含5个字典 AVDictionary 指针, 分别是codec_opts, format_opts,resample_opts,sws_dict,swr_opts
还有头部OptionGroupDef(选项组标识指针) 及一个arg 字符串指针,用以存储文件名称.
5. OptionGroupList 结构.(选项组列表结构)
是一个Group表 OptionGroup *groups, int nb_groups,
还有一个表头部:OptionGroupDef
所有输入文件对应一个选项组列表,所有输出文件对应另一个选项组列表.
6. OptionParseContext (选项分析上下文定义)
包含一个全局的OptionGroup global_opts
包含一个当前的OptionGroup cur_group, 这是暂存group opt 的地方,将来由finish_group 拷贝到group列表.
包含一个OptionGroupList列表集合, OptionGroupList*, nb_groups
其成员一般只有两个列表集合(nb_groups=2),输出,输入.
B组: 字典相关AVDictionary
1.字典是字典项的一个集合.
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
}
2. 字典项 AVDictionaryEntry
字典项是一个key,value键值对
struct AVDictionaryEntry
{
char *key;
char *value;
};
其主要的接口函数:
av_dict_get
av_dict_set
av_dict_count
av_dict_free
关注点: options 选项表. 进一步可关注AV_Option 表
============================================================
2. parse_optgroup
============================================================
应用全局选项:
ret = parse_optgroup(NULL, &octx.global_opts); // 分析一组选项
octx 是OptionParseContext
例如: -y 是全局选项
ret = write_option(NULL, o->opt, o->key, o->val);
根据定义的标志,及偏移,假如是一个整数,算出值存到optctx对应位置.
全局选项其optctx 为NULL, 会直接更改全局变量的值.
例如 -y全局选项, 其opt定义指示把val直接存储到变量,o->opt中存储了变量的地址
数据被写到了static int file_overwrite变量.
2.1 parse_optgroup() 函数分析
----------------------------------------
int parse_optgroup(void *optctx, OptionGroup *g)
{
for (i = 0; i < g->nb_opts; i++) {
Option *o = &g->opts[i];
ret = write_option(optctx, o->opt, o->key, o->val);
}
}
传来的指针optctx 是一个void 指针, 意味着不能使用结构的名称了,只能使用偏移来访问内部数据.
2.2 write_option() 函数分析
----------------------------------------
SpecifierOpt 说明符选项. 说明符是这样的一个结构.
typedef struct SpecifierOpt {
char *specifier; /**< stream/chapter/program/... specifier */
union {
uint8_t *str;
int i;
int64_t i64;
uint64_t ui64;
float f;
double dbl;
} u;
} SpecifierOpt;
别怕!, 最后一个概念了,都是这么回事,慢慢整清楚. 可翻译为描述符选项.
例如-c copy , 其key为c,其value为copy
对于SpecifierOpt 选项, octx中都给它留了位置.(新式的offset 或 spec)旧式用全局变量
static int write_option(void *optctx, const OptionDef *po, const char *opt, const char *arg)
{
/* new-style options contain an offset into optctx, old-style address of a global var*/
//OPT_OFFSET 或OPT_SPEC, optctx 中留了位置, 否则就是全局变量
void *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ? (uint8_t *)optctx + po->u.off : po->u.dst_ptr;
int *dstcount;
if (po->flags & OPT_SPEC) { //如果是描述符.
SpecifierOpt **so = dst; //dst存储的是SpecifierOpt 指针的指针.
dstcount = (int *)(so + 1); //下一个位置是整形指针,也许dstcount叫p_dstcount 更好
*so = grow_array(*so, sizeof(**so), dstcount, *dstcount + 1); //分配一个SpecifierOpt 内存
char *p = strchr(opt, ':'); //opt 是key
char *str= av_strdup(p ? p + 1 : "");
(*so)[*dstcount - 1].specifier = str; //把:后边字符串地址存入specifier 处.(*so)是新分配的内存指针
dst = &(*so)[*dstcount - 1].u; // 调整目标地址指向参数处.
}
if (po->flags & OPT_STRING) {
char *str= av_strdup(arg);
*(char **)dst = str; //存储参数指针
}
}
-c copy 是一个说明符选项, "copy" 指针被存入specifierOpt的union 处
-f hls 是一个位置选项, "hls"地址被存入octx中format地址处
============================================================
3. 打开输入文件
============================================================
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);
其中&octx.groups[1] 就是OptionGroupList l, 输入文件选项组
3.1 open_files()函数分析, 调用一次打开所有的文件列表
----------------------------------------------------
static int open_files(OptionGroupList *l, const char *inout, int (*open_file)(OptionsContext*, const char*))
{
for (i = 0; i < l->nb_groups; i++) { // 只有一个输入文件时,l->nb_groups 为1
OptionGroup *g = &l->groups[i]; //输入参数没有带选项. 不过还是把选项参数传进去.若有在o中存储
OptionsContext o;
init_options(&o);
o.g = g; //从o中还可以方便的找到g
ret = parse_optgroup(&o, g); //g中有选项指针,有个数,还有5个字典
ret = open_file(&o, g->arg);
uninit_options(&o);
}
}
ret = open_file(&o, g->arg); g->arg是文件名, 此处实际调用是
static int open_input_file(OptionsContext *o, const char *filename)
本示例由于对输入文件没有设置选项,所以简单分析如下.
进行一些默认的初始化之后,调用
err = avformat_open_input(&ic, filename, file_iformat, &o->g->format_opts);
其中file_iformat = NULL;
--> 再调用
avformat_find_stream_info ...
============================================================
4. 初始化复杂过滤器
============================================================
ret = init_complex_filters();
初始化复杂过滤器,此处没有使用, 先忽略
============================================================
5. 打开输出文件
============================================================
调用也是open_files, 但传人参数不同.
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);
parse_optgroup(&o, g);
ret = write_option(optctx, o->opt, o->key, o->val);
ret = open_file(&o, g->arg);
实际调用是:
static int open_output_file(OptionsContext *o, const char *filename)
一个庞大的函数呈现眼前:2156->2805, 600多行代码. 先到此.have a rest!
------------------------------------------------------------
static int open_output_file(OptionsContext *o, const char *filename)
------------------------------------------------------------
1. OutputFile 结构定义还算简单.
typedef struct OutputFile {
AVFormatContext *ctx;
AVDictionary *opts;
int ost_index; /* index of the first stream in output_streams */
int64_t recording_time; ///< desired length of the resulting file in microseconds == AV_TIME_BASE units
int64_t start_time; ///< start time in microseconds == AV_TIME_BASE units
uint64_t limit_filesize; /* filesize limit expressed in bytes */
int shortest;
int header_written;
} OutputFile;
OutputFile *of;
of = av_mallocz(sizeof(*of));
...
err = avformat_alloc_output_context2(&oc, NULL, o->format, filename);
of->ctx = oc;
nb_filtergraphs 此处为0, 跳过处理.
new_video_stream(o, oc, idx);
new_audio_stream(o, oc, idx);
代码虽多,大部分都跳过了.
************************************************************
实例分析: 下面是一个复杂的实例
************************************************************
ffmpeg -analyzeduration 10000000 -y -i /opt/test/test1.ts -c:v libx264 -c:a aac -b:v 2M -b:a 128k -s 640*480 -f hls -strftime 1 -hls_list_size 10 -start_number 1 -hls_flags delete_segments+append_list+program_date_time+second_level_segment_index -hls_segment_filename %Y%m%d-%%06d.ts -hls_time 10 playlist.m3u8
选项分析实例:
matched as option ... 是正常选项,在options 列表中给出.
其中 -y 是全局选项,
add_opt(octx, po, opt, arg); //会放到octx->global_opts
其它6个 -c:v,-c:a -b:v,-b:a -s -f // 已经属于输出文件选项组.
add_opt(octx, po, opt, arg); //会放到octx->cur_group, 它们会归为输出文件选项.
matched as AVOption ... 是AV选项, 共7项,从5中类中查找选项.
av_dict_set(&codec_opts, opt, arg, FLAGS);
av_dict_set(&format_opts, opt, arg, FLAGS);
...
其中 -analyzeduration
-strftime
-hls_list_size
-start_number
-hls_flags
-hls_segment_filename
-hls_time
全部是从fc类中找到,所以都添加到format_opts 中.
这样split_commandline 结束.
补充:
finish_group(octx, 0, opt); //将octx->cur_group 内容copy 到输出文件列表中,opt 是输出文件名
全局选项设置
----------------------------------------
将y=1 设置到 file_overwrite 变量上
ret = parse_optgroup(NULL, &octx.global_opts);
输出文件的选项设置
----------------------------------------
ret = parse_optgroup(&o, g);
-c:v libx264 -c:a aac 选项的存储及使用.
1. 被split 到输出文件组,被parse存储到OptionsContext
SpecifierOpt *codec_names;
使用:new_video_stream 或 new_audio_stream 中,调用new_output_stream,调用
choose_encoder()中
MATCH_PER_STREAM_OPT(codec_names, str, codec_name, s, ost->st); // 获取codecs 名字
选择了libx264, aac
ost->enc = find_codec_or_die(codec_name, ost->st->codecpar->codec_type, 1); // 找encoder
最终: avcodec_find_encoder_by_name(name) :
-b:v 2m -b:a 128k 选项的存储及使用
1. 被split 到输出文件组,被parse存储到OptionsContext的OptionGroup中的codec_opts
av_dict_set(&o->g->codec_opts, "b:v", arg, 0);
av_dict_set(&o->g->codec_opts, "b:a", arg, 0);
使用:new_video_stream 调用new_output_stream,调用
ost->encoder_opts = filter_codec_opts(o->g->codec_opts, ost->enc->id, oc, st, ost->enc);
调用 check_stream_specifier(s, st, p + 1)) 过滤, 其中s formatContext,st stream, p+1 流描述符, 可以是v,a,s,d,t,V,及 p(program)
检查流类型和用户输入的流描述符是否一致.一致的保存下来(实际是检查用户指定的流是否合法及是否匹配,由用户来选择流).
transcode(), transcode_step()等会调用
init_output_stream_wrapper(ost, NULL, 1) 调用 init_output_stream 调用
if ((ret = avcodec_open2(ost->enc_ctx, codec, &ost->encoder_opts)) < 0) //使用了编码器选项
-s 选项的存储及使用
被存储到 SpecifierOpt *frame_sizes;
宽高在哪里使用?: new_video_stream()中
MATCH_PER_STREAM_OPT(frame_sizes, str, frame_size, oc, st); //oc,st 是检查用的,从optctx提出value.
//存储到AVCodecContext video_enc的width,height
if (frame_size && av_parse_video_size(&video_enc->width, &video_enc->height, frame_size) < 0)
分析存储到 video_enc(AVCodecContext *), 直接修改了它的width 和 height.
-f 选项的存储及使用
被存储到 OptionsContext 指定偏移处. const char *format 处.
使用: open_output_file()中
err = avformat_alloc_output_context2(&oc, NULL, o->format, filename);
AVOption 的选项存储及使用
----------------------------------------
av_dict_copy(&of->opts, o->g->format_opts, 0);
ret = avformat_write_header(of->ctx, &of->opts); // 在write_header 中使用
附录: 命令行输出log
----------------------------------------
Splitting the commandline.
Reading option '-analyzeduration' ... matched as AVOption 'analyzeduration' with argument '10000000'.
Reading option '-y' ... matched as option 'y' (overwrite output files) with argument '1'.
Reading option '-i' ... matched as input url with argument '/opt/test/fault001.ts'.
Reading option '-c:v' ... matched as option 'c' (codec name) with argument 'libx264'.
Reading option '-c:a' ... matched as option 'c' (codec name) with argument 'aac'.
Reading option '-b:v' ... matched as option 'b' (video bitrate (please use -b:v)) with argument '2M'.
Reading option '-b:a' ... matched as option 'b' (video bitrate (please use -b:v)) with argument '128k'.
Reading option '-s' ... matched as option 's' (set frame size (WxH or abbreviation)) with argument '640*480'.
Reading option '-f' ... matched as option 'f' (force format) with argument 'hls'.
Reading option '-strftime' ... matched as AVOption 'strftime' with argument '1'.
Reading option '-hls_list_size' ... matched as AVOption 'hls_list_size' with argument '10'.
Reading option '-start_number' ... matched as AVOption 'start_number' with argument '1'.
Reading option '-hls_flags' ... matched as AVOption 'hls_flags' with argument 'delete_segments+append_list+program_date_time+second_level_segment_index'.
Reading option '-hls_segment_filename' ... matched as AVOption 'hls_segment_filename' with argument '%Y%m%d-%%06d.ts'.
Reading option '-hls_time' ... matched as AVOption 'hls_time' with argument '10'.
Reading option 'playlist.m3u8' ... matched as output url.
Finished splitting the commandline.
Parsing a group of options: global .
Applying option y (overwrite output files) with argument 1.
Successfully parsed a group of options.
Parsing a group of options: input url /opt/test/fault001.ts.
Successfully parsed a group of options.
Opening an input file: /opt/test/fault001.ts.
[NULL @ 0x55927b549c40] Opening '/opt/test/fault001.ts' for reading
[file @ 0x55927b54ab00] Setting default whitelist 'file,crypto,data'
[mpegts @ 0x55927b549c40] Format mpegts probed with size=2048 and score=50
[mpegts @ 0x55927b549c40] stream=0 stream_type=1b pid=100 prog_reg_desc=
[mpegts @ 0x55927b549c40] stream=1 stream_type=f pid=101 prog_reg_desc=
[mpegts @ 0x55927b549c40] Before avformat_find_stream_info() pos: 0 bytes read:32768 seeks:0 nb_streams:2
[mpegts @ 0x55927b549c40] probing stream 1 pp:2500
[mpegts @ 0x55927b549c40] Probe with size=2560, packets=1 detected aac with score=51
[mpegts @ 0x55927b549c40] probed stream 1
[mpegts @ 0x55927b549c40] Probe buffer size limit of 5000000 bytes reached
[mpegts @ 0x55927b549c40] After avformat_find_stream_info() pos: 0 bytes read:5787792 seeks:2 frames:239
Input #0, mpegts, from '/opt/test/fault001.ts':
Duration: 00:00:30.83, start: 31.370667, bitrate: 10751 kb/s
Program 1
Metadata:
service_name : Service01
service_provider: FFmpeg
Stream #0:0[0x100], 83, 1/90000: Video: h264 (Main), 1 reference frame ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive, left), 1920x1080 (1920x1088), 0/1, 25 fps, 25 tbr, 90k tbn, 50 tbc
Stream #0:1[0x101](eng), 156, 1/90000: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 320 kb/s
Successfully opened the file.
Parsing a group of options: output url playlist.m3u8.
Applying option c:v (codec name) with argument libx264.
Applying option c:a (codec name) with argument aac.
Applying option b:v (video bitrate (please use -b:v)) with argument 2M.
Applying option b:a (video bitrate (please use -b:v)) with argument 128k.
Applying option s (set frame size (WxH or abbreviation)) with argument 640*480.
Applying option f (force format) with argument hls.
Successfully parsed a group of options.
Opening an output file: playlist.m3u8.
Successfully opened the file.
new_video_stream()/ new_audio_stream() 其输出在哪里? 在output_streams里