一、軟編與硬編概念
1、軟編碼:使用CPU進行編碼。
實現直接、簡單,參數調整方便,升級易,但CPU負載重,性能較硬編碼低,低碼率下質量通常比硬編碼要好一點。
2、硬編碼:不使用CPU進行編碼,使用顯卡GPU,專用的DSP、FPGA、ASIC芯片等硬件進行編碼。
性能高,低碼率下通常質量低于軟編碼器,但部分產品在GPU硬件平臺移植了優秀的軟編碼算法(如X264)的,質量基本等同于軟編碼。
蘋果在iOS 8.0系統之前,沒有開放系統的硬件編碼解碼功能,不過Mac OS系統一直有,被稱為Video ToolBox的框架來處理硬件的編碼和解碼,終于在iOS 8.0(即WWDC 2014 513)后,蘋果將該框架引入iOS系統。
二、H.264編碼原理
H.264是新一代的編碼標準,以高壓縮高質量和支持多種網絡的流媒體傳輸著稱,在編碼方面,我理解的理論依據是:參照一段時間內圖像的統計結果表明,在相鄰幾幅圖像畫面中,一般有差別的像素只有10%以內的點,亮度差值變化不超過2%,而色度差值的變化只有1%以內。所以對于一段變化不大圖像畫面,我們可以先編碼出一個完整的圖像幀A,隨后的B幀就不編碼全部圖像,只寫入與A幀的差別,這樣B幀的大小就只有完整幀的1/10或更小!B幀之后的C幀如果變化不大,我們可以繼續以參考B的方式編碼C幀,這樣循環下去。這段圖像我們稱為一個序列(序列就是有相同特點的一段數據),當某個圖像與之前的圖像變化很大,無法參考前面的幀來生成,那我們就結束上一個序列,開始下一段序列,也就是對這個圖像生成一個完整幀A1,隨后的圖像就參考A1生成,只寫入與A1的差別內容。
需要注意的是:
在H264協議里定義了三種幀,完整編碼的幀叫I幀,參考之前的I幀生成的只包含差異部分編碼的幀叫P幀,還有一種參考前后的幀編碼的幀叫B幀。
H264采用的核心算法是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
三、序列的說明
在H264中圖像以序列為單位進行組織,一個序列是一段圖像編碼后的數據流,以I幀開始,到下一個I幀結束。
一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會。IDR圖像之后的圖像永遠不會使用IDR之前的圖像的數據來解碼。
一個序列就是一段內容差異不太大的圖像編碼后生成的一串數據流。當運動變化比較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的內容變動很小,所以就可以編一個I幀,然后一直P幀、B幀了。當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀。
四、對三種幀的簡單介紹
I、B、P各幀是根據壓縮算法的需要,是人為定義的,它們都是實實在在的物理幀。一般來說,I幀的壓縮率是7(跟JPG差不多),P幀是20,B幀可以達到50。可見使用B幀能節省大量空間,節省出來的空間可以用來保存多一些I幀,這樣在相同碼率下,可以提供更好的畫質。
說明:
I幀:紅色;P幀:藍色;B幀:綠色。
五、H264壓縮算法的說明
1、分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多。
2、定義幀:將每組內各幀圖像定義為三種類型,即I幀、B幀和P幀;
3、預測幀:以I幀做為基礎幀,以I幀預測P幀,再由I幀和P幀預測B幀;
4、數據傳輸:最后將I幀數據與預測的差值信息進行存儲和傳輸。
5、幀內(Intraframe)壓縮也稱為空間壓縮(Spatial compression)。
當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。幀內一般采用有損壓縮算法,由于幀內壓縮是編碼一個完整的圖像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼jpeg差不多。
6、幀間(Interframe)壓縮。
相鄰幾幀的數據有很大的相關性,或者說前后兩幀信息變化很小的特點。也即連續的視頻其相鄰幀之間具有冗余信息,根據這一特性,壓縮相鄰幀之間的冗余量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的數據進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
7、有損(Lossy)壓縮和無損(Lossy less)壓縮。
無損壓縮也即壓縮前和解壓縮后的數據完全一致。多數的無損壓縮都采用RLE行程編碼算法。
有損壓縮意味著解壓縮后的數據與壓縮前的數據不一致。在壓縮的過程中要丟失一些人眼和人耳所不敏感的圖像或音頻信息,而且丟失的信息不可恢復。幾乎所有高壓縮的算法都采用有損壓縮,這樣才能達到低數據率的目標。丟失的數據率與壓縮比有關,壓縮比越小,丟失的數據越多,解壓縮后的效果一般越差。此外,某些有損壓縮算法采用多次重復壓縮的方式,這樣還會引起額外的數據丟失。
六、DTS與PTS的區別
DTS主要用于視頻的解碼,在解碼階段使用.
PTS主要用于視頻的同步和輸出.
在display的時候使用.在沒有B frame的情況下.DTS和PTS的輸出順序是一樣的。
下面給出一個GOP為15的例子,其解碼的參照frame及其解碼的順序都在里面:
如上圖:
I frame 的解碼不依賴于任何的其它的幀.而p frame的解碼則依賴于其前面的I frame或者P frame.B frame的解碼則依賴于其前的最近的一個I frame或者P frame 及其后的最近的一個P frame.
七、iOS系統 H.264視頻硬件編解碼說明
1、VideoToolbox的介紹
在iOS中,與視頻相關的Framework庫有5個,從頂層開始分別是 AVKit
-> AVFoundation
-> VideoToolbox
-> Core Media
-> Core Video
其中VideoToolbox可以將視頻解壓到CVPixelBuffer
,也可以壓縮到CMSampleBuffer
。
但是我們常用的是CMSampleBuffer
.
2、VideoToolbox中的對象
1)CVPixelBuffer
編碼前和解碼后的圖像數據結構(未壓縮光柵圖像緩存區-Uncompressed Raster Image Buffer)
2)CVPixelBufferPool
存放CVPixelBuffer
3)pixelBufferAttributes
CFDictionary對象,可能包含了視頻的寬高,像素格式類型(32RGBA, YCbCr420),是否可以用于OpenGL ES等相關信息
4)CMTime
時間戳相關。時間以 64-big/32-bit形式出現。 分子是64-bit的時間值,分母是32-bit的時標(time scale)
5)CMClock
時間戳相關。時間以 64-big/32-bit形式出現。 分子是64-bit的時間值,分母是32-bit的時標(time scale)。它封裝了時間源,其中CMClockGetHostTimeClock()封裝了mach_absolute_time()
6)CMTimebase
時間戳相關。時間以 64-big/32-bit形式出現。CMClock上的控制視圖。提供了時間的映射:CMTimebaseSetTime(timebase, kCMTimeZero); 速率控制:
CMTimebaseSetRate(timebase, 1.0);
7)CMBlockBuffer
編碼后,結果圖像的數據結構
8)CMVideoFormatDescription
編解碼前后的視頻圖像均封裝在CMSampleBuffer中,如果是編碼后的圖像,以CMBlockBuffe方式存儲;解碼后的圖像,以CVPixelBuffer存儲。
9)CMSampleBuffer
存放編解碼前后的視頻圖像的容器數據結構。如圖所示,編解碼前后的視頻圖像均封裝在CMSampleBuffer中,如果是編碼后的圖像,以CMBlockBuffer方式存儲;解碼后的圖像,以CVPixelBuffer存儲。CMSampleBuffer里面還有另外的時間信息CMTime和視頻描述信息CMVideoFormatDesc。
八、硬解碼
目標:如何將從網絡處傳來H.264編碼后的視頻碼流顯示在手機屏幕上?
實現步驟如下:
1、將 H.264碼流轉換為 CMSampleBuffer
CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer
需要從H.264的碼流里面提取出以上的三個信息。最后組合成CMSampleBuffer,提供給硬解碼接口來進行解碼工作。
在H.264的語法中,有一個最基礎的層,叫做Network Abstraction Layer, 簡稱為NAL。H.264流數據正是由一系列的NAL單元(NAL Unit, 簡稱NAUL)組成的。
H264的碼流由NALU單元組成,一個NALU可能包含有:
-
視頻幀,視頻幀也就是視頻片段,具體有 P幀, I幀,B幀
B-006.png
2)H.264屬性合集-FormatDesc(包含 SPS和PPS),即流數據中,屬性集合可能是這樣的:
經過處理之后,在Format Description中則是:
需要注意的是:
要從基礎的流數據將SPS和PPS轉化為Format Desc中的話,需要調用CMVideoFormatDescriptionCreateFromH264ParameterSets()
方法。
3)NALU header
對于流數據來說,一個NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭(兩者都有可能,下面以0x00 00 01作為例子)。0x00 00 01因此被稱為開始碼(Start code).
總結以上知識,我們知道H264的碼流由NALU單元組成,NALU單元包含視頻圖像數據和H264的參數信息。其中視頻圖像數據就是CMBlockBuffer,而H264的參數信息則可以組合成FormatDesc。具體來說參數信息包含SPS(Sequence Parameter Set)和PPS(Picture Parameter Set).如下圖顯示了一個H.264碼流結構:
(1)提取sps和pps生成FormatDesc
- 每個NALU的開始碼是0x00 00 01,按照開始碼定位NALU
- 通過類型信息找到sps和pps并提取,開始碼后第一個byte的后5位,7代表sps,8代表pps
- 使用CMVideoFormatDescriptionCreateFromH264ParameterSets函數來構建CMVideoFormatDescriptionRef
(2)提取視頻圖像數據生成CMBlockBuffer
- 通過開始碼,定位到NALU
- 確定類型為數據后,將開始碼替換成NALU的長度信息(4 Bytes)
- 使用CMBlockBufferCreateWithMemoryBlock接口構造CMBlockBufferRef
(3)根據需要,生成CMTime信息。
(實際測試時,加入time信息后,有不穩定的圖像,不加入time信息反而沒有,需要進一步研究,這里建議不加入time信息)
根據上述得到CMVideoFormatDescriptionRef、CMBlockBufferRef和可選的時間信息,使用CMSampleBufferCreate接口得到CMSampleBuffer數據這個待解碼的原始的數據。如下圖所示的H264數據轉換示意圖。
2、將 CMSampleBuffer顯示出來
顯示的方式有兩種:
1)、將CMSampleBuffers提供給系統的AVSampleBufferDisplayLayer 直接顯示
使用方式和其它CALayer類似。該層內置了硬件解碼功能,將原始的CMSampleBuffer解碼后的圖像直接顯示在屏幕上面,非常的簡單方便。
2)、利用OPenGL渲染
通過VTDecompression接口來,將CMSampleBuffer解碼成圖像,將圖像通過UIImageView或者OpenGL上顯示。
初始化VTDecompressionSession,設置解碼器的相關信息。初始化信息需要CMSampleBuffer里面的FormatDescription,以及設置解碼后圖像的存儲方式。demo里面設置的CGBitmap模式,使用RGB方式存放。編碼后的圖像經過解碼后,會調用一個回調函數,將解碼后的圖像交個這個回調函數來進一步處理。我們就在這個回調里面,將解碼后的圖像發給control來顯示,初始化的時候要將回調指針作為參數傳給create接口函數。最后使用create接口對session來進行初始化。
上所述的回調函數可以完成CGBitmap圖像轉換成UIImage圖像的處理,將圖像通過隊列發送到Control來進行顯示處理。
調用VTDecompresSessionDecodeFrame接口進行解碼操作。解碼后的圖像會交由以上兩步驟設置的回調函數,來進一步的處理。
九、硬解碼
硬編碼的使用也通過一個典型的應用場景來描述。首先,通過攝像頭來采集圖像,然后將采集到的圖像,通過硬編碼的方式進行編碼,最后編碼后的數據將其組合成H264的碼流通過網絡傳播。
1、攝像頭采集數據
攝像頭采集,iOS系統提供了AVCaptureSession來采集攝像頭的圖像數據。設定好session的采集解析度。再設定好input和output即可。output設定的時候,需要設置delegate和輸出隊列。在delegate方法,處理采集好的圖像。
圖像輸出的格式,是未編碼的CMSampleBuffer形式。
2、使用VTCompressionSession進行硬編碼
1)初始化VTCompressionSession
VTCompressionSession初始化的時候,一般需要給出width寬,height長,編碼器類型kCMVideoCodecType_H264等。然后通過調用VTSessionSetProperty接口設置幀率等屬性,demo里面提供了一些設置參考,測試的時候發現幾乎沒有什么影響,可能需要進一步調試。最后需要設定一個回調函數,這個回調是視頻圖像編碼成功后調用。全部準備好后,使用VTCompressionSessionCreate創建session
2)提取攝像頭采集的原始圖像數據給VTCompressionSession來硬編碼
攝像頭采集后的圖像是未編碼的CMSampleBuffer形式,利用給定的接口函數CMSampleBufferGetImageBuffer從中提取出CVPixelBufferRef,使用硬編碼接口VTCompressionSessionEncodeFrame來對該幀進行硬編碼,編碼成功后,會自動調用session初始化時設置的回調函數。
3)利用回調函數,將因編碼成功的CMSampleBuffer轉換成H264碼流,通過網絡傳播。
基本上是硬解碼的一個逆過程。解析出參數集SPS和PPS,加上開始碼后組裝成NALU。提取出視頻數據,將長度碼轉換成開始碼,組長成NALU。將NALU發送出去。
本文主要為轉載學習,部分細節有刪改。
相關資料傳送: