NVDEC 视频解码器 API 编程指南
概述
NVIDIA GPU - 从 NVIDIA® Fermi™ 世代开始 - 包含一个视频解码引擎(在本文档中称为 NVDEC),它提供完全加速的硬件视频解码能力。NVDEC 可用于解码各种格式的码流:AV1、H.264、HEVC (H.265)、VP8、VP9、MPEG-1、MPEG-2、MPEG-4 和 VC-1。NVDEC 完全独立于计算/图形引擎运行。
NVIDIA 提供软件 API 和库,用于编程 NVDEC。软件 API,以下简称 NVDECODE API,使开发人员能够访问 NVDEC 的视频解码功能,并将 NVDEC 与 GPU 上的其他引擎互操作。
NVDEC 解码压缩的视频流,并将生成的 YUV 帧复制到显存。帧位于显存后,可以使用 CUDA 进行视频后处理。NVDECODE API 还提供 CUDA 优化的常用后处理操作实现,例如缩放、裁剪、宽高比转换、去隔行和色彩空间转换到许多流行的输出视频格式。客户端可以选择使用 NVDECODE API 提供的 CUDA 优化实现进行这些后处理步骤,或者选择在解码后的输出帧上实现自己的后处理。
解码后的视频帧可以通过图形互操作性呈现到显示器以进行视频播放,直接传递到专用硬件编码器 (NVENC) 以进行高性能视频转码,用于 GPU 加速推理,或由基于 CUDA 或 CPU 的处理进一步使用。
支持的编解码器
NVDECODE API 支持的编解码器有
- MPEG-1,
- MPEG-2,
- MPEG4,
- VC-1 (Simple/Main/Advanced Profile),
- H.264 (AVCHD) Baseline/Main/High/High10 (排除 MBAFF)/High422 (排除 MBAFF) Profile,
- H.265 (HEVC) (Main/Main10/Main12 Profile, Main 4:2:2/4:4:4 10/12 profile (排除 YUV400)),
- VP8,
- VP9(8bit, 10 bit and 12 bit),
- AV1 Main profile,
- 混合 (CUDA + CPU) JPEG
有关各种 GPU 视频功能的完整详细信息,请参阅第 2 章。
解码器管线由三个主要组件组成 - 解复用器、视频解析器和视频解码器。这些组件彼此独立,因此可以独立使用。NVDECODE API 为 NVIDIA 视频解析器和 NVIDIA 视频解码器提供 API。其中,NVIDIA 视频解析器纯粹是一个软件组件,如果需要,用户可以使用自己的解析器代替 NVIDIA 视频解析器。
图 1. 使用 NVDECODE API 的视频解码器管线

在高层次上,应遵循以下步骤使用 NVDECODEAPI 解码任何视频内容
- 创建 CUDA 上下文。
- 查询硬件解码器的解码能力。
- 创建解码器实例。
- 解复用内容(如 .mp4)。这可以使用 FFMPEG 等第三方软件完成。
- 使用 NVDECODE API 或第三方解析器(如 FFmpeg)提供的解析器解析视频码流。
- 使用 NVDECODE API 启动解码。
- 获取解码后的 YUV 以进行进一步处理。
- 查询解码帧的状态。
- 根据解码状态,将解码后的输出用于进一步处理,例如渲染、推理、后处理等。
- 如果应用程序需要显示输出,
- 将解码后的 YUV 表面转换为 RGBA。
- 将 RGBA 表面映射到 DirectX 或 OpenGL 纹理。
- 将纹理绘制到屏幕。
- 在解码过程完成后销毁解码器实例。
- 销毁 CUDA 上下文。
以上步骤在本文档的其余部分进行了解释,并在视频编解码器 SDK 包中包含的示例应用程序中进行了演示。
所有 NVDECODE API 都在两个头文件中公开:cuviddec.h
和 nvcuvid.h
。这些头文件可以在视频编解码器 SDK 包的 Interface
文件夹下找到。NVIDIA 视频编解码器 SDK 中的示例静态加载库(作为 Windows SDK 包的一部分提供)函数,并在源文件中包含 cuviddec.h
和 nvcuvid.h
。Windows DLL nvcuvid.dll
包含在 Windows 的 NVIDIA 显示驱动程序中。Linux 库 libnvcuvid.so
包含在 Linux 的 NVIDIA 显示驱动程序中。
本章的以下部分解释了应遵循的使用 NVDECODE API 加速解码的流程。
视频解析器
创建解析器
可以通过在填写结构 CUVIDPARSERPARAMS
后调用 cuvidCreateVideoParser()
来创建解析器对象。该结构应填充有关要解码的码流的以下信息:
-
CodecType:
必须来自enum cudaVideoCodec
,指示内容(如 H.264、HEVC、VP9 等)的编解码器类型。 -
ulMaxNumDecodeSurfaces:
这是解析器的 DPB(解码图像缓冲区)中的表面数量。此值在解析器初始化时可能未知,可以设置为虚拟数字(如 1)以创建解析器对象。应用程序必须注册一个回调pfnSequenceCallback
给驱动程序,当解析器遇到第一个序列头或序列中的任何更改时,驱动程序会调用该回调。此回调报告解析器的 DPB 正确解码所需的最小表面数量,该数量在CUVIDEOFORMAT::min_num_decode_surfaces
中。如果序列回调想要更新CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
,则序列回调可以将此值返回给解析器。然后,如果序列回调的返回值大于 1(请参阅下面有关pfnSequenceCallback
的描述),则解析器将使用序列回调返回的值覆盖CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
。因此,为了获得最佳内存分配,解码器对象创建应延迟到CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
已知时,以便可以使用所需的缓冲区数量创建解码器对象,从而使CUVIDDECODECREATEINFO::ulNumDecodeSurfaces
=CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
。 -
ulClockRate:
是以 Hz 为单位的时间戳单位(0=默认值=10000000Hz) -
ulErrorThreshold:
控制解析器中的非一致性码流检查。其有效范围为 0 到 100。0 表示严格检查,如果发现任何非一致性或错误,解析器将返回错误;100 表示忽略解析器中的所有非一致性码流检查。 -
ulMaxDisplayDelay:
最大显示回调延迟。0 = 无延迟 -
bAnnexb:
对于 AV1 annexB 码流,必须设置为 1 -
pfnSequenceCallback:
应用程序必须注册一个函数来处理任何序列更改。解析器为初始序列头或遇到视频格式更改时触发此回调。驱动程序将序列回调的返回值解释如下:- 0:失败
- 1:成功,但驱动程序不应覆盖
CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
- >1:成功,并且驱动程序应使用此返回值覆盖
CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
-
pfnDecodePicture:
当一个帧的码流数据准备就绪时,解析器会触发此回调。对于字段图像,每个显示调用可能有两个解码调用,因为两个字段构成一个帧。此回调的返回值解释为:- 0:失败
- ≥1:成功
-
pfnDisplayPicture
:当显示顺序中的帧准备就绪时,解析器会触发此回调。此回调的返回值解释为:- 0:失败
- ≥1:成功
-
pfnGetOperatingPoint
:解析器触发此回调以获取 AV1 可伸缩流的工作点。如果未设置pfnGetOperatingPoint
或返回值是 -1 或无效工作点,则解析器选择默认工作点为 0,并将 outputAllLayers 标志设置为 0。此回调的返回值解释为:- < 0:失败
- ≥0:成功(位 0-9:currOperatingPoint,位 10-10:bOutputAllLayer)
-
pfnGetSEIMsg
:当解析完帧的所有未注册用户 SEI 消息或元数据 OBU 时,解析器按解码顺序触发此回调。目前,H264、HEVC 和 AV1 编解码器支持此回调。此回调的返回值解释为:- 0:失败
- ≥1:成功
解析数据包
从解复用器提取的码流及其长度和一些其他辅助信息(如时间戳、标志)被打包到结构 CUVIDSOURCEDATAPACKET
中,称为数据包。此数据包使用 cuvidParseVideoData()
馈送到解析器。此数据包初始化为:
-
flags:
这些标志由应用程序设置,并由解析器解释如下:-
CUVID_PKT_ENDOFSTREAM:
必须与此码流的最后一个数据包一起设置。解析器将为显示队列中的所有挂起缓冲区触发显示回调。 -
CUVID_PKT_TIMESTAMP:
表示数据包中的时间戳有效。 -
CUVID_PKT_DISCONTINUITY:
如果存在任何不连续性(如搜索后的数据包),则应设置。 -
CUVID_PKT_ENDOFPICTURE:
当数据包恰好包含一个帧或一个字段数据时,必须设置。基于 NALU 的编解码器对于解码回调具有一个帧延迟,因为当接收到一些属于下一个帧的非 VCL NALU 时,解析器会检测到帧边界。此标志将强制解析器跳过此边界检查并立即触发解码回调。如果数据包具有不完整的数据,则将使用部分帧数据触发解码回调。如果数据包具有多个帧数据,则解析器将为第一个帧数据触发解码回调。其余的 NALU 将被丢弃。 -
CUVID_PKT_NOTIFY_EOS:
如果此标志与 CUVID_PKT_ENDOFSTREAM 一起设置,则将使用 CUVIDPARSERDISPINFO 的空值调用额外的(虚拟)显示回调,该回调应解释为码流的结尾。
-
-
payload_size:
表示有效负载中的字节数 -
payload:
指向码流内存缓冲区 -
timestamp:
显示时间戳(10MHz 时钟),仅当设置了CUVID_PKT_TIMESTAMP
标志时才有效
每当遇到相应的条件时,例如当序列参数发生更改时 pfnSequenceCallback
,或者当帧准备好解码时 pfnDecodePicturepicture
,解析器会从 cuvidParseVideoData()
中同步触发注册的回调。如果回调返回失败,则会通过 cuvidParseVideoData()
传播到应用程序。
解码结果与 CUVIDPICPARAMS
结构中的图片索引值相关联,该结构也由解析器提供。此图片索引稍后用于将解码帧映射到 CUDA 内存。
销毁解析器
用户需要调用 cuvidDestroyVideoParser()
来销毁解析器对象并释放所有已分配的资源。
视频解码器
查询解码能力
API cuvidGetDecoderCaps()
允许用户查询底层硬件视频解码器的功能。
如表 1 所示,不同的 GPU 具有功能不同的硬件解码器。因此,为了确保您的应用程序在所有世代的 GPU 硬件上都能正常工作,强烈建议应用程序查询硬件功能,并根据所需功能/功能的有无做出适当的决定。
API cuvidGetDecoderCaps()
允许用户查询底层硬件视频解码器的功能。调用线程应具有关联的有效 CUDA 上下文。
客户端需要在调用 cuvidGetDecoderCaps()
之前填写 CUVIDDECODECAPS
的以下字段。
-
eCodecType
:编解码器类型(AV1、H.264、HEVC、VP9、JPEG 等) -
eChromaFormat
:enum cudaVideoChromaFormat(Monochrome、420、422、444) -
nBitDepthMinus8
:0 代表 8 位,2 代表 10 位,4 代表 12 位
当调用 cuvidGetDecoderCaps
() 时,底层驱动程序会填写 CUVIDDECODECAPS
的其余字段,指示对查询的功能、支持的输出格式以及硬件支持的最大和最小分辨率的支持。
以下伪代码说明了如何查询 NVDEC 的功能。
CUVIDDECODECAPS decodeCaps = {};
// set IN params for decodeCaps
decodeCaps.eCodecType = cudaVideoCodec_HEVC;//HEVC
decodeCaps.eChromaFormat = cudaVideoChromaFormat_420;//YUV 4:2:0
decodeCaps.nBitDepthMinus8 = 2;// 10 bit
result = cuvidGetDecoderCaps(&decodeCaps);
API 返回的参数可以解释如下,以验证内容是否可以在底层硬件上解码
// Check if content is supported
if (!decodecaps.bIsSupported){
NVDEC_THROW_ERROR(Codec not supported on this GPU", CUDA_ERROR_NOT_SUPPORTED);
}
// validate the content resolution supported on underlying hardware
if ((coded_width > decodecaps.nMaxWidth) ||
(coded_height > decodecaps.nMaxHeight)){
NVDEC_THROW_ERROR(Resolution not supported on this GPU", CUDA_ERROR_NOT_SUPPORTED);
}
// Max supported macroblock count CodedWidth*CodedHeight/256 must be <= nMaxMBCount
if ((coded_width>>4)*(coded_height>>4) > decodecaps.nMaxMBCount){
NVDEC_THROW_ERROR(MBCount not supported on this GPU",
CUDA_ERROR_NOT_SUPPORTED);
}
在大多数情况下,解码器输出中使用的位深和色度二次采样与解码器输入(即内容中)相同。但是,在某些情况下,可能需要解码器生成与输入码流中使用的位深和色度二次采样不同的输出。一般来说,在创建解码器之前,最好先检查是否支持所需的输出位深和色度二次采样格式。这可以通过以下方式完成
// Check supported output format
if (decodecaps.nOutputFormatMask & (1<<cudaVideoSurfaceFormat_NV12)){
// Decoder supports output surface format NV12
}
if (decodecaps.nOutputFormatMask & (1<<cudaVideoSurfaceFormat_P010){
// Decoder supports output surface format P010
}
……
API cuvidGetDecoderCaps()
还返回底层 GPU 的直方图相关功能。直方图数据由 NVDEC 在解码过程中收集,从而实现零性能损失。NVDEC 仅计算解码输出的亮度分量的直方图数据,而不是后处理帧(即应用缩放、裁剪等时)。在 AV1 的情况下,当启用胶片增益时,直方图数据是在应用胶片颗粒之前在解码帧上收集的。
// Check if histogram is supported
if (decodecaps.bIsHistogramSupported){
nCounterBitDepth = decodecaps.nCounterBitDepth; // histogram counter bit depth
nMaxHistogramBins = decodecaps.nMaxHistogramBins; // Max number of histogram bins
}
……
直方图数据计算为:Histogram_Bin[pixel_value >> (pixel_bitDepth - log2(nMaxHistogramBins))]++;
创建解码器
在创建解码器实例之前,用户需要具有有效的 CUDA 上下文,该上下文将在整个解码过程中使用。
可以通过在填写结构 CUVIDDECODECREATEINFO
后调用 cuvidCreateDecoder()
来创建解码器实例。结构 CUVIDDECODECREATEINFO
应填充有关要解码的码流的以下信息:
-
CodecType:
必须来自enum cudaVideoCodec
。它表示内容(如 H.264、HEVC、VP9 等)的编解码器类型。 -
ulWidth, ulHeight
:编码宽度和编码高度(以像素为单位)。 -
ulMaxWidth, ulMaxHeight
:解码器在分辨率更改的情况下支持的最大宽度和最大高度。当视频流中发生分辨率更改(新分辨率 <= ulMaxWidth, ulMaxHeight)时,应用程序可以使用 cuvidReconfigureDecoder() API 重新配置解码器,而不是销毁并重新创建解码器。如果 ulMaxWidth 或 ulMaxHeight 设置为 0,则 ulMaxWidth 和 ulMaxHeight 分别设置为 ulWidth 和 ulHeight。 -
ChromaFormat:
必须来自enum cudaVideoChromaFormat.
它表示内容的色度格式,如 4:2:0、4:4:4 等。 -
bitDepthMinus8
:要解码的视频流的位深减 8,例如 0 代表 8 位,2 代表 10 位,4 代表 12 位。 -
ulNumDecodeSurfaces:
在本文档的其他地方称为解码表面,这是驱动程序将在内部分配用于存储解码帧的表面数量。使用更高的数字可以确保更好的流水线处理,但会增加 GPU 内存消耗。为了正确操作,最小值在CUVIDEOFORMAT::min_num_decode_surfaces
中定义,可以从 Nvidia 解析器的第一个序列回调中获得。NVDEC 引擎将解码后的数据写入这些表面之一。这些表面无法由 NVDECODE API 的用户访问,但映射阶段(包括解码器输出格式转换、缩放、裁剪等)使用这些表面作为输入表面。 -
ulNumOutputSurfaces:
这是客户端将同时映射到解码表面的最大输出表面数量,以便使用cuvidMapVideoFrame().
进行进一步处理。这些表面具有客户端要使用的后处理解码输出。驱动程序在内部分配相应数量的表面(在本文档中称为输出表面)。客户端将有权访问输出表面。有关映射定义的理解,请参阅准备解码帧以进行进一步处理部分。 -
OutputFormat
:输出表面格式定义为enum cudaVideoSurfaceFormat.
此输出格式必须是在cuvidGetDecoderCaps().
中的decodecaps.nOutputFormatMask
中获得的受支持格式之一。如果传递了不受支持的输出格式,API 将失败并显示错误CUDA_ERROR_NOT_SUPPORTED.
-
ulTargetWidth, ulTargetHeight:
这是输出表面的分辨率。对于不涉及缩放的用例,这些应分别设置为ulWidth, ulHeight
。 -
DeinterlaceMode
:对于逐行内容,应将其设置为cudaVideoDeinterlaceMode_Weave
或cudaVideoDeinterlaceMode_Bob
,对于隔行内容,应将其设置为cudaVideoDeinterlaceMode_Adaptive
。cudaVideoDeinterlaceMode_Adaptive
可以产生更好的质量,但会增加内存消耗。 -
ulCreationFlags
:它定义为enum cudaVideoCreateFlags
。显式定义此标志是可选的。如果未定义,驱动程序将选择适当的模式。 -
ulIntraDecodeOnly:
将此标志设置为 1,以指示驱动程序正在解码的内容仅包含 I/IDR 帧。这有助于驱动程序优化内存消耗。如果内容包含非帧内帧,请勿设置此标志。 -
enableHistogram:
将此标志设置为 1 以启用直方图数据收集。
cuvidCreateDecoder()
调用会填充 CUvideodecoder
,其中包含应保留到解码会话处于活动状态的解码器句柄。该句柄需要与其他 NVDECODE API 调用一起传递。
用户还可以在 CUVIDDECODECREATEINFO
中指定以下参数来控制最终输出:
- 缩放尺寸
- 裁剪尺寸
- 用户想要更改宽高比的尺寸
以下代码演示了在缩放、裁剪或宽高比转换的情况下解码器的设置。
// Scaling. Source size is 1280x960. Scale to 1920x1080.
CUresult rResult;
unsigned int uScaleW, uScaleH;
uScaleW = 1920;
uScaleH = 1080;
...
CUVIDDECODECREATEINFO stDecodeCreateInfo;
memset(&stDecodeCreateInfo, 0, sizeof(CUVIDDECODECREATEINFO));
... // Setup the remaining structure members
stDecodeCreateInfo.ulTargetWidth = uScaleWidth;
stDecodeCreateInfo.ulTargetHeight = uScaleHeight;
rResult = cuvidCreateDecoder(&hDecoder, &stDecodeCreateInfo);
...
// Cropping. Source size is 1280x960
CUresult rResult;
unsigned int uCropL, uCropR, uCropT, uCropB;
uCropL = 30;
uCropR = 700;
uCropT = 20;
uCropB = 500;
...
CUVIDDECODECREATEINFO stDecodeCreateInfo;
memset(&stDecodeCreateInfo, 0, sizeof(CUVIDDECODECREATEINFO));
// Setup the remaining structure members
...
stDecodeCreateInfo.display_area.left = uCropL;
stDecodeCreateInfo.display_area.right = uCropR;
stDecodeCreateInfo.display_area.top = uCropT;
stDecodeCreateInfo.display_are.bottom = uCropB;
rResult = cuvidCreateDecoder(&hDecoder, &stDecodeCreateInfo);
...
// Aspect Ratio Conversion. Source size is 1280x960(4:3). Convert to
// 16:9
CUresult rResult;
unsigned int uCropL, uCropR, uCropT, uCropB;
uDispAR_L = 0;
uDispAR_R = 1280;
uDispAR_T = 70;
uDispAR_B = 790;
...
CUVIDDECODECREATEINFO stDecodeCreateInfo;
memset(&stDecodeCreateInfo, 0, sizeof(CUVIDDECODECREATEINFO));
... // setup structure members
stDecodeCreateInfo.target_rect.left = uDispAR_L;
stDecodeCreateInfo.target_rect.right = uDispAR_R;
stDecodeCreateInfo.target_rect.top = uDispAR_T;
stDecodeCreateInfo.target_rect.bottom = uDispAR_B;
reResult = cuvidCreateDecoder(&hDecoder, &stDecodeCreateInfo);
...
解码帧/字段
在解复用和解析之后,客户端可以提交包含帧或字段数据的码流到硬件进行解码。要完成此操作,需要遵循以下步骤
- 填写
CUVIDPICPARAMS
结构。- 客户端需要使用在解析过程中导出的参数填写结构。
CUVIDPICPARAMS
包含特定于每个受支持的编解码器的结构,也应填写该结构。
- 客户端需要使用在解析过程中导出的参数填写结构。
- 调用
cuvidDecodePicture()
并传递解码器句柄和指向 CUVIDPICPARAMS 的指针。cuvidDecodePicture()
启动 NVDEC 上的解码。
准备解码帧以进行进一步处理
用户需要调用 cuvidMapVideoFrame()
以获取 CUDA 设备指针和保存解码和后处理帧的输出表面的步幅。
请注意,cuvidDecodePicture()
指示 NVDEC 硬件引擎启动帧/字段的解码。但是,成功完成 cuvidMapVideoFrame()
表示解码过程已完成,并且解码后的 YUV 帧已从 NVDEC 生成的格式转换为 CUVIDDECODECREATEINFO::OutputFormat
中指定的 YUV 格式。
cuvidMapVideoFrame()
API 将解码表面索引 (nPicIdx
) 作为输入,并将其映射到可用的输出表面之一,对解码帧进行后处理并复制到输出表面,然后返回 CUDA 设备指针和关联的输出表面步幅。
由 cuvidMapVideoFrame()
执行的上述操作在本文档中称为映射。
在用户完成对帧的处理后,必须调用 cuvidUnmapVideoFrame()
以使输出表面可用于存储其他解码和后处理帧。
如果用户在 cuvidMapVideoFrame()
后连续未能调用相应的 cuvidUnmapVideoFrame()
,则 cuvidMapVideoFrame()
最终将失败。一次最多可以映射 CUVIDDECODECREATEINFO::ulNumOutputSurfaces
帧。
cuvidMapVideoFrame()
是一个阻塞调用,因为它会等待解码完成。如果在与 cuvidDecodePicture()
相同的 CPU 线程上调用 cuvidMapVideoFrame()
,它也会阻塞 cuvidDecodePicture()
。在这种情况下,应用程序将无法向 NVDEC 提交解码数据包,直到映射完成。可以通过在与调用 cuvidDecodePicture()
的 CPU 线程(称为解码线程)不同的 CPU 线程(称为映射线程)上执行映射操作来避免这种情况。
从 NVDECODE API 使用 NVIDIA 解析器时,应用程序可以在解码线程(作为生产者)和映射线程(作为消费者)之间实现生产者-消费者队列。该队列可以包含正在解码的帧的图片索引(或其他唯一标识符)。解析器可以在解码线程上运行。解码线程可以将图片索引添加到显示回调中的队列,并立即从回调返回,以继续解码后续可用的帧。另一方面,映射线程将监视队列。如果它看到队列具有非零长度,它将从队列中移除条目,并使用 nPicIdx
作为图片索引调用 cuvidMapVideoFrame(…)
。解码线程必须确保在映射线程使用并释放相应的解码图片缓冲区的条目之前,不重复使用该缓冲区来存储解码输出。
以下代码演示了如何使用 cuvidMapVideoFrame()
和 cuvidUnmapVideoFrame()
。
// MapFrame: Call cuvidMapVideoFrame and get the devptr and associated
// pitch. Copy this surface (in device memory) to host memory using
// CUDA device to host memcpy.
bool MapFrame()
{
CUVIDPARSEDISPINFO stDispInfo;
CUVIDPROCPARAMS stProcParams;
CUresult rResult;
unsigned long long cuDevPtr = 0;
int nPitch, nPicIdx, frameSize;
unsigned char* pHostPtr = nullptr;
memset(&stDispInfo, 0, sizeof(CUVIDPARSEDISPINFO));
memset(&stProcParams, 0, sizeof(CUVIDPROCPARAMS));
/*************************************************
* setup stProcParams
**************************************************/
// retrieve the frames from the Frame Display Queue. This Queue is
// is populated in HandlePictureDisplay.
if (g_pFrameQueue->dequeue(&stDispInfo))
{
nPicIdx = stDispInfo.picture_index;
rResult = cuvidMapVideoFrame(&hDecoder, nPicIdx, &cuDevPtr,
&nPitch, &stProcParams);
frameSize = (ChromaFormat == cudaVideoChromaFormat_444) ? nPitch * (3*nheight) :
nPitch * (nheight + (nheight + 1) / 2);
// use CUDA based Device to Host memcpy
rResult = cuMemAllocHost((void** )&pHostPtr, frameSize);
if (pHostPtr)
{
rResult = cuMemcpyDtoH(pHostPtr, cuDevPtr, frameSize);
}
rResult = cuvidUnmapVideoFrame(&hDecoder, cuDevPtr);
}
... // Dump YUV to a file
if (pHostPtr)
{
cuMemFreeHost(pHostPtr);
}
...
}
在多实例解码用例中,NVDEC 可能会成为瓶颈,因此在不同的 CPU 线程上调用 cuvidMapVideoFrame()
和 cuvidDecodePicture()
不会带来显著的好处。cuvidDecodePicture()
将在驱动程序内部的 NVDEC 等待队列已满时停滞。视频编解码器 SDK 中的示例应用程序为了简单起见,在同一 CPU 线程上使用映射和解码调用。
获取直方图数据缓冲区
直方图数据由 NVDEC 在解码过程中收集,不会产生性能损失。NVDEC 仅计算解码输出的亮度分量的直方图数据,而不是后处理帧(即,应用缩放、裁剪等时)。对于 AV1,当启用胶片颗粒时,直方图数据在应用胶片颗粒之前在解码帧上收集。
如果创建解码器时(使用 API cuvidCreateDecoder()
)设置了 CUVIDDECODECREATEINFO::enableHistogram
标志,则 cuvidMapVideoFrame()
API 返回直方图数据缓冲区的 CUDA 设备指针以及输出表面。直方图缓冲区的 CUDA 设备指针可以从 CUVIDPROCPARAMS::histogram_dptr
中获得。
直方图缓冲区在驱动程序中映射到输出缓冲区,因此 cuvidUnmapVideoFrame()
也同时取消映射直方图缓冲区和输出表面。
以下代码演示了如何使用 cuvidMapVideoFrame()
和 cuvidUnmapVideoFrame()
来访问直方图缓冲区。
// MapFrame: Call cuvidMapVideoFrame and get the output frame and associated
// histogram buffer CUDA device pointer
CUVIDPROCPARAMS stProcParams;
CUresult rResult;
unsigned long long cuOutputFramePtr = 0, cuHistogramPtr = 0;
int nPitch;
int histogram_size = (decodecaps.nCounterBitDepth / 8) *
decodecaps.nMaxHistogramBins;
unsigned char *pHistogramPtr = nullptr;
memset(&stProcParams, 0, sizeof(CUVIDPROCPARAMS));
/*************************************************
* setup stProcParams
**************************************************/
stProcParams.histogram_dptr = &cuHistogramPtr;
rResult = cuvidMapVideoFrame(&hDecoder, nPicIdx, &cuOutputFramePtr,
&nPitch, &stProcParams);
// allocate histogram buffer for cuMemcpy
rResult = cuMemAllocHost((void** )&pHistogramPtr, histogram_size);
if (pHistogramPtr)
{
rResult = cuMemcpyDtoH(pHistogramPtr, cuHistogramPtr, histogram_size);
}
// unmap output frame
rResult = cuvidUnmapVideoFrame(&hDecoder, cuOutputFramePtr);
...
}
查询解码状态
解码启动后,可以随时调用 cuvidGetDecodeStatus()
以查询该帧的解码状态。底层驱动程序在 CUVIDGETDECODESTATUS::*pDecodeStatus
中填充解码状态。
NVDECODEAPI 当前报告以下状态
- 解码正在进行中。
- 帧解码成功完成。
- 帧的码流已损坏,并被 NVDEC 隐藏。
- 帧的码流已损坏,但无法被 NVDEC 隐藏。
预计此 API 将在客户端需要根据帧的解码状态做出进一步决策的场景中提供帮助,例如,是否对帧执行推理。
请注意,NVDEC 可以检测到有限数量的错误,具体取决于编解码器。Maxwell 及更高代 GPU 上的 HEVC、H264 和 JPEG 支持此 API。
重新配置解码器
如果码流的分辨率和/或后处理参数发生变化,用户可以使用 cuvidReconfigureDecoder()
重新配置解码器,而无需销毁正在进行的解码器实例并创建一个新的实例,从而节省过程中的时间(和延迟)。
在早期的 SDK 中,用户必须销毁现有的解码器实例并创建一个新的解码器实例,以处理解码器分辨率或后处理参数(如缩放比例、裁剪尺寸等)的任何变化。
该 API 可用于码流分辨率发生变化的场景,例如,当编码器(在服务器端)频繁更改图像分辨率以符合服务质量 (QoS) 约束时。
使用 cuvidReconfigureDecoder()
需要遵循以下步骤。
- 用户需要在调用
cuvidCreateDecoder()
时指定CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
。用户应选择CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
的值,以确保在整个解码过程中永远不会超过码流的分辨率。请注意,CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
的值在一个会话中不能更改,如果用户想要更改这些值,则应销毁并重新创建解码会话。 - 在解码过程中,当用户需要更改码流或更改后处理参数时,用户需要调用
cuvidReconfigureDecoder()
。理想情况下,当码流更改时,应从CUVIDPARSERPARAMS::pfnSequenceCallback
中进行此调用。用户想要重新配置的参数应填写在::CUVIDRECONFIGUREDECODERINFO
中。请注意,CUVIDRECONFIGUREDECODERINFO::ulWidth
和CUVIDRECONFIGUREDECODERINFO::ulHeight
必须等于或小于CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
,否则cuvidReconfigureDecoder()
将会失败。
所有 NVDECODEAPI 支持的编解码器都支持此 API。
销毁解码器
用户需要调用 cuvidDestroyDecoder()
来销毁解码器会话并释放所有已分配的解码器资源。
Nvidia 库的运行时动态链接
视频编解码器 SDK 示例应用程序使用了两个主要的 Nvidia 库:nvcuvid 和 cuda。这两个库都可以用作加载时动态链接或运行时动态链接。视频编解码器 SDK 示例应用程序使用加载时动态链接。如果需要,用户可以使用这些库的运行时动态链接。以下代码片段可以帮助理解编程风格中需要的更改
运行时动态链接
在运行时动态链接的情况下,库在运行时加载到内存中。以下是在 Windows 和 Linux 系统上运行时动态加载 nvcuvid 库的代码片段
#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64)
#include <Windows.h>
#ifdef UNICODE
static LPCWSTR __DriverLibName = L"nvcuvid.dll";
#else
static LPCSTR __DriverLibName = "nvcuvid.dll";
#endif
typedef HMODULE DLLDRIVER;
static CUresult LOAD_LIBRARY(DLLDRIVER *pInstance)
{
*pInstance = LoadLibrary(__DriverLibName);
if (*pInstance == NULL)
{
printf("LoadLibrary \"%s\" failed!\n", __DriverLibName);
return CUDA_ERROR_UNKNOWN;
}
return CUDA_SUCCESS;
}
#elif defined(__unix__) || defined(__APPLE__) || defined(__MACOSX)
#include <dlfcn.h>
static char __DriverLibName[] = "libnvcuvid.so";
typedef void *DLLDRIVER;
static CUresult LOAD_LIBRARY(DLLDRIVER *pInstance)
{
*pInstance = dlopen(__DriverLibName, RTLD_NOW);
if (*pInstance == NULL)
{
printf("dlopen \"%s\" failed!\n", __DriverLibName);
return CUDA_ERROR_UNKNOWN;
}
return CUDA_SUCCESS;
}
#endif
获取函数指针
可以使用 Windows 上的 GetProcAddress()
和 Linux 上的 dlsym()
获取函数指针
typedef CUresult CUDAAPI tcuvidCreateVideoParser(CUvideoparser *pObj, CUVIDPARSERPARAMS *pParams);
typedef CUresult CUDAAPI tcuvidParseVideoData(CUvideoparser obj, CUVIDSOURCEDATAPACKET *pPacket);
typedef CUresult CUDAAPI tcuvidDestroyVideoParser(CUvideoparser obj);
typedef CUresult CUDAAPI tcuvidGetDecoderCaps(CUVIDDECODECAPS *pdc);
typedef CUresult CUDAAPI tcuvidCreateDecoder(CUvideodecoder *phDecoder, CUVIDDECODECREATEINFO *pdci);
typedef CUresult CUDAAPI tcuvidDestroyDecoder(CUvideodecoder hDecoder);
typedef CUresult CUDAAPI tcuvidDecodePicture(CUvideodecoder hDecoder, CUVIDPICPARAMS *pPicParams);
tcuvidCreateVideoParser *cuvidCreateVideoParser;
tcuvidParseVideoData *cuvidParseVideoData;
tcuvidDestroyVideoParser *cuvidDestroyVideoParser;
tcuvidGetDecoderCaps *cuvidGetDecoderCaps;
tcuvidCreateDecoder *cuvidCreateDecoder;
tcuvidDestroyDecoder *cuvidDestroyDecoder;
tcuvidDecodePicture *cuvidDecodePicture;
#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64)
#define GET_PROC_EX(name, alias, required) \
alias = (t##name *)GetProcAddress(DriverLib, #name); \
if (alias == NULL && required) { \
printf("Failed to find required function \"%s\" in %s\n", \
#name, __DriverLibName); \
return CUDA_ERROR_UNKNOWN; \
}
#elif defined(__unix__) || defined(__APPLE__) || defined(__MACOSX)
#define GET_PROC_EX(name, alias, required) \
alias = (t##name *)dlsym(DriverLib, #name); \
if (alias == NULL && required) { \
printf("Failed to find required function \"%s\" in %s\n", \
#name, __DriverLibName); \
return CUDA_ERROR_UNKNOWN; \
}
#endif
#define GET_PROC_REQUIRED(name) GET_PROC_EX(name,name,1)
#define GET_PROC_OPTIONAL(name) GET_PROC_EX(name,name,0)
#define GET_PROC(name) GET_PROC_REQUIRED(name)
#define CHECKED_CALL(call) \
do { \
CUresult result = (call); \
if (CUDA_SUCCESS != result) { \
return result; \
} \
} while(0)
CUresult CUDAAPI cuvidInit(unsigned int Flags)
{
DLLDRIVER DriverLib;
CHECKED_CALL(LOAD_LIBRARY(&DriverLib));
// fetch all function pointers
GET_PROC(cuvidCreateVideoParser);
GET_PROC(cuvidParseVideoData);
GET_PROC(cuvidDestroyVideoParser);
GET_PROC(cuvidGetDecoderCaps);
GET_PROC(cuvidCreateDecoder);
GET_PROC(cuvidDestroyDecoder);
GET_PROC(cuvidDecodePicture);
// fetch other functions pointers
return CUDA_SUCCESS;
}
编写高效的解码应用程序
NVIDIA GPU 上的 NVDEC 引擎是一个专用硬件模块,它以支持的格式解码输入视频码流。一个典型的视频解码应用程序大致包含以下阶段
- 解复用
- 视频码流解析和解码
- 准备帧以进行进一步处理
其中,解复用和解析不是硬件加速的,因此不在本文档的范围内。解复用可以使用第三方组件(如 FFmpeg)执行,FFmpeg 为许多复用视频格式提供支持。SDK 中包含的示例应用程序演示了使用 FFmpeg 进行解复用。
类似地,后解码或视频后处理(如缩放、色彩空间转换、降噪、色彩增强等)可以使用用户定义的 CUDA 内核有效地执行。
然后,如果需要,可以将后处理帧发送到显示引擎以在屏幕上显示。请注意,此操作不在 NVDECODE API 的范围内。
优化的实现应为解复用、解析、码流解码和处理等使用独立的线程,如下所述
- 解复用:此线程解复用媒体文件,并使原始码流可供解析器使用。
- 解析和解码:此线程执行码流的解析,并通过调用
cuvidDecodePicture()
启动解码。 - 映射和使帧可用于进一步处理:此线程检查是否有任何解码帧可用。如果有,则应调用
cuvidMapVideoFrame()
以获取帧的 CUDA 设备指针和步幅。然后,可以将帧用于进一步处理。
NVDEC 驱动程序内部维护一个 4 帧的队列,用于高效的流水线操作。请注意,此流水线并不意味着解码有任何延迟。解码在第一个帧排队后立即开始,但应用程序可以继续排队输入帧,只要有可用空间而不会停滞。通常,当应用程序排队 2-3 帧时,第一个帧的解码已完成,流水线继续进行。此流水线确保硬件解码器得到最大程度的利用。
对于性能密集型和低延迟视频编解码器应用程序,请确保 PCIE 链路宽度设置为最大可用值。当前配置的 PCIE 链路宽度可以通过运行命令“nvidia-smi -q”获得。PCIE 链路宽度可以在系统的 BIOS 设置中配置。
在使用案例中,如果解码分辨率和/或后处理参数频繁更改,建议使用 cuvidReconfigureDecoder()
而不是销毁现有解码器实例并重新创建一个新的实例。
应遵循以下步骤来优化视频内存使用
- 使
CUVIDDECODECREATEINFO::ulNumDecodeSurfaces = CUVIDEOFORMAT:: min_num_decode_surfaces
。这将确保底层驱动程序分配最少数量的解码表面,以正确解码序列。如果解码器性能下降,客户端可以稍微增加CUVIDDECODECREATEINFO::ulNumDecodeSurfaces
。因此,建议选择CUVIDDECODECREATEINFO::ulNumDecodeSurfaces
的最佳值,以确保解码器吞吐量和内存消耗之间的适当平衡。 -
CUVIDDECODECREATEINFO::ulNumOutputSurfaces
应在经过适当的实验后进行优化,以平衡解码器吞吐量和内存消耗。 -
CUVIDDECODECREATEINFO::DeinterlaceMode
应设置为“cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Weave
”或“cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Bob
”。对于隔行扫描内容,选择cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Adaptive
会产生更高的质量,但会增加内存消耗。使用cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Weave
或cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Bob
会产生最小的内存消耗,尽管这可能会导致较低的视频质量。如果客户端未指定“CUVIDDECODECREATEINFO::DeinterlaceMode
”,则底层显示驱动程序会将其设置为“cudaVideoDeinterlaceMode::cudaVideoDeinterlaceMode_Adaptive
”,这会导致更高的内存消耗。因此,强烈建议根据需求选择CUVIDDECODECREATEINFO::DeinterlaceMode
的正确值。 - 在解码多个流时,建议分配最少数量的 CUDA 上下文并在会话之间共享它。这可以节省与 CUDA 上下文创建相关的内存开销。
-
如果事先知道序列仅包含帧内帧,则应将
CUVIDDECODECREATEINFO::ulIntraDecodeOnly
设置为 1。此功能仅 HEVC、H.264 和 VP9 支持。但是,如果对于具有 P 帧和/或 B 帧的常规码流,启用了该标志,则解码可能会失败。
视频编解码器 SDK 随附的示例应用程序旨在演示各种 API 的功能,但它们可能未完全优化。因此,强烈建议程序员确保他们的应用程序设计良好,解码-后处理-显示流水线中的各个阶段以高效的方式构建,以实现所需的性能和内存消耗。
动态分配解码表面
码流序列 (SPS) 通常具有比必要更大的 DPB 大小。在大多数情况下,并非所有这些表面都是必需的。因此,可以使用以下方法来减少解码会话期间使用的解码表面数量。这将有助于降低大多数会话的内存占用,从而允许增加同时解码会话的数量。但是,单个会话的解码吞吐量可能会略有降低,因为表面将被更频繁地重用。
应遵循以下步骤来进一步优化视频内存使用
- 在调用
cuvidCreateVideoParser()
之前,设置CUVIDPARSERPARAMS::bMemoryOptimize = 1
。这将确保解析器发送尽可能低的CUVIDPICPARAMS::CurrPicIdx
。 -
pfnSequenceCallback()
具有min_num_decode_surfaces
信息。从pfnSequenceCallback()
返回min_num_decode_surfaces
值。这会将 DPB 大小覆盖为min_num_decode_surfaces
。 - 为
CUVIDDECODECREATEINFO::ulNumDecodeSurfaces
分配一个小于min_num_decode_surfaces
的值,最小值为 1。接下来,使用cuvidCreateDecoder()
创建一个解码器。这将创建一个解码表面数量减少的解码器。 - 在
pfnDecodeCallback()
中,检查解析器发送的CUVIDPICPARAMS::CurrPicIdx
的值。- 如果
CUVIDPICPARAMS::CurrPicIdx
小于ulNumDecodeSurfaces
,则继续使用cuvidDecodePicture()
API 进行解码。 - 如果
CUVIDPICPARAMS::CurrPicIdx
大于或等于ulNumDecodeSurfaces
,则调用cuvidReconfigureDecoder()
,并增加CUVIDRECONFIGUREDECODERINFO::ulNumDecodeSurfaces
以增加解码表面的数量。然后,使用cuvidCreateDecoder()
继续解码帧。注意:如果cuvidReconfigureDecoder()
失败并出现CUDA_ERROR_OUT_OF_MEMORY
错误,应用程序可以重试一段时间,如果cuvidReconfigureDecoder()
继续失败,则应用程序需要处理该失败。
- 如果
声明
本文档仅供参考,不应视为对产品的特定功能、状况或质量的保证。NVIDIA Corporation(“NVIDIA”)对本文档中包含的信息的准确性或完整性不作任何明示或暗示的陈述或保证,并且对本文中包含的任何错误不承担任何责任。NVIDIA 对因使用此类信息或因使用此类信息而可能导致的专利或第三方其他权利的侵犯的后果或使用不承担任何责任。本文档不构成对开发、发布或交付任何材料(下文定义)、代码或功能的承诺。
NVIDIA 保留在不另行通知的情况下随时对本文档进行更正、修改、增强、改进和任何其他更改的权利。
客户在下订单之前应获取最新的相关信息,并应核实此类信息是当前且完整的。
NVIDIA 产品根据订单确认时提供的 NVIDIA 标准销售条款和条件进行销售,除非 NVIDIA 和客户的授权代表签署的单独销售协议(“销售条款”)另有约定。NVIDIA 在此明确反对将任何客户通用条款和条件应用于购买本文档中引用的 NVIDIA 产品。本文档未直接或间接地形成任何合同义务。
NVIDIA 产品并非设计、授权或保证适用于医疗、军事、航空、航天或生命支持设备,也不适用于 NVIDIA 产品的故障或失灵可能合理预期会导致人身伤害、死亡或财产或环境损害的应用。NVIDIA 对 NVIDIA 产品包含和/或用于此类设备或应用不承担任何责任,因此此类包含和/或使用由客户自行承担风险。
NVIDIA 不作任何陈述或保证,保证基于本文档的产品将适用于任何特定用途。NVIDIA 不一定会对每个产品的所有参数进行测试。客户全权负责评估和确定本文档中包含的任何信息的适用性,确保产品适用于并符合客户计划的应用,并执行应用所需的测试,以避免应用或产品的默认设置。客户产品设计中的缺陷可能会影响 NVIDIA 产品的质量和可靠性,并可能导致超出本文档中包含的附加或不同的条件和/或要求。NVIDIA 对可能基于或归因于以下原因的任何默认设置、损坏、成本或问题不承担任何责任:(i) 以任何违反本文档的方式使用 NVIDIA 产品或 (ii) 客户产品设计。
商标
NVIDIA、NVIDIA 徽标以及 cuBLAS、CUDA、CUDA Toolkit、cuDNN、DALI、DIGITS、DGX、DGX-1、DGX-2、DGX Station、DLProf、GPU、Jetson、Kepler、Maxwell、NCCL、Nsight Compute、Nsight Systems、NVCaffe、NVIDIA Deep Learning SDK、NVIDIA Developer Program、NVIDIA GPU Cloud、NVLink、NVSHMEM、PerfWorks、Pascal、SDK Manager、Tegra、TensorRT、TensorRT Inference Server、Tesla、TF-TRT、Triton Inference Server、Turing 和 Volta 是 NVIDIA Corporation 在美国和其他国家/地区的商标和/或注册商标。其他公司和产品名称可能是与其关联的各自公司的商标。