21、视频处理:FFmpeg跨平台封装、解码/编码、硬件加速适配

视频处理,说白了就是跟像素和比特流打交道。我做了这么多年跨平台开发,FFmpeg 几乎成了绕不开的坎。你想想看,Windows 上用 MediaFoundation,macOS 上有 VideoToolbox,Linux 下是 V4L2,每个平台都有自己的视频 API。但 FFmpeg 不一样,它把这些全包了,给你一个统一的接口。

嗯,今天我们就来聊聊怎么把 FFmpeg 封装成跨平台的视频处理模块。我会把解码、编码、硬件加速这些实战中踩过的坑,一个一个说清楚。

为什么选 FFmpeg?

我个人习惯,选库之前先看三点:社区活跃度、跨平台能力、License 友好度。FFmpeg 三点全占。

  • 社区活跃:几乎每周都有更新,新格式支持很快
  • 跨平台:Windows/Linux/macOS/Android/iOS 全支持
  • License:LGPL 或 GPL,商业项目注意区分

我在项目中遇到过好几次,客户要求同时支持 H.264 和 H.265 解码,还要在 Intel 和 NVIDIA 显卡上都能跑。用 FFmpeg 一套接口搞定,省了不少事。

核心架构:解码/编码流程

先画一张图,把 FFmpeg 的核心流程理清楚。这张图我每次培训都会拿出来讲,因为它把最关键的几个环节串起来了。

FFmpeg 视频处理核心流程 输入文件/流 解封装 (Demuxer) 解码 (Decode) 原始帧 (YUV/RGB) 滤镜处理 (Filter) 编码 (Encode) 封装输出 (Muxer) 硬件加速:DXVA2 / VAAPI / VideoToolbox 解码/编码环节可插入硬件加速,大幅提升性能

流程其实不复杂:输入文件 → 解封装拿到压缩数据 → 解码成原始帧 → 滤镜处理 → 编码 → 封装输出。但每个环节都有坑,尤其是跨平台和硬件加速这块。

跨平台封装:接口设计

我建议把 FFmpeg 的操作封装成几个核心类,这样上层业务不用关心底层实现。下面是我常用的设计:

// 视频解码器封装
class VideoDecoder {
public:
    virtual ~VideoDecoder() = default;
    
    // 打开解码器
    virtual bool Open(const std::string& url, 
                      const DecoderConfig& config) = 0;
    
    // 解码一帧
    virtual bool DecodeFrame(uint8_t* data, size_t size,
                             VideoFrame& frame) = 0;
    
    // 获取硬件加速类型
    virtual HardwareAccelType GetHardwareAccel() const = 0;
    
    // 释放资源
    virtual void Close() = 0;
};

// 工厂模式创建平台相关实例
class VideoDecoderFactory {
public:
    static std::unique_ptr<VideoDecoder> Create(
        PlatformType platform,
        HardwareAccelType accel = HardwareAccelType::Auto);
};

嗯,这里要注意:工厂模式是跨平台封装的利器。你可以在 Windows 上返回 DXVA2 加速的解码器,macOS 上返回 VideoToolbox 的,Linux 上返回 VAAPI 的。上层代码完全不用改。

核心原则:接口统一,实现分离。把平台相关的代码藏在工厂后面,业务层只看到纯虚函数。

解码实战:从文件到帧

解码这块,我踩过最大的坑是内存管理。FFmpeg 的 AVFrame 有自己的引用计数,跨线程传递时很容易出问题。

// 解码循环 - 带硬件加速支持
bool DecodePacket(AVPacket* pkt, AVCodecContext* ctx, 
                  AVFrame* frame, int* got_frame) {
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        // 我曾经在这里漏了错误处理,结果线上崩溃
        return false;
    }
    
    ret = avcodec_receive_frame(ctx, frame);
    if (ret == 0) {
        *got_frame = 1;
        
        // 如果是硬件加速帧,需要转回 CPU 内存
        if (frame->format == AV_PIX_FMT_DXVA2_VLD ||
            frame->format == AV_PIX_FMT_VAAPI) {
            AVFrame* sw_frame = av_frame_alloc();
            av_hwframe_transfer_data(sw_frame, frame, 0);
            // 使用 sw_frame 做后续处理
            av_frame_free(&sw_frame);
        }
        return true;
    }
    return false;
}

避坑指南:硬件加速帧的 data 指针指向 GPU 内存,不能直接读取像素数据。必须用 av_hwframe_transfer_data() 转回 CPU 内存。我曾经因为直接读取硬件帧,导致内存访问违例,查了两天才找到原因。

编码实战:从帧到文件

编码相对解码简单一些,但参数配置很关键。同样的源素材,参数配不好,要么文件太大,要么画质太差。

// 编码器配置 - H.264 示例
AVCodecContext* CreateEncoderContext(int width, int height,
                                     int fps, int bitrate) {
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVCodecContext* ctx = avcodec_alloc_context3(codec);
    
    ctx->width = width;
    ctx->height = height;
    ctx->time_base = (AVRational){1, fps};
    ctx->framerate = (AVRational){fps, 1};
    ctx->bit_rate = bitrate;
    ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    
    // 编码预设 - 平衡速度和画质
    av_opt_set(ctx->priv_data, "preset", "medium", 0);
    av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);
    
    // 打开编码器
    if (avcodec_open2(ctx, codec, nullptr) < 0) {
        // 错误处理
        avcodec_free_context(&ctx);
        return nullptr;
    }
    return ctx;
}

个人经验:编码参数里,preset 选 "medium" 或 "fast" 比较稳妥。"slow" 虽然压缩率高,但编码时间翻倍。直播场景一定要开 "zerolatency",否则延迟会让你崩溃。

硬件加速适配:三平台实战

硬件加速是视频处理性能的关键。我整理了一个表格,方便你对照:

平台 加速接口 支持的编码 初始化方式
Windows DXVA2 / D3D11VA H.264, H.265, VP9 av_hwdevice_ctx_create(AV_HWDEVICE_TYPE_DXVA2)
macOS/iOS VideoToolbox H.264, H.265, HEVC av_hwdevice_ctx_create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
Linux VAAPI / VDPAU H.264, H.265, MPEG-2 av_hwdevice_ctx_create(AV_HWDEVICE_TYPE_VAAPI)
Android MediaCodec H.264, H.265, VP8 av_hwdevice_ctx_create(AV_HWDEVICE_TYPE_MEDIACODEC)

初始化硬件加速的代码,我建议这样写:

// 自动选择硬件加速类型
AVBufferRef* CreateHardwareDevice(AVCodecContext* ctx) {
    AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;
    
    // 按优先级尝试
    #ifdef _WIN32
        type = AV_HWDEVICE_TYPE_DXVA2;
    #elif defined(__APPLE__)
        type = AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
    #elif defined(__linux__)
        type = AV_HWDEVICE_TYPE_VAAPI;
    #endif
    
    AVBufferRef* hw_device_ref = nullptr;
    int ret = av_hwdevice_ctx_create(&hw_device_ref, type, 
                                     nullptr, nullptr, 0);
    if (ret < 0) {
        // 回退到软件解码
        return nullptr;
    }
    
    ctx->hw_device_ctx = av_buffer_ref(hw_device_ref);
    return hw_device_ref;
}

注意:硬件加速不是万能的。有些老显卡不支持 H.265 解码,有些编码器对分辨率有限制。我建议在初始化时做一次能力检测,不行就回退到软件解码。别让用户看到黑屏。

性能优化:几个关键点

视频处理性能优化,说白了就是减少拷贝、减少同步。我总结了几条实战经验:

  • 零拷贝:用 AVFrame 的引用计数,避免 memcpy 原始帧数据
  • 线程模型:解码线程和渲染线程分离,用队列缓冲帧
  • 内存池:预分配 AVFrame 池,避免频繁 malloc/free
  • 硬件加速:能走 GPU 就走 GPU,CPU 留给业务逻辑

我记得有一次优化直播推流,原来 CPU 占用 80%,换成硬件编码后直接降到 15%。效果立竿见影。

常见问题与避坑

做 FFmpeg 跨平台封装,有几个坑我几乎每次都会遇到:

  1. 版本兼容性:FFmpeg 4.x 和 5.x 的 API 有变化,编译时注意版本检测
  2. License 问题:如果用了 GPL 的模块,商业发布要小心
  3. 日志处理:FFmpeg 默认输出到 stderr,建议用 av_log_set_callback 接管
  4. 内存泄漏:av_frame_free、av_packet_free 一个都不能少

我的建议:写一个 RAII 包装类,把 AVFrame、AVPacket 的生命周期管起来。C++ 的析构函数天然适合做这事,别浪费了。

好了,关于 FFmpeg 跨平台封装和解码编码的内容,今天就聊到这里。硬件加速这块,每个平台的细节其实挺多的,但核心思路是一致的:统一接口、按需适配、做好回退。你动手试试,遇到问题再回来翻翻这篇文章。


公众号:蓝海资料掘金营,微信deep3321