【音视频学习笔记】- FFMPEG - 1.命令行参数解析及生效过程

目录

一、文章简介

二、源码阅读

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:输出文件相关参数

2.1 代码位置

fftools/ffmpeg_opt.c

【音视频学习笔记】- FFMPEG - 1.命令行参数解析及生效过程_第1张图片

2.2 主要功能及函数介绍

2.2.1 解析命令行参数及参数生效

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;
}

2.2.2 解析命令行参数

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;
}

2.2.3 处理命令行的全局参数

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;
}

2.2.4 打开输入文件(输入参数生效)

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的使用,就可以看到命令行参数的使用以及如何生效的

2.2.5 打开输出文件(输出参数生效)

使参数生效的函数是open_output_file,流程和“打开输入文件”相同,可以看相关章节

你可能感兴趣的:(ffmpeg,音视频,学习,笔记)