captura ffmpeg用不了_视频和视频帧:FFMPEG 硬件解码API介绍
写在前面本文将介绍如何用FFMPEG API做硬件解码。如果对解码的流程不是很熟悉的同学,建议先阅读:我是小北挖哈哈:视频和视频帧:FFMPEG CPU解码API介绍zhuanlan.zhihu.com笔者之前看到过类似问题:视频硬解码和软解码有什么区别?本质上没什么区别,都是用芯片执行编解码计算。软硬的称呼容易引起歧义,实质上:用CPU通用计算单元(无论是Intel还是AMD)就是软解;用专用
写在前面
本文将介绍如何用FFMPEG API做硬件解码。如果对解码的流程不是很熟悉的同学,建议先阅读:
我是小北挖哈哈:视频和视频帧:FFMPEG CPU解码API介绍zhuanlan.zhihu.com
笔者之前看到过类似问题:视频硬解码和软解码有什么区别?
本质上没什么区别,都是用芯片执行编解码计算。
软硬的称呼容易引起歧义,实质上:用CPU通用计算单元(无论是Intel还是AMD)就是软解;用专用芯片模组(GPU、QSV等)就是硬解。
因此区别也就出来了:底层接口不同、指令集不同、硬件驱动不同。由此引申出来的问题也就显而易见了:
- 首先,因为CPU是通用计算单元,所以接口通用,移植性好;而专用芯片模组之间无法移植互用;
- 其次,因为CPU接口通用,因此编解码内部很多细节方便开发人员修改;而专用芯片模组,接口和驱动都是不同厂商提供的,很多是非开源,因此比较难控制内部细节。
- 最后,目前用CPU做编解码的效果,在实际测试下来会比专用芯片模组的效果好些。不过这个问题可以通过优化算法和芯片解决,这就是厂商的事儿了,我们控制不了。
至于实际生活生产中,到底选择硬解码还是软解码?
要视不同情况而定。比如:
- CPU富余、需要精准控制解码流程、有解码算法的优化、通用性要求高,直接使用软解(也就是CPU解码);
- 有其他编解码芯片/模组、CPU不够用,就不得不需要转向硬解码(也就是专用芯片解码)。
本文就将介绍如何用FFMPEG API做一般的硬解码,“一般的硬解码”是说在主流的FFMEG中支持的硬解码类型。笔者会结合自己在QSV和CUDA解码上的经验,最后分享一些踩过的坑、扩展的几个小问题。
I. FFMPEG支持的硬解码
首先来看下FFMPEG原生支持哪些硬解码类型,在AVHWDeviceType(libavutil/hwcontext.h)中列举出所有原生支持的硬解码类型:
enum AVHWDeviceType {
AV_HWDEVICE_TYPE_NONE,
AV_HWDEVICE_TYPE_VDPAU,
AV_HWDEVICE_TYPE_CUDA,
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_DXVA2,
AV_HWDEVICE_TYPE_QSV,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DRM,
AV_HWDEVICE_TYPE_OPENCL,
AV_HWDEVICE_TYPE_MEDIACODEC,
};
上面的AV_HWDEVICE_TYPE_CUDA就是笔者目前正在做的CUDA是NVIDIA的硬件加速库,AV_HWDEVICE_TYPE_QSV则是以前做的QSV是Intel提供的一套集显上的硬件加速方案。
那么,究竟要怎么知道系统当前的FFMPEG究竟支持哪些硬件库?
可以通过命令行查看:ffmpeg -hwaccel。在hardware acceleration methods:下面可以看到当前FFMPEG集成的硬解码库。
然后,如果发现自己需要的硬件库不在当前FFMPEG中怎么办?
答案是:很可能需要自己重新编译源码。每个硬件解码库的集成办法不同。如果是QSV,见
我是小北挖哈哈:视频和视频帧:FFMPEG+Intel QSV硬解的环境安装篇zhuanlan.zhihu.com
如果是其他的硬解码库,请自行上网搜索。(笔者后续会补充怎么在FFMPEG中集成CUDA库,敬请关注)
II. FFMPEG硬解码API
硬解步骤和软解步骤类似,笔者绘制了一幅FFMPEG硬件解码流程图:图中橙色部分是硬解码中有而软解码没有的部分。该图的灵感来自于博客《FFmpeg 示例硬件解码hw_decode》,推荐这个文章,简练而详实。
接下来,详细介绍上图橙色部分,和硬解码相关的API函数。
硬解码Step1. 寻找硬解codec
AVCodec* avcodec_find_decoder_by_name(const char *name)
通过名字来寻找对应的AVCodec。每一个解码器的名字一定是全局唯一的,在AVCodec头文件中有相应的描述:
Name of the codec implementation. The name is globally unique among encoders and among decoders (but an encoder and a decoder can share the same name). This is the primary way to find a codec from the user perspective.
其实在FFMPEG内部每一个解码器codec都是一个结构体,维护了该解码器自己的信息、具体执行的函数等信息。比如Intel的QSV解码器(在libavcodec/qsvdec_h2645.c)是:
AVCodec ff_h264_qsv_decoder = {
.name = "h264_qsv",
.long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video acceleration)"),
.priv_data_size = sizeof(QSVH2645Context),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.init = qsv_decode_init,
.decode = qsv_decode_frame,
.flush = qsv_decode_flush,
.close = qsv_decode_close,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_DR1 | AV_CODEC_CAP_AVOID_PROBING | AV_CODEC_CAP_HYBRID,
.priv_class = &class,
.pix_fmts = (const enum AVPixelFormat[]){ AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
AV_PIX_FMT_QSV,
AV_PIX_FMT_NONE },
.hw_configs = ff_qsv_hw_configs,
.bsfs = "h264_mp4toannexb",
.wrapper_name = "qsv",
};
可以看到这个codec支持的codec id是AV_CODEC_ID_H264,支持的目标像素格式有{ AV_PIX_FMT_NV12,AV_PIX_FMT_P010,AV_PIX_FMT_QSV,AV_PIX_FMT_NONE }。
是的,硬件解码器不同于通用解码器,只能支持有限的目标像素格式。
再来看看CUDA解码器(在libavcodec/cuviddec.c),同样的,他也只能支持有限的目标像素格式:
AVCodec ff_##x##_cuvid_decoder = {
.name = #x "_cuvid",
.long_name = NULL_IF_CONFIG_SMALL("Nvidia CUVID " #X " decoder"),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_##X,
.priv_data_size = sizeof(CuvidContext),
.priv_class = &x##_cuvid_class,
.init = cuvid_decode_init,
.close = cuvid_decode_end,
.decode = cuvid_decode_frame,
.receive_frame = cuvid_output_frame,
.flush = cuvid_flush,
.bsfs = bsf_name,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AVOID_PROBING | AV_CODEC_CAP_HARDWARE,
.pix_fmts = (const enum AVPixelFormat[]){ AV_PIX_FMT_CUDA,
AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
AV_PIX_FMT_P016,
AV_PIX_FMT_NONE },
.hw_configs = cuvid_hw_configs,
.wrapper_name = "cuvid",
};
硬解码Step2. 寻找硬解目标像素
在上一节知道了一个事实:硬解码codec支持的目标像素是有限的、且各自不一定相同。因此找到了硬解码codec之后,就得准备设置它的目标像素(pixel format)。
enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)
这个函数是通过名称去寻找对应的AVHWDeviceType,这是一个枚举类型的变量(定义在libavutil/hwcontext.h头文件中):
enum AVHWDeviceType {
AV_HWDEVICE_TYPE_NONE,
AV_HWDEVICE_TYPE_VDPAU,
AV_HWDEVICE_TYPE_CUDA,
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_DXVA2,
AV_HWDEVICE_TYPE_QSV,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DRM,
AV_HWDEVICE_TYPE_OPENCL,
AV_HWDEVICE_TYPE_MEDIACODEC,
AV_HWDEVICE_TYPE_VULKAN,
};
这个类型和名称的关系表就简单多了,在FFMPEG代码中是用hw_type_names关系表来维护的(定义在libavutil/hwcontext.c文件中):
static const char *const hw_type_names[] = {
[AV_HWDEVICE_TYPE_CUDA] = "cuda",
[AV_HWDEVICE_TYPE_DRM] = "drm",
[AV_HWDEVICE_TYPE_DXVA2] = "dxva2",
[AV_HWDEVICE_TYPE_D3D11VA] = "d3d11va",
[AV_HWDEVICE_TYPE_OPENCL] = "opencl",
[AV_HWDEVICE_TYPE_QSV] = "qsv",
[AV_HWDEVICE_TYPE_VAAPI] = "vaapi",
[AV_HWDEVICE_TYPE_VDPAU] = "vdpau",
[AV_HWDEVICE_TYPE_VIDEOTOOLBOX] = "videotoolbox",
[AV_HWDEVICE_TYPE_MEDIACODEC] = "mediacodec",
[AV_HWDEVICE_TYPE_VULKAN] = "vulkan",
};
const AVCodecHWConfig * avcodec_get_hw_config (const AVCodec *codec, int index)
紧接着,调用这个函数去获取到该解码器codec的硬件属性,比如可以支持的目标像素格式等。而这个信息就存储在AVCodecHWConfig中:
typedef struct AVCodecHWConfig {
/**
* A hardware pixel format which the codec can use. !!!硬解码codec支持的像素格式!!!
*/
enum AVPixelFormat pix_fmt;
/**
* Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
* setup methods which can be used with this configuration.
*/
int methods;
/**
* The device type associated with the configuration.
*
* Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
* AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
*/
enum AVHWDeviceType device_type;
} AVCodecHWConfig;
enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
这是一个回调函数,它的作用就是告诉解码器codec自己的目标像素格式是什么。在上一步骤获取到了硬解码器codec可以支持的目标格式之后,就通过这个回调函数告知给codec,具体做法:
enum AVPixelFormat get_hw_format(struct AVCodecContext *s, const enum AVPixelFormat *fmt) {
for (const enum AVPixelFormat *p = fmt; *p != -1; p++) {
if (*p == hw_pix_fmt) return *p;
}
return AV_PIX_FMT_NONE;
}
我们也可以通过阅读AVCodec结构内对于这个回调函数的定义,可以知道:
-
- fmt是这个解码器codec支持的像素格式,且按照质量优劣进行排序;
- 如果没有特别的需要,这个步骤是可以省略的。内部默认会使用“native”的格式。
* callback to negotiate the pixelFormat
* @param fmt is the list of formats which are supported by the codec, it is terminated by -1 as 0 is a valid format, the formats are ordered by quality. The first is always the native one.
* @note The callback may be called again immediately if initialization for the selected (hardware-accelerated) pixel format failed.
* @warning Behavior is undefined if the callback returns a value not in the fmt list of formats.
* @return the chosen format
* - encoding: unused
* - decoding: Set by user, if not set the native format will be chosen.
硬解码Step3. 准备和打开硬解码
int av_hwdevice_ctx_create(AVBufferRef **pdevice_ref, enum AVHWDeviceType type, const char *device, AVDictionary *opts, int flags)
这个函数的作用是,创建硬件设备相关的上下文信息AVHWDeviceContext,包括分配内存资源、对硬件设备进行初始化。
准备好硬件设备上下文AVHWDeviceContext后,需要把这个信息绑定到AVCodecContext,就可以像软解一样的流程执行解码操作了。
硬解码Step4. 取回数据
按照一般软解的流程,在调用avcodec_receive_frame()之后,得到的数据其实还在硬件模组/芯片上,也就是说,如果是用CUDA解码,数据是在显存上(或者说是在显卡encoder/decoder的buffer上)的。对于很多应用而言,解码之后往往还要进行后续操作,比如保存成一幅幅图片之类的,那么就需要把数据取回。
int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags)
这个函数是负责在cpu内存和硬件内存(原文是hw surface)之间做数据交换的。也就是说,它不但可以把数据从硬件surface上搬回系统内存,反向操作也支持;甚至可以直接在硬件内存之间做数据交互。
III. FFMPEG硬解码的问题
问题1:如果要转成硬解码不直接支持的像素格式怎么做?
回答:硬解码不直接支持,那么希望在解码后直接得到是不可能的。实际的做法主要提供2种最常见的:
- 目标格式用硬解格式,接着通过
av_hwframe_transfer_data()转到内存,最后通过sws_scale()完成。 - 目标格式用
NV12(大部分硬解码都支持),接着把AVFrame的数据通过av_image_copy_to_buffer()拷贝出来,最后用opencv工具做转换。
以上提供的两种办法,在最后一步都是用CPU完成的,如果有更好的办法,欢迎留言或者私信交流。
笔者在这里踩过一些坑:
坑1:直接通过回调函数让解码器codec目标像素格式设置为自己需要的格式,比如BGR24。结果马上就报错了,解码根本执行不起来。
坑2:不做av_hwframe_transfer_data()直接sws_scale(),然后就coredump了。
问题2:数据在硬件设备和系统内存直接搬运,有没有更好的办法?
回答:看情况而定。一般来说经常建议采用av_hwframe_transfer_data(),这个函数内部会去调用硬解码内部的数据拷贝做搬运,但是实际效果往往视具体实现而异。比如笔者之前在做qsv的时候发现这个函数极其占用CPU资源(之后和Intel一起修复了这个问题);而在使用CUDA的时候,性能是可以接受的。
在上一问题的第2种解决方案中,如果你需要的格式正好是硬解码codec支持的,那么直接设置为这个目标格式,最后用av_image_copy_to_buffer()拷贝出来,可以省去av_hwframe_transfer_data()。
问题3:怎么知道硬件设备支持哪些目标像素格式?
回答:av_hwdevice_get_hwframe_constraints()!
这个函数可以获取到这个硬件设备的限制,如最大、最小长宽和目标像素格式。
写在后面
笔者在接触了2个平台的硬解码之后,发现他们各自的特性都不相同。而且,除了往更多的平台做扩展、探究和实践之外,硬解码工作实际上也有很多深度可挖掘的课题:
- 除了使用FFMPEG原生的硬解码器codec,其实也可以用户自己使用FFMPEG+厂商提供的编解码API共同完成;
- 如果是FFMPEG没有支持的硬解码方案,也可以自己在FFMPEG源码上增加hw相关的文件做集成。
- ... ...
本文如有任何纰漏或者不正确的地方,欢迎补充和纠正。也欢迎一起交流技术问题。:)
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)