如何开发一个 FFmpeg 自定义滤镜插件?

来源:NoSQL教程作者:郑钧天头衔:网络博主
导读:本期聚焦于郑钧天创作的《如何开发一个 FFmpeg 自定义滤镜插件?》,敬请观看详情。FFmpeg 的 libavfilter 模块通过 AVFilter 结构体描述一个滤镜的能力,用 AVFilterPad 表示输入输出端点,并依靠 filter_frame 回调在滤镜图中传递视频帧。编写自定义滤镜插件,本质上就是实现一个规范的 AVFilter 实例并把它注册到全局滤镜列表中。与简单调用命令行参数不同,开发者需要处理像素格式协商、帧引用计数、内存对齐以及多线程切片等细节。本文以一个水平镜像滤镜为例,完整演示如何定义私有上下文和选项、声明 query_formats、实现帧处理回调,以及如何将源码文件加入 FFmpeg 构建系统后重新编译。还会说明独立编译为共享模块时手动调用 avfilter_register 的注意事项,以及调试阶段通过日志和滤镜图检查快速定位问题的方法。掌握这些步骤后,读者可以把任意图像处理算法封装成 FFmpeg 可调用的滤镜,并融入现有转码或推流流程。

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

如何开发一个 FFmpeg 自定义滤镜插件?

一、定义滤镜私有上下文和注册结构

自定义滤镜的第一步是声明一个私有上下文,用来保存滤镜参数。虽然一个最简单的滤镜可能不需要任何私有字段,但在实际开发中几乎都会通过参数控制行为。例如水平镜像滤镜可以提供一个 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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58919.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。