前言

最近在做一些音视频畛域的工作,这个畛域根本绕不开 FFmpeg ,因而想对其源码进行一些钻研,站着这个伟人的肩膀上,学习一下其设计思维以及实现思路,FFmpeg 很多人最开始接触的应该都是它的命令和 ffplay ,这篇咱们就先剖析下 ffmpeg 命令 的实现。

感兴趣的能够关注下nilstorm与gezilinll

从问题登程

  • FFmpeg的命令构造是什么样的
  • 怎么实现任意性能、配置的随便组装的
  • 倒放这种非线性的状况是怎么解决的
  • 当不须要转码相似只须要转封装的话是如何解决的

命令构造

ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...

流程揣测

  1. 应该要有一个数据层,依照输出参数进行数据解析
  2. 将解码、转码、性能解决、编码四个模块形象离开
  3. 按第一个环节数据解析的构造组装上述四个模块,造成一条或多条责任链的成果
  4. 开始解决

过程中应该有很多分支判断、参数设置之类的状况,在这里都不思考,咱们先只剖析主链路。

主流程

首先 ffmpeg 命令的入口在于 fftools/ffmpeg.cmain 函数上,先从这个函数来看下整体的流程,代码用的是 ffmpeg 4.4 的版本,同时会省略一些无关紧要的内容:

int main(int argc, char **argv){      /* 一些前置的初始化动作 */        ...    /* 数据解析函数,不过在这个函数外面还会去开启输出/输入文件流等解决 */    ret = ffmpeg_parse_options(argc, argv);        /* 一些数据判断、开启基准测试之类的解决 */         ...       /* 开始做理论的文件转换,即按命令开始解决了 */      if (transcode() < 0)        ...          /* 完结了,统计耗时等等 */    ...}

从上述代码能够看到外围的环节就是两局部:数据解析 + 文件转换,接下来咱们将针对这两局部再深刻进去看看。

接下去咱们剖析过程中应用的命令如下:

ffmpeg -i douyin_700x1240.mp4 -vf reverse -af areverse reversed.mp4

上述命令是将一个 douyin_700x1240.mp4 这个视频中的视频流于音频流同时倒放,生成 reversed.mp4

数据解析

在剖析 ffmpeg_parse_options 函数之前,咱们先介绍下 FFmpeg 命令中参数配置相干的几个次要的构造体:

OptionParseContext

typedef struct OptionParseContext {    OptionGroup global_opts; // 全局配置参数集    OptionGroupList *groups; // 输入/输出配置参数集    int           nb_groups; // groups的长度    /* parsing state */    OptionGroup cur_group; // 上面解说到 add_opt 的时候会提到} OptionParseContext;

该对象用来存储被解析后的数据,全局参数,输入输出参数等。

OptionGroupList

/** * A list of option groups that all have the same group type * (e.g. input files or output files) */typedef struct OptionGroupList {    const OptionGroupDef *group_def;    OptionGroup *groups;    int       nb_groups;} OptionGroupList;

下面的正文曾经解释得比较清楚了。

OptionGroup

typedef struct OptionGroup {    const OptionGroupDef *group_def;    const char *arg;    Option *opts;    int  nb_opts;    AVDictionary *codec_opts;    AVDictionary *format_opts;    AVDictionary *resample_opts;    AVDictionary *sws_dict;    AVDictionary *swr_opts;} OptionGroup;

这里就是各类参数的存储地了,再往下就是单个配置参数的存储地 Option 了。

Option

/** * An option extracted from the commandline. * Cannot use AVDictionary because of options like -map which can be * used multiple times. */typedef struct Option {    const OptionDef  *opt;    const char       *key;    const char       *val;} Option;

就是配置的键值对和定义标准了。

OptionDef / OptionGroupDef

这里就是各种参数的标准定义了,比方 OptionGroupDef :

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;

另外 ffmpeg 反对的配置列表在 ffmpeg_opt.c中:

#define OFFSET(x) offsetof(OptionsContext, x)const OptionDef options[] = {    /* main options */    CMDUTILS_COMMON_OPTIONS    { "f",              HAS_ARG | OPT_STRING | OPT_OFFSET |                        OPT_INPUT | OPT_OUTPUT,                      { .off       = OFFSET(format) },        "force format", "fmt" },    { "y",              OPT_BOOL,                                    {              &file_overwrite },    .......

ffmpeg_parse_options

当初开始让咱们看下解析函数:

int ffmpeg_parse_options(int argc, char **argv){    ......    /* 将命令行的参数进行拆分,将拆分后果分类并设置到不同的数据结构中 */    ret = split_commandline(&octx, argc, argv, options, groups,                            FF_ARRAY_ELEMS(groups));    ......    /* 这里解析全局的配置参数,咱们这个范例外面没有间接跳过,跟下面的解析次要是细节解决上的不同,不赘述 */    ret = parse_optgroup(NULL, &octx.global_opts);    if (ret < 0) {        av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: ");        goto fail;    }    /* 开启配置中的所有输出文件流 */    /* open_input_file 接口内如果有配置文件的起始解决工夫的话就会去做 Seek 操作 */    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;    }    /* 如果参数中有设置滤镜相干的话则进行初始化,咱们这里不波及,就先略过 */    ret = init_complex_filters();    if (ret < 0) {        av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");        goto fail;    }    /* 开启输入文件流 */    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;    }    ......}

在下面的代码中,咱们重点关注下 split_commandline ,其余函数按正文晓得其作用即可,大多是一些惯例逻辑上的内容,不同的业务场景可能有不同的设计思路,不影响咱们以后的剖析:

int split_commandline(OptionParseContext *octx, int argc, char *argv[],                      const OptionDef *options,                      const OptionGroupDef *groups, int nb_groups){    ...    /* 初始化 OptionParseContext 内全局、输出、输入参数的数据对象,包含分配内存等 */    init_parse_context(octx, groups, nb_groups);    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);                /* --xxx的跳过 */        if (opt[0] == '-' && opt[1] == '-' && !opt[2]) {            dashdash = optindex;            continue;        }        /* 参数类型为不带 - 或者 第二个字符是空的 比方 reversed.mp4,匹配到了就跳到下一个参数 */        if (opt[0] != '-' || !opt[1] || dashdash+1 == optindex) {            /* 这里就是找到对应的 OptionGroup 对其中的参数进行赋值 */            finish_group(octx, 0, opt);            av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name);            continue;        }        opt++;                ......        /* 匹配以后参数是否-i的配置,并获取其后的参数如douyin_700x1240.mp4,匹配到了就跳到下一个参数 */        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;        }        /* 匹配以后参数是否是之前提到的 options 中定义的参数,如咱们范例的 -vf */        po = find_option(options, opt);        /* po->name 就是 vf */        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";            }                        /* 将解析到的参数配置设置到输出或输入Group的数据结构外面 */            /* 依据options中预存的flag判断之后,负责把参数放入octx->cur_group或者global_opts中,目前从代码上理解到的,               这个 cur_group 就寄存不归属下面的几种条件的数据,最初做下提醒,因为对开篇提到的问题没啥影响,先略过吧。 */            add_opt(octx, po, opt, arg);             av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with "                   "argument '%s'.\n", po->name, po->help, arg);            continue;        }        /* 接下去就是其余一些类型数据的匹配、解析、设置了,比方-nofoo等,不再赘述 */        ...        /* 匹配失败的参数就会报错,就咱们常见的谬误提醒 */        av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt);        return AVERROR_OPTION_NOT_FOUND;    }    ...}

解析后果

通过下面这样一轮解析后, ffmpeg -i douyin_700x1240.mp4 -vf reverse -af areverse reversed.mp4 这样一行命令的数据将按如下构造寄存:

流程图

文件转换

剖析完参数解析就该看接下来文件转换是如何解决的了,这里咱们次要剖析 transcode 这个函数的逻辑,并通过这个函数扩大一下其余相干的局部:

/* * The following code is the main loop of the file converter */static int transcode(void){    int ret, i;    AVFormatContext *os;    OutputStream *ost;    InputStream *ist;    int64_t timer_start;    int64_t total_packets_written = 0;      /* 在这个接口外面会去做一堆的比方帧率计算、缓冲计算、编解码器初始化等操作,最初对所有的输入文件进行写文件头的操作,实现初始化 */    ret = transcode_init();          ......    /* 未接管到进行信号的话就继续进行转换解决,直到实现或接管到进行信号 */    while (!received_sigterm) {        ......        /* 这里就是单步转换操作了,前面会进入代码外部近一步介绍 */        ret = transcode_step();        ...    }    ......    /* 把解码缓冲区的残余数据再解决一下而后 flush 筹备出工 */    for (i = 0; i < nb_input_streams; i++) {        ist = input_streams[i];        if (!input_files[ist->file_index]->eof_reached) {            process_input_packet(ist, NULL, 0);        }    }    flush_encoders();    term_exit();    /* 写一下文件尾 */    for (i = 0; i < nb_output_files; i++) {        os = output_files[i]->ctx;        if (!output_files[i]->header_written) {            av_log(NULL, AV_LOG_ERROR,                   "Nothing was written into output file %d (%s), because "                   "at least one of its streams received no packets.\n",                   i, os->url);            continue;        }        if ((ret = av_write_trailer(os)) < 0) {            av_log(NULL, AV_LOG_ERROR, "Error writing trailer of %s: %s\n", os->url, av_err2str(ret));            if (exit_on_error)                exit_program(1);        }    }      /* 接下去就是一些敞开 codec 呀敞开 file 呀之类的操作了 */    ......          return ret;}

上述代码能够看到,外围的局部就在于 transcode_step 外面:

/** * Run a single step of transcoding. * * @return  0 for success, <0 for error */static int transcode_step(void){    OutputStream *ost;    InputStream  *ist = NULL;    int ret;      /* 通过比照以后编码的pts,挑选出最小值的输入流 */    ost = choose_output();          ......    if (ost->filter && ost->filter->graph->graph) {                ......        /* 这里就是依照输入配置的 filter 链路对下一步的 process_input 输入帧做解决了,当进来这里时则可能通过其外部的 reap_filters 进行编码而不是前面的 reap_filters */          if ((ret = transcode_from_filter(ost->filter->graph, &ist)) < 0)            return ret;        if (!ist)            return 0;    } else if (ost->filter) {        int i;        for (i = 0; i < ost->filter->graph->nb_inputs; i++) {            InputFilter *ifilter = ost->filter->graph->inputs[i];            if (!ifilter->ist->got_output && !input_files[ifilter->ist->file_index]->eof_reached) {                ist = ifilter->ist;                break;            }        }        if (!ist) {            ost->inputs_done = 1;            return 0;        }    } else {        av_assert0(ost->source_index >= 0);        ist = input_streams[ost->source_index];    }      /* 读取一个 AVPacket 当然也包含了一些pts校验计算等等事件,外部先在 get_input_packet 接口中通过 av_read_frame 获取到Packet */      /* 接着调用 process_input_packet --> decode_audio/decode_video 从而实现解码并将解码帧放入外部数据队列中 */    /* 解码后的数据会通过 send_frame_to_filters 做一下 FilterGraph 的初始化和配置,以及通过 av_buffersrc_add_frame 将解码后的 AVFrame 送入 AVFilterContext */    ret = process_input(ist->file_index);    if (ret == AVERROR(EAGAIN)) {        if (input_files[ist->file_index]->eagain)            ost->unavailable = 1;        return 0;    }    if (ret < 0)        return ret == AVERROR_EOF ? 0 : ret;      /* 依据滤波器做滤波解决,并把解决完的音视频编码到输入文件中 */      /* 外部通过调用 do_video_out/do_audio_out 最初在 write_packet 中执行编码操作 */    return reap_filters(0);}

transcode_step 整体的流程就是下面代码中介绍的,不过咱们范例中的例子是一个倒放的性能,它在\最初编码的时候并不会走最上面的 reap_filters 而是走 transcode_from_filter 中的 reap_filters ,因为咱们是用 -vf reverse 这个 filter 来解决输入的。 ffmpeg 这个 filter 解决的逻辑会期待解码数据齐全筹备好了,再倒序进行编码,因而会造成较大的内存压力,特地是挪动端上。

如果咱们命令是抽音频流,比方 ffmpeg -i douyin_700x1240.mp4 audio.aac,那么走的就是最上面的 reap_filters 了。这里能够多提一下就是咱们刚开始说的抽流的状况,当不须要又一次编码的话那么 ffmpeg 命令在 process_intput_packet 中将间接间接调用 do_streamcopy

static int process_input_packet(InputStream *ist, const AVPacket *pkt, int no_eof){    ......          for (i = 0; i < nb_output_streams; i++) {        OutputStream *ost = output_streams[i];        if (!ost->pkt && !(ost->pkt = av_packet_alloc()))            exit_program(1);        if (!check_output_constraints(ist, ost) || ost->encoding_needed)            continue;        do_streamcopy(ist, ost, pkt);    }    return !eof_reached;}

最初再思考倒放这个范例的最初一个问题,从代码逻辑来看真正能开始执行编码的条件简略来说有两个,一个是 ost->filter->graph->graph 非空,另一个就是 transcode_from_filter 外部的 avfilter_graph_request_oldest 返回值 >=0 ,那么什么状况下这两个状态能满足呢?不过局部目前我还没去深入研究,留着对 AVFilter 模块的学习时再一起收尾,这里先晓得是这样一个逻辑就行了。

流程图

总结

至此咱们对 ffmpeg 命令实现的主流程做了一次剖析,整体实现的思路跟咱们一开始预测的还是有些靠近的,不过 ffmpeg 的组装次要还是通过其 AVFilter 及相干的模块来解决的,其 FilterGraph 应该就相当于一个更简单的责任链,不过说归说,理论看其落地代码当中还是无数之不尽的细节和须要解决的问题的,前面的 FFmpeg 源码世界的其余章节将再做更进一步的剖析学习。