在移動端作音視頻開發不一樣於基本的UI業務邏輯工做,音視頻開發須要你懂得音視頻中一些基本概念,針對編解碼而言,咱們必須提早懂得編解碼器的一些特性,碼流的結構,碼流中一些重要信息如sps,pps,vps,start code以及基本的工做原理,而大多同窗都只是只知其一;不知其二,因此致使代碼中的部份內容雖能夠簡單理解殊不知其意,因此,在這裏總結出了當前主流的H.264,H.265編碼相關的原理,以供學習.html
衆所周知,視頻數據原始體積是巨大的,以720P 30fps的視頻爲例,一個像素大約3個字節,以下所得,每秒鐘產生87MB,這樣計算可得一分鐘就將產生5.22GB.git
數據量/每秒=1280*720*33*3/1024/1024=87MB
複製代碼
所以,像這樣體積重大的視頻是沒法在網絡中直接傳輸的.而視頻編碼技術也就因運而生.關於視頻編碼原理的技術能夠參考本人其餘文章,這裏不作過多描述.github
通過不少年的開發迭代,已經有不少大牛實現了視頻編碼技術,其中最主流的有H.264編碼,以及新一代的H.265編碼,谷歌也開發了VP8,VP9編碼技術.對移動端而言,蘋果內部已經實現瞭如H.264,H.265編碼,咱們須要使用蘋果提供的VideoToolbox框架來實現它.算法
優缺點編程
軟編:實現直接、簡單,參數調整方便,升級易,但CPU負載重,性能較硬編碼低,低碼率下質量一般比硬編碼要好一點。數組
硬編:性能高,低碼率下一般質量低於硬編碼器,但部分產品在GPU硬件平臺移植了優秀的軟編碼算法(如X264)的,質量基本等同於軟編碼。bash
iOS系統中的硬編碼 蘋果在iOS 8.0系統以前,沒有開放系統的硬件編碼解碼功能,不過Mac OS系統一直有,被稱爲Video ToolBox的框架來處理硬件的編碼和解碼,終於在iOS 8.0後,蘋果將該框架引入iOS系統。網絡
對視頻執行編碼操做後,原始視頻數據會被壓縮成三種不一樣類型的視頻幀: I幀,P幀,B幀.數據結構
補充: I幀的壓縮率是7(跟JPG差很少),P幀是20,B幀能夠達到50. 可是iOS中通常不開啓B幀,由於B幀的存在會致使時間戳同步較爲複雜.架構
兩種核心算法
當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗餘信息,這實際上與靜態圖像壓縮相似。幀內通常採用有損壓縮算法,因爲幀內壓縮是編碼一個完整的圖像,因此能夠獨立的解碼、顯示。幀內壓縮通常達不到很高的壓縮,跟編碼jpeg差很少。
以下圖:咱們能夠經過第 一、二、三、四、5 塊的編碼來推測和計算第 6 塊的編碼,所以就不須要對第 6 塊進行編碼了,從而壓縮了第 6 塊,節省了空間
相鄰幾幀的數據有很大的相關性,或者說先後兩幀信息變化很小的特色。也即連續的視頻其相鄰幀之間具備冗餘信息,根據這一特性,壓縮相鄰幀之間的冗餘量就能夠進一步提升壓縮量,減少壓縮比。幀間壓縮也稱爲時間壓縮(Temporal compression),它經過比較時間軸上不一樣幀之間的數據進行壓縮。幀間壓縮通常是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它經過比較本幀與相鄰幀之間的差別,僅記錄本幀與其相鄰幀的差值,這樣能夠大大減小數據量。
以下圖:能夠看到先後兩幀的差別實際上是很小的,這時候用幀間壓縮就頗有意義。
有損壓縮與無損壓縮
DTS和PTS
如上圖:I幀的解碼不依賴於任何的其它的幀.而P幀的解碼則依賴於其前面的I幀或者P幀.B幀的解碼則依賴於其前的最近的一個I幀或者P幀 及其後的最近的一個P幀.
在咱們的印象中,一張圖片就是一張圖像,視頻就是不少張圖片的集合.。可是由於咱們要作音視頻編程,就須要更加深刻理解視頻的本質.
在編碼的碼流中圖像是個集合的概念,幀、頂場、底場均可以稱爲圖像,一幀一般就是一幅完整的圖像.
VPS主要用於傳輸視頻分級信息,有利於兼容標準在可分級視頻編碼或多視點視頻的擴展。
(1)用於解釋編碼過的視頻序列的總體結構,包括時域子層依賴關係等。HEVC中加入該結構的主要目的是兼容標準在系統的多子層方面的擴展,處理好比將來的可分級或者多視點視頻使用原先的解碼器進行解碼可是其所需的信息可能會被解碼器忽略的問題。
(2)對於給定視頻序列的某一個子層,不管其SPS相不相同,都共享一個VPS。其主要包含的信息有:多個子層或操做點共享的語法元素;檔次和級別等會話關鍵信息;其餘不屬於SPS的操做點特定信息。
(3)編碼生成的碼流中,第一個NAL單元攜帶的就是VPS信息
包含一個CVS中全部編碼圖像的共享編碼參數。
(1)一段HEVC碼流可能包含一個或者多個編碼視頻序列,每一個視頻序列由一個隨機接入點開始,即IDR/BLA/CRA。序列參數集SPS包含該視頻序列中全部slice須要的信息。
(2)SPS的內容大體能夠分爲幾個部分:一、自引ID;二、解碼相關信息,如檔次級別、分辨率、子層數等;三、某檔次中的功能開關標識及該功能的參數;四、對結構和變換系數編碼靈活性的限制信息;五、時域可分級信息;六、VUI。
包含一幅圖像所用的公共參數,即一幅圖像中全部片斷SS(Slice Segment)引用同一個PPS。
(1)PPS包含每一幀可能不一樣的設置信息,其內容同H.264中的大體相似,主要包括:一、自引信息;二、初始圖像控制信息,如初始QP等;三、分塊信息。
(2)在解碼開始的時候,全部的PPS所有是非活動狀態,並且在解碼的任意時刻,最多隻能有一個PPS處於激活狀態。當某部分碼流引用了某個PPS的時候,這個PPS便被激活,稱爲活動PPS,一直到另外一個PPS被激活。
參數集包含了相應的編碼圖像的信息。SPS包含的是針對一連續編碼視頻序列的參數(標識符seq_parameter_set_id、幀數及POC的約束、參考幀數目、解碼圖像尺寸和幀場編碼模式選擇標識等等)。PPS對應的是一個序列中某一幅圖像或者某幾幅圖像 ,其參數如標識符pic_parameter_set_id、可選的seq_parameter_set_id、熵編碼模式選擇標識、片組數目、初始量化參數和去方塊濾波係數調整標識等等。
一般,SPS 和PPS 在片的頭信息和數據解碼前傳送至解碼器。每一個片的頭信息對應一個 pic_parameter_set_id,PPS被其激活後一直有效到下一個PPS被激活;相似的,每一個PPS對應一個 seq_parameter_set_id,SPS被其激活之後將一直有效到下一個SPS被激活。 參數集機制將一些重要的、改變少的序列參數和圖像參數與編碼片分離,並在編碼片以前傳送 至解碼端,或者經過其餘機制傳輸。
擴展知識點:檔次(Profile)、層(Tier)和級別(Level)
檔次: 主要規定編碼器可採用哪些編碼工具或算法。
級別: 指根據解碼端的負載和存儲空間狀況對關鍵參數(最大采樣率、最大圖像尺寸、分辨率、最小壓縮比、最大比特率、解碼緩衝區DPB大小等)加以限制。
考慮到應用可根據最大的碼率和CPB大小來區分,所以有些級別定義了兩個層Tier:主層和高層,主層用於大多數應用,而高層用於那些最嚴苛的應用。
一個序列的第一個圖像叫作 IDR 圖像(當即刷新圖像),IDR 圖像都是 I 幀圖像。引入 IDR 圖像是爲了解碼的重同步,當解碼器解碼到 IDR 圖像時,當即將參考幀隊列清空,將已解碼的數據所有輸出或拋棄,從新查找參數集,開始一個新的序列。這樣,若是前一個序列出現重大錯誤,在這裏能夠得到從新同步的機會。IDR圖像以後的圖像永遠不會使用IDR以前的圖像的數據來解碼。
由一個接一個的 NALU 組成的,而它的功能分爲兩層,VCL(視頻編碼層)和 NAL(網絡提取層).
下圖以h264的碼流結構爲例,若是是h265則在sps前還有vps.
NALU (Nal Unit) = NALU頭 + RBSP 在 VCL
數據傳輸或存儲以前,這些編碼的 VCL 數據,先被映射或封裝進 NAL 單元(如下簡稱 NALU,Nal Unit) 中。每一個 NALU 包括一個原始字節序列負荷(RBSP, Raw Byte Sequence Payload)、一組 對應於視頻編碼的 NALU 頭部信息。RBSP 的基本結構是:在原始編碼數據的後面填加告終尾 比特。一個 bit「1」若干比特「0」,以便字節對齊。
一個原始的H.264 NALU 單元常由 [StartCode] [NALU Header] [NALU Payload] 三部分組成
StartCode : Start Code 用於標示這是一個NALU 單元的開始,必須是」00 00 00 01」 或」00 00 01」
NALU Header 下表爲 NAL Header Type
例如,下面幅圖分別表明IDR與非IDR幀具體的碼流信息:
在一個NALU中,第一個字節(即NALU header)用以表示其包含數據的類型及其餘信息。咱們假定一個頭信息字節爲0x67做爲例子:
十六進制 | 二進制 |
---|---|
0x67 | 0 11 00111 |
如表所示,頭字節能夠被解析成3個部分,其中:
1>. forbidden_zero_bit = 0:佔1個bit,禁止位,用以檢查傳輸過程當中是否發生錯誤,0表示正常,1表示違反語法;
2>. nal_ref_idc = 3:佔2個bit,用來表示當前NAL單元的優先級。非0值表示參考字段/幀/圖片數據,其餘不那麼重要的數據則爲0。對於非0值,值越大表示NALU重要性越高
3>. nal_unit_type = 7:最後5位用以指定NALU類型,NALU類型定義如上表
從表中咱們能夠獲知,NALU類型1-5爲視頻幀,其他則爲非視頻幀。在解碼過程當中,咱們只須要取出NALU頭字節的後5位,即將NALU頭字節和0x1F進行與計算便可得知NALU類型,即:
NALU類型 = NALU頭字節 & 0x1F
複製代碼
注意: 能夠將start code理解爲不一樣nalu的分隔符,header是某種類型的key,payload是該key的value.
H.264標準中指定了視頻如何編碼成獨立的包,但如何存儲和傳輸這些包卻未做規範,雖然標準中包含了一個Annex附件,裏面描述了一種可能的格式Annex B,但這並非一個必需要求的格式。 爲了針對不一樣的存儲傳輸需求,出現了兩種打包方法。一種即Annex B格式,另外一種稱爲AVCC格式。
從上文可知,一個NALU中的數據並未包含他的大小(長度)信息,所以咱們並不能簡單的將一個個NALU鏈接起來生成一個流,由於數據流的接收端並不知道一個NALU從哪裏結束,另外一個NALU從哪裏開始。 Annex B格式用起始碼(Start Code)來解決這個問題,它在每一個NALU的開始處添加三字節或四字節的起始碼0x000001或0x00000001。經過定位起始碼,解碼器就能夠很容易的識別NALU的邊界。 固然,用起始碼定位NALU邊界存在一個問題,即NALU中可能存在與起始碼相同的數據。爲了防止這個問題,在構建NALU時,須要將數據中的0x000000,0x000001,0x000002,0x000003中插入防競爭字節(Emulation Prevention Bytes)0x03,使其變爲:
0x000000 = 0x0000 03 00 0x000001 = 0x0000 03 01 0x000002 = 0x0000 03 02 0x000003 = 0x0000 03 03 解碼器在檢測到0x000003時,將0x03拋棄,恢復原始數據。
因爲Annex B格式每一個NALU都包含起始碼,因此解碼器能夠從視頻流隨機點開始進行解碼,經常使用於實時的流格式。在這種格式中一般會週期性的重複SPS和PPS,而且常常時在每個關鍵幀以前。
AVCC格式不使用起始碼做爲NALU的分界,這種格式在每一個NALU前都加上一個指定NALU長度的大端格式表示的前綴。這個前綴能夠是一、2或4個字節,因此在解析AVCC格式的時候須要將指定的前綴字節數的值保存在一個頭部對象中,這個都一般稱爲extradata或者sequence header。同時,SPS和PPS數據也須要保存在extradata中。 H.264 extradata語法以下:
bits | line by byte | remark |
---|---|---|
8 | version | always |
8 | avc profile | sps[0][1] |
8 | avc compatibility | sps[0][2] |
8 | avc level | sps[0][3] |
6 | reserved | all bits on |
2 | NALULengthSizeMinusOne | |
3 | reserved | all bits on |
5 | number of SPS NALUs usually | 1 |
16 | SPS size | |
N | variable SPS NALU data | |
8 | number of PPS NALUs usually | 1 |
16 | PPS size | |
N | variable PPS NALU data |
其中第5字節的後2位表示的就是NAL size的字節數。須要注意的是,這個NALULengthSizeMinusOne是NALU前綴長度減一,即,假設前綴長度爲4,那麼這個值應該爲3。 這裏還須要注意的一點是,雖然AVCC格式不使用起始碼,但防競爭字節仍是有的。
AVCC格式的一個優勢在於解碼器配置參數在一開始就配置好了,系統能夠很容易的識別NALU的邊界,不須要額外的起始碼,減小了資源的浪費,同時能夠在播放時調到視頻的中間位置。這種格式一般被用於能夠被隨機訪問的多媒體數據,如存儲在硬盤的文件。
HEVC全稱High Efficiency Video Coding(高效率視頻編碼,又稱H.265),是比H.264更優秀的一種視頻壓縮標準。HEVC在低碼率視頻壓縮上,提高視頻質量、減小容量即節省帶寬方面都有突出表現。 H.265標準圍繞H.264編碼標準,保留原有的某些技術,同時對一些技術進行改進,編碼結構大體上和H.264的架構相似。這裏着重講一下二者編碼格式的區別。 同H.264同樣,H.265也是以NALU的形式組織起來。而在NALU header上,H.264的HALU header是一個字節,而H.265則是兩個字節。咱們一樣假定一個頭信息爲0x4001做爲例子:
十六進制 | 二進制 |
---|---|
0x4001 | 0 100000 000000 001 |
如表所示,頭信息能夠被解析成4個部分,其中:
對比H.264的頭信息,H.265移除了nal_ref_idc,此信息被合併到了nal_unit_type中,H.265NALU類型規定以下:
nal_unit_type | NALU類型 | 備註 |
---|---|---|
0 | NAL_UNIT_CODE_SLICE_TRAIL_N | 非關鍵幀 |
1 | NAL_UNIT_CODED_SLICE_TRAIL_R | |
2 | NAL_UNIT_CODED_SLICE_TSA_N | |
3 | NAL_UINT_CODED_SLICE_TSA_R | |
4 | NAL_UINT_CODED_SLICE_STSA_N | |
5 | NAL_UINT_CODED_SLICE_STSA_R | |
6 | NAL_UNIT_CODED_SLICE_RADL_N | |
7 | NAL_UNIT_CODED_SLICE_RADL_R | |
8 | NAL_UNIT_CODED_SLICE_RASL_N | |
9 | NAL_UNIT_CODE_SLICE_RASL_R | |
10 ~ 15 | NAL_UNIT_RESERVED_X | 保留 |
16 | NAL_UNIT_CODED_SLICE_BLA_W_LP | 關鍵幀 |
17 | NAL_UNIT_CODE_SLICE_BLA_W_RADL | |
18 | NAL_UNIT_CODE_SLICE_BLA_N_LP | |
19 | NAL_UNIT_CODE_SLICE_IDR_W_RADL | |
20 | NAL_UNIT_CODE_SLICE_IDR_N_LP | |
21 | NAL_UNIT_CODE_SLICE_CRA | |
22 ~ 31 | NAL_UNIT_RESERVED_X | 保留 |
32 | NAL_UNIT_VPS | VPS(Video Paramater Set) |
33 | NAL_UNIT_SPS | SPS |
34 | NAL_UNIT_PPS | PPS |
35 | NAL_UNIT_ACCESS_UNIT_DELIMITER | |
36 | NAL_UNIT_EOS | |
37 | NAL_UNIT_EOB | |
38 | NAL_UNIT_FILLER_DATA | |
39 | NAL_UNIT_SEI | Prefix SEI |
40 | NAL_UNIT_SEI_SUFFIX | Suffix SEI |
41 ~ 47 | NAL_UNIT_RESERVED_X | 保留 |
48 ~ 63 | NAL_UNIT_UNSPECIFIED_X | 未規定 |
64 | NAL_UNIT_INVALID |
具體type含義能夠參考這篇文檔type類型 H.265的NALU類型是在信息頭的第一個字節的第2到7位,因此判斷H.265NALU類型的方法是將NALU第一個字節與0x7E進行與操做並右移一位,即:
NALU類型 = (NALU頭第一字節 & 0x7E) >> 1
複製代碼
與H.264相似,H.265碼流也有兩種封裝格式,一種是用起始碼做爲分界的Annex B格式,另外一種則是在NALU頭添加NALU長度前綴的格式,稱爲HVCC。在HVCC中,一樣須要一個extradata來保存視頻流的編解碼參數,其格式定義以下:
bits | line by byte | remark |
---|---|---|
8 | configurationVersion | always 0x01 |
2 | general_profile_space | |
1 | general_tier_flag | |
5 | general_profile_idc | |
32 | general_profile_compatibility_flags | |
48 | general_constraint_indicator_flags | |
8 | general_level_idc | |
4 | reserved | ‘1111’b |
12 | min_spatial_segmentation_idc | |
6 | reserved | ‘111111’b |
2 | parallelismType | |
6 | reserved | ‘111111’b |
2 | chromaFormat | |
5 | reserved | ‘11111’b |
3 | bitDepthLumaMinus8 | |
5 | reserved | ‘11111’b |
3 | bitDepthChromaMinus8 | |
16 | avgFrameRate | |
2 | constantFrameRate | |
3 | numTemporalLayers | |
1 | tmporalIdNested | |
2 | lengthSizeMinusOne | |
8 | numOfArrays |
Repeated of Array(VPS/SPS/PPS) 1| array_completeness 1| reserved| ‘0’b 6| NAL_unit_type 16| numNalus 16| nalUnitLength N| NALU data
從上表能夠看到,在H.265的extradata後半段是一段格式重複的數組數據,裏面須要包含的除了與H.264相同的SPS、PPS外,還需多添加一個VPS。
VPS(Video Parament Set,視頻參數集),在H.265中類型爲32。VPS用於解釋編碼過的視頻的總體結構,包括時域子層依賴關係等,主要目的在於兼容H.265標準在系統的多子層方面的擴展。
iOS中視頻處理框架分層
對於 AVKit、AVFoundation 和 VideoToolbox 來講,他們的功能和可定製性愈來愈強,但相應的使用難度也越大,所以你應該根據實際需求合理的選擇使用哪一個層級的接口。事實上,即便你使用 AVFoundation 或 AVKit 依然能夠得到硬件加速的效果,你失去的只是直接訪問硬編解碼器的權限。對於 VideoToolbox 來講,你能夠經過直接訪問硬編解碼器,將 H.264 文件或傳輸流轉換爲 iOS 上的 CMSampleBuffer 並解碼成 CVPixelBuffer,或將未壓縮的 CVPixelBuffer 編碼成 CMSampleBuffer:
調用 AVCaptureSession 拍攝輸出的每一幀圖像都會被包裝成 CMSampleBuffer 對象,經過這個 CMSampleBuffer 對象你就能夠獲取到未壓縮的 CVPixelBuffer 對象;若是讀取 H.264 文件你也能夠獲取數據生成壓縮的 CMBlockBuffer 對象並建立一個 CMSampleBuffer 對象給 VideoToolbox 來解碼。
也就是說,CMSampleBuffer 既能夠做爲 CVPixelBuffer 對象的容器,也能夠做爲 CMBlockBuffer 對象的容器,CVPixelBuffer 能夠說是未壓縮的圖像數據容器,而 CMBlockBuffer 則是壓縮圖像數據容器。
NALU. 對於一個 H.264 裸流或者文件來講,它是由一個一個的 NALU(Network Abstraction Layer Unit) 單元組成,每一個 NALU 既能夠表示圖像數據,也能夠表示處理圖像所須要的參數數據。它主要有兩種格式:Annex B 和 AVCC。也被稱爲 Elementary Stream 和 MPEG-4 格式,Annex B 格式以 0x000001 或 0x00000001 開頭,AVCC 格式以所在的 NALU 的長度開頭。
在iOS中使用vtCompressionSession編碼後的數據不包含start code.須要咱們自行添加,爲何要添加start code? 如上文所說,爲了區分每一個NALU及之後提取vps,sps,pps等關鍵信息.
以下圖,咱們在編碼回調中能夠拿到編碼後的CMSampleBuffer
數據.若是是h265,CMVideoFormatDesc
中還有vps.而這段數據在推流前必須尋找到關鍵數據vps,sps,pps以及添加start code.由於在CMBlockBufferRef
中可能還含有一個或多個NALU,因此咱們須要經過遍歷它的內存地址找到在每一個NALU的分割點將數據替換爲start code.具體代碼操做參考實戰篇.
接下來,咱們就須要處理這一幀視頻的圖像數據了。經過 CMSampleBufferGetDataBuffer 和 CMBlockBufferGetDataPointer 咱們能夠獲取視頻數據的內存地址。VTCompressionSession 編碼出來的視頻幀爲 AVCC 格式,所以咱們能夠讀取頭部 4 個字節數據來獲取當前 NALU 的長度。這裏有一個須要注意的是,AVCC 格式使用大端字節序,它可能跟當前使用的系統字節序不同,事實上,iOS 系統使用小端字節序,所以咱們須要先將這個長度數據轉換爲 iOS 系統使用的小端字節序:
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength)
硬編碼基本上是硬解碼的一個逆過程。解析出參數集SPS和PPS,加上開始碼後組裝成NALU。提取出視頻數據,將長度碼轉換成開始碼,組長成NALU。將NALU發送出去。
如上圖,咱們知道,CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer . 須要從H264的碼流裏面提取出以上的三個信息。最後組合成CMSampleBuffer,提供給硬解碼接口來進行解碼工做。
在H.264, H.265的語法中,有一個最基礎的層,叫作Network Abstraction Layer, 簡稱爲NAL。編碼數據正是由一系列的NAL單元(NAL Unit, 簡稱NAUL)組成的。
編碼碼流由NALU單元組成,一個NALU可能包含有:
流數據中,屬性集合多是這樣的:(若是是H.265碼流,SPS前面還有VPS)
通過處理以後,在Format Description中則是:
要從基礎的流數據將SPS和PPS轉化爲Format Desc中的話,須要調用CMVideoFormatDescriptionCreateFromH264ParameterSets,CMVideoFormatDescriptionGetHEVCParameterSetAtIndex
方法
對於流數據來講,一個NAUL的Header中,多是0x00 00 01或者是0x00 00 00 01做爲開頭(二者都有可能,下面以0x00 00 01做爲例子)。0x00 00 01所以被稱爲開始碼(Start code).
總結以上知識,咱們知道編碼碼流由NALU單元組成,NALU單元包含視頻圖像數據和編碼器的參數信息。其中視頻圖像數據就是CMBlockBuffer,而編碼器參數信息則能夠組合成FormatDesc。具體來講參數信息包含VPS,SPS(Sequence Parameter Set)和PPS(Picture Parameter Set).以下圖顯示了一個H.264碼流結構:
提取sps和pps生成FormatDesc
提取視頻圖像數據生成CMBlockBuffer
根據須要,生成CMTime信息。(實際測試時,加入time信息後,有不穩定的圖像,不加入time信息反而沒有,須要進一步研究,這裏建議不加入time信息)
根據上述獲得CMVideoFormatDescriptionRef、CMBlockBufferRef和可選的時間信息,使用CMSampleBufferCreate接口獲得CMSampleBuffer數據這個待解碼的原始的數據。以下圖所示的H264數據轉換示意圖。
顯示的方式有兩種: