此前在做 ffmpeg+某个第三库作为 filter 的集成,第三库是做AE特效相关的,与 ffmpeg 结合能让视频渲染效果大大提升。整体流程将第三方库作为 ffmpeg 的一个filter 形式进行结合,其中就涉及到 ffmpeg 的 filter 开发,本文即 对ffmpeg 的滤镜开发流程作一个总结。本文以实现一个视频垂直翻转的 filter 为例,ffmpeg 源码基于FFmpeg6.1

实现自定义 Filter 流程

  • 编写 filter.c 文件

    一般视频滤镜以 vf_ 为前缀,视频滤镜以 af_ 为前缀,放在libavfilter目录下,参考其他 filter 代码逻辑,模块化配置相关参数,本文例以 vf_flip.c 实现视频的上下翻转

  • libavfilter/allfilters.c 注册

    例如:extern const AVFilter ff_vf_flip; ff_vf_flip就是在 vf_flip.c的 filter 注册名称

  • 修改 libavfilter/Makefile 添加编译配置:

    例如:OBJS-$(CONFIG_FLIP_FILTER) += vf_flip.o

  • 编译打包

编写 filter.c 文件

AVFilter主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct AVFilter {
const char *name;
const char *description;
const AVFilterPad *inputs;
const AVFilterPad *outputs;
const AVClass *priv_class;
int flags;
int (*preinit)(AVFilterContext *ctx);
int (*init)(AVFilterContext *ctx);
int (*init_dict)(AVFilterContext *ctx, AVDictionary **options);
void (*uninit)(AVFilterContext *ctx);
int (*query_formats)(AVFilterContext *);
int priv_size;
int flags_internal;
struct AVFilter *next;
int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags);
int (*init_opaque)(AVFilterContext *ctx, void *opaque);
int (*activate)(AVFilterContext *ctx);
} AVFilter;

具体里面的属性作用可以参考:[ffmpeg] 定制滤波器,可以根据需求实现里面的相关函数,接下来以一个最简单的 Filter 和一个较复杂一点的 Filter 举例。

最简单的 AVFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct {
const AVClass *class;
} NoopContext;

static int filter_frame(AVFilterLink *link, AVFrame *frame) {
av_log(NULL, AV_LOG_INFO, "filter frame pts:%lld\n", frame->pts);
NoopContext *noopContext = link->dst->priv;
return ff_filter_frame(link->dst->outputs[0], frame);
}
static const AVFilterPad noop_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.filter_frame = filter_frame,
}
};
static const AVFilterPad noop_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
}
};
const AVFilter ff_vf_noop = {
.name = "noop",
.description = NULL_IF_CONFIG_SMALL("Pass the input video unchanged."),
.priv_size = sizeof(NoopContext),
FILTER_INPUTS(noop_inputs),
FILTER_OUTPUTS(noop_outputs),
};

命令行运行:

1
ffmpeg -i test.mp4 -vf "noop" noop.mp4

正常输出文件(对原片没有做任何更改),这个 filter 的作用是将输入的视频帧不做任何处理地传递给下一个过滤器,在处理每帧的时候会打印处理的 PTS,麻雀虽小五脏俱全,它包含了一个 AVFilter 基础的结构:

  1. NoopContext

    这是一个简单的结构体,包含一个指向 AVClass 的指针。在这个例子中,实际上没有使用到 NoopContext 结构体的任何成员,因为这个过滤器没有需要存储的私有数据。

  2. filter_frame

    这个函数的作用是处理输入的视频帧。在这个例子中,它只是打印帧的 PTS(Presentation Time Stamp,显示时间戳)并将帧传递给下一个过滤器,不对帧做任何修改。

  3. noop_inputsnoop_outputs

    这两个数组定义了过滤器的输入和输出 Pad。在这个例子中,输入 Pad 类型为 AVMEDIA_TYPE_VIDEO,并关联了 filter_frame 函数。输出 Pad 也是 AVMEDIA_TYPE_VIDEO 类型,但没有关联任何函数,因为输出直接由 filter_frame 函数处理。

  4. ff_vf_noop

    这是一个 AVFilter 结构体实例,包含了过滤器的名称、描述、私有数据大小以及输入和输出 Pad。在这个例子中,过滤器的名称为 “noop”,描述为 “Pass the input video unchanged.”,这也就是在执行:ffmpeg -filters 看到的 Filter描述内容。

接下来看一个稍微复杂的一个 AVFilter,实现一个视频的上下翻转

复杂一点的 AVFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
typedef struct FlipContext {
const AVClass *class;
int duration;
} FlipContext;

#define OFFSET(x) offsetof(FlipContext, x)
static const AVOption flip_options[] = {
{"duration", "set flip duration", OFFSET(duration), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, .flags = AV_OPT_FLAG_FILTERING_PARAM},
{NULL}
};

static av_cold int flip_init(AVFilterContext *ctx) {
FlipContext *context = ctx->priv;
av_log(NULL, AV_LOG_ERROR, "Input duration: %d.\n", context->duration);
return 0;
}

static av_cold void flip_uninit(AVFilterContext *ctx) {
FlipContext *context = ctx->priv;
// no-op 本例无需释放滤镜实例分配的内存、关闭文件、资源等
}

// 对输入的 AVFrame 进行翻转
static AVFrame *flip_frame(AVFilterContext *ctx, AVFrame *in_frame) {
AVFilterLink *inlink = ctx->inputs[0];
FlipContext *s = ctx->priv;
int64_t pts = in_frame->pts;
// 将时间戳(pts)转化以秒为单位的时间戳
float time_s = TS2T(pts, inlink->time_base);
if (time_s > s->duration) {
// 超过对应的时间则直接输出in_frame
return in_frame;
}
// 创建输出帧并分配内存
AVFrame *out_frame = av_frame_alloc();
if (!out_frame) {
av_frame_free(&in_frame);
return out_frame;
}
// 设置输出帧的属性
out_frame->format = in_frame->format;
out_frame->width = in_frame->width;
out_frame->height = in_frame->height;
out_frame->pts = in_frame->pts;

// 分配输出帧的数据缓冲区
int ret = av_frame_get_buffer(out_frame, 32);
if (ret < 0) {
av_frame_free(&in_frame);
av_frame_free(&out_frame);
return out_frame;
}
// 这个示例仅适用于 YUV 格式的视频。对于其他格式(如 RGB)
// 翻转输入帧的数据到输出帧
// 翻转了 Y 分量,然后翻转了 U 和 V 分量
//
uint8_t *src_y = in_frame->data[0];
uint8_t *src_u = in_frame->data[1];
uint8_t *src_v = in_frame->data[2];
uint8_t *dst_y = out_frame->data[0] + (in_frame->height - 1) * out_frame->linesize[0];
uint8_t *dst_u = out_frame->data[1] + (in_frame->height / 2 - 1) * out_frame->linesize[1];
uint8_t *dst_v = out_frame->data[2] + (in_frame->height / 2 - 1) * out_frame->linesize[2];

for (int i = 0; i < in_frame->height; i++) {
memcpy(dst_y, src_y, in_frame->width);
src_y += in_frame->linesize[0];
dst_y -= out_frame->linesize[0];

if (i < in_frame->height / 2) {
memcpy(dst_u, src_u, in_frame->width / 2);
memcpy(dst_v, src_v, in_frame->width / 2);
src_u += in_frame->linesize[1];
src_v += in_frame->linesize[2];
dst_u -= out_frame->linesize[1];
dst_v -= out_frame->linesize[2];
}
}
return out_frame;
}

static int activate(AVFilterContext *ctx) {
AVFilterLink *inlink = ctx->inputs[0];
AVFilterLink *outlink = ctx->outputs[0];
AVFrame *in_frame = NULL;
AVFrame *out_frame = NULL;
int ret = 0;
// 获取输入帧
ret = ff_inlink_consume_frame(inlink, &in_frame);
if (ret < 0) {
return ret;
}
// 如果有输入帧,进行翻转处理
if (in_frame) {
// 对输出帧进行上下翻转处理
out_frame = flip_frame(ctx, in_frame);
// 将处理后的帧放入输出缓冲区
ret = ff_filter_frame(outlink, out_frame);
if (ret < 0) {
av_frame_free(&out_frame);
return ret;
}
}
// 如果没有输入帧,尝试请求一个新的输入帧
if (!in_frame) {
ff_inlink_request_frame(inlink);
}

int status;
int64_t pts;
ret = ff_inlink_acknowledge_status(inlink, &status, &pts);
if (ret < 0)
return ret;
if (status == AVERROR_EOF) {
// 输入链接已经结束,设置输出链接的状态为 EOF
ff_outlink_set_status(outlink, AVERROR_EOF, pts);
return 0;
}
return 0;
}

AVFILTER_DEFINE_CLASS(flip);
static const AVFilterPad flip_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
}
};
static const AVFilterPad flip_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
}
};
const AVFilter ff_vf_flip = {
.name = "flip",
.description = NULL_IF_CONFIG_SMALL("Flip the input video."),
.priv_size = sizeof(FlipContext),
.priv_class = &flip_class,
.activate = activate,
.init = flip_init,
.uninit = flip_uninit,
FILTER_INPUTS(flip_inputs),
FILTER_OUTPUTS(flip_outputs),
};

命令行运行:

1
ffmpeg -i test.mp4 -filter_complex "[0:v]flip=duration=5[out];" -map "[out]" flip.mp4

得到渲染好的视频,前5s是上下翻转的,后面的内容正常。

相比于最简单的 AVFilter 多了几个实现:

  1. AVOption flip_options

    用于设置翻转持续时间的选项,外部命令配置可选输入duration=5,会自动对数据合法性进行校验。参数类型为 AV_OPT_TYPE_INT,默认值为 0,取值范围为 0 到 INT_MAX.flags 设置为 AV_OPT_FLAG_FILTERING_PARAM,表示这是一个过滤参数。

  2. .priv_class

    配置的flip_class实际是通过 AVFILTER_DEFINE_CLASS(flip); 宏实现的一个声明:见:internal.h#AVFILTER_DEFINE_CLASS_EXT

  3. **init& uninit

    滤镜在初始化或者释放资源的时候将会调用

  4. activate

    这个函数首先获取输入帧,然后调用 flip_frame 函数进行翻转操作,并将处理后的帧放入输出链接。如果没有输入帧,它会请求一个新的输入帧。最后,它会确认输入链接的状态,并根据需要设置输出链接的状态。

这个例子相比最简单的 filter 使用了 activate 函数 用于帧渲染,而不是使用 filter_frame去渲染,这两个方法有什么区别于联系呢?查看:filter_frame和activate方法

也能通过 filter_frame实现,对代码部分逻辑更新更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static const AVFilterPad flip_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.filter_frame = filter_frame, //添加filter_frame 实现
}
};

const AVFilter ff_vf_flip = {
……
.priv_class = &flip_class,
// .activate = activate,
.init = flip_init,
……
};

static int filter_frame(AVFilterLink *inlink, AVFrame *in) {
AVFilterContext *ctx = inlink->dst;
FlipContext *s = ctx->priv;
AVFilterLink *outlink = ctx->outputs[0];

int64_t pts = in->pts;
// 将时间戳(pts)转化以秒为单位的时间戳
float time_s = TS2T(pts, inlink->time_base);

if (time_s > s->duration) {
// 超过对应的时间则直接输出in_frame
return ff_filter_frame(outlink, in);
} else {
av_log(NULL, AV_LOG_ERROR, "time_s s: %f.\n", time_s);
}

AVFrame *out = flip_frame(ctx, in);
// 释放输入帧
av_frame_free(&in);

// 将输出帧传递给下一个滤镜
return ff_filter_frame(outlink, out);
}



命令行运行,得到的输出结果是一样的。

filter_frame()和activate()函数

对于这点查了相关资料,看看源码相关的实现

参考:https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html

The purpose of these rules is to ensure that frames flow in the filter graph without getting stuck and accumulating somewhere. Simple filters that output one frame for each input frame should not have to worry about it. There are two design for filters:one using the filter_frame() and request_frame() callbacks and the other using the activate() callback. The design using filter_frame() and request_frame() is legacy, but it is suitable for filters that have a single input and process one frame at a time. New filters with several inputs, that treat several frames at a time or that require a special treatment at EOF should probably use the design using activate(). activate ——– This method is called when something must be done in a filter

大意,实现滤镜有两种实现方式:

  • filter_frame()

    可以被认为是历史遗留产物。在早期的 AVFilter 设计中,filter_frame()request_frame() 是主要用于处理输入帧和请求输出帧的回调函数。这种设计适用于简单的过滤器,例如单输入且每次处理一个帧的过滤器。

  • activate()

    随着 ffmpeg 和 AVFilter 的发展,处理需求变得越来越复杂,例如需要处理多个输入、一次处理多个帧或在文件结束(EOF)时进行特殊处理等。为了满足这些需求,引入了 activate() 函数,它提供了更灵活和强大的处理能力。因此,虽然 filter_frame() 在某些简单场景下仍然可以使用,但对于新的或复杂的过滤器,建议使用 activate() 函数。

如果两个方法都实现了,那他们谁会先执行呢?

对应的源码处理逻辑: avfilter.c

1
2
3
4
5
6
7
8
9
int ff_filter_activate(AVFilterContext *filter)
{
int ret;
……
ret = filter->filter->activate ? filter->filter->activate(filter) :
ff_filter_activate_default(filter);
……
return ret;
}

如果配置了activate() 函数则执行,否则执行 ff_filter_activate_default()->ff_filter_frame_to_filter()->ff_filter_frame_framed() 最终执行到配置的 filter_frame() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int ff_filter_frame_framed(AVFilterLink *link, AVFrame *frame)
{
int (*filter_frame)(AVFilterLink *, AVFrame *);
AVFilterContext *dstctx = link->dst;
AVFilterPad *dst = link->dstpad;
int ret;

if (!(filter_frame = dst->filter_frame))
filter_frame = default_filter_frame;
……
ret = filter_frame(link, frame); // 最终调用到的地方
link->frame_count_out++;
return ret;
fail:
……
}

总结

本文介绍了 FFmpeg 滤镜开发的整体流程,如何编写 filter.c 文件,并以一个最简单的 AVFilter 和一个较为复杂的 AVFilter 为例,解析了滤镜开发的具体步骤和代码实现,并介绍了 filter_frame() 和 activate() 函数的区别与联系。

在滤镜开发过程中,需要注意的是,filter_frame() 和 activate() 函数的使用取决于滤镜的复杂性。对于简单的滤镜,可以使用 filter_frame() 函数;而对于需要处理多个输入、一次处理多个帧或在文件结束(EOF)时进行特殊处理的复杂滤镜,建议使用 activate() 函数。

文中的源码可以查看:add most simplest AVFilter and a simple video flip filter.

参考:

https://www.cnblogs.com/TaigaCon/p/10171464.html

https://www.cnblogs.com/ranson7zop/p/7728639.html

https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html