ffmpeg命令分析-filter_complex - 弦外之音

/ 0评 / 0

本文 以 ffmpeg4.4 源码为准。

a.mp4下载链接:百度网盘,提取码:nl0s 。logo.jpg 地址:点击查看


命令如下:

ffmpeg.exe -i a.mp4 -i logo.jpg -filter_complex "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" output.mp4 -y

上面命令实现的功能就是 把 "弦外之音" 的 logo 放在视频左上角。

ffmpeg 命令行有两种 filter 用法:

1,-vf,普通滤镜, 在 《ffmpeg命令分析-vf》有过讲解。

什么是简单滤镜?只有一个输入流是简单滤镜

2,-filter_complex-lavfi 这两个命令参数是一样的,这是复杂滤镜,lavfi 是估计是 libavfilter 的缩写。

什么是复杂滤镜?有多个输入流的就是复杂滤镜,本文命令有2个输入流,属于复杂滤镜

复杂滤镜 就是本文的分析重点。

首先 filter_complex 在 ffmpeg_opt.c 的定义如下:

{ "filter_complex", HAS_ARG | OPT_EXPERT,                        { .func_arg = opt_filter_complex },
        "create a complex filtergraph", "graph_description" }

从定义可以看出 filter_complex 会调用 opt_filter_complex 函数。

opt_filter_complex 函数的定义如下:

static int opt_filter_complex(void *optctx, const char *opt, const char *arg)
{
    GROW_ARRAY(filtergraphs, nb_filtergraphs);
    if (!(filtergraphs[nb_filtergraphs - 1] = av_mallocz(sizeof(*filtergraphs[0]))))
        return AVERROR(ENOMEM);
    filtergraphs[nb_filtergraphs - 1]->index      = nb_filtergraphs - 1;
    filtergraphs[nb_filtergraphs - 1]->graph_desc = av_strdup(arg);
    if (!filtergraphs[nb_filtergraphs - 1]->graph_desc)
        return AVERROR(ENOMEM);
​
    input_stream_potentially_available = 1;
​
    return 0;
}

从上面的代码可以看出, opt_filter_complex 做的事情非常简单,就是 malloc 一个 struct FilterGraph,然后放进行 全局变量 filtergraphs 里面。

-filter_complex 后面的参数字符串 "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" 就被放进行 graph_desc 进行保存。

ffmpeg 会通过 graph_desc 这个参数判断这个 FilterGraph 是不是一个复杂 FilterGraph ,通过 filtergraph_is_simple() 函数实现。



在之前文章 《ffmpeg源码分析-open_output_file》里,我们知道 init_simple_filtergraph函数 是在 open_output_file 里面执行的。

init_simple_filtergraph 是初始化 简单filter。init_complex_filters 是初始化 复杂filter,他们之间的调用流程如下:

流程图如下:

从上面流程图 可以看出 ,init_complex_filters 是在 init_simple_filtergraph 之前执行的,如果执行了 init_complex_filters 就不会执行 init_simple_filtergraph,只有一个会执行,例如 视频 滤镜用了 init_complex_filters,就不会执行 init_simple_filtergraph,但是本命令中 音频 会用 init_simple_filtergraph 初始化滤镜。

下面仔细分析 init_complex_filters 的逻辑。

init_complex_filters 函数代码如下:

static int init_complex_filters(void)
{
    int i, ret = 0;
​
    for (i = 0; i < nb_filtergraphs; i++) {
        ret = init_complex_filtergraph(filtergraphs[i]);
        if (ret < 0)
            return ret;
    }
    return 0;
}

这个函数比较简单,就是循环执行 init_complex_filtergraph,本文命令只有一个 复杂 filter,所以只能循环一次,这里的循环其实为了处理那种很复杂的filter的。


接着分析 init_complex_filtergraph 函数的逻辑,重点如下:

从上图可以看到,init_complex_filtergraph() 函数里面 调 avfilter_graph_parse2() 来解析 "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0"

这里注意 第三个参数 inputs 变量是一个 struct AVFilterInOut 数组 ,从 debug 器可以看出 inputs 数组有两个值,1:v0:v,跟命令行参数是对得上的。

代表这个 filter-graph 有两个输入流,这里跟以往的文章分析不同,以前的filter文章分析都只讲了一个输入流的情况。


接着 分析 init_input_filter() 函数里面做了什么事情,流程图如下:

init_input_filter 的代码有点长,只贴部分重点代码进行讲解。

从流程图跟代码中可以分析出来,init_input_filter 函数前半部分都是为了找出 ist,ist 是一个 struct InputStream,放在全局变量 input_streams 里面。

重点代码如下:

//找出 文件 ctx
s = input_files[file_idx]->ctx;
​
for (i = 0; i < s->nb_streams; i++) {
    enum AVMediaType stream_type = s->streams[i]->codecpar->codec_type;
    if (stream_type != type &&
        !(stream_type == AVMEDIA_TYPE_SUBTITLE &&
          type == AVMEDIA_TYPE_VIDEO /* sub2video hack */))
        continue;
    if (check_stream_specifier(s, s->streams[i], *p == ':' ? p + 1 : p) == 1) {
        //找出 指定流,重点
        st = s->streams[i];
        break;
    }
}
​
//找出 InputStream
ist = input_streams[input_files[file_idx]->ist_index + st->index];

init_input_filter 函数一开始 就会把 in->name 进行提取,本命令中 in->name 是 1:v ,它的逻辑会把 1 提取出来 赋值给 file_idx ,因为下标是0开始的,所以这里的 1 是指定第二个文件,然后把 ":v" 也提取出来,用 check_stream_specifier 来获取到指定的流,v 代表是视频流,取文件的第一个视频流 赋值给 st,如果文件有多个视频流,只取第一个视频流,其他不管。

所以 命令行参数 中的 [1:v] ,就是指定第二个输入文件 的 第一个视频流 。

最后通过 st = input_streams[input_files[file_idx]->ist_index + st->index] 获取的 InputStream。

继续分析,后面就是添加以及初始化 fg->inputs,代码如下:

GROW_ARRAY(fg->inputs, fg->nb_inputs);
if (!(fg->inputs[fg->nb_inputs - 1] = av_mallocz(sizeof(*fg->inputs[0]))))
    exit_program(1);
//重点代码
fg->inputs[fg->nb_inputs - 1]->ist   = ist;
fg->inputs[fg->nb_inputs - 1]->graph = fg;
fg->inputs[fg->nb_inputs - 1]->format = -1;
fg->inputs[fg->nb_inputs - 1]->type = ist->st->codecpar->codec_type;
fg->inputs[fg->nb_inputs - 1]->name = describe_filter_link(fg, in, 1);
​
fg->inputs[fg->nb_inputs - 1]->frame_queue = av_fifo_alloc(8 * sizeof(AVFrame*));
if (!fg->inputs[fg->nb_inputs - 1]->frame_queue)
    exit_program(1);

上面这段代码有三个重点:

1,这个 filter 有两个输入流,但是并没有先后顺序的区分,没有字段存储哪个是0,哪个是1。

2,fg->inputs[fg->nb_inputs - 1]->ist = ist; 这句是重点代码。这里关联了 InputFilter 跟 InputStream。

3,strcut InputFilter 里面的 frame_queue 是一个 AVFrame 的临时存储区,为什么要临时存储,是因为 FilterGraph 里面的所有 InputFilter 都初始化完成才能 往 某个filter 里面写 AVframe ,ffmpeg 是这样判断 InputFilter是否初始化完成的,InputFilter::format 不等于 -1 就是 初始化完成了。具体实现在 ifilter_has_all_input_formats() 函数里。如果 A InputFilter 初始化完成了,B InputFilter 没初始化完成,就不会往 A 的 InputFilter::filter 写数据,而是先写到 A 的 InputFilter::frame_queue,后面再从 InputFilter::frame_queue 里拿出来,写到 InputFilter::filter。部分代码如下:

//ffmpeg.c 2213行
if (!ifilter_has_all_input_formats(fg)) {
    AVFrame *tmp = av_frame_clone(frame);
    if (!tmp)
        return AVERROR(ENOMEM);
    av_frame_unref(frame);
​
    if (!av_fifo_space(ifilter->frame_queue)) {
        ret = av_fifo_realloc2(ifilter->frame_queue, 2 * av_fifo_size(ifilter->frame_queue));
        if (ret < 0) {
            av_frame_free(&tmp);
            return ret;
        }
    }
    av_fifo_generic_write(ifilter->frame_queue, &tmp, sizeof(tmp), NULL);
    return 0;
}

PS:上面这段代码的命令场景我也没有,具体什么样的命令会跑上面这种临时存储逻辑,埋个坑,后续填,有朋友知道的,可以在文章评价留意。

init_input_filter 函数含有一个重点,最后两句代码如下:

GROW_ARRAY(ist->filters, ist->nb_filters);
ist->filters[ist->nb_filters - 1] = fg->inputs[fg->nb_inputs - 1];

这里是把 InputStream 里面的 filters 同步了,为什么这样做我也不太清楚,想记一下,应该有地方用到。

至此,init_input_filter 函数分析完毕,接着分析上层函数 init_complex_filtergraph 后面的逻辑,代码如下:

for (cur = outputs; cur;) {
    GROW_ARRAY(fg->outputs, fg->nb_outputs);
    fg->outputs[fg->nb_outputs - 1] = av_mallocz(sizeof(*fg->outputs[0]));
    if (!fg->outputs[fg->nb_outputs - 1])
        exit_program(1);
​
    fg->outputs[fg->nb_outputs - 1]->graph   = fg;
    fg->outputs[fg->nb_outputs - 1]->out_tmp = cur;
    fg->outputs[fg->nb_outputs - 1]->type    = avfilter_pad_get_type(cur->filter_ctx->output_pads,
                                                                     cur->pad_idx);
    fg->outputs[fg->nb_outputs - 1]->name = describe_filter_link(fg, cur, 0);
    cur = cur->next;
    fg->outputs[fg->nb_outputs - 1]->out_tmp->next = NULL;
}

上面的代码实际上 就是操作处理 fg->outputs,本文的命令 outputs 数组只有一个值,所以只会循环一次。这段代码比较易懂,不需要做太多分析。

至此,init_complex_filtergraph 函数分析完毕。



从之前的流程图可以看出,init_complex_filters 是在 init_simple_filtergraph 前面调用的,这里要着重讲解一下 init_complex_filters 跟 init_simple_filtergraph 的区别。

init_complex_filters 是用来初始化复杂滤镜的,什么是复杂滤镜?有多个输入流的就是复杂滤镜。

init_simple_filtergraph 是用来初始化简单滤镜的,什么是简单滤镜,只有一个输入流就是简单滤镜。

对于视频而言,如果用了 init_complex_filters 来初始化滤镜,代码 就不会执行 init_simple_filtergraph ,两者只有一个执行。

例如,本命令中,视频滤镜是用 init_complex_filters 实现,音频滤镜是调 init_simple_filtergraph 实现,代码如下:

/* create streams for all unlabeled output pads */
for (i = 0; i < nb_filtergraphs; i++) {
    FilterGraph *fg = filtergraphs[i];
    for (j = 0; j < fg->nb_outputs; j++) {
        OutputFilter *ofilter = fg->outputs[j];
​
        if (!ofilter->out_tmp || ofilter->out_tmp->name)
            continue;
​
        switch (ofilter->type) {
        case AVMEDIA_TYPE_VIDEO:    o->video_disable    = 1; break;
        case AVMEDIA_TYPE_AUDIO:    o->audio_disable    = 1; break;
        case AVMEDIA_TYPE_SUBTITLE: o->subtitle_disable = 1; break;
        }
        init_output_filter(ofilter, o, oc);
    }
}

上面的代码会把 o->video_disable 设为 1,导致 init_simple_filtergraph 没有执行。

这里说个重点,因为 视频输出流 对应多个输入流,如下图,所以 ost->source_index 会在 init_output_filter 函数里面设置为 -1,因为不是单个输入流。

一开始解析复杂滤镜参数的时候,已经往 全局变量 filtergraphs 数组 插入了一个 FilterGraph ,然后在 open_output_file 函数里面处理音频时,执行了 init_simple_filtergraph ,又插入了一个 FilterGraph 。所以现在数据结构如下图所示:

PS:init_simple_filtergraph 在《ffmpeg源码分析-open_output_file》有讲解。本命令里,音频滤镜是一个空的FilterGraph。

注意上面的结构图,复杂 filter 是有 nb_input 等于 2,代表有两个输入流的。

执行 init_complex_filters 跟 init_simple_filtergraph 初始化 简单跟复杂的 filtergraph 之后, 后面会执行 configure_filtergraph() 函数,下面就来分析 configure_filtergraph 在本命令中的逻辑,代码如下图所示:重点已圈出。

if (simple) {
    //简单 filter 处理 省略
} else {
    fg->graph->nb_threads = filter_complex_nbthreads;
}

从上的代码可以看出, configure_filtergraph 入口一开始就是对 简单跟复杂 filter 的区别处理,在这里复杂滤镜 的逻辑比较简单。

然后就又执行了 avfilter_graph_parse2 ,我为什么说 “又”,大家注意看,在开始的时候 init_complex_filtergraph 函数里面已经执行过一次 avfilter_graph_parse2 。对于复杂 filter 而已,这个概念是重中之重。

复杂滤镜 之所以 会执行两次 avfilter_graph_parse2 ,不是因为写错代码,而是有必要的。

第一次 avfilter_graph_parse2 是为了弄出来 FilterGraph::InputFilter 跟 FilterGraph::OutputFilter,把这两个东西弄好。

第二次 avfilter_graph_parse2 是为了 给后面的 configure_input_filter (第三个红圈)用。

第二次 avfilter_graph_parse2 是 简单滤镜 跟 复杂滤镜通用的,所以他执行两次,实际上是为了通用,我们如果调 API 函数,即使是复杂滤镜,也可以只调一次avfilter_graph_parse2 搞定。

还有一个重点,代码如下:

for (cur = inputs, i = 0; cur; cur = cur->next, i++)
    if ((ret = configure_input_filter(fg, fg->inputs[i], cur)) < 0) {
        avfilter_inout_free(&inputs);
        avfilter_inout_free(&outputs);
        goto fail;
    }

上面的 for 循环会循环两次,他没有用 下标之类的定位,是因为 两次 avfilter_graph_parse2 返回的 inputs 数组顺序都是一样的。

configure_input_filter 函数的重点代码如下:

if ((ret = avfilter_graph_create_filter(&ifilter->filter, buffer_filt, name,
                                        args.str, NULL, fg->graph)) < 0)
    goto fail;

把 ifilter->filter 初始化为 buffer filter (入口 filter),然后插入一些默认的 filter,例如 trim filter,最后 关联 到传进来的 cur 变量。

configure_filtergraph 函数分析完毕。


可以看到 复杂滤镜 在 ffmpeg.c 的实现确实比较复杂。但是在 API 函数的角度看来,复杂滤镜 跟 简单滤镜使用的 api 函数是一样的,都是用 avfilter_graph_parse2 。

复杂滤镜里面的复杂性是为了 命令行参数更 易用一些。

复杂滤镜 第一次调 avfilter_graph_parse2 是为了处理好 filter 跟 stream 的关联,在程序里写明,某个 InputStream 需要发往 某个 InputFilter 。

第二次调 avfilter_graph_parse2 才真正开始关联 InputFilter 跟 OutputFilter ,因为中间可能需要插入一些其他 filter,例如 trim filter,rotate filter 等等。

如果 自己调 api 函数, 我们代码里,哪个 InputStream 需要发往 哪个 InputFilter 已经确定了,不需要通过命令参数改变,就可以调一次 avfilter_graph_parse2 ,就搞定了,然后从不同的流读AVFrame,然后 往不同的 buffer filter发送 AVFrame 即可。


版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注