無論是解析視頻文件或者通過網絡傳輸,其實都是一串字節序列。H264 碼流就是按照一定的規則組織排列的字節串。
一、碼流的組織形式
在 H264 中完全沒有 I 幀、P 幀、B 幀、IDR 幀的概念,之所以沿用這些說法是為了表明數據的編碼模式。H264 碼流的組織形式從大到小排序是:視頻序列(video sequence)、圖像(frame/field-picture)、片組(slice group)、片(slice)、宏塊(macroblock)、子塊(sub-block)、像素(pixel)。
二、碼流功能的角度
從碼流功能的角度可以分為兩層:視頻編碼層(VCL)和網絡提取層(NAL)
- VCL:進行視頻編解碼,包括預測(幀內預測和幀間預測),DCT 變化和量化,熵編碼和切分數據等功能,是為了實現更高的視頻壓縮比。
- NAL:負責以網絡所要求的恰當的方式對 VCL 數據進行打包和傳送。
VCL 是管理 H264 的視頻數據層,是為了實現更高的視頻壓縮比,那 VCL 究竟是怎么管理 H264 視頻數據的呢?拋開 H264 壓縮算法細節來看就 3 步:
- 壓縮:預測(幀內預測和幀間預測)-> DCT 變化和量化 -> 比特流編碼;
- 切分數據,主要為了第三步。這里一點,網上看到的“切片(slice)”、“宏塊(macroblock)”是在VCL 中的概念,一方面提高編碼效率和降低誤碼率、另一方面提高網絡傳輸的靈活性。
- 壓縮切分后的 VCL 數據會包裝成為 NAL 中的一部分。
下面要重點講解下 NAL。
三、網絡提取層(NAL)
NAL,英文全稱為 Network Abstraction Layer,這塊和 H264 壓縮算法無關,涉設計出 NAL 的目的就是為了獲得 “network-friendly”,即為了實現良好的網絡親和性,即可適用於各種傳輸網絡。
終於要講 NAL 了,但是,我們需要先看 NAL的組成單元 - NALU。
3.1 NAL單元 - NALU
NALU 的格式如下圖(引用H264 PDF)所示:
很明顯,NALU 由頭和身體兩個部分組成:
- 頭:一般存儲標志信息,譬如 NALU 的類型。前面講過 NAL 會打包 VCL 數據,但是這並不意味着所有的 NALU 負載的都是 VCL,也有一些 NALU 僅僅存儲了和編解碼信息相關的數據;
- 身體:存儲了真正的數據。但實際上,這塊也會相對比較復雜,前面其實也提到過 H264 的一個目的是“網絡友好性”,說白了就是能夠很好地適配各種傳輸格式。所以根據實際采用數據傳輸流格式,也會對這部分數據格式再進行處理。
3.2 NALU Header
首先,NALU Header只占 1 個字節,即 8 位,其組成如下圖所示:
-
forbidden_zero_bit
在網絡傳輸中發生錯誤時,會被置為 1,告訴接收方丟掉該單元;否則為 0。
-
nal_ref_idc
用於表示當前NALU的重要性,值越大,越重要。
解碼器在解碼處理不過來的時候,可以丟掉重要性為 0 的 NALU。 -
nal_unit_type
表示 NALU 數據的類型,有以下幾種:
其中比較注意的應該是以下幾個:
- 1-4:I/P/B幀,如果 nal_ref_idc 為 0,則表示 I 幀,不為 0 則為 P/B 幀。
- 5:IDR幀,I 幀的一種,告訴解碼器,之前依賴的解碼參數集合(接下來要出現的 SPS\PPS 等)可以被刷新了。
- 6:SEI,英文全稱 Supplemental Enhancement Information,翻譯為“補充增強信息”,提供了向視頻碼流中加入額外信息的方法。
- 7:SPS,全稱 Sequence Paramater Set,翻譯為“序列參數集”。SPS 中保存了一組編碼視頻序列(Coded Video Sequence)的全局參數。因此該類型保存的是和編碼序列相關的參數。
- 8: PPS,全稱 Picture Paramater Set,翻譯為“圖像參數集”。該類型保存了整體圖像相關的參數。
- 9:AU 分隔符,AU 全稱 Access Unit,它是一個或者多個 NALU 的集合,代表了一個完整的幀,有時候用於解碼中的幀邊界識別。
特殊的 NALU 類型:SPS和PPS
SPS 和 PPS 存儲了編解碼需要一些圖像參數,SPS,PPS 需要在 I 幀前出現,不然解碼器沒法解碼。而 SPS,PPS 出現的頻率也跟不同應用場景有關,對於一個本地 h264 流,可能只要在第一個 I 幀前面出現一次就可以,但對於直播流,每個 I 幀前面都應該插入 sps 或 pps,因為直播時客戶端進入的時間是不確定的。
3.3 NALU Payload
很少有資料會稱身體部分為 Payload,絕大部分資料對 NALU 組成的定義是這樣子的:
NALU = NALU Header + SODB // 定義1
NALU = NALU Header + RBSP // 定義2
NALU = NALU Header + EBSP // 定義3
於是新的問題來了:SODB,RBSP和EBSP都是什么東西呢?這塊概念,在博客NALU詳解二(EBSP、RBSP與SODB)中介紹得非常清楚,總結來說就是:
SODB
英文全稱 String Of Data Bits,稱原始數據比特流,就是最原始的編碼/壓縮得到的數據。
RBSP
英文全稱 Raw Byte Sequence Payload,又稱原始字節序列載荷。和 SODB 關系如下:
RBSP = SODB + RBSP Trailing Bits(RBSP尾部補齊字節)
引入 RBSP Trailing Bits 做 8 位字節補齊。
EBSP
英文全稱 Encapsulated Byte Sequence Payload,稱為擴展字節序列載荷。和 RBSP 關系如下:
EBSP :RBSP插入防競爭字節(`0x03`)
這里說明下防止競爭字節(0x03):讀者可以先認為 H264 會插入一個叫做 StartCode 的字節串來分割 NALU,於是問題來了,如果 RBSP 中也包括了 StartCode(0x000001 或 0x00000001)怎么辦呢?所以,就有了防止競爭字節(0x03):
編碼時,掃描 RBSP,如果遇到連續兩個 0x00 字節,就在后面添加 防止競爭字節(0x03);解碼時,同樣掃描 EBSP,進行逆向操作即可。
最后,以一幅圖總結 NALU 這段內容:
四、碼流解析的角度
H264 碼流實際可以理解為由一個一個的 NALU 單元組成。(下圖中的 RBSP 類似 NALU Payload)
前面提到的一幀圖像(I 幀, P 幀, B 幀)就是一個 NALU 單元,NALU 單元除了代表圖像外還能包含其他類型的數據,如 PPS 和 SPS。
五、H264更詳細的分層結構
- 第一層:比特流。該層有兩種格式:Annexb 格式和 RTP 格式。
- 第二層:NAL Unit 層。包含了 NAL Header 和 NAL Body 信息。
- 第三層:Slice 層。一幀視頻圖像可編碼成一個或者多個片,每片包含整數個宏塊,即每片至少 一個宏塊,最多時包含整個圖像的宏塊。
- 第四層:Slice data 層。Slice 由宏塊(macro block, MB)組成。宏塊是編碼處理的基本單元。
- 第五層:PCM 類。
- 第六層:殘差層。
片的目的:
為了限制誤碼的擴散和傳輸,使編碼片相互間保持獨立。片共有 5 種類型: I 片(只包含 I 宏塊)、P 片(P 和 I 宏塊)、B 片(B 和 I 宏塊)、SP 片(用於不同編碼流之 間的切換)和 SI 片(特殊類型的編碼宏塊)。
六、擴展:怎么區分 NALU 的邊界?
了解了 NALU 之后,關於 H264 格式,還有一個問題:解碼器怎么知道一個 NALU 要結束了?或者說它怎么區分 NALU 的邊界?
要回答這個問題,就必須了解 H264 的打包方式,通俗來說是H264 如何組織一連串的 NALU 為完整的 H264 碼流。目前 H264 主流的兩種格式:
-
Annex-B:本文關於 NALU 的很多細節介紹都是 Annex-B,它依靠前文提到的 Start Code 來分隔 NALU,打包方式如下:
[start code]--[NALU]--[start code]--[NALU]...
-
AVCC:筆者對這個格式了解的不多,從網上找到很多資料知道以下幾點:
-
-
由 NALU 和 extradata/sequence header 組成,由於在 extradata/sequence header 中存儲了 NALU 的長度,因此 NALU Payload 不需要做字節對齊,不過防競爭字節還是有的;
-
SPS 和 PPS 被放在了 extradata/sequence header。
-
打包方式如下:
[SIZE (4 bytes)]--[NAL]--[SIZE (4 bytes)]--[NAL]... // 請注意,SIZE一般為4字節,但是具體以實際為准
-
至於為什么要有這兩類格式,還需要查閱更多的資料。不過 StackOverflow 上關於Possible Locations for Sequence/Picture Parameter Set(s) for H.264 Stream的回答可以幫助深入了解這兩種格式,推薦閱讀。
下面是一個 H264 碼流,可以看到每個 NALU 前有一個 StartCode(0x000001 或 0x00000001),作為 NALU 的分割符:
分析其中比較有代表性的3幀:
- 00 00 00 01 67
00 00 00 01 是一個 NALU 開始,67 是Header, 二進制為 0110 0111, nal_unit_type 為00111,即7為 SPS 幀。 - 00 00 00 01 68
68 二進制為 0110 1000,nal_unit_type 為00111,即 8 為 SPS 幀。 - 00 00 00 01 65
65 二進制為 0110 0101,nal_unit_type 為 00101, 即 5 為 IDR 幀。
參考: