ffmpeg 源码分析 命令参数篇(一)

在使用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 变量中。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值