英偉達硬件解碼器分析

這篇文章主要分析 NVCUVID 提供的解碼器,裏面提到的所有的源文件都可以在英偉達的 nvenc_sdk 中找到。

解碼器的代碼分析

SDK 中的 sample 文件夾下的 NvTranscoder 中包含了編碼器和解碼器的用法,編碼器的內容不在這裏分析,因爲 FFMPEG 中已經包含了相關的代碼,不需要其他的處理。

解碼器在 SDK 中有一份封裝,主要是 NvTranscoder 下的 VideoDecoder 類。目前這個類的具體用法還不是特別的清楚。分析將會從 main 函數開始。

main

NvTranscoder 有一個單獨的文件,執行邏輯從 main() 函數開始。

cuInit(0) 應該是初始化 cuda 的相關代碼。目前沒有找到定義,估計類似於FFMPEG 中的 av_register_all

105 行之前的代碼是編碼相關的代碼, 85 行以前設置各種編碼參數,87 行分析從命令行讀入的參數,100 行打開目標輸出文件。

110 行開始代碼應該是和解碼相關的核心代碼。cuDeviceGetcuCtxCreate cuCtxPopCurrentcuvidCtxLockCreate 應該是固定寫法。初始化一些內部機制。

120 行的 InitVideoDecoder 是解碼器創建的地方。這個函數需要認真的分析,整個解碼器的關鍵代碼應該就在這裏面。

137 行初始化 119 行中創建的 FrameQueue。這個隊列應該相當於解碼器和編碼器之間的一個緩衝區,解碼器方內容進去,編碼器從中取內容出來。

139 行到 180 都是關於編碼器的設置。這裏不做詳細的分析。

187 行的註釋顯示 pthread_create 創建出來解碼線程,所以解碼工作是由這個線程完成的。線程執行的函數是 DecodeProc 這個函數,而這個函數只不過是調用的解碼器的 Start() 方法。

195 行的代碼註釋來看,這後續的代碼都是編碼相關的代碼,這裏不做分析。最後在245 行和 246 行的未知 cuvidCtxLockDestroy 和 cuCtxDestroy 應該是對應於110 行的那些代碼。

VideoDecoder.cpp

這個文件實現瞭解碼器的封裝類 CudaDecoder, 這個類是整個硬件解碼器實現的關鍵,但是這個類其實比較簡單。它在 main 函數中涉及到的方法只有InitVideoDecoderGetCodecParamStart 和 GetDecoder 這四個。

InitVideoDecoder

這個方法是首先被調用的方法。它負責編碼器的初始化操作。

從 VideoDecoder.cpp 中的實現來看,初始化主要包括三個部分:

創建視頻源

視頻源的參數是 CUVIDSOURCEPARAMS,其中設置了一個 pfnVideoDateHandler,從字面上理解它是一個視頻數據的回調處理函數。

創建視頻源的方法是 cuvidCreateVideoSource() 函數,目前來說這個函數的致命問題在於它的接收 videoPath 作爲參數,這似乎意味着它只能處理文件視頻源。這個函數的函數原型定義在 nvcuvid.h 這個頭文件中,這個頭文件只定義了下面這些和視頻源相關的接口:

cuvidCreateVideoSource();
cuvidCreateVideoSourceW();
cuvidDestroyVideoSource();
cuvidSetVideoSourceState();
cuvidGetVideoSourceState();
cuvidGetSourceVideoFormat();
cuvidGetSourceAudioFormat();

從接口來看只有 cuvidCreateVideoSource(); cuvidCreateVideoSourceW(); 這兩個函數可用,而它們唯一的區別在於接收不同的文件路徑字符串,前者是普通字符而後者是寬字符。

目前暫時沒有其他的資料表明可以創建非文件類型的視頻源,所以這個解碼器的用處估計不會太大,至少在傳屏應用中的用處會相對較小。

獲取視頻源的參數,並創建 cuvid 庫的解碼器

視頻源的參數信息在創建視頻源之後可以通過 cuvidGetSourceVideoFormat() 函數獲得,在 InitVideoDecoder() 函數中獲取參數最詭異的地方在於 111 行創建了一個 CUVIDOFORMATEX 類型的變量 oFormatEx,然後讓 oFormat 引用這個變量的 format 字段。在調用 cuvidGetSourceVideoFormat 之後竟然可以直接訪問oFormatEx 這個變量的 raw_seqhdr_data 字段,個人估計它的內部實現使用了類似 container_of 這樣的技術訪問了 oFormatEx。但是爲什麼這樣設計不得而知。

創建解碼器的函數是 cuvidCreateDecoder(),這個函數的原型定義在 cuviddec.h文件中。

cuvidcreatedecoder(cuvideodecoder *, CUVIDDECODECREATEINFO *);
cuvidDestroyDecoder(CUvideodecoder);

解碼器的參數是通過 CUVIDDECODECREATEINFO 傳遞的,這個結構體的大部分字段都是通過前面獲得的 oFormat 中的信息獲得。

創建視頻源的解析器

初始化的最後一步是創建一個視頻源的解析器,其中設置了三個回調函數,HandleVideoSequenceHandlePictureDecodeHandlePictureDisplay, 在nvidia 的文檔中並沒有說這些回調函數會在什麼時候調用,也沒有說明這些回調函數要完成的事情是什麼,只能從名字中猜測 HandlePictureDecode這個函數是用來解碼的。

Start

在 main 函數的解碼線程函數中只調用了 CudaDecoder 類的 Start 函數。而Start 函數本身也非常的簡單,只不過調用了 cuvidSetVideoSourceState() 把狀態變成 cudaVideoState_Started 然後一直取狀態直到狀態不再是 started

從這個函數的實現來看,它的內部應該在把視頻源設置爲cudaVideoState_Started 狀態之後開始讀取視頻源(文件)中的數據。然後通過回調函數進行處理。應該是首先調用 HandleVideoData(), 個人猜測這個函數在數據從文件中讀取出來之後會被調用來解析原始數據,HandleVideoSequence 這個函數沒有太大的用途,只是一些參數的檢測而已。HandlePictureDecode 應該是在成功解析到數據幀的時候調用,這個函數調用了 cuvidDecodePicture 解碼數據。數據解碼出來之後會調用 HandlePictureDisplay 函數,該函數把數據放入到數據緩衝區 FrameQueue 中以便編碼器能夠把數據取出來。

總結

使用 CudaDecoder 首先需要調用 cuvidCreateVideoSource 創建一個文件視頻源,然後調用 cuvidGetSourceVideoFormat 從文件中讀取解碼參數信息並使用參數信息創建一個 CUvideodecoder 解碼器,之後再創建一個視頻源解析器,設置回調函數處理視頻的解碼。

上面的初始化完成之後調用 cuvidSetVideoSourceState 把視頻源的狀態設置爲cudaVideoState_Started,之後庫的內部會開始讀文件,把讀取的數據交給HandleVideoData 解析,解析完成之後會把數據交給 HandlePictureDecode 調用 cuvidDecodePicture 進行解碼。在解碼完成之後調用HandlePictureDisplay 把數據放入到 FrameQueue 緩衝區裏面。

補充:video source 和 nvcuvid

在 SDK 給出的例子中,數據是通過 video source 接口來提供的。但是這並不意味着我們在編寫程序的時候只能使用它提供的 video source 接口。根據官方文檔 中第三小節最後給出的解釋

Note: The low level decode APIs are supported on both Linux and Windows platforms. The NVCUVID APIs for Parsing and Source Stream input are available only on Windows platforms.

NVCUVID 的 video source 只在 windows 平臺可用,不過從最新的nvenc_sdk 的代碼來看,videosource 和 sourcepraser 在 linux 平臺下也是可用的。只不過從接口來看,這兩個 API 只能用於文件的解析。

在文檔的第四小節 4.2 中有這麼一段話:

For Linux platforms, you will need to write your own video source andparsing functions that connect to the Video Decoding functions.

這一點明確說明,其實我們可以不使用它本身的 video source 接口,使用自己的接口提供視頻源,然後使用 nvcuvid 最底層的解碼接口對數據進行解碼和後續處理。

MAP

在 nvcuvid 的官方文檔中給出的接口中,最詭異的兩個接口是

cuvidMapVideoFrame()
cuvidUnmapVideoFrame()

這兩個函數好像是用於處理解碼之後的數據的,但是這其中的原理是什麼並不清楚,有待後續研究。

發佈了121 篇原創文章 · 獲贊 51 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章