author: hjjdebug
date: 2023年 07月 31日 星期一 14:32:15 CST
ffmpeg 的帮助系统
目的: 搞清楚它都打印了什么? 它是怎样实现的.
$ffprobe -h 1996行输出
$ffmpeg -h 111行输出
$ffplay -h 8492行输出
########################################
甲: 先分析ffprobe -h
########################################
$ffprobe -h 1996行输出
剥皮之后来到
void show_help_default(const char *opt, const char *arg)
它包含3个部分:
show_help_options(options, "Main options:", 0, 0, 0); // 把options 选项的help 信息全部打印出来,68行输出,实现是枚举这个options表
show_help_children(avformat_get_class(), AV_OPT_FLAG_DECODING_PARAM); // 把av_format_context_class类及其子类解码参数的帮助信息打印, 长到1270
show_help_children(avcodec_get_class(), AV_OPT_FLAG_DECODING_PARAM); // 把av_codec_context_class类及其子类解码参数的帮助信息打印, 长到1994
show_help_options() 函数后面三参数是3个flag, 包含flag,抛弃flag,alt_flag, 可看后面描述, 为0是不考虑的意思.
目前可简单理解为把 ffprobe 的主选项 的帮助信息(或说描述信息)都打印出来
看第2部分, avformat 类及其子类的帮助
show_help_children(avformat_get_class(), AV_OPT_FLAG_DECODING_PARAM); // 把av_format_context_class类族解码参数的帮助信息打印, 长到1270
void show_help_children(const AVClass *class, int flags, int level) // 这是一个递归调用函数, level 是我加上的参数,为了搞清楚它的层次调用参数
{
if (class->option) { //它传来的AVClass 是名叫AVFormatContext的类. 根据该对象的options 表位置,
printf("----level %d ----\n",level);
av_opt_show2(&class, NULL, flags, 0); // 先处理自己, 依据flags, 漂亮的组织打印内容.
printf("\n");
}
void *iter = NULL;
const AVClass *child;
while (child = av_opt_child_class_iterate(class, &iter)) //然后枚举它的子类,
{
show_help_children(child, flags,++level); //递归调用该函数.
level--;
}
}
先处理自己,再枚举递归叫先序遍历. 反之叫后序遍历.
处理很简单(代码简单,递归),但关系很复杂,这么多类(几百个类)它们的关系是什么?
我看了一下,大部分都是一级(儿子), 少部分有层级关系,最大3级.
例如:
----level 1 ----
AVIOContext AVOptions:
-protocol_whitelist <string> .D......... List of protocols that are allowed to be used
----level 2 ----
URLContext AVOptions:
-protocol_whitelist <string> .D......... List of protocols that are allowed to be used
-protocol_blacklist <string> .D......... List of protocols that are not allowed to be used
-rw_timeout <int64> ED......... Timeout for IO operations (in microseconds) (from 0 to I64_MAX) (default 0)
----level 3 ----
Async AVOptions:
----level 3 ----
cache AVOptions:
-read_ahead_limit <int> .D......... Amount in bytes that may be read ahead when seeking isn't supported, -1 for unlimited (from -1 to INT_MAX) (default 65536)
----level 3 ----
crypto AVOptions:
-key <binary> ED......... AES encryption/decryption key
-iv <binary> ED......... AES encryption/decryption initialization vector
-decryption_key <binary> .D......... AES decryption key
-decryption_iv <binary> .D......... AES decryption initialization vector
----level 3 ----
ffrtmphttp AVOptions:
-ffrtmphttp_tls <boolean> .D......... Use a HTTPS tunneling connection (RTMPTS). (default false)
----level 3 ----
file AVOptions:
-follow <int> .D......... Follow a file as it is being written (from 0 to 1) (default 0)
-seekable <int> ED......... Sets if the file is seekable (from -1 to 0) (default -1)
----level 3 ----
ftp AVOptions:
-timeout <int> ED......... set timeout of socket I/O operations (from -1 to INT_MAX) (default -1)
-ftp-anonymous-password <string> ED......... password for anonymous login. E-mail address should be used.
-ftp-user <string> ED......... user for FTP login. Overridden by whatever is in the URL.
-ftp-password <string> ED......... password for FTP login. Overridden by whatever is in the URL.
----level 3 ----
http AVOptions:
-seekable <boolean> .D......... control seekability of connection (default auto)
-http_proxy <string> ED......... set HTTP proxy to tunnel through
-headers <string> ED......... set custom HTTP headers, can override built in default headers
-content_type <string> ED......... set a specific content type for the POST messages
我终于明白为什么要标注代码了,
因为上层描述,不足以描述清楚其细节.
代码之精妙,需要一步步标注其代码才能说清其中的关系,
并且还要用更多语言描述其精妙之处,包括参数,返回值,甚至直达bit
下面把代码copy 过来标注:
//分析av_opt_child_class_iterate 函数, 这类似于c++代理模式, 你让我干什么,我就让代理干什么.
//而这里的代理就是调用者自己, 你让我干什么,传给我函数,我让函数干什么, 这也就c的回调函数
const AVClass *av_opt_child_class_iterate(const AVClass *class, void **iter)
{
if (class->child_class_iterate) // 如果传来的类参数包含枚举函数,
return class->child_class_iterate(iter); // 就调用这个类参数包含的枚举函数并返回
return NULL;
}
每一个层次对孩子们的枚举方式不同,我们看看AVFormatContext 的枚举方式, 这是AVFormatContext 的枚举回调
enum {
CHILD_CLASS_ITER_AVIO = 0,
CHILD_CLASS_ITER_MUX,
CHILD_CLASS_ITER_DEMUX,
CHILD_CLASS_ITER_DONE,
};
#define ITER_STATE_SHIFT 16
static const AVClass *format_child_class_iterate(void **iter)
{
// we use the low 16 bits of iter as the value to be passed to
// av_(de)muxer_iterate()
// 用val 作为muxer,demuxer 枚举下一个的索引
void *val = (void*)(((uintptr_t)*iter) & ((1 << ITER_STATE_SHIFT) - 1)); //就是说val不会超过16位,它实际上是表格的索引!!
//用state (状态), 描述它目前处于那个层次
unsigned int state = ((uintptr_t)*iter) >> ITER_STATE_SHIFT;
const AVClass *ret = NULL;
if (state == CHILD_CLASS_ITER_AVIO) {
ret = &ff_avio_class; // avio 只会进来一次, 但avio 还会有自己的孩子们
state++;
goto finish;
}
if (state == CHILD_CLASS_ITER_MUX) {
const AVOutputFormat *ofmt;
while ((ofmt = av_muxer_iterate(&val))) { // muxer 都是format 的儿子, val为下一个的索引
ret = ofmt->priv_class; //找到包含类的地址,返回
if (ret)
goto finish;
}
val = NULL;
state++;
}
if (state == CHILD_CLASS_ITER_DEMUX) {
const AVInputFormat *ifmt;
while ((ifmt = av_demuxer_iterate(&val))) { // muxer 都是format 的儿子, val为下一个的索引
ret = ifmt->priv_class; //找到包含类的地址,返回
if (ret)
goto finish;
}
val = NULL;
state++;
}
finish:
// make sure none av_(de)muxer_iterate does not set the high bits of val
av_assert0(!((uintptr_t)val >> ITER_STATE_SHIFT));
*iter = (void*)((uintptr_t)val | (state << ITER_STATE_SHIFT));
return ret;
}
好了,对av_format_context_class 的枚举搞清楚了, 以后想在其下加一个context 也可以操作了.!
看第3部分. avcodec 类的帮助
show_help_children(avcodec_get_class(), AV_OPT_FLAG_DECODING_PARAM); // 把av_codec_context_class类族解码参数的帮助信息打印, 长到1994
有了第2部分的铺垫,第3部分就容易了,因为它们调用同一个函数,只是第一个参数class 指针不同而已.
void show_help_children(const AVClass *class, int flags, int level)
{
if (class->option) {
printf("----level %d ----\n",level);
av_opt_show2(&class, NULL, flags, 0); //传来的类实例是AVCodecContext, 先打印自己,这是0级,
printf("\n");
}
void *iter = NULL; // 它如果改为 int index=0, 读者会更容易理解, 因为*iter是按0,1,2,3...来改变的,并不是指针.
const AVClass *child;
while (child = av_opt_child_class_iterate(class, &iter)) //再枚举孩子们打印
{
show_help_children(child, flags,++level);
level--;
}
}
//再分析一次av_opt_child_class_iterate 函数, 这类似于c++代理模式, 你让我干什么,我就让代理干什么.
//而这里的代理就是调用者自己, 你让我干什么,传给我函数,我让函数干什么, 这也就c的回调函数
const AVClass *av_opt_child_class_iterate(const AVClass *class, void **iter)
{
if (class->child_class_iterate) // 如果传来的类参数包含枚举函数,
return class->child_class_iterate(iter); // 就调用这个类参数包含的枚举函数并返回
return NULL;
}
我们看看这个回调函数,
static const AVClass *codec_child_class_iterate(void **iter) //这个传来的地址,包含着索引信息
{
const AVCodec *c;
/* find next codec with priv options */
while (c = av_codec_iterate(iter)) //它就是很简单的表格(数组)枚举了.
if (c->priv_class)
return c->priv_class;
return NULL;
}
//说它简单,功能确实简单,但也有可圈点之处.
const AVCodec *av_codec_iterate(void **opaque)
{
uintptr_t i = (uintptr_t)*opaque; //怎样拿到索引号,靠参数
const AVCodec *c = codec_list[i];
if (c)
*opaque = (void*)(i + 1); //把下一个索引号,还保留到参数中,参数是个地址. 地址是调用者的一个局部变量地址
return c;
}
########################################
乙: 分析ffmpeg -h
########################################
ffmpeg 的分析多了一个split_commandline()函数, 它的实现不是这里分析的重点.执行结果是把argc,argv参数转移到了octx中
/* split the commandline into an internal representation */
ret = split_commandline(&octx, argc, argv, options, groups, FF_ARRAY_ELEMS(groups));
下一句完成所有打印输出工作.
ret = parse_optgroup(NULL, &octx.global_opts); --> 关键是执行了下一句
ret = write_option(octx, o->opt, o->key, o->val); --> 下面是根据option定义找到一个要执行的函数.
//代码有简化
int show_help(void *optctx, const char *arg)
{
av_log_set_callback(log_callback_help);
char *topic = arg
char *par = strchr(topic, '='); // -h 后面参数, =前面是topic, =后面是arg, 无参数 *topic 为0
if (par) *par++ = 0;
if (!*topic) {
show_help_default(topic, par); // -h 无参数走的是这路, *topic=0
} else if (!strcmp(topic, "decoder")) { //例 -h decoder=h264, =后必需要跟名字, 名字可以用 -decoders查询
show_help_codec(par, 0);
} else if (!strcmp(topic, "encoder")) { //例 -h encoder=h264, =后必需要跟名字,名字可以用 -encoders查询
show_help_codec(par, 1);
} else if (!strcmp(topic, "demuxer")) {//例 -h demuxer=h264, =后必需要跟名字,名字可以用 -demuxers查询
show_help_demuxer(par);
} else if (!strcmp(topic, "muxer")) {//例 -h muxer=h264, =后必需要跟名字,名字可以用 -muxers查询
show_help_muxer(par);
} else if (!strcmp(topic, "protocol")) {//例 -h protocol=file =后必需要跟名字,名字可以用 -protocols查询
show_help_protocol(par);
#if CONFIG_AVFILTER
} else if (!strcmp(topic, "filter")) { //例 -h filter=overlay =后必需要跟名字,名字可以用 -filters查询
show_help_filter(par);
#endif
} else if (!strcmp(topic, "bsf")) { //例 -h bsf=h264_mp4toannexb =后必需要跟名字,名字可以用 -bsfs查询
show_help_bsf(par);
} else {
show_help_default(topic, par); //默认走这里
}
av_freep(&topic);
return 0;
}
void show_help_default(const char *opt, const char *arg)
{
int show_advanced = 0, show_avoptions = 0;
if (opt && *opt) {
if (!strcmp(opt, "long"))
show_advanced = 1;
else if (!strcmp(opt, "full"))
show_advanced = show_avoptions = 1;
else
av_log(NULL, AV_LOG_ERROR, "Unknown help option '%s'.\n", opt);
}
show_usage(); // 打印一行台头
printf("Getting help:\n" //打印如何获取帮助. -h, -h long, -h full, -h type=name
" -h -- print basic options\n"
" -h long -- print more options\n"
" -h full -- print all options (including all format and codec specific options, very long)\n"
" -h type=name -- print all options for the named decoder/encoder/demuxer/muxer/filter/bsf/protocol\n"
" See man %s for detailed description of the options.\n"
"\n", program_name);
//打印具体包含哪些type, type 是个类,知道了type, 还可以用 -types 查看该type下具体包含哪些种. 这样 -h type=name 就完整了.
show_help_options(options, "Print help / information / capabilities:", OPT_EXIT, 0, 0);
/* per-file options have at least one of those set */
const int per_file = OPT_SPEC | OPT_OFFSET | OPT_PERFILE;
//req_flags 为0,所有项参与,rej_flags=per_file |OPT_EXIT|OPT_EXPERT, 有这些标志的项均不显示
//此为显示全局标志
show_help_options(options, "Global options (affect whole program" "instead of just one file):", 0, per_file | OPT_EXIT | OPT_EXPERT, 0);
if (show_advanced)
show_help_options(options, "Advanced global options:", OPT_EXPERT, per_file | OPT_EXIT, 0); //显示OPT_EXPERT项,但不包含per_file|OPT_EXIT标志
//要求有per_file标志,但过滤了一些标志
//此为显示每个文件都具有的主选项,项fmt,codec,start time,duration, frame等
show_help_options(options, "Per-file main options:", 0, OPT_EXPERT | OPT_AUDIO | OPT_VIDEO | OPT_SUBTITLE | OPT_EXIT, per_file);
if (show_advanced)
show_help_options(options, "Advanced per-file options:", OPT_EXPERT, OPT_AUDIO | OPT_VIDEO | OPT_SUBTITLE, per_file);
//要求是OPT_VIDEO,过滤了OPT_EXPERT|OPT_AUDIO
//此为video 选项
show_help_options(options, "Video options:", OPT_VIDEO, OPT_EXPERT | OPT_AUDIO, 0);
if (show_advanced)
show_help_options(options, "Advanced Video options:", OPT_EXPERT | OPT_VIDEO, OPT_AUDIO, 0); //添一部分,但不能是OPT_AUDIO flag
//此为audio 选项
show_help_options(options, "Audio options:", OPT_AUDIO, OPT_EXPERT | OPT_VIDEO, 0); // 类似处理
if (show_advanced)
show_help_options(options, "Advanced Audio options:", OPT_EXPERT | OPT_AUDIO, OPT_VIDEO, 0);
//此为subtitle 选项
show_help_options(options, "Subtitle options:", OPT_SUBTITLE, 0, 0); //要求是OPT_SUBTITLE flag
printf("\n");
if (show_avoptions) { // 如果打开了 show_avoptions, 还会显示子类的帮助信息,这里就不分析了.
int flags = AV_OPT_FLAG_DECODING_PARAM | AV_OPT_FLAG_ENCODING_PARAM;
show_help_children(avcodec_get_class(), flags,0);
show_help_children(avformat_get_class(), flags,0);
show_help_children(avfilter_get_class(), AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_FILTERING_PARAM,0);
show_help_children(av_bsf_get_class(), AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_BSF_PARAM,0);
}
}
关键是show_help_options() 函数
不懂的东西都在代码里,怪不得要标注代码呢. 一下就明白req, rej 是如何起作用的了.
void show_help_options(const OptionDef *options, const char *msg, int req_flags, int rej_flags, int alt_flags)
{
const OptionDef *po;
int first;
first = 1;
for (po = options; po->name; po++) {
char buf[128];
//option flag 必需要包含 req_flag,如果alt_flag存在,也必需包含alt_flag, 不能包含rej_flag 才会打印输出
if (((po->flags & req_flags) != req_flags) ||
(alt_flags && !(po->flags & alt_flags)) ||
(po->flags & rej_flags))
continue;
if (first) {
printf("%s\n", msg);
first = 0;
}
av_strlcpy(buf, po->name, sizeof(buf));
if (po->argname) {
av_strlcat(buf, " ", sizeof(buf));
av_strlcat(buf, po->argname, sizeof(buf)); //把选项名称,选项参数名称连接起来输出
}
printf("-%-17s %s\n", buf, po->help);
}
printf("\n");
}
########################################
丙: 分析ffplay -h
########################################
show_help_default(topic, par);
但这次连接的是ffplay.c 下的, 代码非常简洁,但打印的内容很多,8千多行输出. 所以你要会看主要的.
void show_help_default(const char *opt, const char *arg)
{
av_log_set_callback(log_callback_help);
show_usage(); //一行台头
show_help_options(options, "Main options:", 0, OPT_EXPERT, 0); // ffplay主选项输出
show_help_options(options, "Advanced options:", OPT_EXPERT, 0, 0); // ffplay高级选项输出
printf("\n");
show_help_children(avcodec_get_class(), AV_OPT_FLAG_DECODING_PARAM,0); //AVCodecContext 类族的帮助信息
show_help_children(avformat_get_class(), AV_OPT_FLAG_DECODING_PARAM,0); //AVFormatContext 类族的帮助信息
#if !CONFIG_AVFILTER
show_help_children(sws_get_class(), AV_OPT_FLAG_ENCODING_PARAM,0);
#else
show_help_children(avfilter_get_class(), AV_OPT_FLAG_FILTERING_PARAM,0); //AVFilterContext 类族的帮助信息
#endif
printf("\nWhile playing:\n" //这是ffplay 的操控按键, 很有用!
"q, ESC quit\n"
"f toggle full screen\n"
"p, SPC pause\n"
"m toggle mute\n"
"9, 0 decrease and increase volume respectively\n"
"/, * decrease and increase volume respectively\n"
"a cycle audio channel in the current program\n"
"v cycle video channel\n"
"t cycle subtitle channel in the current program\n"
"c cycle program\n"
"w cycle video filters or show modes\n"
"s activate frame-step mode\n"
"left/right seek backward/forward 10 seconds or to custom interval if -seek_interval is set\n"
"down/up seek backward/forward 1 minute\n"
"page down/page up seek backward/forward 10 minutes\n"
"right mouse click seek to percentage in file corresponding to fraction of width\n"
"left double-click toggle full screen\n"
);
}
在 /home/hjj/FFmpeg-n4.4/fftools/cmdutils.c 下调用 show_help_default() 函数, 根据执行文件的不同,它可能会连接到
ffprobe.c 或 ffmpeg.c 或 ffplay.c , 而这三个执行文件中各有各的实现. 这属于不同的执行文件,没什么好说的.
它们各自调用的函数,主要还是show_help_options(), show_help_children() 前面已经详细分析过了,如此帮助系统分析完毕!