音視頻編解碼(二) —— iOS中的H264硬編解碼的實(shí)現(xiàn)(一)

版本記錄

版本號 時間
V1.0 2017.12.23

前言

對于做過音視頻的開發(fā)者,編解碼都不陌生,接下來這幾篇就詳細(xì)的看一下音視頻編解碼相關(guān)知識。感興趣的可以看這幾篇文章。
1. 音視頻編解碼(一) —— H264基本概覽(一

視頻H264編解碼數(shù)據(jù)結(jié)構(gòu)

下面我們看一下H264編碼前后數(shù)據(jù)結(jié)構(gòu),如下圖所示。

下圖為H264解碼前后數(shù)據(jù)結(jié)構(gòu)示意圖,這里有幾個對象需要說明一下。

  • CVPixelBuffer
    • 解碼后的圖像數(shù)據(jù)結(jié)構(gòu)。
  • CMTime、CMClock和CMTimebase
    • 這個和時間戳相關(guān),可能是32或者64位的形式。
  • CMBlockBuffer
    • 編碼后的圖像數(shù)據(jù)結(jié)構(gòu)
  • CMVideoFormatDescription
    • 這里面存放的就是圖像存儲方式,編解碼器等格式描述。
  • CMSampleBuffer
    • 這里面存放編解碼前后的視頻圖像的容器數(shù)據(jù)結(jié)構(gòu)。

從上圖中可以看出來:

  • 編解碼前后的視頻數(shù)據(jù)封裝在CMSampleBuffer中。
  • 編碼后的圖像存儲方式為CMBlockBuffer
  • 解碼后的圖像存儲方式為CVPixelBuffer
  • CMSampleBuffer中還存儲和時間已經(jīng)描述相關(guān)的信息。

具體上面幾個對象怎么在代碼中使用,后續(xù)會加上使用方法的Demo。


硬編碼和軟編碼優(yōu)缺點(diǎn)

利用CPU做視頻的編碼和解碼,稱為軟編軟解。該方法比較通用,但是占用CPU資源,編解碼效率不高。

一般系統(tǒng)都會提供GPU或者專用處理器來對視頻流進(jìn)行編解碼,也就是硬件編碼和解碼。蘋果在iOS 8.0系統(tǒng)之前,沒有開放系統(tǒng)的硬件編碼解碼功能,不過Mac OS系統(tǒng)一直有,被稱為Video ToolBox的框架來處理硬件的編碼和解碼,終于在iOS 8.0后,蘋果將該框架引入iOS系統(tǒng)。

硬編碼具有很大的優(yōu)勢,它不像軟編碼大量占用CPU資源。可以更好的利用GPU以及專門的視頻編解碼芯片的高性能,可以實(shí)現(xiàn)很好的實(shí)時性。其實(shí),對于VFoundation也使用硬件對視頻進(jìn)行硬件編解碼,但是編碼后直接寫入文件,解碼后就直接顯示了。而使用Video Toolbox框架可以得到編碼后的幀結(jié)構(gòu),也可以得到解碼后的原始圖像,因此具有更大的靈活性做一些視頻圖像處理。也就是說使用Video Toolbox框架更加靈活,方便進(jìn)一步進(jìn)行視頻處理。


硬解碼

硬解碼其實(shí)就是從服務(wù)端下載視頻數(shù)據(jù),但是是編碼后的,在客戶端呈現(xiàn)出來視頻數(shù)據(jù)之前,需要進(jìn)行解碼,然后才可以拿出來圖像像素?cái)?shù)據(jù)進(jìn)行顯示。下面我們先看一下硬編碼相關(guān)原理及理論,先看一張圖。

1. 將H264碼流轉(zhuǎn)換為解碼前CMSampleBuffer對象

由前面的內(nèi)容我們知道,解碼前的CMSampleBuffer對象,包括CMTime、CMVideoFormatDesc、CMBlockBuffer等,我們解碼的任務(wù)就是從H264碼流里面提取上面三處的信息,合成解碼后的CMSampleBuffer對象,提供給硬解碼接口進(jìn)行解碼工作。

H264碼流由NALU單元組成,NALU單元包含視頻圖像數(shù)據(jù)CMBlockBuffer和H264的參數(shù)信息則可以組合成FormatDesc,具體參數(shù)信息包含SPS(Sequence Parameter Set)PPS(Picture Parameter Set),如下圖所示為H264的碼流結(jié)構(gòu)。

還可以看下面這個示意圖

H264碼流結(jié)構(gòu)

下面我們就看一下這個解析過程。

  • 提取spspps生成format description
    • 每個NALU開始碼位0x000001,按照開始碼定位NALU
    • 通過類型信息找到sps和pps,開始碼后的第一個byte的后5位,7代表sps,8代表pps。
//sps
_spsSize =format.getCsd_0_size()-4;_sps = (uint8_t *)malloc(_spsSize);memcpy(_sps,format.getCsd_0()+4, _spsSize);

//pps
_ppsSize =format.getCsd_1_size()-4;_pps = (uint8_t *)malloc(_ppsSize);memcpy(_pps,format.getCsd_1()+4, _ppsSize);
  • 利用函數(shù)CMVideoFormatDescriptionCreateFromH264ParameterSets來構(gòu)建CMVideoFormatDescriptionRef,以獲取描述信息。

  • 提取視頻數(shù)據(jù)生成待解碼對象CMBlockBuffer

    • 通過上面提到的開始碼,定位到NALU
    • 確定類型為數(shù)據(jù)后,將開始碼替換成NALU的長度信息(4Bytes)
    • 利用函數(shù)CMBlockBufferCreateWithMemoryBlock構(gòu)造CMBlockBufferRef對象
CMBlockBufferRef blockBuffer=NULL;
CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,                                  
(void*)frame.bytes,                                   
 frame.length,                                  
kCFAllocatorNull,NULL,0, frame.length,0,&blockBuffer);
  • 根據(jù)需要,生成CMTime信息。不過加入time信息可能產(chǎn)生不穩(wěn)定的圖像,如果不是特別需要,不建議加入time信息。

根據(jù)前面產(chǎn)生的CMVideoFormatDescriptionRef、CMBlockBufferRef和可選的時間信息,使用函數(shù)CMSampleBufferCreate得到CMSampleBuffer這個待解碼的原始數(shù)據(jù)。

CMSampleBufferRef sampleBuffer =NULL;
CMSampleBufferCreateReady(kCFAllocatorDefault,                          
blockBuffer,                          
_decoderFormatDescription,1,0,NULL,1, sampleSizeArray,                         
 &sampleBuffer);

具體如下所示,為H264解碼數(shù)據(jù)轉(zhuǎn)換圖。

H264碼流轉(zhuǎn)換CMSampleBuffer示意圖

2. 硬解碼后的圖像顯示

下面我們就看一下硬解碼后的圖像顯示,具體的顯示方式有兩種:

  • 通過系統(tǒng)提供的AVSampleBufferDisplayLayer來解碼并顯示。
  • 通過VTDecompression接口來,將CMSampleBuffer解碼成圖像,將圖像通過UIImageView或者OpenGL上顯示。

通過系統(tǒng)提供的AVSampleBufferDisplayLayer來解碼并顯示

AVSampleBufferDisplayLayer是蘋果提供的一個專門顯示解碼后的H264數(shù)據(jù)的顯示層,它是CALayer的子類,因此使用方式和其它CALayer類似。使用方法enqueueSampleBuffer :進(jìn)行顯示該層內(nèi)置了硬件解碼功能,將原始的CMSampleBuffer解碼后的圖像直接顯示在屏幕上面,如下圖所示。

AVSampleBufferDisplayLayer顯示硬解碼后的圖像

下面看一下實(shí)例代碼

CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments,0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
if(status == kCMBlockBufferNoErr) {

if([_avslayer isReadyForMoreMediaData]) {dispatch_sync(dispatch_get_main_queue(),^{        

    [_avslayer enqueueSampleBuffer:sampleBuffer];      

    });   
  }    

CFRelease(sampleBuffer);
}

下面看一下這個顯示方式的解碼流程。

通過VTDecompression接口來,將CMSampleBuffer解碼成圖像,將圖像通過UIImageView或者OpenGL上顯示

  • 初始化VTDecompressionSession,設(shè)置解碼器的相關(guān)信息,初始化需要CMSampleBuffer里面的FormatDescription,以及設(shè)置解碼后的圖像存儲方式。編碼后的圖像解碼后,會調(diào)用一個回調(diào)函數(shù),在這個回調(diào)函數(shù)里面,你可以獲得解碼后的圖像。我們將解碼后的圖像發(fā)給control來顯示,初始化的時候需要回調(diào)指針作為參數(shù)傳給create接口函數(shù),同時利用create接口函數(shù)對session進(jìn)行初始化。
VTDecompressionSessionRef _deocderSession;
VTDecompressionSessionCreate(kCFAllocatorDefault,                           
 _decoderFormatDescription,NULL, attrs,                            
&callBackRecord,                           
 &_deocderSession);
  • 上面的回調(diào)函數(shù)可以完成由CGBitmapUIImage之間的轉(zhuǎn)換,將圖像通過隊(duì)列發(fā)送到control來處理顯示。
CIImage *ciImage= [CIImage imageWithCVPixelBuffer:outputPixelBuffer];
UIImage *uiImage= [UIImage imageWithCIImage:ciImage];
  • 通過接口VTDecompresSessionDecodeFrame進(jìn)行解碼操作,并將解碼后的圖像交給上面兩個步驟的回調(diào)函數(shù),以便進(jìn)一步處理。具體如下圖所示。
// 使用VTDecompressionSessionDecodeFrame接口解碼成CVPixelBufferRef數(shù)據(jù):

CVPixelBufferRef outputPixelBuffer=NULL;
VTDecompressionSessionDecodeFrame(_deocderSession,
sampleBuffer,
flags
&outputPixelBuffer,
&flagOut);
VTDecompression硬解碼過程示意圖

下面看一下這種解碼方式和顯示流程。

下面看一下這兩種解碼方式的優(yōu)缺點(diǎn)。

  • 解碼方式一

    • 優(yōu)點(diǎn): 該方式通過系統(tǒng)提供的AVSampleBufferDisplayLayer顯示層來解碼并顯示。該層內(nèi)置了硬件解碼功能,將原始的CMSampleBuffer解碼后的圖像直接顯示在屏幕上,非常的簡單方便,且執(zhí)行效率高,占用內(nèi)存相對較少。

    • 缺點(diǎn): 從解碼的數(shù)據(jù)中不能直接獲取圖像數(shù)據(jù)并對其做相應(yīng)處理,解碼后的數(shù)據(jù)不能直接進(jìn)行其他方面的應(yīng)用(一般要做較復(fù)雜的轉(zhuǎn)換)。

  • 解碼方式二

    • 優(yōu)點(diǎn): 該方式通過VTDecompressionSessionDecodeFrame接口,得到CVPixelBufferRef數(shù)據(jù),我們可以直接從CVPixelBufferRef數(shù)據(jù)中獲取圖像數(shù)據(jù)并對其做相應(yīng)處理,方便于其他應(yīng)用。

    • 缺點(diǎn): 解碼中執(zhí)行效率相對降低,占用的內(nèi)存也會相對較大。


硬編碼

硬編碼我們也經(jīng)常見,比如說,我們直播錄制視頻,就要先通過攝像頭采集圖像,然后進(jìn)行硬編碼,最后將硬編碼后的數(shù)據(jù)組合成H264碼流通過網(wǎng)絡(luò)傳播。

1. 視頻采集

這個硬件設(shè)備就是攝像頭了,通過AVFoundation框架中的AVCaptureSession類來采集圖像,并設(shè)定好input和output,同時設(shè)定deleagte代理和輸出隊(duì)列,在代理delegate方法中,處理采集好的圖像。圖像輸出的格式是未編碼的CMSampleBuffer形式。

2. 使用VTCompressionSession進(jìn)行硬編碼

獲取采集后的圖像,我們需要使用VTCompressionSession進(jìn)行硬編碼。

  • 初始化VTCompressionSession。

    • 在初始化VTCompressionSession的時候,我們需要給出width和height,還有編碼器類型kCMVideoCodecType_H264等。然后,通過VTSessionSetProperty接口設(shè)置幀率等屬性。最后,需要設(shè)定一個回調(diào)函數(shù),這個回調(diào)是視頻編碼成功后調(diào)用,全部準(zhǔn)備好后,調(diào)用VTCompressionSessionCreate創(chuàng)建session
  • 提取攝像頭采集的原始圖像數(shù)據(jù)給VTCompressionSession來硬編碼

    • 攝像頭采集后的圖像是未編碼的CMSampleBuffer形式,利用給定的接口函數(shù)CMSampleBufferGetImageBuffer從中提取出CVPixelBufferRef,使用硬編碼接口VTCompressionSessionEncodeFrame來對該幀進(jìn)行硬編碼,編碼成功后,會自動調(diào)用session初始化時設(shè)置的回調(diào)函數(shù)。
  • 利用回調(diào)函數(shù),將因編碼成功的CMSampleBuffer轉(zhuǎn)換成H264碼流,通過網(wǎng)絡(luò)傳播。

    • 解析成SPS和PPS參數(shù),加上開始碼后組裝成NALU,提取出視頻數(shù)據(jù),將長度碼轉(zhuǎn)換成開始碼,組長成NALU,并將NALU發(fā)送出去。
硬編碼處理流程示意圖

參考文章

1. iOS8系統(tǒng)H264視頻硬件編解碼說明
2. iOS-H264 硬解碼
3. 一輪圓月作者關(guān)于硬編解碼的GitHub Demo

后記

未完,待續(xù)~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容