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 需要知道目标硬件帧上下文。怎么传?有两种方式:
- 隐式传递:在滤镜图初始化时,把
hw_device_ctx设到滤镜图的hw_device_ctx字段上。hwupload 会自动从那里读取设备信息。 - 显式指定:在滤镜参数里写
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 完整流程图:硬件加速滤镜链的骨架
下面这张图,我画的是典型的「解码→滤镜→编码」硬件加速链路。你看一眼就明白各个上下文之间的关系了。
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 小结
这一章的核心就三件事:
- 初始化硬件设备上下文,告诉FFmpeg用哪块GPU。
- 配置硬件帧上下文,管理显存池的大小和格式。
- 用hwupload/hwdownload 在CPU和GPU之间搬运数据。