一、概述
FFmpeg 提供了一种以管道的方式对音视频进行滤镜操作的框架。其也内置了非常多的滤镜,如果这些滤镜还不能满足需求的话也可以自行开发。
本文主要梳理 FFmpeg 滤镜相关的一些基本概念和 API,为之后对具体内置滤镜的分析做准备。
二、滤镜图和滤镜简述
1、什么是滤镜图和滤镜
滤镜图(Filter graph)是一种包含了多个已连接的滤镜的有向图。源码中,AVFilterGraph
类定义了滤镜图。避免眼花,下文使用 Filter graph
来表示滤镜图。
从表面上看滤镜是 Filter graph 节点。源码中,使用 AVFilter
类定义滤镜,运行时使用 AVFilterContext
类的实例表示位于 Filter graph 中的滤镜实例。
通常,滤镜有 0 或多个输入并且有 0 或多个输出——输入输出至少得有 1 个。一个滤镜的输出可以连接到另一个滤镜的输入,多个滤镜连接起来从而构成滤镜链。这里说的输入输出特指视频帧/音频采样在 Filter graph 中的处理流程,不是指整个 FFmpeg 的处理流程。源码中,使用 AVFilterPad
类定义滤镜有哪些输入和输出,运行时使用 AVFilterInOut
类的实例表示滤镜在 Filter graph 中的输入和输出实例。在 Filter graph 中使用 AVFilterLink
将滤镜连接起来。
截止 FFmpeg 4.3.1 版本,其包含了 460 个滤镜。可在 allfilters.c
这个源文件中搜索 “extern AVFilter” 看看搜索出来的数量。也可以用命令 ffmpeg -filters
查看安装了的 滤镜。
定义滤镜的源文件在 libavfilter
目录中。一个源文件定义了一或多个滤镜。比如 buffersrc.c
就包含了 buffer
和 abuffer
这两个滤镜, 分别是视频和音频的一种 Source (Source 是特殊的滤镜见下文)。
可以用命令
ffmpeg -h filter=<名称>
看具体滤镜的使用帮助。
2、滤镜的分类
可以使用两种方式对滤镜进行大致的分类。
从输入输出(Pad
)数量的角度可划分为 Sources
、Sinks
和 Filters
。Source 没有输入有 1 个输出,Sink 有 1 个输入没有输出,Filter 有 1 个或多个输入并有 1 个或多个输出。实际上,Source 和 Skin 本质上是特殊的滤镜。有时候为了区分,本文把 Source 和 Sink 之外的滤镜称为 普通滤镜
。
从滤镜能够处理的媒体类型的角度可划分为三类:Audio
、 Video
和 Multimedia
。Audio 分类的滤镜用于处理音频,Video 分类的滤镜包含用于处理视频,而 Multmedia 分类包含用于处理视频或音频的输入以及音频通用的滤镜。特别地,单独提出了 OpenCL Video
和 VAAPI Video
这两个仅用于处理视频的滤镜小类。
另外,Multimedia
只有 Sources 和普通滤镜,没有 Sinks;OpenCL Video
只有普通滤镜; VAAPI Video
只有一个用于视频编解码的普通滤镜。
实际上,普通滤镜也可以输入和输出都没有。比如
af_volumedetect
这个滤镜可以获取音频的音量(均方根值)、最大最小音量和音量直方图并以日志形式打印出来,不需要处理数据传递给下一个节点(Pad)。另外,如果输入为 NULL(AVFilte 的 outputs 字段为 NULL) 且有AVFILTER_FLAG_DYNAMIC_INPUTS
标志则表示有动态地多个输入, 比如amerge
和mix
等滤镜;如果输出为 NULL(AVFilte 的 inputs 字段为 NULL) 且有AVFILTER_FLAG_DYNAMIC_OUTPUTS
标志则表示有动态地多个输出,比如split
和aplit
等滤镜;甚至输入和输出都为 NULL 则表示输入和输出都是动态的,且有 AVFILTER_FLAG_DYNAMIC_INPUTS 和 AVFILTER_FLAG_DYNAMIC_OUTPUTS 两个标志,比如streamselect
、astreamselect
和concat
这三个滤镜——所以严格来说Sources
、Sinks
和Filters
不能简单按输入输出数量来区分,这样做只是考虑一般情况。
图1:Filter 的分类
三、滤镜相关类
FFmpeg 虽然是用 C 语言实现了,其也用到了一些 OOP 思想。在这里直接将这些 C 结构称为类。和滤镜相关的主要类如下。
类 | 说明 |
---|---|
AVFilterGraph | Filter graph。 |
AVFilter | 滤镜的定义。它一种定义类。它的实例包含用于处理命令(process_command,见下文)等操作。 |
AVFilterPad | 滤镜的输入或输出定义。它一种定义类。它的实例的 filter_frame 指针指向实际的滤镜逻辑 。 |
AVFilterContext | 滤镜的实例。AVFitler 实例是孤立的对滤镜的描述实例,而 AVFilterContext 实例是滤镜在 Filter graph 中的实例。 |
AVFilterLink | AVFilterLink 类通过将两个 AVFilterContext 实例的 Pad 放在一起从而实现滤镜的连接。 |
AVFilterInOut | AVFilterInOut 类是用于对滤镜参数字符串进行语句解析(Parse)时的辅助类。在此过程中会将[] 命名的的名称构造成 AVFilterInOut。它是一个链表结构。 |
四、滤镜的定义
1、AVFilter 类
AVFilter 可以看做是抽象类或接口。
定义了 1 个 const AVClass *priv_class;
字段描述了实际的类型。priv_class 指向的类型并非直接“继承”自 AVFilter,它更像包含了”子类”的字段的元数据——而真正的子类是一个虚拟的存在。比如滤镜 ff_af_volume
是一个 AVFrame 型变量,可以想象其是一个虚拟的名为 AVFilterVolume
的子类型的变量。而 AVFilterVolume
的字段定义在 VolumeContext
中。
定义了 1 个 int priv_size;
字段表示子类的结构大小,用于初始化时分配内存。比如滤镜 ff_af_volume
是 VolumeContext
结构的结果大小。
定义了 3 个函数指针字段 init
、init_opaque
和 init_dict
类似于面向对象语言中的构造函数,其中 init 看做是默认构造函数, init_opaue 和 init_dict 是构造函数重载。
定义了 1 个函数指针字段 uninit
类似于面向对象语言中的析构函数。
特别地,函数指针 preinit
指向的函数在给 AVFilterContext 分配内存后调用,为处理帧同步(framesync)做准备。
以下是 AVFitler 的不完整定义:
1 | // File: libavfilter/avfilter.h |
上述函数指针都是可 NULL 的,比如 abuffer
滤镜就只实现了 init
和 uninit
:
1 | // File: libavfilter/buffersrc.c |
再比如 abuffersink
滤镜实现了 init_opaque
、 query_formats
和 activate
而没有实现 init
和 uninit
:
1 | // File: libavfilter/buffersink.c |
甚至 acopy
滤镜没有实现上述的任何函数:
1 | // File: libavfilter/af_acopy.c |
上述的 ff_asrc_abuffer
、 ff_asink_abuffer
可以看做是实现了 AVFrame 接口的虚拟子类的实例,而 ff_af_acopy
直接是 AVFilter 的实例。
然后看看 AVFilter 的其他字段:
1 | // File: libavfilter/avfilter.h |
再然后看看 AVFilte 的其他函数指针:
1 | // File: libavfilter/avfilter.h |
activate
在处理数据之前调用进行一些准备工作。如果没有定义该函数,会使用 ff_filter_activate_default
函数。query_formats
用于查询输入和输出 Pad 支持的媒体格式。process_command
用于处理命令。注意并不是处理数据,命令里可能包含设置某些参数之类的。实际处理数据的是 AVFilterPad 的 filter_frame
指针所指向的函数。而该函数一般在滤镜所在文件进行定义,然后让 AVFilterPad 的 filter_frame 指针指向它。
2、AVFilterPad 类
使用 AVFilterPad 描述滤镜的输入和输出,比如描述 buffer
滤镜的输出:
1 | // File: libavfilter/buffersrc.c |
1 | // File: libavfilter/internal.c |
五、Filter graph 的创建和释放
1、创建 Filter graph
调用 avfilter_graph_alloc
函数创建 Filter graph,除了给 AVFilterGraph 结构也会给其内部使用的 AVFilterGraphInternal
型的 internal 字段分配内存。然后设置 AVOption 集的初始值。类似于面向对象语言的默认构造函数。
1 | // File: libavfilter/avfiltergraph.c |
2、释放 Filter graph
在使用 Filter graph 完成后,调用 avfilter_graph_free
函数以释放其内存。
六、滤镜(AVFilterContext)的创建和释放
AVFilterContext 实例是滤镜在 Filter graph 中的实例。它包含一个 AVFilter 实例——当然不同的滤镜实现不同。比如对于 overlay
这个滤镜,AVFilterContext 的 AVFilter 指针指向的是 ff_vf_overlay
对象。
1、创建滤镜
调用 avfilter_graph_create_filter
创建滤镜。
2、释放滤镜
在释放 Filter graph 的时候会将滤镜释放,无需手工释放。
七、Filter graph 语句解析(Parse)
AVFilterInOut 是用于对滤镜参数字符串进行语句解析(Parse)时的辅助类。在此过程中会将[]
命名的的名称构造成 AVFilterInOut。它是一个链表结构。
在使用 FFmpeg 滤镜 API (比如官方的 filter_video 这个 Demo) 时,也会手工创建以将 Source 和 Sink 与其他滤镜连接起来。
AVFilterLink 通过将两个滤镜的 Pad 放在一起从而实现滤镜的连接。滤镜相连是通过 AVFilterLink 对象而不是 AVFilterContext 或 AVFilter 等对象。
1、AVFilterInOut 类
1 | // File: libavfilter/avfilter.h |
2、AVFilterLink 类
1 | // File: libavfilter/avfilter.h |
3、Filter graph 语句解析
调用 avfilter_graph_parse_ptr
等方法对 Filter graph 的字符串参数进行语句解析。