上兩篇博文解析H264視頻編碼原理——從孫藝珍的電影說起(一)解析H264視頻編碼原理——從孫藝珍的電影說起(二)已經比較詳細地叙述了一個視頻從原始的yuv數據流如何轉化為一個H264碼流,那麼今天就來講一講整個過程的最終産物H264碼流究竟是什麼樣子的。如果還沒看過這兩篇博文,建議先看一下,不然本文說的很多概念會不理解。
如果你願意一層一層,一層地剝開我的心,你會發現,你會訝異,你是我,最壓抑,最深處的秘密
這是楊宗緯一首很經典的歌,之所以引用它是因為今天的内容就猶如《洋蔥》一般,需要一層層地剝開,剝開H264碼流緊緊裹着的外衣。
什麼是碼流結構 在解析H264視頻編碼原理——解析H264視頻編碼原理——從孫藝珍的電影說起(二)最後層列出這樣一張H264編碼整體流程的圖,最右端末尾(熵編碼)輸出的就是H264碼流了,所謂的碼流本質就是一串長長的二進制數據,就像一條很長的河流,緩緩的流淌,那麼接收端收到這些碼流之後,需要解析它才能讀取裡面的信息,所以碼流一定是需要一定規律組織起來的,讓接收端知道哪裡是視頻的開始,哪裡是一幀圖像的開始,甚至哪裡是一個宏塊的開始。這樣的組織方式就是碼流結構。

H264碼流結構解析宏塊數據 解析視頻編碼原理——解析H264視頻編碼原理——從孫藝珍的電影說起(一)已經說過,視頻編碼的編碼最小單元是宏塊,那我們可以從宏塊入手,由小至大地近距離觀看碼流的模樣。
我們已經知道編碼器在編碼宏塊之後會将預測數據和相關參數以及殘差數據裝入碼流,所以在碼流的最小單元中,即宏塊數據的存儲如圖所示:

讓我們“走近一點”看得更清楚些:

預測數據部分(prediction)中,還包含預測模式(intra(幀内預測)、inter(幀間預測),)幀間預測包含參考幀(reference frame)和運動矢量(motion vector),以及量化系數(QP),殘差數據(residual)。這些都是解析H264視頻編碼原理——從孫藝珍的電影說起(一) 中提到的編碼用到的數據,也是解碼必不可少的信息。
C 學習資料免費獲取方法:關注音視頻開發T哥, 「鍊接」即可免費獲取2023年最新C 音視頻開發進階獨家學習資料!
由于視頻中的宏塊非常多,所以宏塊是這樣子組織起來的:

其中MB(Macro block)表示一個宏塊,skip表示的是跳過的宏塊。
Slice 那麼碼流就隻有宏塊麼?想象下,如果是把一連串宏塊發送出去,那麼是雜亂無章的,你無法分清楚那些宏塊是屬于哪一幀的。Slice,一般翻譯為“片”,便應運而生。
Slice是什麼呢?如果把宏塊當做一箱貨物的話,那麼Slice可以當做集裝箱,它制定了相互傳輸的格式,将宏快 有組織,有結構,有順序的形成一系列的碼流。
Slice 其實是為了并行編碼設計的,一般來說是為了提高編碼速度的,将一幀圖像劃分成幾個 Slice,并且 Slice 之間相互獨立、互不依賴、獨立編碼。所以幀内預測時候,不能跨Slice預測。
所以一幀圖像包含一或者若幹個slice,一個slice包含若幹個宏塊。
有了Slice,宏塊就可以組織起來了。

先是有了Slice Header,然後是有了Slice Data,Slice Header 中存放了這個 Slice 會用到的參數項,而 Data 中則存放了真正的圖像信息,即具體的宏塊數據。
Slice header主要是當前Slice包含的宏塊的一些基本的數據,例如Slice的類型,Slice屬于的那一幀的信息,以及當前Slice使用的圖像序列參數以及量化參數等信息。
Slice的類型:(最後2種類型屬于擴展類,就不細講了)
I Slice:僅包含I宏塊P Slice:包含P宏塊和I宏塊B Slice:包含B宏塊和I宏塊SP Slice:包含B宏塊和I宏塊,用于使編碼流之間容易交換SI Slice:包含SI宏塊(一種特殊的編碼宏塊),用于使編碼流之間容易交換 Slice包含的具體參數字段:

這裡講下幾個重要參數有:
first_mb_in_slice: 表示當前 Slice 的第一個宏塊 MB 在當前編碼圖像幀中的序号。經常用于判斷當前Slice是否是一幀的第一個Slice。如果 first_mb_in_slice 的值等于 0,就代表了當前 Slice 的第一個宏塊是一幀的第一個宏塊,也就是說當前 Slice 就是一幀的第一個 Slice。
slice type: Slice類型。
pic_parameter_set_id: 當前Slice所使用的PPS(圖像參數集)的序号id,關于PPS後面會講到。
SPS(序列參數集)和PPS(圖像參數集) 當然Slice并不孤單,在同一個層次中,還有2個“兄弟”陪伴,分别是SPS(序列參數集)和PPS(圖像參數集),這兩雖然數據量不大可是來頭可不小,沒有他們,碼流根本無法解碼。
其中,SPS 主要包含的是圖像的寬、高、YUV 格式和位深等基本信息;PPS 則主要包含熵編碼類型、基礎 QP 和最大參考幀數量等基本編碼信息。
上面講Slice的時候說過,有個參數pic_parameter_set_id,就是表示當前Slice所使用的PPS(圖像參數集)的序号id,而一個PPS又會關聯一個SPS,這樣相當于一個Slice就關聯了PPS和SPS。
可以打個比方,Slice的集裝箱雖然可以運輸到遠方,但是集裝箱内的物品怎麼打開怎麼使用卻需要一份說明書(SPS(序列參數集)和PPS(圖像參數集)),但是說明書并不需要每個集裝箱一份,所以多個Slice會共用SPS和PPS。
所以Slice會和SPS和PPS形成這樣一個結構:

Slice裡面存放着具體的視頻編碼數據,稱為 Video Coding Layer (VCL),PPS和SPS存放編碼相關信息供解碼端解碼。
綜上所述,H264 的碼流主要是由 SPS、PPS、I Slice、P Slice和B Slice 組成的。
NALU 上面說Slice就像集裝箱,那麼同一個層次的PPS和SPS也可以看做集裝箱,為了收貨方方便區分這些集裝箱,所以必須給集裝箱加個數據頭部表明類型,所以NALU(Network Abstraction Layer Units)就應運而生。
通過NALU的頭部,我們就能很方便區分集裝箱裡是一個Slice還是PPS還是SPS。所以一個Slice或者PPS或者SPS,統稱為RBSP單元(Raw Byte Sequence Payload),都是一個NALU中。
所謂Network Abstraction Layer Units,即網絡抽象層單元,就是碼流在網絡中傳輸的基本單元,每個NALU通過一個或者若幹個網絡數據包傳輸,每個NALU由一個字節的NALU header和NALU數據,即Slice和PPS和SPS,NALU header主要用于指定NALU類型和其優先級。
NALU header由1 bit的禁止位forbidden_zero_bit、2 bit重要性nal_ref_idc以及5 bits的nal_unit_type組成(圖來源:# 碼流結構:原來你是這樣的H264):

禁止位forbidden_zero_bit H264碼流必須為 0,重要性nal_ref_idc指定了當前NALU的重要性,即越重要越不能讓其在網絡中丢失,參考幀、SPS 和 PPS 對應的 NALU 必須要大于 0。
而NALU type取值如圖所示:

所以綜上所述整個結構如下圖所示:
# 碼流結構:原來你是這樣的H264這張圖太好了,忍不住拿來用了~

起始碼 現在還有一個問題,一串1、0的碼流過來,如何區分一個個NALU?最常見的方法莫過于先指定某一串特殊的碼流為标記去作為分割線,H264也是使用這種方式去作為分隔符。其中每個NALU之間通過startcode(起始碼)進行分隔,起始碼分成兩種:0x000001(3Byte)或者0x00000001(4Byte)。如果NALU對應的Slice為一幀的開始就用0x00000001,否則就用0x000001。
解碼端一旦識别到起始碼,就知道這是一個NALU的結束和另一個NALU的開始。
由于圖像編碼出來的數據中也有可能出現“00 00 00 01”和“00 00 01”的數據。那這種情況怎麼辦呢?
為了使NALU主體不包括起始碼,在編碼時每遇到兩個字節(連續)的0,就插入一字節0x03,以和起始碼相區别。解碼時,則将相應的0x03删除掉。
為了防止出現這種情況,H264 會将圖像編碼數據中的下面的幾種字節串做如下處理:
(1)“00 00 00”修改為“00 00 03 00”;(2)“00 00 01”修改為“00 00 03 01”;(3)“00 00 02”修改為“00 00 03 02”;(4)“00 00 03”修改為“00 00 03 03”。
在解碼端,我們在去掉起始碼之後,也需要将對應的字節串轉換回來。
所以,在NAL層,實際上準确來說是這樣的結構(圖來源:# 碼流結構:原來你是這樣的H264):

碼流結構整體 最後,讓我們站得更高一些,回望今天講的内容:
從網絡抽象層到宏塊層按層次劃分的整體圖:

從幀的角度來看的整體圖:

可以看出都是由類似樹的結構,一層層展開。
先等等,這個結構是不是有點熟悉?
放一張我們熟悉的圖:

是的,這種分層結構是一種在計算機領域非常經典的思維,通過分層,不同層負責不同的事務并且互不幹涉其他層的事務,做到即條理清晰,又互相解耦。
碼流實戰 所謂紙上得來終覺淺,絕知此事要躬行。接下來根據上面的理論知識,我們來對一個具體的碼流進行分析。
這是一份H264文件的二進制碼流:

首先是起始碼0x000001,表示第一個NALU的開始:

起始碼之後應該就是NALU Header了,因為NALU header由1 bit的禁止位forbidden_zero_bit、2 bit重要性nal_ref_idc以及5 bits的nal_unit_type組成,即占了8位,所以看下碼流後面的8位:

十六進制67,二進制為01100111,nal_unit_type是最後5位,即00111,即7。再看一眼type對應的NALU類型:

7位參數序列集,即為sps,所以接下來的一個NALU為sps,一直到下一個起始碼:

接下來的一個NALU類型為68:

依照上面的方法可以得出該NALU為pps:

同樣的方式,可以得到下一個NALU為IDR SLICE(很漫長):

同樣的方式,可以得到下一個NALU為非IDR SLICE(很漫長):

總結 今天從下到上整體地剖析了H264的碼流,分别是預測信息和殘差數據組成了宏塊,多個宏塊又組成了Slice數據部分,Slice數據部分和Slice頭部又組成了Slice,Slice和PPS和SPS又組成了一個NALU主體,NALU主體又和NALU頭部組成了一個完整的NALU,一個個NALU和一個個起始碼最終組成一串完整的碼流,慢慢地在網絡中流淌,或者靜靜地躺在我們的硬盤中。
最後通過一個h264碼流文件的簡單具體分析,親自體驗了這種文件結構的構成。
作者:半島鐵盒裡的貓鍊接:htt
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!