硬體編碼相關知識(H264,H265)
閱讀人群:研究硬體編碼器應用於iOS開發中,從0研究關於硬體編解碼,碼流中解析資料結構
內容概述:關於H264,H265的背景,資料結構,在iOS開發中編解碼的應用
簡書地址 : 硬體編碼相關知識
部落格地址 : 硬體編碼相關知識
掘金地址 : 硬體編碼相關知識
一. 背景及概述
1. 在升級 iOS 11 之後,iPhone 7 及更新的裝置內的照片儲存將不再用 JPEG 了,而採用了一種新的圖片格式 HEIF(發音同 heef),在 iOS 中對應的檔案字尾為 .heic ,其編碼用的是 HEVC(這個發不了音,哈哈哈)格式,又稱 H.265 (這個就很熟悉了 H.264 的下一代),同時視訊也用 HEVC 作為編碼器,對應的檔案字尾還是 .mov 。
2. 這裡要注意他們倆的關係, HEIF 是圖片格式,而 HEVC 是編碼格式(類似 H.264,VP8),HEIF 是圖片容器(類似於視訊的 mkv,mp4 字尾),而用 HEVC 進行編碼的 HEIF 圖片就是字尾為 .heic 的圖片,也是蘋果主要使用的格式。
3. HEIF 全稱 High Efficiency Image Format (HEIF)。是由 Moving Picture Experts Group 制定的,儲存圖片和圖片序列的格式。下圖是形容HEIF的一句英文詩,JPEG很大,但是HEIF很小。
4. 優點
- 壓縮比高,在相同圖片質量情況下,比JPEG高兩倍
- 能增加如圖片的深度資訊,透明通道等輔助圖片。
- 支援存放多張圖片,類似相簿和集合。(實現多重曝光的效果)
- 支援多張圖片實現GIF和livePhoto的動畫效果。
- 無類似JPEG的最大畫素限制
- 支援透明畫素
- 分塊載入機制
- 支援縮圖
5. 檔案組成
- 在視訊檔案中,容器和編碼是獨立開的,比如mp4,mkv等格式是容器而H.264,vp8等是編碼
- 但是影像檔案中,像JPEG就是混合在一起的,所以自然不太好用。HEIF就把容器和編碼分開了,有用來存放單個或多個影像的容器。
6. 相容
一般情況下,使用者是對這個格式無感知的,因為只有在新款支援硬解碼的 iOS 手機內部是以 heif & hevc 格式來儲存照片和視訊的,而在使用者通過 Airdrop或者資料線傳送到電腦上的時候,對不相容的裝置會自動轉換到 JPEG 的格式。所以也不會影響你使用微信,微博等軟體。
二. 視訊編解碼
1.軟編與硬編概念
- 軟編碼:使用CPU進行編碼。
- 硬編碼:不使用CPU進行編碼,使用顯示卡GPU,專用的DSP、FPGA、ASIC晶片等硬體進行編碼。
- 比較
- 軟編碼:實現直接、簡單,引數調整方便,升級易,但CPU負載重,效能較硬編碼低,低位元速率下質量通常比硬編碼要好一點。
- 效能高,低位元速率下通常質量低於軟編碼器,但部分產品在GPU硬體平臺移植了優秀的軟編碼演算法(如X264)的,質量基本等同於軟編碼。
- 蘋果在iOS 8.0系統之前,沒有開放系統的硬體編碼解碼功能,不過Mac OS系統一直有,被稱為Video ToolBox的框架來處理硬體的編碼和解碼,終於在iOS 8.0後,蘋果將該框架引入iOS系統。
2. h.264編碼原理
H264是新一代的編碼標準,以高壓縮高質量和支援多種網路的流媒體傳輸著稱,在編碼方面,我理解的他的理論依據是:參照一段時間內影像的統計結果表明,在相鄰幾幅影像畫面中,一般有差別的畫素只有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幀的演算法。
3. 對序列的說明
在H264中影像以序列為單位進行組織,一個序列是一段影像編碼後的資料流,以I幀開始,到下一個I幀結束。
一個序列的第一個影像叫做 IDR 影像(立即重新整理影像),IDR 影像都是 I 幀影像。H.264 引入 IDR 影像是為了解碼的重同步,當解碼器解碼到 IDR 影像時,立即將參考幀佇列清空,將已解碼的資料全部輸出或拋棄,重新查詢引數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這裡可以獲得重新同步的機會。IDR影像之後的影像永遠不會使用IDR之前的影像的資料來解碼。
一個序列就是一段內容差異不太大的影像編碼後生成的一串資料流。當運動變化比較少時,一個序列可以很長,因為運動變化少就代表影像畫面的內容變動很小,所以就可以編一個I幀,然後一直P幀、B幀了。當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀。
4. 對三種幀的介紹
-
I幀
- 幀內編碼幀 ,I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀資料就可以完成(因為包含完整畫面)。
- 特點
- 它是一個全幀壓縮編碼幀。它將全幀影像資訊進行JPEG壓縮編碼及傳輸
- 解碼時僅用I幀的資料就可重構完整影像
- I幀描述了影像背景和運動主體的詳情
- I幀不需要參考其他畫面而生成
- I幀是P幀和B幀的參考幀(其質量直接影響到同組中以後各幀的質量)
- I幀是幀組GOP的基礎幀(第一幀),在一組中只有一個I幀
- I幀不需要考慮運動向量
- I幀所佔資料的資訊量比較大
-
P幀
- 前向預測編碼幀。P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼時需要用之前快取的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面資料,只有與前一幀的畫面差別的資料),通過充分將低於影像序列中前面已編碼幀的時間冗餘資訊來壓縮傳輸資料量的編碼影像,也叫預測幀
- P幀的預測與重構:P幀是以I幀為參考幀,在I幀中找出P幀“某點”的預測值和運動向量,取預測差值和運動向量一起傳送。在接收端根據運動向量從I幀中找出P幀“某點”的預測值並與差值相加以得到P幀“某點”樣值,從而可得到完整的P幀。
- 特點:
- P幀是I幀後面相隔1~2幀的編碼幀
- P幀採用運動補償的方法傳送它與前面的I或P幀的差值及運動向量(預測誤差)
- 解碼時必須將I幀中的預測值與預測誤差求和後才能重構完整的P幀影像
- P幀屬於前向預測的幀間編碼。它只參考前面最靠近它的I幀或P幀
- P幀可以是其後面P幀的參考幀,也可以是其前後的B幀的參考幀
- 由於P幀是參考幀,它可能造成解碼錯誤的擴散
- 由於是差值傳送,P幀的壓縮比較高
-
B幀
- 雙向預測內插編碼幀。B幀是雙向差別幀,也就是B幀記錄的是本幀與前後幀的差別(具體比較複雜,有4種情況,但我這樣說簡單些),換言之,要解碼B幀,不僅要取得之前的快取畫面,還要解碼之後的畫面,通過前後畫面的與本幀資料的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累。
- B幀的預測與重構:B幀以前面的I或P幀和後面的P幀為參考幀,“找出”B幀“某點”的預測值和兩個運動向量,並取預測差值和運動向量傳送。接收端根據運動向量在兩個參考幀中“找出(算出)”預測值並與差值求和,得到B幀“某點”樣值,從而可得到完整的B幀。
- 特點:
- B幀是由前面的I或P幀和後面的P幀來進行預測的
- B幀傳送的是它與前面的I或P幀和後面的P幀之間的預測誤差及運動向量
- B幀是雙向預測編碼幀
- B幀壓縮比最高,因為它只反映丙參考幀間運動主體的變化情況,預測比較準確
- B幀不是參考幀,不會造成解碼錯誤的擴散
I、B、P各幀是根據壓縮演算法的需要,是人為定義的,它們都是實實在在的物理幀。一般來說,I幀的壓縮率是7(跟JPG差不多),P幀是20,B幀可以達到50。可見使用B幀能節省大量空間,節省出來的空間可以用來儲存多一些I幀,這樣在相同位元速率下,可以提供更好的畫質。
5.對壓縮演算法得說明
h264的壓縮方法:
-
分組:把幾幀影像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多。
-
定義幀:將每組內各幀影像定義為三種型別,即I幀、B幀和P幀;
-
預測幀:以I幀做為基礎幀,以I幀預測P幀,再由I幀和P幀預測B幀;
-
資料傳輸:最後將I幀資料與預測的差值資訊進行儲存和傳輸。
-
幀內(Intraframe)壓縮也稱為空間壓縮(Spatial compression)。
- 當壓縮一幀影像時,僅考慮本幀的資料而不考慮相鄰幀之間的冗餘資訊,這實際上與靜態影像壓縮類似。幀內一般採用有失真壓縮演算法,由於幀內壓縮是編碼一個完整的影像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼jpeg差不多。
-
幀間(Interframe)壓縮
- 相鄰幾幀的資料有很大的相關性,或者說前後兩幀資訊變化很小的特點。也即連續的視訊其相鄰幀之間具有冗餘資訊,根據這一特性,壓縮相鄰幀之間的冗餘量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的資料進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)演算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少資料量。
-
有損(Lossy )壓縮和無損(Lossy less)壓縮。
- 無失真壓縮也即壓縮前和解壓縮後的資料完全一致。多數的無失真壓縮都採用RLE行程編碼演算法。
- 有失真壓縮意味著解壓縮後的資料與壓縮前的資料不一致。在壓縮的過程中要丟失一些人眼和人耳所不敏感的影像或音訊資訊,而且丟失的資訊不可恢復。幾乎所有高壓縮的演算法都採用有失真壓縮,這樣才能達到低資料率的目標。丟失的資料率與壓縮比有關,壓縮比越小,丟失的資料越多,解壓縮後的效果一般越差。此外,某些有失真壓縮演算法採用多次重複壓縮的方式,這樣還會引起額外的資料丟失。
6. DTS和PTS的不同
DTS主要用於視訊的解碼,在解碼階段使用.PTS主要用於視訊的同步和輸出.在display的時候使用.在沒有B frame的情況下.DTS和PTS的輸出順序是一樣的。
EX:下面給出一個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中,與視訊相關的介面有5個,從頂層開始分別是 AVKit – AVFoundation – VideoToolbox – Core Media – Core Video
其中VideoToolbox可以將視訊解壓到CVPixelBuffer,也可以壓縮到CMSampleBuffer。
如果需要使用硬編碼的話,在5個介面中,就需要用到AVKit,AVFoundation和VideoToolbox。在這裡我就只介紹VideoToolbox。
2.VideoToolbox中的物件
- CVPixelBuffer : 編碼前和解碼後的影像資料結構(未壓縮光柵影像快取區-Uncompressed Raster Image Buffer)
- CVPixelBufferPool : 顧名思義,存放CVPixelBuffer
- pixelBufferAttributes : CFDictionary物件,可能包含了視訊的寬高,畫素格式型別(32RGBA, YCbCr420),是否可以用於OpenGL ES等相關資訊
- CMTime : 時間戳相關。時間以 64-big/32-bit形式出現。 分子是64-bit的時間值,分母是32-bit的時標(time scale)
- CMClock : 時間戳相關。時間以 64-big/32-bit形式出現。 分子是64-bit的時間值,分母是32-bit的時標(time scale)。它封裝了時間源,其中CMClockGetHostTimeClock()封裝了mach_absolute_time()
- CMTimebase : 時間戳相關。時間以 64-big/32-bit形式出現。CMClock上的控制檢視。提供了時間的對映:CMTimebaseSetTime(timebase, kCMTimeZero); 速率控制: CMTimebaseSetRate(timebase, 1.0);
- CMBlockBuffer : 編碼後,結果影像的資料結構
- CMVideoFormatDescription : 影像存如圖所示,編解碼前後的視訊影像均封裝在CMSampleBuffer中,如果是編碼後的影像,以CMBlockBuffe方式儲存;解碼後的影像,以CVPixelBuffer儲存。CMSampleBuffer裡面還有另外的時間資訊CMTime和視訊描述資訊CMVideoFormatDesc。儲方式,編解碼器等格式描述
- CMSampleBuffer : 存放編解碼前後的視訊影像的容器資料結構
- 如圖所示,編解碼前後的視訊影像均封裝在CMSampleBuffer中,如果是編碼後的影像,以CMBlockBuffe方式儲存;解碼後的影像,以CVPixelBuffer儲存。CMSampleBuffer裡面還有另外的時間資訊CMTime和視訊描述資訊CMVideoFormatDesc。
3. 硬解碼
通過如圖所示的一個典型應用,來說明如何使用硬體解碼介面。該應用場景是從網路處傳來H264編碼後的視訊碼流,最後顯示在手機螢幕上。
要完成以上功能需要經過以下幾個步驟:
1> 將 H.264碼流轉換為 CMSampleBuffer
我們知道,CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer . 需要從H264的碼流裡面提取出以上的三個資訊。最後組合成CMSampleBuffer,提供給硬解碼介面來進行解碼工作。
在H.264的語法中,有一個最基礎的層,叫做Network Abstraction Layer, 簡稱為NAL。H.264流資料正是由一系列的NAL單元(NAL Unit, 簡稱NAUL)組成的。
H264的碼流由NALU單元組成,一個NALU可能包含有:
- 視訊幀,視訊幀也就是視訊片段,具體有 P幀, I幀,B幀
- H.264屬性合集-FormatDesc(包含 SPS和PPS)
流資料中,屬性集合可能是這樣的:
經過處理之後,在Format Description中則是:
要從基礎的流資料將SPS和PPS轉化為Format Desc中的話,需要呼叫CMVideoFormatDescriptionCreateFromH264ParameterSets()方法
- 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碼流結構:
-
提取sps和pps生成FormatDesc
- 每個NALU的開始碼是0x00 00 01,按照開始碼定位NALU
- 通過型別資訊找到sps和pps並提取,開始碼後第一個byte的後5位,7代表sps,8代表pps
- 使用CMVideoFormatDescriptionCreateFromH264ParameterSets函式來構建CMVideoFormatDescriptionRef
-
提取視訊影像資料生成CMBlockBuffer
- 通過開始碼,定位到NALU
- 確定型別為資料後,將開始碼替換成NALU的長度資訊(4 Bytes)
- 使用CMBlockBufferCreateWithMemoryBlock介面構造CMBlockBufferRef
-
根據需要,生成CMTime資訊。(實際測試時,加入time資訊後,有不穩定的影像,不加入time資訊反而沒有,需要進一步研究,這裡建議不加入time資訊)
根據上述得到CMVideoFormatDescriptionRef、CMBlockBufferRef和可選的時間資訊,使用CMSampleBufferCreate介面得到CMSampleBuffer資料這個待解碼的原始的資料。如下圖所示的H264資料轉換示意圖。
2> 將 CMSampleBuffer顯示出來
顯示的方式有兩種:
- 將CMSampleBuffers提供給系統的AVSampleBufferDisplayLayer 直接顯示
- 使用方式和其它CALayer類似。該層內建了硬體解碼功能,將原始的CMSampleBuffer解碼後的影像直接顯示在螢幕上面,非常的簡單方便。
- 利用OPenGL自己渲染
通過VTDecompression介面來,將CMSampleBuffer解碼成影像,將影像通過UIImageView或者OpenGL上顯示。- 初始化VTDecompressionSession,設定解碼器的相關資訊。初始化資訊需要CMSampleBuffer裡面的FormatDescription,以及設定解碼後影像的儲存方式。demo裡面設定的CGBitmap模式,使用RGB方式存放。編碼後的影像經過解碼後,會呼叫一個回撥函式,將解碼後的影像交個這個回撥函式來進一步處理。我們就在這個回撥裡面,將解碼後的影像發給control來顯示,初始化的時候要將回撥指標作為引數傳給create介面函式。最後使用create介面對session來進行初始化。
- a中所述的回撥函式可以完成CGBitmap影像轉換成UIImage影像的處理,將影像通過佇列傳送到Control來進行顯示處理。
- 呼叫VTDecompresSessionDecodeFrame介面進行解碼操作。解碼後的影像會交由以上兩步驟設定的回撥函式,來進一步的處理。
4.硬編碼
硬編碼的使用也通過一個典型的應用場景來描述。首先,通過攝像頭來採集影像,然後將採集到的影像,通過硬編碼的方式進行編碼,最後編碼後的資料將其組合成H264的碼流通過網路傳播。
-
攝像頭採集資料
攝像頭採集,iOS系統提供了AVCaptureSession來採集攝像頭的影像資料。設定好session的採集解析度。再設定好input和output即可。output設定的時候,需要設定delegate和輸出佇列。在delegate方法,處理採集好的影像。
影像輸出的格式,是未編碼的CMSampleBuffer形式。
-
使用VTCompressionSession進行硬編碼
- 初始化VTCompressionSession
VTCompressionSession初始化的時候,一般需要給出width寬,height長,編碼器型別kCMVideoCodecType_H264等。然後通過呼叫VTSessionSetProperty介面設定幀率等屬性,demo裡面提供了一些設定參考,測試的時候發現幾乎沒有什麼影響,可能需要進一步除錯。最後需要設定一個回撥函式,這個回撥是視訊影像編碼成功後呼叫。全部準備好後,使用VTCompressionSessionCreate建立session
- 提取攝像頭採集的原始影像資料給VTCompressionSession來硬編碼
攝像頭採集後的影像是未編碼的CMSampleBuffer形式,利用給定的介面函式CMSampleBufferGetImageBuffer從中提取出CVPixelBufferRef,使用硬編碼介面VTCompressionSessionEncodeFrame來對該幀進行硬編碼,編碼成功後,會自動呼叫session初始化時設定的回撥函式。
- 利用回撥函式,將因編碼成功的CMSampleBuffer轉換成H264碼流,通過網路傳播
基本上是硬解碼的一個逆過程。解析出引數集SPS和PPS,加上開始碼後組裝成NALU。提取出視訊資料,將長度碼轉換成開始碼,組長成NALU。將NALU傳送出去。