硬件編碼相關知識(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很小。
[圖片上傳失敗...(image-b8d9fb-1519380720352)]
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及其解碼的順序都在里面:
[圖片上傳失敗...(image-2a98f6-1519380720352)]
如上圖: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)
[圖片上傳失敗...(image-655c29-1519380720352)]
- CVPixelBufferPool : 顧名思義,存放CVPixelBuffer
[圖片上傳失敗...(image-b48648-1519380720352)]
- 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);
[圖片上傳失敗...(image-ce8930-1519380720352)]
- CMBlockBuffer : 編碼后,結果圖像的數據結構
- CMVideoFormatDescription : 圖像存如圖所示,編解碼前后的視頻圖像均封裝在CMSampleBuffer中,如果是編碼后的圖像,以CMBlockBuffe方式存儲;解碼后的圖像,以CVPixelBuffer存儲。CMSampleBuffer里面還有另外的時間信息CMTime和視頻描述信息CMVideoFormatDesc。儲方式,編解碼器等格式描述
- CMSampleBuffer : 存放編解碼前后的視頻圖像的容器數據結構
- 如圖所示,編解碼前后的視頻圖像均封裝在CMSampleBuffer中,如果是編碼后的圖像,以CMBlockBuffe方式存儲;解碼后的圖像,以CVPixelBuffer存儲。CMSampleBuffer里面還有另外的時間信息CMTime和視頻描述信息CMVideoFormatDesc。
[圖片上傳失敗...(image-ea0dbd-1519380720352)]
3. 硬解碼
通過如圖所示的一個典型應用,來說明如何使用硬件解碼接口。該應用場景是從網絡處傳來H264編碼后的視頻碼流,最后顯示在手機屏幕上。
[圖片上傳失敗...(image-6459d6-1519380720352)]
要完成以上功能需要經過以下幾個步驟:
1> 將 H.264碼流轉換為 CMSampleBuffer
我們知道,CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer . 需要從H264的碼流里面提取出以上的三個信息。最后組合成CMSampleBuffer,提供給硬解碼接口來進行解碼工作。
在H.264的語法中,有一個最基礎的層,叫做Network Abstraction Layer, 簡稱為NAL。H.264流數據正是由一系列的NAL單元(NAL Unit, 簡稱NAUL)組成的。
[圖片上傳失敗...(image-c27508-1519380720352)]
H264的碼流由NALU單元組成,一個NALU可能包含有:
- 視頻幀,視頻幀也就是視頻片段,具體有 P幀, I幀,B幀
[圖片上傳失敗...(image-79c3ab-1519380720352)]
- H.264屬性合集-FormatDesc(包含 SPS和PPS)
流數據中,屬性集合可能是這樣的:
[圖片上傳失敗...(image-59ef69-1519380720352)]
經過處理之后,在Format Description中則是:
[圖片上傳失敗...(image-841239-1519380720352)]
要從基礎的流數據將SPS和PPS轉化為Format Desc中的話,需要調用CMVideoFormatDescriptionCreateFromH264ParameterSets()方法
- NALU header
對于流數據來說,一個NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭(兩者都有可能,下面以0x00 00 01作為例子)。0x00 00 01因此被稱為開始碼(Start code).
[圖片上傳失敗...(image-31ec88-1519380720352)]
總結以上知識,我們知道H264的碼流由NALU單元組成,NALU單元包含視頻圖像數據和H264的參數信息。其中視頻圖像數據就是CMBlockBuffer,而H264的參數信息則可以組合成FormatDesc。具體來說參數信息包含SPS(Sequence Parameter Set)和PPS(Picture Parameter Set).如下圖顯示了一個H.264碼流結構:
[圖片上傳失敗...(image-a228f3-1519380720352)]
-
提取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數據轉換示意圖。
[圖片上傳失敗...(image-d9b93d-1519380720352)]
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發送出去。