目录
一、文章简介
二、源码阅读
2.1 代码位置
2.2 主要功能及函数介绍
2.2.1 解析命令行参数及参数生效
2.2.2 解析命令行参数
2.2.3 处理命令行的全局参数
2.2.4 打开输入文件(输入参数生效)
2.2.5 打开输出文件(输出参数生效)
音视频学习笔记系列主要介绍笔者在学习音视频过程中,学习了解到的音视频相关知识点,涉及内容包括但不限于音视频基础、音视频封装、传输协议、FFMPEG。本系列持续更新
本文主要介绍FFMPEG中ffmpeg可执行文件,命令行参数解析实现方式和生效方式。
标准命令行格式:
ffmpeg [global_options] {[input_file_options] -i input_file}...{[output_file_options] output_file}...
global_options:全局参数
input_file_options:输入文件相关参数
output_file_options:输出文件相关参数
fftools/ffmpeg_opt.c
由ffmpeg_parse_options函数完成,主要做了如下工作:
1、split the commandline into an internal representation:split_commandline
2、apply global options:parse_optgroup
3、open input files:open_input_file
4、open output files:open_output_file
int ffmpeg_parse_options(int argc, char **argv)
{
OptionParseContext octx;
uint8_t error[128];
int ret;
memset(&octx, 0, sizeof(octx));
/* split the commandline into an internal representation */
ret = split_commandline(&octx, argc, argv, options, groups,
FF_ARRAY_ELEMS(groups));
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: ");
goto fail;
}
/* apply global options */ //让全局变量生效
ret = parse_optgroup(NULL, &octx.global_opts);//全局变量直接设置全局参数,不会再将变量保存在某一个结构体中,所以第一个参数是NULL
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error parsing global options: ");
goto fail;
}
/* configure terminal and setup signal handlers */
term_init();
/* open input files */
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening input files: ");
goto fail;
}
/* create the complex filtergraphs */
ret = init_complex_filters();
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");
goto fail;
}
/* open output files */
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening output files: ");
goto fail;
}
check_filter_outputs();
fail:
uninit_parse_context(&octx);
if (ret < 0) {
av_strerror(ret, error, sizeof(error));
av_log(NULL, AV_LOG_FATAL, "%s\n", error);
}
return ret;
}
1、参数分成3类:全局参数、输入参数、输出参数:init_parse_context
2、从左至右读取参数,识别输入文件、输出文件,并将作用于输入文件的参数和输入文件绑定,输出文件也是一样:finish_group
3、将用户输入参数转化成 ffmpeg 认识的参数(比如输入是-b:v,ffmpeg内部是b):find_option、opt_default
4、输入参数、输出参数的存储方式
通常情况下 一个 OptionGroup 对应一个输入文件或者输出文件,所有的 OptionGroup 都存储在 OptionGroupList 中,名称中有List其实以数组的方式存储
如果有3个输入文件,那么 OptionParseContext::group[1]->nb_groups 就会是3。
OptionParseContext::groups[1]->groups[0] 代表第一个输入文件命令行参数信息,(OptionParseContext::groups的类型是OptionGroupList,OptionParseContext::groups[1]->groups[0]的类型是OptionGroup)
OptionParseContext::groups[1]->groups[1] 代表第二个输入文件命令行参数信息。
如果有4个输出文件,那么 OptionParseContext::group[0]->nb_groups 就会是4。
(原文链接:ffmpeg源码分析-ffmpeg_parse_options_Loken2020的博客-CSDN博客)
一个文件对应多个参数的时候,参数会存放在OptionGroup->Option中,以数组形式存放。OptionParseContext::groups[1]->groups[0] ->opts[0]代表第一个输入文件第一个参数
根据命令行规则 [input_file_options] -i input_file},先会识别出参数,再识别出输入文件,先识别出来的参数会存到临时变量cur_group中,识别到输入文件后输入文件的名称和之前识别到的变量会都写到OptionGroup中,同时清空cur_group;(finish_group实现)
int split_commandline(OptionParseContext *octx, int argc, char *argv[],
const OptionDef *options,
const OptionGroupDef *groups, int nb_groups)
{
int optindex = 1;
int dashdash = -2;
/* perform system-dependent conversions for arguments list */
prepare_app_arguments(&argc, &argv);
init_parse_context(octx, groups, nb_groups);//参数分成3类:全局参数、输入参数、输出参数
av_log(NULL, AV_LOG_DEBUG, "Splitting the commandline.\n");
while (optindex < argc) {
const char *opt = argv[optindex++], *arg;
const OptionDef *po;
int ret;
av_log(NULL, AV_LOG_DEBUG, "Reading option '%s' ...", opt);
if (opt[0] == '-' && opt[1] == '-' && !opt[2]) {
dashdash = optindex;
continue;
}
/* unnamed group separators, e.g. output filename */
if (opt[0] != '-' || !opt[1] || dashdash+1 == optindex) {
//把缓存的 cur_group 赋值给新申请的 OptionGroup。
//还会把 swr_opts,codec_opts 等也赋值,赋值之后会清空 cur_group,再次进入循环 解析下一个文件参数。
finish_group(octx, 0, opt);
av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name);
continue;
}
opt++;
#define GET_ARG(arg) \
do { \
arg = argv[optindex++]; \
if (!arg) { \
av_log(NULL, AV_LOG_ERROR, "Missing argument for option '%s'.\n", opt);\
return AVERROR(EINVAL); \
} \
} while (0)
/* named group separators, e.g. -i */
// -i 后面是输入文件,这里可以理解成识别输入文件
if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) {
GET_ARG(arg);
finish_group(octx, ret, arg);
av_log(NULL, AV_LOG_DEBUG, " matched as %s with argument '%s'.\n",
groups[ret].name, arg);
continue;
}
/* normal options */
//将用户输入参数转化成 ffmpeg 认识的参数
//判断此 opt 是否为 options 中定义的参数
po = find_option(options, opt);
if (po->name) {
if (po->flags & OPT_EXIT) {
/* optional argument, e.g. -h */
arg = argv[optindex++];
} else if (po->flags & HAS_ARG) {
GET_ARG(arg);
} else {
//允许参数后面的值缺失,直接设置为1 e.g. -y
arg = "1";
}
//e.g. -b:v 230k opt == -b:v arg == 230k
add_opt(octx, po, opt, arg);
//Reading option '-b:v' ... matched as option 'b' (video bitrate (please use -b:v)) with argument '230k'
av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with "
"argument '%s'.\n", po->name, po->help, arg);
continue;
}
/* AVOptions */
//解析编码器参数或解复用参数
if (argv[optindex]) {
ret = opt_default(NULL, opt, argv[optindex]);
if (ret >= 0) {
av_log(NULL, AV_LOG_DEBUG, " matched as AVOption '%s' with "
"argument '%s'.\n", opt, argv[optindex]);
optindex++;
continue;
} else if (ret != AVERROR_OPTION_NOT_FOUND) {
av_log(NULL, AV_LOG_ERROR, "Error parsing option '%s' "
"with argument '%s'.\n", opt, argv[optindex]);
return ret;
}
}
/* boolean -nofoo options */
//boolean类型变量可以在前面加"no",此时变量的值就是false e.g. -noy
if (opt[0] == 'n' && opt[1] == 'o' &&
(po = find_option(options, opt + 2)) &&
po->name && po->flags & OPT_BOOL) {
add_opt(octx, po, opt, "0");
av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with "
"argument 0.\n", po->name, po->help);
continue;
}
av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt);
return AVERROR_OPTION_NOT_FOUND;
}
if (octx->cur_group.nb_opts || codec_opts || format_opts || resample_opts)
av_log(NULL, AV_LOG_WARNING, "Trailing option(s) found in the "
"command: may be ignored.\n");
av_log(NULL, AV_LOG_DEBUG, "Finished splitting the commandline.\n");
return 0;
}
ret = parse_optgroup(NULL, &octx.global_opts);//全局变量直接设置全局参数,不会再将变量保存在某一个结构体中,所以第一个参数是NULL
1、处理参数的函数是parse_optgroup,全局变量、输入参数、输出参数都是由这个参数处理,只是处理全局变量时第一个参数是NULL,处理输入参数、输出参数时第一个变量不为空
2、处理全局变量时第一个参数传的是NULL (ret = parse_optgroup(NULL, &octx.global_opts);),原因是全局变量直接设置全局参数,不会再将变量保存在某一个结构体中(以loglevel为例,最后调用opt_loglevel,然后设置全局参数);而输入参数、输出参数则还要分别放到结构体中,供后续使用
3、全局参数都在"CMDUTILS_COMMON_OPTIONS"中
4、全局参数都没有offset,也说明不需要像输入参数、输出参数那样存入某个结构体中
有些参数有func_arg,则说明要用此函数处理传入的参数。对于命令行的全局参数来说,可能会给代码内部的全局变量赋值
5、parse_optgroup、write_option函数解析
parse_optgroup(void *optctx, OptionGroup *g)会将g中的参数写入到optctx中。在ffmpeg_parse_options()中和open_files()中都有调用。分别处理octx.global_opts全局参数、octx.groups[GROUP_INFILE]输入文件参数、octx.groups[GROUP_OUTFILE]输出文件参数。(ffmpeg.c源码中关于参数设置的流程_you are trying to apply an input option to an outp_sidumqz的博客-CSDN博客)
此函数就是逐个取出参数调用write_option()。write_option()根据预定好的设置规则。是设置到全局变量,还是到OptionsContext结构体的变量中。了解此规则后,想要知道参数具体是设置到了哪里。只需要参考options定义就好了。但是大家有没有发现这里只处理了。OptionGroup结构体中的Option *opts;这一项。codec_opts,format_opts,resample_opts,sws_dict,swr_opts这些项还没有处理。这些都会在open_input_file()和open_output_file()中用到。这里不做解释。等到分析这两个函数的时候再具体说(ffmpeg.c源码中关于参数设置的流程_you are trying to apply an input option to an outp_sidumqz的博客-CSDN博客)
int parse_optgroup(void *optctx, OptionGroup *g)
{
int i, ret;
av_log(NULL, AV_LOG_DEBUG, "Parsing a group of options: %s %s.\n",
g->group_def->name, g->arg);//在split_commandline->prepare_app_arguments里进行初始化,在split_commandline->add_opt里进行赋值
for (i = 0; i < g->nb_opts; i++) {
Option *o = &g->opts[i];
if (g->group_def->flags &&
!(g->group_def->flags & o->opt->flags)) {
av_log(NULL, AV_LOG_ERROR, "Option %s (%s) cannot be applied to "
"%s %s -- you are trying to apply an input option to an "
"output file or vice versa. Move this option before the "
"file it belongs to.\n", o->key, o->opt->help,
g->group_def->name, g->arg);
return AVERROR(EINVAL);
}
//打印示例:Applying option loglevel (set logging level) with argument debug.
av_log(NULL, AV_LOG_DEBUG, "Applying option %s (%s) with argument %s.\n",
o->key, o->opt->help, o->val);
//o->opt是"const OptionDef options[]"中的内容
//o->key, o->val见上一条日志打印
ret = write_option(optctx, o->opt, o->key, o->val);
if (ret < 0)
return ret;
}
av_log(NULL, AV_LOG_DEBUG, "Successfully parsed a group of options.\n");
return 0;
}
write_option中各个参数可看parse_optgroup中对write_option的调用
write_option
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*/
void *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ?
(uint8_t *)optctx + po->u.off : po->u.dst_ptr; //u.off决定放到optctx中哪个变量里
int *dstcount;
if (po->flags & OPT_SPEC) {
SpecifierOpt **so = dst;
char *p = strchr(opt, ':');
char *str;
dstcount = (int *)(so + 1);
*so = grow_array(*so, sizeof(**so), dstcount, *dstcount + 1);
str = av_strdup(p ? p + 1 : "");
if (!str)
return AVERROR(ENOMEM);
(*so)[*dstcount - 1].specifier = str;
dst = &(*so)[*dstcount - 1].u;
}
if (po->flags & OPT_STRING) {
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) {
*(int *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT_MIN, INT_MAX);
} else if (po->flags & OPT_INT64) {
*(int64_t *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT64_MIN, INT64_MAX);
} else if (po->flags & OPT_TIME) {
*(int64_t *)dst = parse_time_or_die(opt, arg, 1);
} else if (po->flags & OPT_FLOAT) {
*(float *)dst = parse_number_or_die(opt, arg, OPT_FLOAT, -INFINITY, INFINITY);
} else if (po->flags & OPT_DOUBLE) {
*(double *)dst = parse_number_or_die(opt, arg, OPT_DOUBLE, -INFINITY, INFINITY);
} 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)
exit_program(0);
return 0;
}
1、调用方式和传参见“2.1.1 ffmpeg_parse_options”
2、先解析输入参数(parse_optgroup),然后再打开文件。这里open_file是函数指针,实际调用的是open_input_file来打开输入文件
3、每一个输入文件的参数都会在l->groups[i]中,此处通过循环解析第一个输入文件参数再打开第一个输入文件,在解析第二个输入文件参数再打开第二个输入文件
open_files
static int open_files(OptionGroupList *l, const char *inout,
int (*open_file)(OptionsContext*, const char*))
{
int i, ret;
for (i = 0; i < l->nb_groups; i++) {//遍历每一个输入文件
OptionGroup *g = &l->groups[i];
OptionsContext o;
init_options(&o);
o.g = g;
ret = parse_optgroup(&o, g);//g的参数写入o,即将split_commandline解析出来的参数都存放到o中
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error parsing options for %s file "
"%s.\n", inout, g->arg);
uninit_options(&o);
return ret;
}
//Opening an input file: test.mp4.
av_log(NULL, AV_LOG_DEBUG, "Opening an %s file: %s.\n", inout, g->arg);
ret = open_file(&o, g->arg);//从打印日志中可以看出,g->arg是输入文件名称
uninit_options(&o);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error opening %s file %s.\n",
inout, g->arg);
return ret;
}
av_log(NULL, AV_LOG_DEBUG, "Successfully opened the file.\n");
}
return 0;
}
4、打开输入文件,并使参数生效:open_input_file
static int open_input_file(OptionsContext *o, const char *filename)
通过open_files的函数指针调用。open_input_file的第一个参数是命令行传入的参数,第二个参数输入文件。
在open_input_file中查看OptionsContext *o的使用,就可以看到命令行参数的使用以及如何生效的
使参数生效的函数是open_output_file,流程和“打开输入文件”相同,可以看相关章节