FFmpeg 在音视频处理中把滤镜作为图节点进行调度,自定义滤镜并不需要重新理解整套转码逻辑,只需按照 libavfilter 的约定实现一个 AVFilter 结构体。滤镜的输入和输出由 AVFilterPad 描述,每个 pad 上会绑定一个 filter_frame 回调,当上游送来一帧数据时,libavfilter 会自动调用这个函数。我们需要关注三件事:告诉框架支持哪些像素格式、如何从输入帧生成输出帧、以及如何正确释放不再使用的帧。

一、定义滤镜私有上下文和注册结构
自定义滤镜的第一步是声明一个私有上下文,用来保存滤镜参数。虽然一个最简单的滤镜可能不需要任何私有字段,但在实际开发中几乎都会通过参数控制行为。例如水平镜像滤镜可以提供一个 h 选项,当值为 1 时执行水平翻转,值为 0 时直接透传。这个上下文需要继承 AVClass,FFmpeg 的选项系统会通过它来读取参数描述。
接下来编写 vf_myflip.c 文件,包含必要的头文件并定义上下文结构体。滤镜名称会出现在命令行中,所以尽量使用小写字母和数字组合,避免与内置滤镜冲突。以下代码同时声明了选项数组和 AVFilter 注册实例的骨架。
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#include "avfilter.h"
#include "internal.h"
#include "video.h"
typedef struct MyFlipContext {
const AVClass *class;
int flip_h;
} MyFlipContext;
#define OFFSET(x) offsetof(MyFlipContext, x)
#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
static const AVOption myflip_options[] = {
{ "h", "set horizontal flip", OFFSET(flip_h), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, FLAGS },
{ NULL }
};
AVFILTER_DEFINE_CLASS(myflip);
static av_cold int init(AVFilterContext *ctx)
{
return 0;
}
static int query_formats(AVFilterContext *ctx)
{
static const enum AVPixelFormat pix_fmts[] = {
AV_PIX_FMT_RGB24,
AV_PIX_FMT_NONE
};
return ff_set_common_formats(ctx, ff_make_format_list(pix_fmts));
}
static const AVFilterPad myflip_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.filter_frame = filter_frame,
},
};
static const AVFilterPad myflip_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
},
};
AVFilter ff_vf_myflip = {
.name = "myflip",
.description = NULL_IF_CONFIG_SMALL("Flip video horizontally."),
.priv_size = sizeof(MyFlipContext),
.init = init,
.query_formats = query_formats,
.inputs = myflip_inputs,
.outputs = myflip_outputs,
.priv_class = &myflip_class,
.flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC,
};
这里 query_formats 先把支持的像素格式限定为 AV_PIX_FMT_RGB24,这样后续帧处理代码可以假定每个像素占 3 个字节,不需要处理 YUV 色度子采样带来的高度和宽度对齐问题。实际项目如果希望兼容更多格式,可以在这个函数中返回多个格式,并在 filter_frame 中根据 in->format 分支处理。
AVFILTER_DEFINE_CLASS 宏会生成一个名为 myflip_class 的 AVClass 实例,它关联到选项数组 myflip_options。这样当用户在命令行写出 myflip=h=1 时,FFmpeg 会解析参数并填充 MyFlipContext 中的字段。
二、实现帧处理回调与引用计数
filter_frame 是自定义滤镜最核心的部分。输入帧已经由上游滤镜写入 AVFrame,函数内部要负责建立输出帧、复制或生成像素数据,然后通过 ff_filter_frame 交给下一个滤镜。对于只需要变换像素位置的滤镜,不必复制完整帧再修改,直接调用 ff_get_video_buffer 分配一个与输出链路尺寸一致的新帧即可。
内存管理方面有两个关键点:如果分配输出帧失败,一定要释放输入帧,否则会出现帧泄漏;成功输出后也要释放输入帧,因为它的引用已经被消费,不能再返回给调用方。水平镜像的实现可以按行处理,对每一行内部的像素位置进行左右交换。
static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
AVFilterContext *ctx = inlink->dst;
MyFlipContext *s = ctx->priv;
AVFilterLink *outlink = ctx->outputs[0];
AVFrame *out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
int width = inlink->w;
int height = inlink->h;
int linesize = in->linesize[0];
if (!out) {
av_frame_free(&in);
return AVERROR(ENOMEM);
}
av_frame_copy_props(out, in);
out->width = width;
out->height = height;
uint8_t *src = in->data[0];
uint8_t *dst = out->data[0];
int bpp = 3;
for (int y = 0; y < height; y++) {
uint8_t *src_row = src + y * linesize;
uint8_t *dst_row = dst + y * out->linesize[0];
if (s->flip_h) {
for (int x = 0; x < width; x++) {
int src_x = width - 1 - x;
dst_row[x * bpp + 0] = src_row[src_x * bpp + 0];
dst_row[x * bpp + 1] = src_row[src_x * bpp + 1];
dst_row[x * bpp + 2] = src_row[src_x * bpp + 2];
}
} else {
memcpy(dst_row, src_row, width * bpp);
}
}
av_frame_free(&in);
return ff_filter_frame(outlink, out);
}
上面的代码只处理了 RGB24 格式,所以每个像素固定为 3 字节。实际工程里帧的 linesize 可能大于 width * bpp,因为内存对齐会带来额外的填充字节,因此逐行复制时必须使用 linesize 而不是简单计算 width * bpp 作为行偏移。垂直翻转时还要注意色度平面的高度与亮度平面不同,需要借助 AVPixFmtDescriptor 计算每个分量的实际高度。
如果希望滤镜支持更多的像素格式,可以在 query_formats 中返回多个候选格式,并在 filter_frame 里根据 in->format 判断字节数和平面数。但多格式处理会显著增加代码分支,初学者可以先从单一格式开始验证整条链路,再逐步扩展。
三、加入 FFmpeg 构建系统并重新编译
把 vf_myflip.c 放入 FFmpeg 源码树的 libavfilter 目录后,还需要修改两个文件才能让构建系统识别这个新滤镜。首先是 libavfilter/Makefile,在其中添加一行目标对象定义,表示在启用 myflip 滤镜时编译 vf_myflip.o。这个配置项会与 FFmpeg 的 configure 脚本生成的配置符号对应。
OBJS-$(CONFIG_MYFLIP_FILTER) += vf_myflip.o
接着修改 libavfilter/allfilters.c,在文件顶部添加 extern AVFilter ff_vf_myflip; 声明,并在滤镜注册表中增加一个条目。该文件中的注册表是一个大的 AVFilter 指针数组,libavfilter 初始化时会遍历它,把每个滤镜加入全局列表。添加时保持字母顺序,便于后续维护。
extern AVFilter ff_vf_myflip; /* 在滤镜数组中添加 */ &ff_vf_myflip,
完成修改后,重新执行配置和编译命令。为了让新滤镜被启用,可以显式传入 --enable-filter=myflip,或者直接使用默认配置并执行 make。编译完成后,运行 ffmpeg -filters 可以看到 myflip 出现在列表中。命令行测试可以使用类似 ffmpeg -i input.mp4 -vf "myflip=h=1" output.mp4 的形式,观察输出画面是否按预期发生水平镜像。
这里有一个容易忽视的细节:修改 allfilters.c 后必须重新执行 make,如果只编译 vf_myflip.o 而不更新注册表,FFmpeg 命令行不会识别新滤镜。另一种做法是在自己的应用程序中直接调用 avfilter_register 注册滤镜,而不修改源码树,但这需要额外管理动态库加载顺序和版本匹配。
四、选项声明、时间轴支持与调试技巧
为了让滤镜在命令行中更灵活,选项数组中的每一项都需要明确名称、类型、偏移和范围。例如 h 选项使用 AV_OPT_TYPE_BOOL,取值范围限制为 0 到 1,这样用户传入非法值时会直接报错。除了布尔型,还可以使用 AV_OPT_TYPE_INT、AV_OPT_TYPE_FLOAT、AV_OPT_TYPE_STRING 等类型,并配合 AV_OPT_FLAG_FILTERING_PARAM 声明该选项只能在滤镜参数中指定。
滤镜结构体中的 AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC 标志允许滤镜在 enable 表达式的控制下部分时间启用。比如 -vf "myflip=h=1:enable='between(t,2,5)'" 会让镜像只在第 2 秒到第 5 秒生效。如果不加这个标志,enable 参数会被框架拒绝。这个特性在转码或预览时很实用,但它要求滤镜的实现必须是线程安全的,并且不会跨帧保存非引用状态。
调试自定义滤镜时,建议先在 init 或 filter_frame 中加入 av_log(ctx, AV_LOG_DEBUG, "myflip frame pts=%ld\n", in->pts); 这样的日志,然后用 ffmpeg -v debug 查看输出。日志可以帮助确认滤镜是否被调用、帧的时间戳是否正确。内存问题可以用 valgrind 跑一次简单转码,重点检查输入帧是否在错误路径下被释放两次,或者输出帧是否因为缺少 av_frame_copy_props 导致下游滤镜读取到未初始化的元数据。
如果滤镜在运行时出现格式协商失败,通常是 query_formats 返回的格式列表与输入源不匹配。可以通过 ffmpeg -v verbose -i input -vf format=rgb24,myflip -f null - 强制把输入转换为 RGB24 后再进入自定义滤镜,验证滤镜本身逻辑是否正确。排查完格式问题后,再考虑扩展多种像素格式支持。
FFmpeg滤镜开发自定义滤镜FFmpeg插件修改时间:2026-09-18 18:38:11