版权声明:本文为CSDN博主「LIEYz」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_18998145/article/details/108535188
概述
从 NVIDIA® Fermi™ 一代开始,英伟达GPU包含一个视频解码器引擎(本文中简称NVDEC),它提供全硬件加速解码视频的能力。NVDEC能解码许多码流格式: H.264, HEVC (H.265), VP8, VP9, MPEG-1, MPEG-2, MPEG-4 and VC-1. NVDEC运行完全独立于计算/图像引擎。
英伟达提供编程NVDEC的软件API和库。这套软件API,以下称为NVDECODE API,它使开发者能访问NVDEC的解码特性,并且能让NVDEC与GPU上的其它引擎交互。
NVDEC解码压缩的视频流,然后复制解码后的YUV帧到显存,因此CUDA可以后续处理位于显存中的YUV帧。对于大多数流行的输出视频格式,NVDECODE API 也提供一些常用的CUDA优化的后处理,比如缩放、裁剪、宽高比转换、反交错和色彩空间转换。
解码后的视频帧可以传递给显示器播放,直接传给NVENC进行高性能视频转码,用于GPU加速的推理,通过CUDA或者基于CPU后续处理。
NVDECODE API 支持的解码格式如下:
‣ MPEG-1,
‣ MPEG-2,
‣ MPEG4,
‣ VC-1,
‣ H.264 (AVCHD) (8 bit),
‣ H.265 (HEVC) (8bit, 10 bit and 12 bit),
‣ VP8,
‣ VP9(8bit, 10 bit and 12 bit),
‣ Hybrid (CUDA + CPU) JPEG
视频解码能力
下表展示每种GPU架构支持的解码格式和视频解码能力。
[1] 只支持 GP104, Turing 和 GA100
[2] GP10x GPUs 支持 VP9 10-bit 和 12-bit 解码
视频解码管道
解码管道包含三个主要的组件:解封装器,视频解析器,视频解码器。每个组件互不依赖因此能独立使用。NVDECODE API提供英伟达视频解析器和英伟达视频解码器API。英伟达视频解析器是一个纯软件组件,用户可以用自己的插件替换(比如ffmpeg解析器)。
使用NVDECODE API 解码视频的步骤大致如下:
- 建一个CUDA上下文。
- 查询硬件解码器的解码能力。
- 创建解码器实例。
- 解封装,这个可以使用第三方软件,如FFMPEG。
- 使用NVDECODE API 提供的解析器提取视频流,或者第三方解析器如FFMPEG。
- 使用NVDECODE API 开始解码。
- 获得解码后的YUV数据,用于后续处理。
- 查询解码帧的状态。
- 根据解码帧的状态,使用解码后的输出进行后续处理,比如:渲染,推理,后加工等。
- 如果应用程序需要显示解码后的输出:
将解码后的YUV 转换为RGBA
映射RGBA 到 DirectX or OpenGL 纹理
在屏幕上绘制纹理 - 完成解码过程后销毁解码器实例 。
- 销毁CUDA上下文。
使用英伟达视频解码器
所有的 NVDECODE APIs 声明在两个头文件中: cuviddec.h 和 nvcuvid.h。这些头文件存放在Video Codec SDK中的 …\Samples\NvCodec\NvDecoder 目录下。 NVIDIA Video Codec SDK 中的示例静态加载库函数,在源文件中包含cuviddec.h 和nvcuvid.h ,SDK包中包含静态库文件。windows 动态库nvcuvid.dll 包含在windows显卡驱动中,linux 动态库libnvcuvid.so 则包含在linux显卡驱动中。
视频解析器
创建解析器
填充好 CUVIDPARSERPARAMS
结构体后,通过调用 cuvidCreateVideoParser()
可以创建解析器对象。这个结构体应该填充好以下关于需要解码的流的信息。
-
CodecType :必须取值于
enum cudaVideoCodec
,指示解码的类型,比如 H.264, HEVC, VP9等 -
ulMaxNumDecodeSurfaces:这是解析器的解码图片缓存的表面数量。这个值在解析器初始化时可能不知道,在创建解析器对象时可以预先设置成1. 应用必须向驱动注册一个回调函数
pfnSequenceCallback
,当解析器遇到第一个序列头或者这个序列发生了任何变化时会回调这个函数。这个回调函数通过参数CUVIDEOFORMAT::min_num_decode_surfaces
报告为了正确解码,解析器的解码图片缓存需要的最小的表面数量。如果想更新CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
,这个序列回调函数可能返回这个值给解析器,如果这个值大于1,解析器可以使用这个值重新设置CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
。因此,为了内存分配优化,解码器对象创建时使用的缓存数量,应该使用:CUVIDDECODECREATEINFO::ulNumDecodeSurfaces = CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
. -
ulMaxDisplayDelay:调用显示回调函数延迟。0代表没有延迟
-
pfnSequenceCallback : 应用必须注册一个回调函数序列改变的情况。当解析器初始化序列头或者视频格式改变时,触发这个回调函数。序列回调函数的返回值由驱动程序解释如下:
- 0:失败
- 1:成功,但是不应该重新设置
CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
- > 1 : 成功,应该使用返回的值重新设置
CUVIDPARSERPARAMS::ulMaxNumDecodeSurfaces
-
pfnDecodePicture : 当一帧的码流数据准备好时,解析器触发这个回调函数。对于奇偶场的图片,因为两个场组成一帧,每一次显示回调会有两次解码回调。这个回调函数的返回值解释如下:
- 0 : failed
- >= 1 :成功
pfnDisplayPicture : 当一帧按显示顺序准备就绪时,解析器调用这个函数。返回值同上。
提取数据包
从解封装器中提取的数据包使用 cuvidParseVideoData()
函数传递给解析器,与此同时,当序列发生改变或者一帧画面已准备好解码或显示时,解析器触发创建解析器对象时注册的回调函数。
解码后数据通过一个 CUVIDPICPARAMS
里的图片索引绑定,它由解析器提供。这个图片索引后续用于将解码帧映射到CUDA内存。
销毁解析器
用户需要调用 cuvidDestroyVideoParser()
函数销毁解析器对象以及释放所有分配的资源。
视频解码器
查询解码能力
如表1所示,不同的GPU包含不同的能力. 因此,为了确保你的应用能在所有的GPU硬件上运行,很有必要查询硬件能力,然后根据所需的能力是否满足进行合适的操作。
API cuvidGetDecoderCaps()
让用户能够查询底层硬件视频的解码能力。进行调用的线程应该有一个有效的CUDA上下文与之绑定。
客户端在调用 cuvidGetDecoderCaps()
之前应该填充以下参数。
‣ eCodecType: Codec type (H.264, HEVC, VP9, JPEG etc.)
‣ eChromaFormat: 4:2:0, 4:4:4, etc.
‣ nBitDepthMinus8: 0 for 8-bit, 2 for 10-bit, 4 for 12-bit
当 cuvidGetDecoderCaps()
调用后,底层驱动填充 CUVIDDECODECAPS
的剩余字段,显示对于查询的能力的支持情况。
示例:
// 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);
// 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
}
创建解码器
在创建解码器示例之前,用户需要一个有效的CUDA上下文,它将在整个解码过程中使用。
在填充 CUVIDDECODECREATEINFO
结构体后,通过调用 cuvidCreateDecoder()
创建解码器实例。CUVIDDECODECREATEINFO
应该填充以下关于需要解码的流的信息:
- CodecType :必须取值于
enum cudaVideoCodec
,指示解码的类型,比如 H.264, HEVC, VP9等 - ulWidth, ulHeight : 编码的宽和高,使用像素单位
- ulMaxWidth, ulMaxHeight : 解码器支持的最大宽高,当视频流的分辨率发生改变,并且不超过(ulMaxWidth, ulMaxHeight ),app 可以
cuvidReconfigureDecoder()
API 重新配置解码器,而不必销毁再重新创建解码器。如果ulMaxWidth
或ulMaxHeight
设置为0,ulMaxWidth
和ulMaxHeight
会分别设置为ulWidth
和ulHeight
. - ChromaFormat:必须取值于
enum cudaVideoChromaFormat
,它代表色彩格式 4:2:0, 4:4:4,等 - bitDepthMinus8 : 视频流的位宽减去8,例如0代表8位,2代表10位。
- ulNumDecodeSurfaces : 正如本文中其它地方提到的解码表面,这是驱动为了保存解码帧内部分配的解码表面数量。更大的数字可以提供更好的流水线但会增大GPU内存消耗。
CUVIDEOFORMAT::min_num_decode_surfaces
中定义了最小的数字,可以从英伟达解析器的第一次序列回调函数中获取。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帧,这个参数帮助优化内存消耗。如果包含非I帧不要设置此参数。
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()
并传递解码器句柄,在 NVDEC上开始解码。
准备解码后的帧进行后处理
用户需要通过调用 cuvidMapVideoFrame()
函数获取输出表面的CUDA设备指针和pitch大小,输出表面保存经过解码和后处理的帧。
cuvidDecodePicture()
指导NVDEC硬件引擎开始解码帧/场.。然而,cuvidMapVideoFrame()
调用成功意味着解码过程完成,而且解码后的格式转换成了CUVIDDECODECREATEINFO::OutputFormat
中指定的yuv格式。
API cuvidMapVideoFrame()
使用解码表面索引作为输入,将它映射到一个有效的输出表面,对解码帧进行后处理然后拷贝到输出表面,返回CUDA设备指针和输出表面关联的pitch.
上文的 cuvidMapVideoFrame()
操作以下称为mapping.
在用户处理完帧后,必须调用 cuvidUnmapVideoFrame()
使得输出表面再次可用。
如果在 cuvidMapVideoFrame()
之后,用户调用 cuvidUnmapVideoFrame()
持续失败, cuvidMapVideoFrame()
最终也会失败,最多 CUVIDDECODECREATEINFO::ulNumOutputSurfaces
数量的帧可以被映射。
cuvidMapVideoFrame()
是一个阻塞式调用,它等待解码完成。如果 cuvidMapVideoFrame()
和 cuvidDecodePicture()
在同一个cpu线程调用,它会阻塞 cuvidDecodePicture()
。建议用户使用生产者-消费者队列,解码线程作为生产者,映射线程作为消费者。解码线程在显示回调函数中添加图片索引到队列,并且从回调函数迅速返回继续进行解码后面的帧。另一方面,映射线程监测队列,一旦有数据,使用 nPicIdx
作为图片索引调用 cuvidMapVideoFrame(…)
。解码线程必须确保避免用于存放解码后的输出的解码图片缓存,在被映射线程消费并释放之前被重复使用。
示例:
// 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 int cuDevPtr; int nPitch, nPicIdx;
unsigned char* pHostPtr;
memset(&stDispInfo, 0, sizeof(CUVIDPARSEDISPINFO));
memset(&stProcParams, 0, sizeof(CUVIDPROCPARAMS));
... // setup stProcParams if required
// 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);
// use CUDA based Device to Host memcpy
pHostPtr = cuMemAllocHost((void** )&pHostPtr, nPitch);
if (pHostPtr)
{
rResult = cuMemcpyDtoH(pHostPtr, cuDevPtr, nPitch);
}
rResult = cuvidUnmapVideoFrame(&hDecoder, cuDevPtr);
}
... // Dump YUV to a file
if (pHostPtr)
{
cuMemFreeHost(pHostPtr);
}
...
}
在大多数使用场景下,NVDEC会成为性能瓶颈,如果驱动里的NVDEC 等待队列满了,cuvidDecodePicture()
会卡住。
查询解码状态
解码开始后, 可以随时调用 cuvidGetDecodeStatus()
查询帧的解码状态。底层驱动填充解码状态到 CUVIDGETDECODESTATUS::*pDecodeStatus
。
NVDECODEAPI目前报告以下状态:
- 解码中
- 解码完成
- 码流损坏,被NVENC隐藏
- 码流损坏,无法被NVENC隐藏
这个API用于需要根据帧的状态进行进一步操作的场景,比如是否推理一帧。
重新配置解码器
在码流分辨率发生改变或者修改后处理参数,API cuvidReconfigureDecoder()
可以直接重新配置解码器,而不需要销毁解码器实例再重新创建。
以下是使用 cuvidReconfigureDecoder()
的步骤:
- 用户在调用
cuvidCreateDecoder()
时需要指定CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
,用户需要确定在整个解码过程中码流的分辨率都不会超过这两个值。请注意,在一个解码会话中,这两个值都不允许改变。 - 当码流发生改变时,
cuvidReconfigureDecoder()
理论上应该在CUVIDPARSERPARAMS::pfnSequenceCallback
中被调用。用户想重新配置的参数应该填充在::CUVIDRECONFIGUREDECODERINFO
。请注意,CUVIDRECONFIGUREDECODERINFO::ulWidth
和CUVIDRECONFIGUREDECODERINFO::ulHeight
必须小于等于CUVIDDECODECREATEINFO::ulMaxWidth
和CUVIDDECODECREATEINFO::ulMaxHeight
的值,否则cuvidReconfigureDecoder()
会失败。
销毁解码器
用户应该调用 cuvidDestroyDecoder()
销毁解码器会话,并且释放所以分配的解码器资源。
编写高性能解码应用
英伟达GPU上的NVDEC引擎是一个专用的硬件块,它解码支持的视频码流格式。一个典型视频解码应用大致包含以下几步:
解封装
视频码流解析和解码
准备帧进行进一步处理
这些步骤中,解封装和解析不带硬件加速因此超出了本文范围. 解封装可以使用第三方组件比如ffmpeg,它支持许多视频格式。SDK中的示例使用ffmpeg解封装。同样的,解码后处理可以使用用户自定义的CUDA核。进一步处理后的视频帧可以送往显示引擎显示。
为了有效的管道操作,NVDEC驱动内部维护了一个4帧的队列。请注意,这个队列并不意味着解码延迟,一旦有帧进入队列,解码立即开始,只要队列未满,应用就可以排队帧。
对于高性能和低延迟的视频解码应用,确保 PCIE 链接宽度设置为最大的有效值,PCIE 链接宽度的当前值可以通过运行 'nvidia-smi -q'
.命令获取,它可以在系统的BIOS设置中配置。
在码流分辨率或者后处理参数经常改变的使用场景中,推荐使用 cuvidReconfigureDecoder()
,而不是先销毁再重新创建解码器。
为了优化视频内存使用,以下步骤应该被遵守:
- 保证
CUVIDDECODECREATEINFO::ulNumDecodeSurfaces = CUVIDEOFORMAT:: min_num_decode_surfaces
,它是在正确解码的情况下,驱动分配最小的最小解码表面。如果解码性能下降,可以适当增大CUVIDDECODECREATEINFO::ulNumDecodeSurfaces
CUVIDDECODECREATEINFO::ulNumOutputSurfaces
应该在经过平衡吞吐量和内存消耗的测试后选择合适的值。CUVIDDECODECREATEINFO::DeinterlaceMode
应该设置为cudaVideoDeinterlaceMode_Weave
或cudaVideoDeinterlaceMode_Bob
.对于交错模式,使用cudaVideoDeinterlaceMode_Adaptive
会提过质量但是会增大内存消耗,使用cudaVideoDeinterlaceMode_Weave
或cudaVideoDeinterlaceMode_Bob
则相反- 解码多路视频流时,推荐分配最小数量的CUDA上下然后在不同的会话间共享,这有利于节省创建CUDA上下文的内存支出。
- 如果预先知道码流只包含I帧
CUVIDDECODECREATEINFO::ulIntraDecodeOnly
应该设置为1,这个特性只支持HEVC, H.264 和 VP9。然而如果这个标志位打开,但是码流中包含P帧或B帧,解码可能失败。
SDK中示例用于演示各种API的功能,但可能没有全部优化,因此开发者应该确保他们的应该是精心设计的,在解封装、解析,解码各个阶段,以有效的方式结构化,以达到预期的性能和内存消耗。