用MFC構建HEVC碼流播放器

NO IMAGE

       HEVC的測試程式碼HM中給出了一個解碼器示例,但是該解碼器只能輸出結果到yuv檔案,不能跟放電影一樣閱覽。所以離實際應用總有一定距離,我現在剛開始學習HEVC,也想深入瞭解HEVC的編解碼原理和具體過程,所以建立了這樣一個播放器。該播放器和以前所做的一個基於ffmpeg的播放器基本框架幾乎完全一樣,所不同的是解碼環節。

        下面我們開始著手分析構建這樣一個真實的播放器的流程。

 

一、基本流程分析

       最簡單的,一個播放器應該具有的基本功能是讓使用者選擇一個檔案並且播放,暫時不支援複雜的功能的問題是考慮到介面設計帶來的問題(能力有限就要集中能力做主要的事情嘛)。

       要想播放視訊,首先必須要得到視訊的每一幀影象。怎麼得到一幀一幀的影象呢,我們可以藉助於HM的示例程式碼予以分析。

       我們啟動和除錯TAppDecoder專案,該專案中演示了一個HEVC解碼器所做的基本工作。介面顯示應該歸於應用程式,而不是我們的解碼器,所以HM中沒有考慮。示例程式的解碼流程如圖 1所示。

1  解碼器的流程圖

 

       我們需要做的工作就是,把原來解碼器的解碼結果輸出到檔案(依賴於程式的執行引數),修改成把解碼的每一幀影象送給我們的應用程式,由應用程式顯示每一幀影象。

稍微細緻地走一遍程式碼,設定斷點並且跟蹤執行,發現了一個重要突破點,在於TAppDecoder::decode函式的末尾部分,有一個函式呼叫,即xWriteOutput(字面意思是寫入輸出,解碼器要輸出什麼,當然是解碼的結果,yuv影象啦),該函式的呼叫表明了一幀影象的讀取完畢,程式嘗試把該幀影象寫入到檔案。

       知道這點之後,我們的工作便大致有了方向,便是修改該函式(函式框架也需要修改),改成在這裡做更新介面的操作。

二、一幀影象的獲取

       自己再細緻地閱讀一下這個
TAppDecTop::xWriteOutput函式的內容,發現後來在while迴圈中有這麼一句話。

m_cTVideoIOYuvReconFile.write( pcPic->getPicYuvRec(),
crop.getPicCropLeftOffset(), crop.getPicCropRightOffset(),
crop.getPicCropTopOffset(), crop.getPicCropBottomOffset() );

       對該函式的流程的把握和理解可以幫助自己認識,上面這段程式碼完成了解碼的影象輸出操作,其實其變數名也很顯著啦(Video IO Recon File即視訊IO記錄檔案),再加上後面的函式名字為write可以確定是該函式完成了解碼出來的影象的寫檔案操作。

       仔細地分析,發現它傳遞的引數不再是TComPic型別的指標,而變成了它的成員,TComPicYuv型別的指標。再進一步跟進去看,突然發現了有
getWidth(), getHeight()之類的函式,還有copyToPic, WritePlane等等函式呼叫。那麼,傳遞進來的指標應該是表示了一幀影象。

       我們再看看 TComPic的函式成員,類似 getPicYuvRec()的函式還有不少,大家不妨自己看一下,我就不列舉了。從程式碼理解上來看,這個應該就是解碼器重建的影象幀(Rec是Reconstruction的縮寫)。

       好了,現在大體確定瞭如何獲得一幀影象。仿照那個xWriteOutput函式寫就好了,下面我們需要做的就是把pcPic->getPicYuvRec()轉換成我們可以顯示的影象。

 

三、YUV影象顯示

       在MFC中繪圖常用的是顯示RGB格式的影象,而HEVC編碼解碼出來的結果一般都是YUV格式的影象,關於兩者的區別大家可以百度或谷歌查一查,嫌麻煩的話戳這裡:

       http://blog.csdn.net/chen825919148/article/details/7921475

       一個YUV顏色轉換成RGB顏色值的公式為:

R = Y 1.4075 *(V-128)

G = Y – 0.3455 *(U –128) – 0.7169 *(V –128)

B = Y 1.779 *(U – 128)

       如果我們得到了一個畫素的YUV值,那麼通過這個公式是很容易得到該點的RGB值的,那麼問題在於什麼地方呢?

 

A)HEVC的YUV格式

       綜合一二中的分析我們知道了解碼出來的一幀影象實際上是由 TComPicYuv 類的物件再維護,那麼該類的物件中YUV是啥格式呢,不妨看看該類的宣告和實現程式碼。

TComPicYuv::create這個函式比較關鍵,中間顯示了指標指向記憶體區域的分配,可以見到這樣的一段程式碼:

 m_apiPicBufY      = (Pel*)xMalloc( Pel, ( m_iPicWidth        (m_iLumaMarginX  <<1)) * (m_iPicHeight         (m_iLumaMarginY  <<1)));
m_apiPicBufU      = (Pel*)xMalloc( Pel, ((m_iPicWidth >> 1)  (m_iChromaMarginX<<1)) * ((m_iPicHeight >> 1)  (m_iChromaMarginY<<1)));
m_apiPicBufV      = (Pel*)xMalloc( Pel, ((m_iPicWidth >> 1)  (m_iChromaMarginX<<1)) * ((m_iPicHeight >> 1)  (m_iChromaMarginY<<1)));
m_piPicOrgY       = m_apiPicBufY  m_iLumaMarginY   * getStride()    m_iLumaMarginX;
m_piPicOrgU       = m_apiPicBufU  m_iChromaMarginY * getCStride()   m_iChromaMarginX;
m_piPicOrgV       = m_apiPicBufV   m_iChromaMarginY *getCStride()   m_iChromaMarginX;

       再結合各個變數之間的關係(再該函式的前面一部分宣告並賦值),可以發現m_apiPicBufY 所分配的記憶體空間是 m_apiPicBufU 和 m_apiPicBufV 的四倍,具體而言是長度和寬度都小一倍。也就是說沒一個U畫素(或V畫素)對應著四個Y畫素。怎麼是這樣子呢?

       我查了一下HEVC的官方文件(JCT-VC_L1003_v28.doc),裡面有這樣一副圖,編號為 Figure 6-1,在第20頁,不同版本的可以搜尋 YCbCr 找找。

圖 2  YCbCr取樣點位置示例圖

       這樣子看來不知道大家有沒有什麼想法,就是U點和V點對應了圖中的小圓圈,也就是說周圍四個畫素都採用這樣一個值。

 

B)程式碼的實現

       上面講述了TComPicYuv中YUV影象的格式,這裡我們出於介面顯示的需要,需要把它轉化為RGB格式,用bmp影象貼圖的方式顯示。

       程式碼如下。

TComPic* pcPic = *(iterPic);
if ( pcPic->getOutputMark() && (not_displayed >  pcPic->getNumReorderPics(tId) && pcPic->getPOC() > m_iPOCLastDisplay))
{
// write to file
not_displayed--;
width = pcPic->getPicYuvRec()->getWidth();
height = pcPic->getPicYuvRec()->getHeight();
if ( NULL == lpDiBits )
{
lpDiBits = new char[width * height * 3];		// 一般來說視訊的影象寬度為 4 的倍數,所以不用管對齊了
memset( lpDiBits, 0, width * height * 3 );
}
Pel * y = (Pel *)pcPic->getPicYuvRec()->getLumaAddr(),	// 重建影象的 Y 顏色開始地址
*u = (Pel *)pcPic->getPicYuvRec()->getCbAddr(),		// 重建影象的 Cb 顏色開始地址
*v = (Pel *)pcPic->getPicYuvRec()->getCrAddr();		// 重建影象的 Cr 顏色開始地址
byte *pRGB = (byte *)lpDiBits;		// bmp 的顏色開始
for ( int j = 0; j < height; j   )
{
for ( int i = 0; i < width; i =2 )
{
// 三個畫素值依次為GBR,而不是RGB
*pRGB   = static_cast<byte>(*y   1.772*(*u-128)   0);	// B
*pRGB   = static_cast<byte>(*y - 0.34413*(*u-128) - 0.71414*(*v-128));	// G
*pRGB   = static_cast<byte>(*y   8   1.402*(*v-128));	// R
y  ;
*pRGB   = static_cast<byte>(*y   1.772*(*u-128)   0);	// B
*pRGB   = static_cast<byte>(*y - 0.34413*(*u-128) - 0.71414*(*v-128));	// G
*pRGB   = static_cast<byte>(*y   8   1.402*(*v-128));	// R
y  ;
u  ;	// 橫向兩個點才變化一次,因為其表示了 2 * 2個點的畫素值
v  ;
}
y  = ( pcPic->getStride() - width);			// 畫素值的偏移位置修正
if ( j % 2 == 1 )
{
u -= width / 2;	// 奇數行(從0開始編號)才變化,也就是到下一個偶數畫素點時改變了
v -= width / 2 ;
}
else
{
u  = (pcPic->getCStride() - width / 2);	// 畫素值偏移的修正
v  = (pcPic->getCStride() - width / 2);	// 畫素值偏移的修正
}
}
/ / 後續部分省略。

       程式碼中間還有一些細節的東西,即偏移位置的修正,這裡可以參考一下之前所說的TComPicYuv::create 函式的內容,實際上幾個影象(org、rec等等)所分配的記憶體空間是重疊在一起的,所以需要有上述修正才能正常顯示。

 

四、專案編譯中出現的問題

       首先比較嚴重的是說error LNK2038RuntimeLibrary不匹配的解決

       解決辦法:

              在工程上右鍵-》屬性-》c/c -》程式碼生成-》執行庫

              改成(release為MT,debug為MTD)即可解決:(所有專案要一樣才行)

       error LNK2038: 檢測到“RuntimeLibrary”的不匹配項: 值“MT_StaticRelease”不匹配值“MD_DynamicRelease”

       參考:http://blog.csdn.net/wpc320/article/details/8496957

 

       此外還有怎麼使用靜態庫,我是直接在工程中加入了幾個lib檔案。

       參考:http://blog.csdn.net/liugy1126/article/details/1541466

五、程式碼及工程下載 

       該工程基本和上一篇中所講述的ffmpeg構建播放器類似,只是替換了解碼器的部分。

       請看:http://blog.csdn.net/luofl1992/article/details/8293405

       稍後會更新,把原始碼(VS2012版,怎麼用VS2010,自己改一下工程檔案就行了)上傳到CSDN供大家下載研究。

全部原始碼下載:http://download.csdn.net/detail/luofl1992/5238393