在使用ffmpeg进行转码、推流的各种操作时,会输入一些命令参数,这些参数是如何被解析,如何被存储,以及如何最终被转移到几个最重要的结构变量中去的,通过对其源码的分析,来看看是如何实现的。由于涉及到的模块及函数较多,流程也颇为复杂,故分几篇文章来剖析
ffmpeg 命令参数的格式:
ffmpeg [全局选项] [输入选项] -i [输入源] [输出选项] -f [格式] [输出流及文件]
注:
1:此处的输入源 可以是接收到的实时流数据,也可以是本地文件
2:输入源可以为多个,输出流及文件也可以为多个 ,
3:全局选项可以在任何位置,不局限于最前面
当有多个输入或输出时 其前面的 参数选项也要有多个
例:
ffmpeg -y -ss 30 -t 60 -i AdamAndDog.flv -c copy -f mp4 test_30_60.mp4
-y 为全局选项 表示可以覆盖已有的输出文件
-ss 为输入选项 表示从源文件的第30秒处开始读取
-t 为输入选项 表示要获取60秒的数据(输出文件有60秒的数据),
如果此处为 -to 60 表示到60秒结束(输出文件有30秒的数据)
-i 为输入源 其后 跟着流地址或文件名称及地址
-c 为输出选项 ,值为copy 表示音视频的信息及编码都不改变,
原封不动的copy
-f 为输出选项,值为 mp4 表示输出的文件容器为mp4
特别注意:这里所说的输入选项和 输出选项,并不是固定的 ,比如 -ss、-t 如果放置在 输出流及文件的前面,那么他们就是输出选项了,同样 -f 也可以为 输入选项
例:
ffmpeg -y -f flv -ss 30 -t 60 -i D:\tool\AdamAndDog.flv -c copy -f mp4 test_30_60.mp4
这里把 -f flv 放置了 -i 的前面,那么 -f flv 就是 输入选项了,它在ffmpeg 内部的作用是 ,提前告诉了输入源的文件格式为 flv,就不用单独探测了,直接定位到 flv 文件的读写模块了
(如果输入选项中 没有 -f flv,ffmpeg就需要探测输入源的格式了,同时会打印出下面的日志 Format flv probed with size=2048 and score=100 )
例:
ffmpeg -d -re -i xxxx.flv -c copy -f flv “rtmp://192.168.3.182/live/stream_name1”
-d 为全局选项 表示输出日志及debug 信息
-re 为输入选项 ,其后没有值 ,表示按帧率读取数据
-i 为输入源,其后跟着输入源的文件名及地址
-c 为输出选项 ,值为copy表示音视频的信息及编码都不改变,原封不动的copy
-f 为输出选项,值为 flv 表示 ,以rtmp协议输出
多个输入源的情况
ffmpeg -i 1.mov -i 2.wmv -filter_complex “[0:0] [0:1] [1:0] [1:1] concat=n=2:v=1:a=1 [v] [a]” -map [v] -map [a] output.mp4
当输入各种命令参数后,ffmpeg 会将它们解析到临时的结构变量 OptionParseContext octx中,然后再通过一系列的操作和中间变量 AVDictionary *tmp、 const AVClass *c 、av_format_context_class 中的avformat_options等数据,转移到 AVFormatContext *ic 中,并会释放 OptionParseContext octx 中先前分配的内存
先看一下命令参数是如何被解析到 OptionParseContext octx 中去的
被解析到OptionParseContext 的groups中
下面的函数只保留了本次需要重点了解的语句,其余的代码省略掉了
两个重要的常量定义
options
const OptionDef options[] = {…} //由于过于庞大,这里不再列出
在源文件 ffmpeg_opt.c 中
options 是 OptionDef 类型的数组列表 ,里面有name及所对应的变量等数据,这里面绝大部分变量是对应的OptionsContext 中的变量,也就是说 从外部传入的参数选项 通过与 options 中每个元素的name进行比对,然后存入到相应的变量中**
groups
static const OptionGroupDef groups[] = {
[GROUP_OUTFILE] = { “output url”, NULL, OPT_OUTPUT },
[GROUP_INFILE] = { “input url”, “i”, OPT_INPUT },
};
主要有以下几个函数
ffmpeg_parse_options、
split_commandline、
init_parse_context、
find_option、
add_opt、
finish_group
其中find_option是将命令参数与事先定义好的 options 数组列表进行查找,
根据name 进行查找(比如 -c copy,name为 “c”)
查找出options 数组中哪个OptionDef 元素与之相对应
int main(int argc, char **argv)
{
//此函数是解析命令参数的总函数
ret = ffmpeg_parse_options(argc, argv);
if (ret < 0)
exit_program(1);
}
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 */
//注意此处的两个参数options 和 groups (这两个参数是事先定义好的)
ret = split_commandline(&octx, argc, argv, options, groups,
FF_ARRAY_ELEMS(groups));
}
split_commandline 的功能就是将所有的 参数选项 解析到 OptionParseContext 的变量 octx 中去
octx->groups[0].groups 输出选项的存放位置
octx->groups[1].groups 输入选项的存放位置
在存入octx->groups[0].groups 和octx->groups[1].groups 之前 是通过 add_opt 和 finish_group 两个函数,进行收集及分类的
add_opt功能:
将相同选项类型的参数收集到一起,并存储在octx->cur_group 中(这里的相同选项类型的意思是:同是输入选项或者同是输出选项),
finish_group 功能
将先前收集到一起的参数(目前在octx->cur_group中)转移到相应的 octx->groups[1].groups 或 octx->groups[0].groups 中去
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);
//为octx->groups 分配空间,注意 nb_groups 为 2
//这里之所以会把groups 传进去 注意是 把 groups中的 那两个值
//赋值给 octx->groups[0].group_def = groups[0]
//octx->groups[1].group_def = groups[1]
//这样便于octx后面的操作(主要是将参数选项分开为输入和输出)
init_parse_context(octx, groups, nb_groups);
//以 ffmpeg -re -i xxx.flv -c copy -f flv "rtmp://192.168.3.182/live/streamname1" 为例
while (optindex < argc) {
const char *opt = argv[optindex++], *arg;
const OptionDef *po;
int ret;
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) {
//遇到输出流及文件("rtmp://192.168.3.182/live/streamname1")
//当某个输出文件(可以有多个)前面的 参数全部分析完后,
//就会利用finish_group 函数统一将 针对这个输出文件的
//所有参数数据转移到 octx->groups[0].groups
finish_group(octx, 0, opt);
continue;
}
opt++; //跳过 - ,此时的opt 为 re 或 i 或 c 或 f
#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 */
//opt 为输入源选项,也就是 -i
if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) {
GET_ARG(arg);
//此时的 ret 为 1,arg 为 xxx.flv,
//此函数会将输入源前面的输入选项从octx->cur_group 转移到
//octx->groups[1].groups 中去
finish_group(octx, ret, arg);
continue;
}
/* normal options */
//根据opt 查找出 options 数组列表中所对应的 OptionDef 变量
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 {
arg = "1";
}
//将opt(key) 和 arg(value) 赋值到 octx->cur_group 中(如果不是全局选项的话)
//如果有n 多个选项,
//都是追加到octx->cur_group中去,只到遇到输入源选项或者输出流及文件选项
//(也就是 -i 或 "rtmp://192.168.3.182/live/streamname1")
//遇到输入源选项或输出流及文件时会调用finish_group ,
//将octx->cur_group 数据转移到各自的变量当中去
add_opt(octx, po, opt, arg);
continue;
}
}
return 0;
}
static void add_opt(OptionParseContext *octx, const OptionDef *opt,
const char *key, const char *val)
{
int global = !(opt->flags & (OPT_PERFILE | OPT_SPEC | OPT_OFFSET));
//如果不是全局命令,则暂时存储在octx->cur_group 的 opts 中;
OptionGroup *g = global ? &octx->global_opts : &octx->cur_group;
//追加新的内存空间(注意并不是真正的追加,是重新分配所有的内存)
GROW_ARRAY(g->opts, g->nb_opts);
g->opts[g->nb_opts - 1].opt = opt;
g->opts[g->nb_opts - 1].key = key;
g->opts[g->nb_opts - 1].val = val;
}
static void finish_group(OptionParseContext *octx, int group_idx,
const char *arg)
{
OptionGroupList *l = &octx->groups[group_idx];
OptionGroup *g;
//当有 多个源或者多个输出时,nb_groups 就会有多个了
//GROW_ARRAY 经过一系列的调用,最终执行的是realloc
//如果不知道是怎么追加新内存的,可以查查realloc的原理
//grow_array(array, sizeof(*array), &nb_elems, nb_elems + 1)
GROW_ARRAY(l->groups, l->nb_groups);
g = &l->groups[l->nb_groups - 1];
*g = octx->cur_group;//所有参数数据转移
g->arg = arg;
//把最外层的数据 octx->groups[i].group_def 转移到 内部,便于以后的使用
g->group_def = l->group_def;
g->sws_dict = sws_dict;
g->swr_opts = swr_opts;
g->codec_opts = codec_opts;
g->format_opts = format_opts;
g->resample_opts = resample_opts;
codec_opts = NULL;
format_opts = NULL;
resample_opts = NULL;
sws_dict = NULL;
swr_opts = NULL;
init_opts();
memset(&octx->cur_group, 0, sizeof(octx->cur_group));
}
在split_commandline 函数中 有两处 调用了 finish_group 函数;
注意它们的传入参数:
finish_group(octx, 0, opt); 这里是 转移 输出选项的
还有一处
finish_group(octx, ret, arg); 这是 转移输入选项的。
另外: parse_optgroup 函数是 转移 全局 参数选项的,
转移到了 octx.global_opts 中
这样通过上面的几个函数就把所有的参数存储到了 OptionParseContext 中去了
输入选项 存入到
OptionParseContext 的 groups[1].groups 的 opts 中
输入源信息 存入到 OptionParseContext 的 groups[1].groups 的 arg 中
(注意后面groups也是数组,可以适应多个输入源)
一个是 opts 一个是arg
输出选项 存入到
OptionParseContext 的 groups[0].groups 的 opts 中
输出流及文件信息 存入到 OptionParseContext 的 groups[0].groups 的 arg 中
(注意后面groups也是数组,可以适应多个输出流及文件)
一个是 opts 一个是arg
全局选项 存入到了
OptionParseContext 的 global_opts 中**
这里有必要看一下 这几个结构类型的定义,更有助于理解数据存储的位置
typedef struct Option {
//opt 存储的是上面那个全局变量options 中的某一个元素(便于后续使用)
const OptionDef *opt;
const char *key; // 命令参数的name
const char *val; //命令参数的value
} Option;
typedef struct OptionGroupDef {
/**< group name */
const char *name;
/**
* Option to be used as group separator. Can be NULL for groups which
* are terminated by a non-option argument (e.g. ffmpeg output files)
*/
const char *sep;
/**
* Option flags that must be set on each option that is
* applied to this group
*/
int flags;
} OptionGroupDef;
typedef struct OptionGroup {
//存储的是 上面那个全局变量 groups 中的某一个元素(便于后续使用)
const OptionGroupDef *group_def;
const char *arg; //这里存储的是 输入源信息 或者 输出流及文件
Option *opts; //其实这个是数组列表,存储的是 相同选项类型的 参数(命令)
int nb_opts; //列表个数
//下面这几个字典表会在 将参数数据从OptionsContext 中转移到
//AVFormatContext 中的过程中用到,临时使用
//将所有的参数命令 分别临时存入到 编解码的字典表、
//流格式的字典表,重采样的字典表等等
AVDictionary *codec_opts;
AVDictionary *format_opts;
AVDictionary *resample_opts;
AVDictionary *sws_dict;
AVDictionary *swr_opts;
} OptionGroup;
/**
* A list of option groups that all have the same group type
* (e.g. input files or output files)
*/
typedef struct OptionGroupList {
//存储的是 上面那个全局变量 groups 中的某一个元素(便于后续使用)
const OptionGroupDef *group_def;
//这里其实是数组列表, 当输入源 或者输出流及文件有多个时,数组列表有很有意义了
OptionGroup *groups;
int nb_groups;
} OptionGroupList;
typedef struct OptionParseContext {
OptionGroup global_opts; //存储的是全局参数
OptionGroupList *groups; //这里其实是数组列表,只有两个元素:输入和输出
int nb_groups; //它的值为 2
/* parsing state */
OptionGroup cur_group; //在解析参数时,作为临时变量使用
} OptionParseContext;
从上面的定义来看,到目前为止,命令参数数据实际上存储在了OptionGroup 类型的数组列表中的 arg 和 *opts 中了。 下一篇的 分析,会把这里存储的数据转移到
OptionsContext 变量中。