FFmpeg + OpenCV:quink_oc_plugin 插件架构设计与实践

分享来自“Fun With FFmpeg”的文章《FFmpeg + OpenCV:quink_oc_plugin 插件架构设计与实践》。作者:quink

一、背景:FFmpeg 的巨大价值与开发门槛

1.1 FFmpeg — 多媒体领域的基石

FFmpeg 是当今最强大、最广泛使用的开源多媒体处理框架。它支持几乎所有已知的音视频编解码器、封装格式和协议,被无数产品作为核心引擎。无论是视频转码、直播推流、缩略图生成,还是滤镜处理,FFmpeg 都是事实上的行业标准。

FFmpeg 的libavfilter子系统提供了丰富的视频/音频滤镜,覆盖了缩放、裁剪、去噪、色彩调整、叠加、字幕烧录等主流需求。通过 filter graph 语法(如 -vf “scale=1280:720,fps=30″),用户可以灵活地组合多个滤镜构建处理流水线。

1.2 难点:FFmpeg 开发门槛极高

然而,一旦用户需要添加自定义的视频处理逻辑,FFmpeg 的优势就变成了障碍:

编译难

  • FFmpeg 使用 自定义的 configure 构建系统(非 autoconf),依赖复杂,交叉编译配置繁琐
  • 完整构建需要几十个外部依赖库(x264、x265、libvpx、libopus……),配置选项多达数百个
  • Windows 上通常需要 MSYS2/MinGW 环境,对新手极不友好
  • 每次修改一个滤镜,都需要重新走完 configure → make 的完整流程

加新功能难

  • 编写一个新的 libavfilter 滤镜需要深入理解 FFmpeg 内部 API:AVFrame、AVFilterLink、AVFilterFormats、FFFrameSync、像素格式协商……
  • 代码用C 语言编写,遵循 FFmpeg 严格的编码规范(命名风格、内存管理、错误处理)
  • 新滤镜需要修改 FFmpeg 源码树中的 allfilters.c、Makefile 等多个文件
  • 提交到 FFmpeg 上游的门槛极高,代码审核周期长

对新手极不友好

  • 一个 CV(计算机视觉)工程师可能精通 OpenCV、PyTorch,但完全不了解 AVFrame 的引用计数、像素格式协商、硬件帧上下文等 FFmpeg 概念
  • 想要做一个简单的”对每帧跑一下人脸检测”,却需要先学习整个 libavfilter 架构
  • FFmpeg 和 OpenCV 之间缺少一个简洁的桥梁

1.3 与 OpenCV 自带 FFmpeg 集成的对比

OpenCV 自身就集成了 FFmpeg 作为后端,通过 cv::VideoCapture 和 cv::VideoWriter 可以直接读写视频文件。那么,在 FFmpeg 里加 OpenCV 插件,和 OpenCV 自己调用 FFmpeg 有什么不同?

OpenCV 的 FFmpeg 集成:面向”读/写帧”的简单场景

OpenCV 调用 FFmpeg 的方式本质上是:FFmpeg 作为 OpenCV 的 I/O 后端。典型用法:

cv::VideoCapture cap("input.mp4");   // FFmpeg 负责解封装 + 解码
cv::Mat frame;
while (cap.read(frame)) {
    // 用 OpenCV 处理每一帧
    cv::GaussianBlur(frame, frame, cv::Size(5, 5), 0);
}
cv::VideoWriter writer("output.mp4", ...);  // FFmpeg 负责编码 + 封装

这种模式适合简单的逐帧处理场景,但有明显的局限性:

  • 无法利用 FFmpeg 的 filter graph — 不能组合 FFmpeg 内置的 200+ 个滤镜(缩放、去隔行、字幕烧录、音视频同步……),只能用 OpenCV 自己重新实现
  • 无法利用 FFmpeg 的硬件加速全流程 — VideoCapture 解码出来的帧是 CPU 内存中的 cv::Mat,即使底层解码器支持硬件加速,帧也必须经过 GPU → CPU 拷贝。无法实现 nvdec → GPU 处理 → nvenc 的全 GPU 路径
  • 无法利用 FFmpeg 的复杂管线能力 — 多输入(画中画、拼接)、多输出(一路输入同时输出多种分辨率)、音视频同步处理等场景,在 OpenCV 中需要自己手动管理
  • 与 FFmpeg 生态隔离 — FFmpeg 命令行、流媒体推拉(RTMP/SRT/HLS)、字幕处理、元数据操作等能力无法直接使用

quink_oc_plugin:面向”FFmpeg 管线中的自定义处理”场景

quink_oc_plugin 的思路恰好相反:OpenCV 作为 FFmpeg 的处理后端。你的处理逻辑作为 FFmpeg filter graph 中的一个节点,嵌入到 FFmpeg 的完整管线中:

输入 → 解封装 → 解码 → [FFmpeg 滤镜] → [你的 OpenCV 插件] → [FFmpeg 滤镜] → 编码 → 封装 → 输出这带来了根本性的不同:

 OpenCV 调用 FFmpegquink_oc_plugin
谁是主控OpenCV 程序FFmpeg(命令行或 libav* API)
FFmpeg 滤镜无法使用可以与任意 FFmpeg 滤镜组合
全 GPU 路径帧必须到 CPUnvdec → CUDA 插件 → nvenc,全程 GPU
多输入/多输出需手动管理FFmpeg 的 filter_complex 原生支持
流媒体需自己实现FFmpeg 原生支持 RTMP/SRT/HLS 等
音频处理需手动同步FFmpeg 自动处理音视频同步
适用场景简单逐帧处理、快速原型生产级转码管线、流媒体、复杂滤镜链

两者互补,而非替代

这两种方式各有适用场景:

  • 快速原型、简单逐帧处理 → 用 OpenCV 的 VideoCapture/VideoWriter,简单直接
  • 生产级转码管线、需要利用 FFmpeg 全部能力 → 用 quink_oc_plugin,将 OpenCV 处理嵌入 FFmpeg 管线

quink_oc_plugin 的核心价值在于:让你既能享受 FFmpeg 作为多媒体引擎的全部能力(硬件加速、协议支持、滤镜组合),又能用 OpenCV 这个你最熟悉的工具来编写自定义处理逻辑。

1.4 我们需要什么?

我们需要一种方式,让开发者能够:

  1. 不修改、不重新编译 FFmpeg — 插件以动态库形式加载
  2. 用 OpenCV API 写处理逻辑 — 输入是 cv::Mat,输出也是 cv::Mat
  3. 零拷贝 — FFmpeg 的 AVFrame 和 OpenCV 的 cv::Mat 共享同一块内存
  4. 支持 GPU 加速 — CUDA 插件直接操作 cv::cuda::GpuMat,全程不碰 CPU 内存
  5. 支持复杂拓扑 — 多输入(如画面混合)、多输出(如画面分割)
  6. 即插即用 — 写完 .cpp,编译成 .so/.dll,直接用 ffmpeg 命令行加载

这就是quink_oc_plugin要解决的问题。

二、整体架构设计

2.1 架构全景

FFmpeg + OpenCV:quink_oc_plugin 插件架构设计与实践

整个系统分为两层:

●宿主层(vf_oc_plugin.cpp):编译进 FFmpeg,负责与 libavfilter 交互。处理帧分配、像素格式协商、framesync 多输入同步、CUDA 上下文管理、detection bbox 侧数据附加等所有 FFmpeg 内部细节。

●插件层(用户编写的 .so/.dll):只依赖一个头文件 quink_oc_plugin.h,用 OpenCV API 实现处理逻辑。不需要链接任何 FFmpeg 库。

2.2 为什么选择 OpenCV 作为接口?

在设计插件 API 时,我们需要选择一种”数据载体”作为宿主和插件之间的数据交换格式。有以下几种选择:

方案优点缺点
裸指针 + 宽高步长零依赖插件需自行管理,不方便
AVFrame原生 FFmpeg 类型插件必须链接 FFmpeg,违背初衷
cv::Mat / cv::cuda::GpuMatCV 开发者最熟悉的类型;OpenCV 生态丰富需要 OpenCV 依赖
自定义 struct完全可控又造了一个轮子

我们选择 OpenCV 的理由:

1.目标用户就是 CV 开发者— 他们日常工作就是用 cv::Mat 做图像处理、用 DNN 模块做推理。让他们直接操作熟悉的类型,学习成本为零。

2.零拷贝可行— cv::Mat 支持外部数据指针构造(不拥有数据),我们可以将 AVFrame 的缓冲区直接包装为 cv::Mat,反之亦然。通过自定义 cv::MatAllocator,我们将 cv::Mat 的生命周期绑定到 AVFrame 的引用计数上,实现真正的零拷贝。

3.GPU 同样零拷贝— cv::cuda::GpuMat 同样支持外部指针构造。FFmpeg 的 AV_PIX_FMT_CUDA 帧的 data[0] 就是 CUDA device pointer,直接包装为 GpuMat 即可,也可以打通引用计数。

4.OpenCV 生态— 2500+ 个函数,涵盖图像处理、特征提取、DNN 推理、CUDA 加速……插件可以直接调用这些函数,不需要重新造轮子。

2.3 三种插件类型

根据实际使用场景,我们定义了三种互斥的插件类型:

类型基类数据类型应用场景
Processquink::ProcessPlugincv::MatCPU 帧变换:模糊、混合、分裂、缩放
CudaProcessquink::CudaProcessPlugincv::cuda::GpuMatGPU 帧变换:全 GPU 转码流水线
Detectquink::DetectPlugincv::Mat目标检测:输出边界框作为侧数据

为什么要区分 Process 和 Detect?

Process 插件可以修改帧内容(如模糊、混合),也可以改变输出分辨率。而 Detect 插件的核心诉求是分析帧并输出结构化数据(边界框、类别、置信度),帧本身通常原样传递。将两者分开,可以让宿主自动处理 detection 结果到 AV_FRAME_DATA_DETECTION_BBOXES 侧数据的转换,插件只需要填充一个简单的 Detections 结构体即可。

为什么要单独出 CudaProcess?

CUDA 插件和 CPU 插件的数据类型不同(GpuMat vs Mat),内存模型也不同(GPU 显存 vs CPU 内存)。此外,CUDA 插件需要接收一个 cv::cuda::Stream 参数以支持异步操作,宿主会在 process() 返回后自动同步 stream。将它作为独立类型,API 更清晰,类型安全更好。

2.4 I/O 拓扑支持

模式inputsoutputs示例
1:111模糊、去噪、检测
N:12-81画面混合、对比
1:N12-8画面分裂、多路分析
N:M>1>1❌ 不支持

多输入模式(N:1)由 FFmpeg 内置的 FFFrameSync 机制处理帧同步,插件无需关心。多输出模式(1:N)中,宿主为每个输出预分配独立的帧缓冲区。

三、核心设计细节

3.1 零拷贝内存模型

这是整个架构的关键设计。

输入帧:引用计数绑定

// 宿主内部实现(简化)
class AVFrameMatAllocator : public cv::MatAllocator {
    void deallocate(cv::UMatData* u) const override {
        AVFrame* frame = static_cast<AVFrame*>(u->userdata);
        av_frame_free(&frame);  // Mat 释放时,AVFrame 引用计数 -1
    }
};

当宿主将 AVFrame 包装为 cv::Mat 时,通过自定义的 MatAllocator 将 Mat 的生命周期绑定到 AVFrame 的引用计数上。这意味着:

●cv::Mat 和 AVFrame共享同一块内存

●插件可以安全地保存输入 Mat 的引用(例如用于时域缓冲),只要 Mat 还活着,底层的 AVFrame 缓冲区就不会被释放

●这对 GPU 帧同样适用:GpuMat 通过自定义 Allocator 绑定到 CUDA AVFrame

输出帧:预分配临时视图

// 宿主为插件预分配输出帧
AVFrame *out = ff_get_video_buffer(outlink, width, height);
cv::Mat output_mat(height, width, cv_type, out->data[0], out->linesize[0]);
// output_mat 是一个轻量级视图,没有引用计数

输出 Mat 只是 FFmpeg 预分配的 AVFrame 的一个轻量级视图(没有引用计数)。插件必须在 process() 返回前完成写入,不能保存输出 Mat 的引用。

 输入帧输出帧
所有权引用计数绑定到 AVFrameFFmpeg 拥有,预分配
可保存引用?✅ 安全❌ 禁止
零拷贝?
用途读取 + 缓冲写入结果

零拷贝传递(Pass-through)

对于不需要修改帧内容的场景(如 Detect 插件、Split 的第一路输出),CPU 插件可以直接:

outputs[0] = inputs[0];  // 零拷贝:Mat 头部赋值,共享底层数据

宿主检测到输出 Mat 的 data 指针发生了变化(指向了输入帧的缓冲区),会自动处理引用计数转移,避免任何内存拷贝。

注意:CUDA 插件不支持 pass-through(outputs[0] = inputs[0]),因为每个输出的 GpuMat 背后绑定了不同的 hw_frames_ctx 池。必须使用 inputs[0].copyTo(outputs[0], stream) 实现 GPU 到 GPU 的拷贝。

3.2 宿主内部的策略模式

宿主使用Strategy 模式为三种插件类型实现不同的帧处理逻辑:

class FrameHandler {                    // 抽象策略
    virtual int processFrame(...) = 0;
    virtual int processFrameMulti(...) = 0;
    virtual int flush() = 0;
};

class CpuFrameHandler : public FrameHandler { ... };    // CPU Process
class CudaFrameHandler : public FrameHandler { ... };   // CUDA Process
class DetectFrameHandler : public FrameHandler { ... };  // Detect

在插件加载时,宿主根据插件描述符中的 capabilities 字段选择对应的 Handler。之后所有帧处理调用都通过 Handler 的虚函数分发,主流程代码完全统一。

3.3 CUDA 上下文管理

对于 CUDA 插件,宿主负责所有 CUDA 上下文操作:

class PushPopCudaCtx {
public:
    PushPopCudaCtx(AVCUDADeviceContext *hwctx) {
        cu->cuCtxPushCurrent(hwctx->cuda_ctx);  // 进入 CUDA 上下文
    }
    ~PushPopCudaCtx() {
        cu->cuCtxPopCurrent(&dummy);              // 离开 CUDA 上下文
    }
};

●每次调用插件的 process() 前,宿主自动 push CUDA context

●process() 返回后,宿主自动 pop context

●宿主从 FFmpeg 的 AVCUDADeviceContext 中获取 CUDA stream,包装为 cv::cuda::Stream 传给插件

●插件的所有 GPU 操作都应该使用这个 stream,宿主在 process() 返回后会自动同步

插件开发者完全不需要关心 CUDA context 的生命周期。

3.4 像素格式协商

宿主在 query_formats 阶段向 FFmpeg 报告支持的像素格式:

●CPU 插件默认支持 BGR24 和 BGRA(OpenCV 的原生格式)

●CUDA 插件支持 AV_PIX_FMT_CUDA,底层的 sw_format 可以是 NV12、P010、P016、BGR24、BGRA

插件可以在 configure() 中修改输出格式。例如,CUDA 插件可以将 NV12 输入转换为 BGRA 输出:

bool configure(const std::vector<quink::FrameConfig> &inputs,
               std::vector<quink::FrameConfig> &outputs) override {
    if (inputs[0].pix_fmt == quink::QPixelFormat::NV12) {
        outputs[0].pix_fmt = quink::QPixelFormat::BGRA;  // 告诉宿主输出是 BGRA
    }
    return true;
}

3.5 插件加载与 ABI 稳定性

插件通过 dlopen()(Linux/macOS)或 LoadLibrary()(Windows)动态加载。宿主查找的导出符号为:

extern "C" const QuinkOCPluginDescriptor* quink_oc_plugin_get_descriptor();

QuinkOCPluginDescriptor 使用纯 C 类型(int、unsigned int、函数指针)以确保 ABI 稳定性。api_version 字段用于版本兼容性检查。

struct QuinkOCPluginDescriptor {
    int api_version;            // 必须等于 QUINK_OC_PLUGIN_API_VERSION
    const char *name;
    const char *description;
    unsigned int capabilities;  // quink::Capability 枚举的位掩码
    quink::PluginBase* (*create)();
    void (*destroy)(quink::PluginBase*);
};

三个宏 QUINK_OC_PROCESS_PLUGIN_ENTRY、QUINK_OC_CUDA_PROCESS_PLUGIN_ENTRY、QUINK_OC_DETECT_PLUGIN_ENTRY 自动生成上述描述符和导出函数,插件开发者只需一行代码完成注册。

3.6 Buffering 与 TryAgain/Flush 模式

并非所有滤镜都是逐帧处理的。时域滤镜(如帧平均、光流)需要缓冲多帧后才能输出。oc_plugin 通过 ProcessResult::TryAgain 和 flush() 机制原生支持这种模式:

Frame 1 → process() → TryAgain  (缓冲中,无输出)
Frame 2 → process() → TryAgain  (缓冲中,无输出)
Frame 3 → process() → Ok        (输出 avg(1,2,3))
Frame 4 → process() → Ok        (输出 avg(2,3,4))
...
EOF     → flush()   → true      (输出 avg(N-1,N))
        → flush()   → true      (输出 avg(N))
        → flush()   → false     (完成)

宿主在收到 TryAgain 时会自动释放预分配的输出帧(因为没有输出),在流结束时反复调用 flush() 直到返回 false。

PS: 目前存在时间戳、metadata如何对应问题,待优化。

四、CUDA 插件与全 GPU 流水线

4.1 CUDA 插件的核心价值

CUDA 插件的真正价值不仅仅是”用 GPU 加速一个滤镜”,而是实现全 GPU 转码流水线:

硬件解码 (nvdec) → CUDA 插件处理 → 硬件编码 (nvenc)

在这条路径上,视频帧从解码到编码全程在 GPU 显存中,从不经过 CPU 内存。这是 CUDA 插件相比 CPU 插件的根本性优势 — 如果帧要从 GPU 下载到 CPU 再处理再上传回 GPU,那瓶颈就在 PCIe 传输,GPU 计算的速度优势会被抵消。

# 全 GPU 流水线:硬件解码 → CUDA 插件 → 硬件编码
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \
    -vf "oc_plugin=plugin=./libcuda_blur_plugin.so:params='ksize=15'" \
    -c:v hevc_nvenc -b:v 2M -c:a copy output.mp4

4.2 NV12/P016 格式处理 — 关键难点

在全 GPU 流水线中,硬件解码器输出的帧格式通常是NV12(8 位)或P016(10/16 位),而非 RGB/BGRA。这是 CUDA 插件开发中最重要的技术挑战。

NV12 GpuMat 内存布局

宿主将 NV12 帧包装为一个高度 × 1.5的单通道 GpuMat:

┌───────────────────────┐
│        Y              │  height 行, width 列, CV_8UC1
│                       │
├───────────────────────┤
│       UV              │  height/2 行, width 列, 交错 U/V
│                       │
└───────────────────────┘
 总计: height × 1.5 行 × width 列

大多数 OpenCV CUDA 函数(滤镜、混合等)期望 BGRA(CV_8UC4)输入,因此插件必须在 GPU 上完成 NV12 → BGRA 的色彩空间转换。

转换方案

使用 OpenCV cudacodec 模块的 cv::cudacodec::createNVSurfaceToColorConverter:

convert_ = cv::cudacodec::createNVSurfaceToColorConverter(
    static_cast<cv::cudacodec::ColorSpaceStandard>(in.colorspace),
    !in.limited_range);

⚠️ 这个函数设计上是要处理 BT.601/BT.709/BT.2020 色彩空间标准和 limited/full range 的差异,但是我测试过程中发现处理BT.2020有bug,已提交PR到opencv_contrib:

⚠️不要使用 cv::cuda::cvtColor(COLOR_YUV2BGRA_NV12)— OpenCV CUDA 暂时不支持这个转换码 (2026-02-09)。

关键技巧:只需正向转换

插件不需要将 BGRA 转回 NV12。在 configure() 中设置:

outputs[0].pix_fmt = quink::QPixelFormat::BGRA;

告诉宿主输出格式是 BGRA。宿主会分配 BGRA 格式的输出缓冲区,而硬件编码器(如 hevc_nvenc)可以直接接受 BGRA 输入。

五、如何实现自己的插件

5.1 项目结构

参考 demo 项目的目录结构:https://github.com/ffmpeg-plugin/avfilter-plugin

ffmpeg_oc_plugins/
├── CMakeLists.txt              # 顶层构建配置
├── include/
│   └── quink_oc_plugin.h       # SDK 头文件(除OpenCV之外的唯一依赖)
├── src/
│   ├── CMakeLists.txt          # 插件构建 + 测试配置
│   ├── plugin_common.h         # Demo 插件共享的参数解析工具
│   ├── blur_plugin.cpp         # CPU 模糊插件 (1:1)
│   ├── blend_plugin.cpp        # CPU 混合插件 (N:1)
│   ├── split_plugin.cpp        # CPU 分裂插件 (1:N)
│   ├── avgframes_plugin.cpp    # CPU 帧平均插件 (TryAgain/Flush)
│   ├── detect_plugin.cpp       # 目标检测插件
│   ├── cuda_blur_plugin.cpp    # CUDA 模糊插件
│   ├── cuda_blend_plugin.cpp   # CUDA 混合插件
│   ├── cuda_split_plugin.cpp   # CUDA 分裂插件
│   └── verify_output.cmake     # 测试验证脚本
└── docs/
    └── plugin_api.md           # API 详细文档

5.2 构建配置

顶层 CMakeLists.txt 将 SDK 定义为 header-only interface library:

add_library(quink_oc_plugin INTERFACE)
target_include_directories(quink_oc_plugin INTERFACE include)

每个插件就是一个动态库:

macro(add_plugin name)
    add_library(${name}_plugin SHARED ${name}_plugin.cpp)
    target_link_libraries(${name}_plugin PRIVATE quink_oc_plugin ${OpenCV_LIBS})
    set_target_properties(${name}_plugin PROPERTIES PREFIX "lib")
endmacro()

add_plugin(blur)
add_plugin(blend)

构建和测试:

cmake -B build -DFFMPEG_CMD=/path/to/ffmpeg
cmake --build build
ctest --test-dir build --output-on-failure

5.3 示例一:最简 CPU 插件 — 高斯模糊 (1:1)

这是最简单的插件形式 — 一帧输入,一帧输出:

#include <opencv2/imgproc.hpp>
#include "quink_oc_plugin.h"

class GaussianBlurPlugin : public quink::ProcessPlugin {
public:
    bool init(const char *params, int nb_inputs, int nb_outputs) override {
        if (nb_inputs != 1 || nb_outputs != 1)
            return false;
        // 解析参数,如 "ksize=15"
        if (params) {
            const char *p = strstr(params, "ksize=");
            if (p) ksize_ = atoi(p + 6) | 1;  // 确保奇数
        }
        return true;
    }

    void uninit() override {}

    bool configure(const std::vector<quink::FrameConfig> &inputs,
                   std::vector<quink::FrameConfig> &outputs) override {
        // 输出尺寸与输入相同(默认),不需要修改
        return true;
    }

    quink::ProcessResult process(const std::vector<cv::Mat> &inputs,
                                 std::vector<cv::Mat> &outputs) override {
        cv::GaussianBlur(inputs[0], outputs[0],
                         cv::Size(ksize_, ksize_), 0);
        return quink::ProcessResult::Ok;
    }

    bool flush(std::vector<cv::Mat> &) override { return false; }

private:
    int ksize_ = 5;
};

// 一行代码完成注册
QUINK_OC_PROCESS_PLUGIN_ENTRY(GaussianBlurPlugin, "blur", "Gaussian blur effect")

使用方式:

ffmpeg -i input.mp4 \
    -vf "oc_plugin=plugin=./libblur_plugin.so:params='ksize=15'" \
    output.mp4

就这么简单。不需要理解 AVFrame,不需要像素格式协商,不需要链接 FFmpeg — 只需要 OpenCV 的 GaussianBlur 和一个宏。

5.4 示例二:多输入混合插件 (N:1)

将两路视频按 alpha 比例混合:

class AlphaBlendPlugin : public quink::ProcessPlugin {
public:
    bool init(const char *params, int nb_inputs, int nb_outputs) override {
        if (nb_inputs != 2 || nb_outputs != 1)
            return false;
        if (params) {
            const char *p = strstr(params, "alpha=");
            if (p) alpha_ = atof(p + 6);
        }
        return true;
    }

    quink::ProcessResult process(const std::vector<cv::Mat> &inputs,
                                 std::vector<cv::Mat> &outputs) override {
        cv::addWeighted(inputs[0], 1.0 - alpha_, inputs[1], alpha_, 0.0, outputs[0]);
        return quink::ProcessResult::Ok;
    }

    // ... configure, flush, uninit 省略(同上)

private:
    double alpha_ = 0.5;
};

QUINK_OC_PROCESS_PLUGIN_ENTRY(AlphaBlendPlugin, "blend", "Alpha blend two streams")

使用方式(注意 inputs=2):

ffmpeg -i bg.mp4 -i fg.mp4 \
    -filter_complex "[0:v][1:v]oc_plugin=plugin=./libblend_plugin.so:inputs=2:params='alpha=0.3'" \
    output.mp4

多路输入的帧同步由 FFmpeg 的 FFFrameSync 自动处理,插件 process() 被调用时,inputs[0] 和 inputs[1] 已经是时间对齐的帧。

5.5 示例三:多输出split插件 (1:N)

一路输入,同时输出原始画面、灰度画面和边缘检测画面:

class SplitPlugin : public quink::ProcessPlugin {
public:
    bool init(const char *params, int nb_inputs, int nb_outputs) override {
        if (nb_inputs != 1 || nb_outputs < 1) return false;
        num_outputs_ = nb_outputs;
        return true;
    }

    quink::ProcessResult process(const std::vector<cv::Mat> &inputs,
                                 std::vector<cv::Mat> &outputs) override {
        const cv::Mat &src = inputs[0];

        outputs[0] = src;  // 零拷贝传递

        if (num_outputs_ >= 2) {
            cv::Mat gray;
            cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
            cv::cvtColor(gray, outputs[1], cv::COLOR_GRAY2BGR);
        }

        if (num_outputs_ >= 3) {
            cv::Mat gray, edges;
            cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
            cv::Canny(gray, edges, 50, 150);
            cv::cvtColor(edges, outputs[2], cv::COLOR_GRAY2BGR);
        }

        return quink::ProcessResult::Ok;
    }

    // ... 省略 configure, flush, uninit
};

QUINK_OC_PROCESS_PLUGIN_ENTRY(SplitPlugin, "split", "Single input to multiple outputs")

使用方式:

ffmpeg -i input.mp4 \
    -filter_complex "oc_plugin=plugin=./libsplit_plugin.so:outputs=3[out0][out1][out2]" \
    -map "[out0]" pass.mp4 -map "[out1]" gray.mp4 -map "[out2]" edges.mp4

5.6 示例六:CUDA 模糊插件 — 全 GPU 流水线

展示完整的 NV12 → BGRA 转换和 GPU 处理:

#include <opencv2/cudacodec.hpp>
#include <opencv2/cudafilters.hpp>

class CudaGaussianBlurPlugin : public quink::CudaProcessPlugin {
public:
    bool init(const char *params, int nb_inputs, int nb_outputs) override {
        if (nb_inputs != 1 || nb_outputs != 1) return false;
        if (params) {
            const char *p = strstr(params, "ksize=");
            if (p) ksize_ = atoi(p + 6) | 1;
        }
        return true;
    }

    bool configure(const std::vector<quink::FrameConfig> &inputs,
                   std::vector<quink::FrameConfig> &outputs) override {
        const quink::FrameConfig &in = inputs[0];
        quink::FrameConfig &out = outputs[0];

        if (in.pix_fmt == quink::QPixelFormat::NV12 ||
            in.pix_fmt == quink::QPixelFormat::P016) {
            // ★ 关键:告诉宿主输出格式是 BGRA
            out.pix_fmt = quink::QPixelFormat::BGRA;

            auto sf = (in.pix_fmt == quink::QPixelFormat::NV12)
                ? cv::cudacodec::SF_NV12 : cv::cudacodec::SF_P016;

            // 创建 GPU 色彩转换器,处理色彩空间和范围
            convert_ = cv::cudacodec::createNVSurfaceToColorConverter(
                static_cast<cv::cudacodec::ColorSpaceStandard>(in.colorspace),
                !in.limited_range);

            blur_ = cv::cuda::createGaussianFilter(CV_8UC4, CV_8UC4,
                        cv::Size(ksize_, ksize_), 0);
        } else {
            out.pix_fmt = in.pix_fmt;
            blur_ = cv::cuda::createGaussianFilter(in.cv_type, in.cv_type,
                        cv::Size(ksize_, ksize_), 0);
        }
        return true;
    }

    quink::ProcessResult process(const std::vector<cv::cuda::GpuMat> &inputs,
                                 std::vector<cv::cuda::GpuMat> &outputs,
                                 cv::cuda::Stream &stream) override {
        cv::cuda::GpuMat in = inputs[0];

        if (convert_) {
            cv::cuda::GpuMat bgra;
            convert_->convert(in, bgra, surface_format_,
                              cv::cudacodec::BGRA, cv::cudacodec::EIGHT,
                              false, stream);
            in = bgra;
        }

        blur_->apply(in, outputs[0], stream);
        return quink::ProcessResult::Ok;
    }

    bool flush(std::vector<cv::cuda::GpuMat> &, cv::cuda::Stream &) override {
        return false;
    }

    void uninit() override { blur_.release(); }

private:
    int ksize_ = 5;
    cv::Ptr<cv::cuda::Filter> blur_;
    cv::cudacodec::SurfaceFormat surface_format_ = cv::cudacodec::SF_NV12;
    cv::Ptr<cv::cudacodec::NVSurfaceToColorConverter> convert_;
};

QUINK_OC_CUDA_PROCESS_PLUGIN_ENTRY(CudaGaussianBlurPlugin, "cuda_blur", "CUDA Gaussian blur")

使用方式:

# 全 GPU 流水线(推荐)
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \
    -vf "oc_plugin=plugin=./libcuda_blur_plugin.so:params='ksize=15'" \
    -c:v hevc_nvenc -b:v 2M output.mp4

# 测试用:CPU 解码 → 上传 GPU → 插件 → 下载 CPU → CPU 编码
ffmpeg -i input.mp4 \
    -vf "format=bgra,hwupload_cuda,oc_plugin=plugin=./libcuda_blur_plugin.so:params='ksize=15',hwdownload,format=bgra" \
    output.mp4

六、插件生命周期

完整的生命周期如下图所示:

FFmpeg + OpenCV:quink_oc_plugin 插件架构设计与实践

关键点:

1.create() 和 destroy() 是描述符中的工厂函数,由宏自动生成

2.init() 用于解析参数和验证 I/O 配置

3.configure() 在 FFmpeg filter graph 配置阶段调用,此时分辨率和像素格式已确定

4.process()/detect() 在每帧(或每组帧)到达时调用

5.flush() 在流结束时反复调用直到返回 false

6.uninit() 在 filter graph 销毁时调用,释放所有资源

七、测试与调试

7.1 CTest 集成

项目使用 CMake 的 CTest 框架进行自动化测试,每个插件都有对应的测试用例:

cmake -B build -DFFMPEG_CMD=/path/to/ffmpeg
cmake --build build
ctest --test-dir build --output-on-failure

测试会自动:

●使用 FFmpeg 的 lavfi testsrc 生成测试视频(不需要外部视频文件)

●运行插件处理

●验证输出文件大小、framemd5 一致性

●对比参考输出(确认滤镜确实产生了效果)

7.2 Valgrind 内存检查

ctest --test-dir build -T memcheck \
    --overwrite MemoryCheckCommand=/usr/bin/valgrind \
    --overwrite MemoryCheckCommandOptions="--leak-check=full --track-origins=yes --error-exi

7.3 AddressSanitizer

cmake -B build \
    -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
    -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
    -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
    -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address"
cmake --build build
ctest --test-dir build --output-on-failure

7.4 NVIDIA Compute Sanitizer(CUDA 插件)

compute-sanitizer --tool memcheck \
    ./ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \
    -vf "oc_plugin=plugin=./libcuda_blur_plugin.so:params=ksize=15" \
    -c:v hevc_nvenc -b:v 2M output.mp4

八、获取与编译

8.1 源码仓库

带有 vf_oc_plugin 的 FFmpeg 源码托管在:

●FFmpeg(含 oc_plugin): https://github.com/ffmpeg-plugin/FFmpeg

●Demo 插件:https://github.com/ffmpeg-plugin/avfilter-plugin

8.2 macOS / Linux 编译

在 macOS 和 Linux 上,可以从源码编译 FFmpeg 并启用 OpenCV 支持:

# 1. 克隆源码
git clone https://github.com/ffmpeg-plugin/FFmpeg.git
cd FFmpeg

# 2. 配置(启用 libopencv)
./configure --enable-libopencv \
    --extra-cxxflags="$(pkg-config --cflags opencv4)" \
    --extra-ldflags="$(pkg-config --libs opencv4)"

# 3. 编译
make -j$(nproc)

# 4. 安装(可选)
sudo make install

提示:–enable-libopencv 是必需的,它会编译 vf_oc_plugin 滤镜。确保系统已安装 OpenCV 4.x 开发库(通过 apt install libopencv-dev、brew install opencv 或源码编译均可)。

8.3 Windows 预编译版本

对于 Windows 用户,可以直接下载预编译的 FFmpeg(含 oc_plugin 支持):

●下载地址: https://github.com/ffmpeg-plugin/FFmpeg/releases/tag/v0.01

下载后解压即可使用,无需编译 FFmpeg。只需要编译你自己的插件 .dll 即可。

8.4 编译 Demo 插件

无论使用哪种方式获取 FFmpeg,Demo 插件的编译方式相同:

git clone https://github.com/ffmpeg-plugin/avfilter-plugin.git
cd avfilter-plugin
cmake -B build -DFFMPEG_CMD=/path/to/ffmpeg
cmake --build build
ctest --test-dir build --output-on-failure

Windows端让cmake找到vcpkg构建的opencv package,可以使用:

cmake -B build -DFFMPEG_CMD=/path/to/ffmpeg \
    -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake

8.5 项目状态

⚠️当前项目处于开发阶段(Development),API 可能会有变化。

关于是否提交到 FFmpeg 上游社区:FFmpeg 社区对于是否支持插件化架构一直存在争议,因此目前暂不考虑上游提交。计划在FFmpeg 8.1 发布后,基于 FFmpeg 8.1 发布一个正式的 release 版本。

九、总结与展望

quink_oc_plugin 通过插件架构,在 FFmpeg 强大的多媒体处理能力和 OpenCV 丰富的计算机视觉生态之间搭建了一座桥梁。

核心设计理念:

设计目标实现方式
零拷贝自定义 MatAllocator 绑定 AVFrame 引用计数
动态加载dlopen/LoadLibrary,运行时加载
全 GPU 路径CUDA 插件直接操作 GpuMat,宿主管理 CUDA context
灵活拓扑支持 1:1、N:1、1:N 三种 I/O 模式
帧缓冲TryAgain/Flush 原生支持时域滤镜
跨平台Linux、macOS、Windows

对于 CV 开发者来说,只需要:

1.继承一个基类

2.实现 init()、configure()、process()、flush()、uninit()

3.用一个宏注册

4.编译为 .so

5.ffmpeg …… -vf “oc_plugin=plugin=./your_plugin.so” …… — Done!

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论