前言
音視頻開發需要你懂得音視頻中一些基本概念,針對編解碼而言,我們必須提前懂得編解碼器的一些特性,碼流的結構,碼流中一些重要信息如sps,pps,vps,start code以及基本的工作原理,而大多同學都只是一知半解,所以導致代碼中的部分內容雖可以簡單理解卻不知其意,所以,在這里總結出了當前主流的H.264,H.265編碼相關的原理,以供學習.
1. 概覽
1.1. 為什么要編碼
眾所周知,視頻數據原始體積是巨大的,以720P 30fps的視頻為例,一個像素大約3個字節,如下所得,每秒鍾產生87MB,這樣計算可得一分鍾就將產生5.22GB。
數據量/每秒=1280*720*33*3/1024/1024=87MB
因此,像這樣體積重大的視頻是無法在網絡中直接傳輸的.而視頻編碼技術也就因運而生.關於視頻編碼原理的技術可以參考本人其他文章,這里不做過多描述.
1.2. 編碼技術
經過很多年的開發迭代,已經有很多大牛實現了視頻編碼技術,其中最主流的有H.264編碼,以及新一代的H.265編碼,谷歌也開發了VP8,VP9編碼技術.對移動端而言,蘋果內部已經實現了如H.264,H.265編碼,我們需要使用蘋果提供的VideoToolbox框架來實現它.
1.3. 編碼分類
-
軟件編碼(簡稱軟編):使用CPU進行編碼。
-
硬件編碼(簡稱硬編):不使用CPU進行編碼,使用顯卡GPU,專用的DSP、FPGA、ASIC芯片等硬件進行編碼。
優缺點
-
軟編:實現直接、簡單,參數調整方便,升級易,但CPU負載重,性能較硬編碼低,低碼率下質量通常比硬編碼要好一點。
-
硬編:性能高,低碼率下通常質量低於硬編碼器,但部分產品在GPU硬件平台移植了優秀的軟編碼算法(如X264)的,質量基本等同於軟編碼。
iOS系統中的硬編碼
蘋果在iOS 8.0系統之前,沒有開放系統的硬件編碼解碼功能,不過Mac OS系統一直有,被稱為Video ToolBox的框架來處理硬件的編碼和解碼,終於在iOS 8.0后,蘋果將該框架引入iOS系統。
1.4. 編碼原理
對視頻執行編碼操作后,原始視頻數據會被壓縮成三種不同類型的視頻幀: I幀,P幀,B幀.
-
I幀:關鍵幀.完整編碼的幀.可以理解成是一張完整畫面,不依賴其他幀
-
P幀:參考前面的I幀或P幀,即通過前面的I幀與自己記錄的不同的部分可以形成完整的畫面.因此,單獨的P幀無法形成畫面.
-
B幀:參考前面的I幀或P幀以及后面的P幀
補充: I幀的壓縮率是7(跟JPG差不多),P幀是20,B幀可以達到50. 但是iOS中一般不開啟B幀,因為B幀的存在會導致時間戳同步較為復雜.
兩種核心算法
-
幀內壓縮
當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。幀內一般采用有損壓縮算法,由於幀內壓縮是編碼一個完整的圖像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼jpeg差不多。
如下圖:我們可以通過第 1、2、3、4、5 塊的編碼來推測和計算第 6 塊的編碼,因此就不需要對第 6 塊進行編碼了,從而壓縮了第 6 塊,節省了空間

幀內預測.png
-
幀間壓縮: P幀與B幀的壓縮算法
相鄰幾幀的數據有很大的相關性,或者說前后兩幀信息變化很小的特點。也即連續的視頻其相鄰幀之間具有冗余信息,根據這一特性,壓縮相鄰幀之間的冗余量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的數據進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
如下圖:可以看到前后兩幀的差異其實是很小的,這時候用幀間壓縮就很有意義。

幀間壓縮11.jpg
有損壓縮與無損壓縮
-
有損壓縮: 解壓縮后的數據與壓縮前的數據不一致.在壓縮的過程中要丟失一些人眼和人耳所不敏感的圖像或音頻信息,而且丟失的信息不可恢復
-
無損壓縮: 壓縮前和解壓縮后的數據完全一致.優化數據的排列等.
DTS和PTS
DTS和PTS的解釋
FFmpeg里有兩種時間戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。顧名思義,前者是解碼的時間,后者是顯示的時間。要仔細理解這兩個概念,需要先了解FFmpeg中的packet和frame的概念。
FFmpeg中用AVPacket結構體來描述解碼前或編碼后的壓縮包,用AVFrame結構體來描述解碼后或編碼前的信號幀。對於視頻來說,AVFrame就是視頻的一幀圖像。這幀圖像什么時候顯示給用戶,就取決於它的PTS。DTS是AVPacket里的一個成員,表示這個壓縮包應該什么時候被解碼。如果視頻里各幀的編碼是按輸入順序(也就是顯示順序)依次進行的,那么解碼和顯示時間應該是一致的。可事實上,在大多數編解碼標准(如H.264或HEVC,當出現B幀的時候)中,編碼順序和輸入順序並不一致。於是才會需要PTS和DTS這兩種不同的時間戳。
-
DTS:主要用於視頻的解碼,在解碼階段使用.
-
PTS:主要用於視頻的同步和輸出.在渲染的時候使用.在沒有B frame的情況下.DTS和PTS的輸出順序是一樣的。

1.dtspts
如上圖:I幀的解碼不依賴於任何的其它的幀.而P幀的解碼則依賴於其前面的I幀或者P幀.B幀的解碼則依賴於其前的最近的一個I幀或者P幀 及其后的最近的一個P幀.
2. 編碼數據碼流結構
在我們的印象中,一張圖片就是一張圖像,視頻就是很多張圖片的集合.。但是因為我們要做音視頻編程,就需要更加深入理解視頻的本質.
2.1 刷新圖像概念.
在編碼的碼流中圖像是個集合的概念,幀、頂場、底場都可以稱為圖像,一幀通常就是一幅完整的圖像.
-
逐行掃描:每次掃描得到的信號就是一副圖像,也就是一幀. 逐行掃描適合於運動圖像
-
隔行掃描:掃描下來的一幀圖像就被分為了兩個部分,這每一部分就稱為「場」,根據次序分為:「頂場」和「底場」.適合於非運動圖像

逐行掃描與隔行掃描.png

2.2. 重要參數
-
視頻參數集VPS(Video Parameter Set)
VPS主要用於傳輸視頻分級信息,有利於兼容標准在可分級視頻編碼或多視點視頻的擴展。
(1)用於解釋編碼過的視頻序列的整體結構,包括時域子層依賴關系等。HEVC高效率視頻編碼(High Efficiency Video Coding)中加入該結構的主要目的是兼容標准在系統的多子層方面的擴展,處理比如未來的可分級或者多視點視頻使用原先的解碼器進行解碼但是其所需的信息可能會被解碼器忽略的問題。
(2)對於給定視頻序列的某一個子層,無論其SPS相不相同,都共享一個VPS。其主要包含的信息有:多個子層或操作點共享的語法元素;檔次和級別等會話關鍵信息;其他不屬於SPS的操作點特定信息。
(3)編碼生成的碼流中,第一個NAL(Network Abstract Layer)單元,攜帶的就是VPS信息
-
序列參數集SPS(Sequence Parameter Set)
包含一個CVS 編碼視頻序列(Coded Video Sequence)中所有編碼圖像的共享編碼參數。
(1)一段HEVC碼流可能包含一個或者多個編碼視頻序列,每個視頻序列由一個隨機接入點開始,即IDR/BLA/CRA。序列參數集SPS包含該視頻序列中所有slice需要的信息。
(2)SPS的內容大致可以分為幾個部分:1、自引ID;2、解碼相關信息,如檔次級別、分辨率、子層數等;3、某檔次中的功能開關標識及該功能的參數;4、對結構和變換系數編碼靈活性的限制信息;5、時域可分級信息;6、VUI。
-
圖像參數集PPS(Picture Parameter Set)
包含一幅圖像所用的公共參數,即一幅圖像中所有片段SS(Slice Segment)引用同一個PPS。
(1)PPS包含每一幀可能不同的設置信息,其內容同H.264中的大致類似,主要包括:1、自引信息;2、初始圖像控制信息,如初始QP等;3、分塊信息。
(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:主層和高層,主層用於大多數應用,而高層用於那些最嚴苛的應用。
2.3. 原始碼流
-
IDR
一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會。IDR圖像之后的圖像永遠不會使用IDR之前的圖像的數據來解碼。
-
結構
由一個接一個的 NALU 組成的,而它的功能分為兩層,VCL(視頻編碼層)和 NAL(網絡提取層).
H.264從層次來看分為兩層:視頻編碼層(VCL, Video Coding Layer)和網絡提取層(NAL,Network Abstraction Layer)。VCL輸出的是原始數據比特流(SODB,String of data bits),表示H.264的語法元素編碼完成后的實際的原始二進制碼流。SODB通常不能保證字節對齊,故需要補齊為原始字節序列負荷(RBSP,Raw Byte Sequence Payload)。NAL層實際上就是最終輸出的H.264碼流,它是由一個個NALU組成的,每個NALU包括一組對應於視頻編碼數據的NAL頭信息和一個原始字節序列負荷(RBSP,Raw Byte Sequence Payload)。以上名詞之間的關系如下:
RBSP = SODB + RBSP trailing bits NALU = NAL header(1 byte) + RBSP H.264 = Start Code Prefix(3 bytes) + NALU + Start Code Prefix(3 bytes) + NALU +…
所以H.264碼流的結構如下:

-
組成
NALU (Nal Unit) = NALU頭 + RBSP 在 VCL
數據傳輸或存儲之前,這些編碼的 VCL 數據,先被映射或封裝進 NAL 單元(以下簡稱 NALU,Nal Unit) 中。每個 NALU 包括一個原始字節序列負荷(RBSP, Raw Byte Sequence Payload)、一組 對應於視頻編碼的 NALU 頭部信息。RBSP 的基本結構是:在原始編碼數據的后面填加了結尾 比特。一個 bit“1”若干比特“0”,以便字節對齊。
2.3.1. H.264碼流
一個原始的H.264 NALU 單元常由 [StartCode] [NALU Header] [NALU Payload] 三部分組成

NALU組成.jpeg
-
StartCode : Start Code 用於標示這是一個NALU 單元的開始,必須是”00 00 00 01” 或”00 00 01”(Annex B碼流格式才必須是”00 00 00 01” 或”00 00 01”)
-
NALU Header下表為 NAL Header Type

例如,下面幅圖分別代表IDR與非IDR幀具體的碼流信息:

2.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.
2.3.2.碼流格式
H.264標准中指定了視頻如何編碼成獨立的包,但如何存儲和傳輸這些包卻未作規范,雖然標准中包含了一個Annex附件,里面描述了一種可能的格式Annex B,但這並不是一個必須要求的格式。
為了針對不同的存儲傳輸需求,出現了兩種打包方法。一種即Annex B格式,另一種稱為AVCC格式。
-
Annex B
從上文可知,一個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
另一個存儲H.264流的方式是AVCC格式,在這種格式中,每一個NALU包都加上了一個指定其長度(NALU包大小)的前綴(in big endian format大端格式),這種格式的包非常容易解析,但是這種格式去掉了Annex B格式中的字節對齊特性,而且前綴可以是1、2或4字節,這讓AVCC格式變得更復雜了,指定前綴字節數(1、2或4字節)的值保存在一個頭部對象中(流開始的部分),這個頭通常稱為'extradata'或者'sequence header',SPS和PPS數據也需要保存在extradata中。
你會發現SPS和PPS被存儲在了非NALU包中(out of band帶外),即獨立於基本流數據。這些數據的存儲和傳輸是文件容器的任務,超出了本文的范疇。注意:雖然AVCC格式不使用起始碼,防競爭字節還是有的。
H.264 extradata語法如下:
bits | line by byte | remark |
|
8 | version | always | 0x01 |
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 |
|
|
我們注意一下這個值 NALULengthSizeMinusOne,通過將這個值加 1 ,我們就得出了后續每個 NALU 前面前綴(也就是表示長度的整數)的字節數
例如,這個 NALULengthSizeMinusOne 是 3,那么每個 NALU 前面前綴的長度就是 4 個字節。我們在讀取后續數據時,可以先讀 4 個字節,然后把這四個字節轉成整數,就是這個 NALU 的長度了,注意,這個長度並不包含起始的4個字節,是單純 NALU 的長度。
這里還需要注意的一點是,雖然AVCC格式不使用起始碼,但防競爭字節還是有的。
AVCC格式的一個優點在於解碼器配置參數在一開始就配置好了,系統可以很容易的識別NALU的邊界,不需要額外的起始碼,減少了資源的浪費,同時可以在播放時調到視頻的中間位置。這種格式通常被用於可以被隨機訪問的多媒體數據,如存儲在硬盤的文件。
2.3.3. H.265碼流
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個部分,其中:
-
forbidden_zero_bit = 0:占1個bit,與H.264相同,禁止位,用以檢查傳輸過程中是否發生錯誤,0表示正常,1表示違反語法;
-
nal_unit_type = 32:占6個bit,用來用以指定NALU類型
-
nuh_reserved_zero_6bits = 0:占6位,預留位,要求為0,用於未來擴展或3D視頻編碼
-
nuh_temporal_id_plus1 = 1:占3個bit,表示NAL所在的時間層ID
對比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 |
|
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標准在系統的多子層方面的擴展。