4、FFmpeg硬件加速框架:hwaccel初始化、硬件设备上下文、硬件帧上下文、hwupload/hwdownload滤镜

好,咱们进入正题。这一章讲的是FFmpeg硬件加速的骨架部分。说白了,就是告诉FFmpeg:「嘿,我要用GPU干活了,你帮我准备好环境。」

很多初学者一上来就写滤镜链,结果发现hwupload死活传不上去,或者解码出来的帧还是CPU的。为什么?因为硬件设备上下文没配好。我当年第一次调这个的时候,卡了整整两天,最后发现是hw_device_ctx少设了一个AVDictionary参数……嗯,从那以后我再也不敢跳过初始化步骤了。

4.1 hwaccel初始化:告诉FFmpeg你要用哪块GPU

硬件加速的第一步,是选择一个「硬件设备类型」。FFmpeg支持的设备类型不少,我列几个常用的:

设备类型 对应后端 适用平台
AV_HWDEVICE_TYPE_CUDA NVIDIA CUDA / NVENC / NVDEC Linux / Windows
AV_HWDEVICE_TYPE_VAAPI Intel / AMD VA-API Linux
AV_HWDEVICE_TYPE_DXVA2 Microsoft DirectX VA Windows
AV_HWDEVICE_TYPE_VIDEOTOOLBOX Apple VideoToolbox macOS / iOS
AV_HWDEVICE_TYPE_VULKAN Vulkan Compute 跨平台

初始化代码其实很简洁:

AVBufferRef *hw_device_ctx = NULL;
int ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, NULL, NULL, 0);
if (ret < 0) {
    // 处理错误:可能是驱动没装,或者设备不可用
}

这里有个细节:av_hwdevice_ctx_create 的第三个参数是设备路径,比如CUDA下可以指定GPU序号("0" 或 "1"),VAAPI下可以指定DRM设备("/dev/dri/renderD128")。不传的话,FFmpeg会选默认设备。我个人习惯还是显式传一下,避免在多卡机器上翻车。

注意: 初始化成功后,hw_device_ctx 是一个 AVBufferRef 引用。你需要在解码器上下文、编码器上下文、滤镜图中共享这个引用,而不是复制它。复制会导致引用计数混乱,程序崩溃是迟早的事。

4.2 硬件设备上下文:解码器与GPU的桥梁

有了设备句柄,接下来要把它「挂载」到解码器上。怎么做?

解码器上下文里有一个字段叫 hw_device_ctx,类型就是 AVBufferRef *。你把刚才创建的引用赋值给它,解码器就知道:「哦,我解码出来的帧要放在GPU显存里。」

AVCodecContext *dec_ctx = ...; // 假设你已经创建了解码器上下文
dec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);

等等,这里有个坑。赋值之后,解码器内部会创建一个「硬件帧上下文」(hw_frames_ctx),用来管理显存池。但有些解码器不会自动创建,你需要手动指定:

dec_ctx->hw_frames_ctx = av_hwframe_ctx_alloc(hw_device_ctx);
// 然后设置帧池参数:宽、高、像素格式、池大小
AVHWFramesContext *frames_ctx = (AVHWFramesContext *)dec_ctx->hw_frames_ctx->data;
frames_ctx->width = 1920;
frames_ctx->height = 1080;
frames_ctx->format = AV_PIX_FMT_CUDA;
frames_ctx->sw_format = AV_PIX_FMT_NV12;
frames_ctx->initial_pool_size = 20;
av_hwframe_ctx_init(dec_ctx->hw_frames_ctx);

为什么要设 sw_format?因为GPU内部格式和CPU内存格式不一样。比如NVDEC解码出来是NV12,但显存里存的是CUDA平面格式。FFmpeg需要知道「软件格式」是什么,才能在hwdownload时正确转换。

经验之谈: 我建议把 initial_pool_size 设得比解码线程数大一些。比如8路解码,池大小至少设20。太小了会导致解码器频繁等待空闲帧,性能反而下降。

4.3 硬件帧上下文:显存里的帧池管理

硬件帧上下文,说白了就是显存里的一块「停车场」。解码器解码出一帧,就停在这个停车场里;滤镜要处理,就从停车场里取;处理完了,再停回去或者释放。

它的核心结构是 AVHWFramesContext,里面有几个关键字段:

  • device_ctx:指向硬件设备上下文,告诉帧池「你的显存归哪个GPU管」。
  • format:硬件像素格式,比如 AV_PIX_FMT_CUDA、AV_PIX_FMT_VAAPI。
  • sw_format:软件像素格式,比如 AV_PIX_FMT_NV12、AV_PIX_FMT_YUV420P。
  • width / height:帧的分辨率。
  • initial_pool_size:预分配的帧数量。

你想想看,如果 sw_format 设错了,hwdownload 出来的数据全是花的。我曾经在VAAPI上把 sw_format 设成了 AV_PIX_FMT_YUV420P,结果下载到内存后颜色完全不对。后来查文档才发现,VAAPI的软件格式应该是 AV_PIX_FMT_NV12。嗯,这种细节文档里写得很清楚,但你不踩一次坑真的记不住。

4.4 hwupload / hwdownload 滤镜:CPU与GPU的数据搬运工

好,现在解码器已经在GPU里了。但滤镜链默认跑在CPU上。怎么把GPU帧送到滤镜?又怎么把滤镜处理完的结果拿回CPU?

答案就是两个滤镜:hwupload 和 hwdownload。

4.4.1 hwupload:从CPU内存上传到GPU显存

这个滤镜的输入是CPU帧(比如 AV_PIX_FMT_NV12),输出是GPU帧(比如 AV_PIX_FMT_CUDA)。用法很简单:

hwupload_cuda=device=0

但注意,hwupload 需要知道目标硬件帧上下文。怎么传?有两种方式:

  1. 隐式传递:在滤镜图初始化时,把 hw_device_ctx 设到滤镜图的 hw_device_ctx 字段上。hwupload 会自动从那里读取设备信息。
  2. 显式指定:在滤镜参数里写 device=0 或 device=/dev/dri/renderD128。

我个人推荐第一种,因为更干净,不容易写错设备路径。

4.4.2 hwdownload:从GPU显存下载到CPU内存

这个正好反过来。输入是GPU帧,输出是CPU帧。用法:

hwdownload

但这里有个大坑:hwdownload 默认下载到 sw_format 指定的格式。如果你的滤镜链中间改变了分辨率或像素格式,下载前必须确保格式匹配。否则FFmpeg会报错:hwdownload: the input format not supported。

核心流程总结:
解码器(hw_frames_ctx) → hwupload → 滤镜处理 → hwdownload → CPU内存

注意:如果解码器本身已经输出GPU帧,可以跳过hwupload,直接进滤镜。但滤镜必须支持硬件帧格式。

4.5 完整流程图:硬件加速滤镜链的骨架

下面这张图,我画的是典型的「解码→滤镜→编码」硬件加速链路。你看一眼就明白各个上下文之间的关系了。

FFmpeg硬件加速框架流程图 FFmpeg硬件加速框架:上下文与滤镜关系 硬件设备上下文 解码器 (hw_frames_ctx) 硬件帧上下文 (显存池) hwupload 滤镜 GPU滤镜 (scale/transpose等) hwdownload 滤镜 CPU内存帧 (AVFrame) 关键说明 • 解码器输出GPU帧时,可跳过hwupload • hwdownload前必须格式匹配 • 所有上下文共享同一设备句柄 避坑提示 • 不要复制hw_device_ctx,用av_buffer_ref • 池大小设大一点,避免解码阻塞

4.6 实战:一条完整的硬件加速滤镜链

光说不练假把式。咱们写一条实际的滤镜链,把解码、hwupload、scale、hwdownload串起来:

// 伪代码,展示核心逻辑
AVFilterGraph *graph = avfilter_graph_alloc();
graph->hw_device_ctx = av_buffer_ref(hw_device_ctx);

// 输入:解码器输出的GPU帧(假设已经hw_frames_ctx)
// 滤镜链:hwupload(如果解码器输出CPU帧) → scale_cuda → hwdownload → format
// 这里假设解码器输出已经是GPU帧,所以不需要hwupload

// 创建滤镜节点
const char *filters_desc = 
    "scale_cuda=1280:720,"  // GPU上的缩放
    "hwdownload,"           // 下载到CPU
    "format=nv12";          // 确保输出格式

// 解析并配置
int ret = avfilter_graph_parse_ptr(graph, filters_desc, &inputs, &outputs, NULL);
// ... 检查错误,初始化,运行

注意看,scale_cuda 是NVIDIA专属的GPU缩放滤镜。如果你用VAAPI,对应的是 scale_vaapi。不同后端的滤镜名字不一样,但逻辑完全一样。

小技巧: 调试硬件滤镜链时,可以在中间加一个 null 滤镜,用来检查每一帧的格式和尺寸。比如:hwupload, null, scale_cuda, null, hwdownload。这样你能看到每一步的输入输出是否正常。

4.7 避坑指南:我踩过的几个雷

  • 设备上下文重复创建:同一个进程里,多个解码器应该共享同一个 hw_device_ctx。重复创建会导致显存浪费,甚至驱动崩溃。用 av_buffer_ref 增加引用计数就好。
  • hwdownload 后格式不对:下载后的帧默认是 sw_format 指定的格式。如果你需要其他格式,记得在 hwdownload 后面加一个 format 滤镜做转换。
  • 滤镜图不释放旧帧:硬件帧的释放依赖引用计数。如果你在滤镜回调里手动 av_frame_unref,可能导致显存泄漏。让FFmpeg自己管理就好。
  • 跨GPU操作:不要把GPU A的帧传给GPU B的滤镜。不同设备上下文不兼容,会直接报错 Invalid argument。

嗯,这些坑我基本都踩过一遍。尤其是最后一个,当时我在一台双卡机器上做测试,解码用卡0,滤镜用卡1,结果折腾了一下午才发现是设备上下文没对上。

4.8 小结

这一章的核心就三件事:

  1. 初始化硬件设备上下文,告诉FFmpeg用哪块GPU。
  2. 配置硬件帧上下文,管理显存池的大小和格式。
  3. 用hwupload/hwdownload 在CPU和GPU之间搬运数据。