本节摘要:FFmpeg 的滤镜系统是可扩展的——通过注册 AVFilter 结构就能把自定义处理插进滤镜图。本节讲透滤镜注册的机制、回调函数的分工,以及「扩展 FFmpeg」与「绕开 FFmpeg」两种路线各自的成本与适用场景。
阅读完本节,你应当能够:
有团队要给视频加「企业专属的防伪动态水印」,翻遍 FFmpeg 滤镜列表没找到,于是决定「自己写滤镜」。结果写了一个月,踩遍了内存与帧格式的坑。其实他们要的效果,用 overlay + drawtext + 时间参数就能拼出来。
这个反例想说明:扩展 FFmpeg 是最后的手段,不是首选方案。 先确认标准滤镜真做不到(很多人连 drawtext、overlay 组合都没试过),再谈写代码。但确实存在 FFmpeg 做不了的需求——比如把解码帧直接送进某个私有算法再回来——这时扩展机制就值得掌握了。
FFmpeg 的滤镜系统设计成「注册表」模式:每个滤镜是一个 AVFilter 结构体,登记好名字、输入输出约束、回调函数,FFmpeg 在构建滤镜图时按名字查找并调用。
static const AVFilterPad myfilter_inputs[] = { { .name = "default", .type = AVMEDIA_TYPE_VIDEO, // 视频滤镜 }, }; static const AVFilterPad myfilter_outputs[] = { { .name = "default", .type = AVMEDIA_TYPE_VIDEO, }, }; AVFilter ff_vf_myfilter = { .name = "myfilter", // 滤镜名,命令里 -vf "myfilter" 用它 .description = "My custom filter", .init = myfilter_init, // 参数解析后初始化 .config_props = myfilter_config, // 输入输出格式协商 .filter_frame = myfilter_frame, // 核心:处理一帧 .uninit = myfilter_uninit, // 释放资源 };
| 回调 | 触发时机 | 干什么 |
|---|---|---|
| init | 滤镜创建时 | 解析参数,分配持久资源 |
| config_props | 输入输出协商时 | 确定帧格式、分辨率 |
| filter_frame | 每来一帧 | 核心处理逻辑,产出新帧 |
| uninit | 滤镜销毁时 | 释放 init 分配的资源 |
filter_frame 是灵魂:它接收一帧(AVFrame),处理后生成一帧,通过 ff_filter_frame 推给下游。你的业务逻辑——无论是亮度调整还是调用私有算法——都写在这里。
滤镜实现完后,把它挂进注册表:
allfilters.c 里加一行 REGISTER_FILTER(MYFILTER, myfilter, vf);编译后 ffmpeg -filters | grep myfilter 能看到它,于是它和内置滤镜一样可以用在 -vf 里了。
| 路线 | 适合场景 | 成本 |
|---|---|---|
| 写自定义滤镜 | 处理逻辑要长驻在管线里、要复用 FFmpeg 的帧搬运与调度 | 要维护一份自编译 FFmpeg,滤镜生命周期管理复杂 |
| 绕开 FFmpeg | 只在某环节插自己的算法 | 只需在 SDK 程序的 receive_frame 后插代码(见 8.2) |
关键判断:你的处理是「滤镜形态」还是「程序逻辑」? 如果是「对每帧做同样变换、希望它能和 scale/overlay 一样组合」——写滤镜合理;如果只是「解码后调一下我的算法再继续」——在 8.2 那个程序骨架里插代码,成本低一个数量级。

滤镜的生命周期四个回调一路推进,filter_frame 是核心。但先别急着写滤镜——上图决策树给出两条路线:滤镜形态还是程序逻辑。多数「私有算法接入」需求,插代码比写滤镜省事得多。
⚠️ 常见坑:写完滤镜忘了在 allfilters.c 注册,
-filters里永远看不到它;像素格式假设错误导致部分输入花屏;帧引用计数没管理好导致内存泄漏或悬空。自编译版本一旦固定,升级 FFmpeg 等于要重新适配自己的滤镜代码。
💡 关键直觉:扩展 FFmpeg 前先问「这是滤镜问题还是程序问题」。滤镜问题(和内置滤镜组合、长驻管线)才值得写滤镜;程序问题(接私有算法)就在 SDK 程序里插代码。用最小的改动解决问题,是工程的第一原则。
下一章把这些知识全部用起来——第 9 章实战场景,从短视频到监控安防。