Nvidia硬解碼總結
1.前言
本文的主要目的是對近期進行的nvidia硬件解碼工作的記錄和總結。至於為什么研究nvidia硬件解碼的具體內容,其實主要是為了在項目中能夠利用nvidia的硬件解碼和編碼能力,提高單機的編解碼並行能力。截止當前,nvidia的硬件編碼官方提供了nvenc的方法,且在ffmpeg中已經增加了對nvenc的編碼庫。對於硬件解碼,官方提供了基於cuda的解碼方法,但是ffmpeg中還沒有相應的解碼庫。所以,我的目的就是調研一下這個硬解方案,並將其自定義增加到ffmpeg中。
官方提供的資料比較少,只包括一頁的視頻解碼器介紹和示例代碼。
吐槽一下:官網那個一頁的介紹參考量真不大,主要還是參考例程代碼。
2.例程介紹
官網提供的例程代碼解壓后如下圖所示,因為是調用解碼,所以主要參考了"NvDecodeD3D9"和"NvTranscoder"的代碼。
總的來說,nvidia提供了source, parser, decoder三個基本模塊。其中source是用來解析視頻文件(例如:純h.264文件),parser是用來解析視頻並得到一幀幀的數據,decoder就是解碼了。

這三個模塊相輔相成,其主要操作流程如上圖所示。source模塊輸出h264數據,parser解析這些h264數據,並通過3個重要的回調函數(pfnSequenceCallback, pfnDecodePicture, pfnDisplayPicture)完成解碼及輸出功能。其中,pfnSequenceCallback是parser解析到序列及圖像參數信息時的回調函數,其傳入的參數是parser解析好的視頻參數,可以用於初始化解碼器或重置解碼器。pfnDecodePicture是parser解析到視頻編碼數據后的回調函數,其傳入的參數parser處理好待解碼的視頻編碼數據,需要在該函數中調用decoder的接口進行解碼操作。pfnDisplayPicture是parser對解碼后的數據處理的回調函數,可以在該回調中對已解碼的數據進行獲取(從顯存到系統內存)並處理。
3.主要接口說明
cuvidCreateVideoSource : 該接口的作用是創建source,主要參數是設置視頻文件路徑和回調函數。source會去解析指定視頻文件,並通過回調函數實現對視頻數據的自定義處理。源碼中在視頻數據回調函數中,調用了cuvidParseVideoData,即向parser中傳遞數據。
//init video source
CUVIDSOURCEPARAMS oVideoSourceParameters;
memset(&oVideoSourceParameters, 0, sizeof(CUVIDSOURCEPARAMS));
oVideoSourceParameters.pUserData = this;
oVideoSourceParameters.pfnVideoDataHandler = HandleVideoData;
oVideoSourceParameters.pfnAudioDataHandler = NULL;
oResult = cuvidCreateVideoSource(&m_videoSource, videoPath, &oVideoSourceParameters);
if (oResult != CUDA_SUCCESS) {
fprintf(stderr, "cuvidCreateVideoSource failed\n");
fprintf(stderr, "Please check if the path exists, or the video is a valid H264 file\n");
exit(-1);
}
cuvidCreateVideoParser : 該接口是用來創建video parser,主要參數是設置三個回調函數,實現對解析出來的數據的處理。
//init video parser
CUVIDPARSERPARAMS oVideoParserParameters;
memset(&oVideoParserParameters, 0, sizeof(CUVIDPARSERPARAMS));
oVideoParserParameters.CodecType = oVideoDecodeCreateInfo.CodecType;
oVideoParserParameters.ulMaxNumDecodeSurfaces = oVideoDecodeCreateInfo.ulNumDecodeSurfaces;
oVideoParserParameters.ulMaxDisplayDelay = 1;
oVideoParserParameters.pUserData = this;
oVideoParserParameters.pfnSequenceCallback = HandleVideoSequence;
oVideoParserParameters.pfnDecodePicture = HandlePictureDecode;
oVideoParserParameters.pfnDisplayPicture = HandlePictureDisplay;
oResult = cuvidCreateVideoParser(&m_videoParser, &oVideoParserParameters);
if (oResult != CUDA_SUCCESS) {
fprintf(stderr, "cuvidCreateVideoParser failed, error code: %d\n", oResult);
exit(-1);
}
cuvidParseVideoData : 該接口是用來向parser塞數據,通過不斷地塞h.264數據,parser會通過回調接口對解析出來的數據進行處理。在例程中,cuvidParseVideoData是在source的pfnVideoDataHandler回調中被使用的,即source獲取到視頻數據,就將其傳遞給parser。
// the callback of source pfnVideoDataHandler
static int CUDAAPI HandleVideoData(void* pUserData, CUVIDSOURCEDATAPACKET* pPacket) {
assert(pUserData);
CudaDecoder* pDecoder = (CudaDecoder*)pUserData;
CUresult oResult = cuvidParseVideoData(pDecoder->m_videoParser, pPacket);
if(oResult != CUDA_SUCCESS) {
printf("error!\n");
}
return 1;
}
cuvidCreateDecoder : 該接口是用來創建decoder,通過設置一些解碼參數,會返回一個decoder的句柄。這個句柄會在之后的解碼接口中被使用。該接口的具體使用方法在例程中有詳細的參數設置,這里就繁瑣地描述了。
cuvidDecodePicture : 該接口就是向解碼器傳遞待解碼的數據。需要說明一下,該接口是異步解碼,不能通過該接口得到解碼后的視頻數據,它只是向解碼器傳數據而已。解碼后的數據,是通過parser的pfnDisplayPicture回調得到。
4.技術點說明
庫的使用
nvidia解碼需要使用cuda和nvcuvid兩個庫(在linux中是libcuda.so和libnvcuvid.so),使用的時候要加載它們,並使用其中一些接口。主要使用到的接口主要有:
cuInit
cuDeviceGetCount
cuDeviceGet
cuDeviceGetName
cuDeviceComputeCapability
cuCtxCreate
cuCtxPushCurrent
cuCtxPopCurrent
cuCtxDestroy
cuMemAllocHost
cuMemFreeHost
cuStreamCreate
cuStreamDestroy
cuMemcpyDtoHAsync
cuvidCreateDecoder
cuvidDestroyDecoder
cuvidDecodePicture
cuvidCtxLockCreate
cuvidCtxLockDestroy
cuvidCtxLock
cuvidCtxUnlock
cuvidMapVideoFrame
cuvidUnmapVideoFrame
cuvidCreateVideoParser
cuvidParseVideoData
cuvidDestroyVideoParser
注意:根據庫的版本不同,接口有的需要使用v2版本。例如:cuCtxCreate和cuCtxCreate_v2。
device內存和system內存
使用nvidia進行硬件解碼需要了解一下device內存(可以叫顯存或設備內存)和系統內存的數據處理方法。在解碼完成后,視頻YUV數據是在device內存中的,所以需要使用nvidia提供的接口把數據弄出來。涉及的接口主要有:cuMemAllocHost, cuMemFreeHost, cuvidMapVideoFrame, cuvidUnmapVideoFrame, cuMemcpyDtoHAsync。其中,cuMemAllocHost是用來創建系統及顯卡都可訪問的系統內存。cuvidMapVideoFrame可以獲取到設備內存中指定的YUV數據地址。最后通過cuMemcpyDtoHAsync將設備內存中指定的數據copy到系統內存中。
