本文 以 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:v
跟 0: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。