author: hjjdebug
date: 2025年 07月 11日 星期五 10:51:23 CST
descrip: ffmpeg 中 write_option()函数详细注释
文章目录
- 1. 函数原型
- 1.1 参数说明
- 1.2 SpecifierOpt 说明符选项结构
- 2. write_option 代码注释
- 2.1 谁调用了write_option 函数?
- 3. 小结:
write_option()不仅在ffmpeg 中, ffplay,ffprobe 都调用了该函数,
write_option是处理命令行参数解析的核心函数,
负责将参数值写入目标地址(全局变量或结构体字段)
所以这里着重介绍一下:
1. 函数原型
static int write_option(void *optctx,
const OptionDef *opt,
const char *key,
const char *val)
1.1 参数说明
optctx:目标上下文(NULL时写入全局变量)
opt:选项实例指针,"选项定义OptionDef"完整定义如下
typedef struct OptionDef {const char *name; //这里的name 就是 查找时的key值, 由此找到实例指针int flags; // val的解释由flags来定义,它还有一个功能确定目标类型union { //目标位置,函数指针,偏移量共用一个union, 到底是什么由flags确定void *dst_ptr;int (*func_arg)(void *, const char *, const char *);size_t off;} u; //下面2个字符串在帮助信息中使用const char *help; //帮助的提示信息const char *argname; //参数的名称信息 } OptionDef;
key:原始命令行键字符串
由key 值找到的OptionDef 实例指针 opt, 但key在这里不是多余的,因为key可能还带有:及附加信息
val 键对应的值, 是一个字符串.
通过opt->flags 可以将其转换为多种数据类型.
OPT_BOOL: 转换为bool值
OPT_STRING:分配内存并拷贝字符串
OPT_INT64:用strtol转换成数值
OPT_OFFSET|OPT_SPEC 决定ctx中的偏移位置
目标地址由opt->u决定,可能是全局固定地址,或者optctx中的一个偏移位置,或者全局函数
举几个OptionDef 实例的例子.
-f 选项,有参数,是OFFSET,可为输入或输出参数,偏移量OFFSET(format),帮助信息 "force format", 帮助参数名 "fmt"{ "f", HAS_ARG | OPT_STRING | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT,{ .off = OFFSET(format) }, "force format", "fmt" }-y 选项, 布尔值, 直接给全局变量file_overwrite赋值,帮助信息:"overwrite output files", 参数名称没有设置{ "y", OPT_BOOL, { &file_overwrite }, "overwrite output files" }-c 选项, 带参数,是字符串, 是OPT_SPEC, 可为输入或输出参数,偏移位置在OFFSET(codec_names)帮助信息是 "codec name", 帮助参数是"codec"{ "c", HAS_ARG | OPT_STRING | OPT_SPEC | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(codec_names) }, "codec name", "codec" },-vcodec key,带参数是视频参数,输入或输出参数,属于文件范围.没有说明值值是什么类型,目标位置是函数{ "vcodec", OPT_VIDEO | HAS_ARG | OPT_PERFILE | OPT_INPUT | OPT_OUTPUT,{ .func_arg = opt_video_codec }, "force video codec ('copy' to copy stream)", "codec" },
OPT_SPEC 的意思是说明符选项,在目标位置,存储的是一个数组指针,可以保存很多条信息.
此例codec_name 可以保留-c:v h264 -c:a aac 等, 要把"v"对应的"h264","a"对应的"aac"这些信息都保留下来
与上面类似的还有-b:v -b:a 设置等.
其实SPEC 的使用还有很多,例如 -r frame_rate; -aspect aspect 设置; -pix_fmt pixel 设置
他们都是一个键对应着若干个值. 这些值可以作为候选值.
1.2 SpecifierOpt 说明符选项结构
OPT_SEC 对应的是一个SpecifierOpt 选项数组, 这里给出 SpecifierOpt的结构定义:
typedef struct SpecifierOpt {char *specifier; /**< stream/chapter/program/... specifier,一般是"v","a","s","d" 字符串 */union {uint8_t *str;int i;int64_t i64;uint64_t ui64;float f;double dbl;} u; } SpecifierOpt;
2. write_option 代码注释
铺垫差不多了,把代码copy 来标注一下:
static int write_option(void *optctx, const OptionDef *po, const char *opt, const char *arg)
{//确定目标地址,带OPT_OFFSET或OPT_SPEC, 地址在optctx结构内部(optctx+offset),否则是全局变量地址void *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ?(uint8_t *)optctx + po->u.off : po->u.dst_ptr;int *dstcount;double num;int ret;if (po->flags & OPT_SPEC) { //有SPEC 标志SpecifierOpt **so = dst; //目标位置是一个SpecifierOpt数组的地址char *p = strchr(opt, ':'); //把key值用:分割char *str;dstcount = (int *)(so + 1); //数组地址下面so+1是一个整数地址,这里将要存储数组的大小ret = grow_array((void**)so, sizeof(**so), dstcount, *dstcount + 1);//把数组扩大1个if (ret < 0)return ret;str = av_strdup(p ? p + 1 : ""); //有:复制:后面部分,无冒号不复制(复制空)if (!str)return AVERROR(ENOMEM);
//dup的字符串就是specifier,翻译为"说明符",一般是"v""a""s""d"(*so)[*dstcount - 1].specifier = str; dst = &(*so)[*dstcount - 1].u; //调整dst 为新分配的SpecifierOpt的u位置,把值存到这里}if (po->flags & OPT_STRING) { //如果是字符串,把字符串dup,地址放到目标处char *str;str = av_strdup(arg);av_freep(dst);if (!str)return AVERROR(ENOMEM);*(char **)dst = str;} else if (po->flags & OPT_BOOL || po->flags & OPT_INT) {ret = parse_number(opt, arg, OPT_INT64, INT_MIN, INT_MAX, &num);if (ret < 0)return ret;*(int *)dst = num; //bool值或int,将参数变成数值,保存到目标} else if (po->flags & OPT_INT64) {ret = parse_number(opt, arg, OPT_INT64, INT64_MIN, INT64_MAX, &num);if (ret < 0)return ret;*(int64_t *)dst = num; //64位,那就按64位存储} else if (po->flags & OPT_TIME) {ret = av_parse_time(dst, arg, 1); //目标存时间if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Invalid duration for option %s: %s\n",opt, arg);return ret;}} else if (po->flags & OPT_FLOAT) {ret = parse_number(opt, arg, OPT_FLOAT, -INFINITY, INFINITY, &num);if (ret < 0)return ret;*(float *)dst = num; //目标存float值} else if (po->flags & OPT_DOUBLE) {ret = parse_number(opt, arg, OPT_DOUBLE, -INFINITY, INFINITY, &num);if (ret < 0)return ret;*(double *)dst = num; //目标存float值} else if (po->u.func_arg) { //如果实例设置了函数指针,则执行该函数int ret = po->u.func_arg(optctx, opt, arg);if (ret < 0) {av_log(NULL, AV_LOG_ERROR,"Failed to set value '%s' for option '%s': %s\n",arg, opt, av_err2str(ret));return ret;}}if (po->flags & OPT_EXIT) //选项实例要求退出,那就返回退出码return AVERROR_EXIT;return 0;
}
2.1 谁调用了write_option 函数?
1. ffplay ffprobe 有 parse_options -> parse_option ->write_option 调用
int parse_option(void *optctx, const char *opt, const char *arg, const OptionDef *options)
它们定义的OptionDef表options 基本上都是直接分析到全局变量.
optctx 就是空,没有使用.
opt 是key, arg 是value
其功能是依据options 表, 根据key值 opt,将val值 arg写入目的地址.
其再上一层parse_options() 其输入参数直接就是命令行参数. 就是循环调用parse_option
void parse_options(void *optctx, int argc, char **argv, const OptionDef *options,
void (*parse_arg_function)(void , const char))
这2个函数都比较简单,就不多做分析了.
整体的功能是把命令行参数写到指定的全局变量中以供使用
2. ffmpeg 中调用 parse_optgroup() -> write_option()
int parse_optgroup(void *optctx, OptionGroup *g)
这一次较为复杂,因为它使用了optctx, 不再是空了,
其2引入了OptionGroup 概念. 什么是OptionGroup? 就是选项组.
我们看看它的简化定义,忽略暂时未使用部分.
typedef struct OptionGroup {Option *opts; //OptionGroup 就是 Option 动态数组,Option就是3个指针int nb_opts;... //忽略暂时不关注部分
} OptionGroup;
parse_optgroup()功能也很好理解, write_option 写了一条option,
parse_optgroup就是循环调用write_option, 把group中所有的option都写出去.
那OptionGroup 从哪里来? 谁调用了parse_optgroup? 有2处
在ffmpeg_parse_options() 函数中, 有调用分析选项组函数parse_optgroup,全局选项组为参数,
这些分析的结果都为写到全局变量中,比较简单
int ffmpeg_parse_options(int argc, char **argv)
{...ret = parse_optgroup(NULL, &octx.global_opts);ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);...
}
还有一处,在打开文件的时候. 实际是2次,打开输入文件列表,打开输出文件列表
static int open_files(OptionGroupList *l, const char inout,
int (open_file)(OptionsContext, const char))
看参数我们知道,它又引入了2个概念. 先看第一个OptionGroupList
typedef struct OptionGroupList {const OptionGroupDef *group_def; //不重要,主要是名称,分割符等OptionGroup *groups; //选项组的数组int nb_groups;
} OptionGroupList;
既然是选项组的数组,那就循环调用parse_optgroup(),把结果存到OptionsContext 结构中 o
一个文件对应一个选项组,多个文件对应多个选项组,多个选项组组织成一个表叫选项组列表
这个函数的关键是调用回调函数open_file,传递参数OptionContext &o 和 文件名g->arg
ret = open_file(&o, g->arg);
那OptionsContext 是什么结构呢?
这个很复杂,就不用copy了,它是ffmpeg 特有的,ffplay,ffprobe 都不使用这个结构.
该context定义了ffmpeg options中所有的选项值的存储位置, 并为后面的open_file服务.
这就是ffmpeg 为什么命令行参数多样化的来历.
3. 小结:
-
选项key/value的引入
本来选项用argv[i]访问挺好的.
但是由于参数变得复杂,我们引入了key/value的概念.
只所以引入key, 就是因为这样val不再依赖位置i来确定了,而是依赖key来确定.
例如原来是arg[3]为输入文件名, 现在规定-i 选项后面跟文件名,
显然后者更灵活. -
选线组的引入
一个选项只能说明一个属性,多个选项说明多个属性,所以选线组是很自然的. 一个文件对应一个选项组 -
选项组列表的引入
一个文件对应一个选项组,那多个文件对应多个选项组,把多个"选项组"组织成表叫选项组列表,
我们把多个文件按输入文件,输出文件分组. 则可以分成2个文件组,
输入文件组对应输入选项组列表,
输出文件组对应输出选项组列表,
好了,有了这些概念,您大概可以读懂ffmpeg 的ffmpeg_parse_options 函数了.
至于它的split_commandline()及open_files() 这里就不展开了. 保持简洁和高效!
读懂了ffmpeg_parse_options函数,就读懂了框架的1/2.
作为实战,不妨分析一下下面还算简单的命令行参数
ffmpeg -v debug -c:v h264 -i ./1.ts -f null -