init_complex_filtergraph初始化复杂滤镜—ffmpeg.c源码分析
作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音
ffmpeg.exe
的滤镜分为两种,简单滤镜 和 复杂滤镜。简单滤镜已经在 FFmpeg转换器分析-基础篇 一章的《init_simple_filtergraph初始化简单滤镜》《configure_filtergraph配置滤镜容器》文章中讲解过了。
现在来重温一下简单滤镜的知识,简单滤镜是指命令行没有使用 -filter_complex
选项,如下:
ffmpeg -i juren.mp4 juren.flv
ffmpeg -i juren.mp4 -vf "null" juren.flv
上面两条命令是等价的,即便你命令行没有使用 -vf
指定视频滤镜,在 ffmpeg.c
内部也会创建一个 null
的滤镜把输入输出连接起来。
而复杂滤镜的命令行示例如下:
ffmpeg -i juren-30s.mp4 -i logo.jpg -filter_complex "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" output.mp4 -y
效果图如下:
本文主要讲解 -filter_complex
在 ffmpeg.c
里面的实现。从下面的定义可以看出解析 filter_complex
参数会调用 opt_filter_complex()
函数。
opt_filter_complex()
函数的定义如下:
opt_filter_complex()
函数的代码量比较少,首先用 GROW_ARRAY()
宏函数动态扩容一下 filtergraphs
数组,GROW_ARRAY()
宏函数可以把数组的容量加1
然后用 av_mallocz()
一个 struct FilterGraph
结构体的内存,放进去 filtergraphs
数组。而-filter_complex
后面的参数字符串 "[1:v]scale=176:144[logo]..."
会被存储在 graph_desc
字段。
这里有一个重点就是 graph_desc
字段,这个字段只有是复杂滤镜才会有值,如果是简单滤镜,这个字段是 NULL。
那简单滤镜 -vf
后面的字符串存储在哪里的呢?
ffmpeg -i juren.mp4 -vf "split[main][tmp];[tmp]crop=iw:ih/2:0:0,vflip[flip];[main][flip]overlay=0:H/2" out.mp4
答:绑定在 OutputStream
的 filters
字段里。具体请阅读《FFmpeg命令参数分析-vf》
其实 简单滤镜 与 复杂滤镜,是非常相似的,因为他们共用一套数据结构,struct FilterGraph
,InputFilter
,OutputFilter
,InputStream
,OutputStream
。
简单滤镜场景下,只有一个 InputFilter
(输入),一个 OutputFilter
(输出)。
复杂滤镜场景下,可以有一个,也可以有多个 InputFilter
(输入),多个 OutputFilter
(输出)。
InputFilter
最后是跟 InputStream
绑定在一起,而 OutputFilter
跟 OutputStream
绑定。
简单滤镜 与 复杂滤镜都是调 configure_filtergraph()
来正式打开滤镜容器的,但是他们的初始化函数不一样,以及初始化的位置也不一样。
简单滤镜 的初始化函数是 init_simple_filtergraph()
,复杂滤镜的初始化函数是 init_complex_filtergraph()
,他们在函数调用中的位置如下:
从上面流程图 可以看出 ,init_complex_filters()
是在 init_simple_filtergraph()
之前执行的。
整个逻辑是这样的,如果执行了 init_complex_filters()
就不会执行 init_simple_filtergraph()
,反之亦然。
init_complex_filters()
实际上是对 init_complex_filter()
函数进行了一层包装,所以直接讲 init_complex_filter()
函数的实现了,流程图如下:
建议读者用 ubuntu + clion 调试下面这条命令,配合着阅读本文更容易理解。
ffmpeg -i juren-30s.mp4 -i logo.jpg -filter_complex "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" output.mp4 -y
juren-30s.mp4
可点击 此链接 下载,logo.jpg
可点击 此链接 下载。
上图中,avfilter_graph_parse2()
会解析滤镜字符串 [1:v]scale=176:144...
,生成对应的数据结构,也可以说是开放了 输入输出的 AVFilter
给外部进行连接,因为后面可能还会插入 trim
时长裁剪滤镜,等等。命令行用了 -t 10
就会插入裁剪滤镜。
输入输出的 AVFilter
就是 AVFilterInOut *inputs, *outputs
,不过在 init_complex_filtergraph()
里面还没开始连接 inputs
跟 outputs
,这里只是提取了里面的 name
字段的值 [1:v]
来确认输入流。
真正连接 inputs
跟 outputs
是在 configure_filtergraph()
函数里,这个后面会提到。
init_complex_filtergraph()
函数的重点是调用了 init_input_filter()
来绑定 InputFilter
跟 InputStream
,下面我们来学习一下它是如何绑定的。
init_input_filter()
第一步,就是根据 in->name
来确认应该由哪个输入流来输入数据给这个 InputFilter
。
在此刻,in->name
等于 1:v
,这个 1 是 input_files[]
数组的索引,代表第二个输入文件。后面的 v
代表这个输入文件里面的视频流,如果文件有多个视频流,只匹配第一个视频流,其他不管。
找到输入流 ist
之后,就开始设置相关的属性,如下:
ist->discard = 0;
ist->decoding_needed |= DECODING_FOR_FILTER;
ist->st->discard = AVDISCARD_NONE;
这里比较重点,在 open_input_file()
打开输入文件的时候,所有的输入流的 discard
都是 1,都是丢弃的状态,只有在被需要的时候,输入流才会启用。同理,所有输入流默认是不需要解码的,只有 decoding_needed
被设置成非零才会进行解码操作。
接下来就会申请 InputFilter
的内存,然后绑定 InputFilter
跟 InputStream
,这两个数据结构是双向绑定的,代码如下:
InputFilter
只会关联一个 InputStream
,代表这个滤镜的数据是由那个输入流提供的。而 InputStream
会关联多个 InputFilter
,代表这个输入流的数据需要发送给哪些滤镜。
提醒:上图中把 InputFilter
里面的 frame_queue
初始化为 8 个 AVFrame
大小,这是缓存队列。在未调 avfilter_graph_config()
函数正式打开滤镜容器之前,发送给滤镜的数据都会先缓存到 frame_queue
队列里面,等到打开了滤镜容器后,就需要把他们全部送给滤镜容器。
因为需要所有的输入流都解码出数据,才会正式打开滤镜容器,在有多个输入流的时候,有些输入流的数据是快一点解码出来的,解码出来就要先缓存在 frame_queue
里面。
InputFilter
跟 InputStream
绑定后的数据结构如下:
到这里,init_input_filter()
函数就讲解完毕了,做个小总结, init_input_filter()
的职责就是绑定 InputFilter
跟 InputStream
。
现在接着看回去 init_complex_filtergraph()
函数后面的代码,如下:
上图中,注释是初始化了 OutputFilter
的数据,不过需要注意是他在循环条件里面没有执行 cur = cur->next
,而是在循环内部执行的 cur = cur->next
,应该是有需要的。
不过在 init_complex_filtergraph()
函数里面没有绑定 OutputFilter
与 OutputStream
,这两个东西的绑定是在 open_output_file()
里面的 init_output_filter()
函数进行的。
可以看一下上面的流程图, open_output_file()
就在 init_complex_filtergraph()
函数后面被调用。
init_complex_filtergraph()
函数还有一个重点,就是他会释放掉 graph
,如下:
/* this graph is only used for determining the kinds of inputs
* and outputs we have, and is discarded on exit from this function */
graph = avfilter_graph_alloc();
ret = avfilter_graph_parse2(graph, fg->graph_desc, &inputs, &outputs);
...省略代码...
fail:
avfilter_inout_free(&inputs);
avfilter_graph_free(&graph);
他的注释写的很清楚,这个 graph
只是临时用一下而已,用确定输入输出。后面在 configure_filtergraph()
函数里会再调 avfilter_graph_parse2()
生成一个新的 graph
来完成滤镜的功能。
至此,init_complex_filtergraph()
函数的源码分析完毕。
init_complex_filtergraph()
以及 open_output_file()
函数执行完毕之后,整个数据结构的关系如下:
提醒:虽然init_complex_filtergraph()
里面申请了 InputFilter
跟 OutputFilter
的内存,但是里面的滤镜上下文(AVFilterContext)还是 NULL,还没开始创建的。
AVFilterContext
的创建是在 configure_filtergraph()
函数里的,推荐阅读《configure_filtergraph配置滤镜容器》
补充:在 transcode_init()
函数里会打印哪些输入流 输入到 哪些滤镜里,如下:
感谢 NETINT(镕铭微电子) 赞助《FFmpeg原理》免费版一书的服务器费用,下面是 VPU 产品介绍