視訊有很多種編碼标準,H.264,H.265,AV1等等,其中我們可能最常見的便是H.264,是以,本文我們就主要來分下下H.264編碼的碼流結果具體是怎麼樣的。
一、前置知識
了解H264碼流結構之前,需要先了解一些前置的知識。
1、I/P/B幀和GOP
需要先了解圖像序列的層次結構,也就是一幀幀視訊幀的一些概念。這些可以參考我前面的一篇文章 視訊相關的一些基本概念 ,會比較系統和具體地講解。
2、Slice
上面主要是圖像序列的層次結構,而圖像的内部層次結構是怎麼樣的,這主要涉及Slice這個概念。
Slice其實就是“片”的概念,圖像内的層次結構就是一幀圖像可以劃分成一個或多個 Slice,而一個 Slice 包含多個宏塊,且一個宏塊又可以劃分成多個不同尺寸的子塊。大概就是類似下面這樣的結構圖:
二、H264碼流結構
1、碼流格式
H264 碼流有兩種格式:一種是 Annexb 格式;一種是 MP4 格式。
(1)、Annexb 格式使用起始碼來表示一個編碼資料的開始。起始碼本身不是圖像編碼的内容,隻是用來分隔用的。起始碼有兩種,一種是 4 位元組的“00 00 00 01”,一種是 3 位元組的“00 00 01”。
由于圖像編碼出來的資料中也有可能出現“00 00 00 01”和“00 00 01”的資料。為了防止出現這種情況,H264 會将圖像編碼資料中的下面的幾種位元組串做如下處理:
“00 00 00”修改為“00 00 03 00”;
“00 00 01”修改為“00 00 03 01”;
“00 00 02”修改為“00 00 03 02”;
“00 00 03”修改為“00 00 03 03”。
其實也就是轉義,同樣地在解碼端,我們在去掉起始碼之後,也需要将對應的位元組串轉換回來。
(2)、MP4 格式沒有起始碼,而是在圖像編碼資料的開始使用了 4 個位元組作為長度辨別,用來表示編碼資料的長度,這樣我們每次讀取 4 個位元組,計算出編碼資料長度,然後取出編碼資料,再繼續讀取 4 個位元組得到長度,一直繼續下去就可以取出所有的編碼資料了。
這兩種格式差别不大,接下來我們主要使用 Annexb 格式來講解 H264 碼流結構。
2、SPS和PPS
視訊編碼的時候還有一些編碼參數資料的,為了能夠将一些通用的編碼參數提取出來,不在圖像編碼資料中重複,H264 設計了兩個重要的參數集:一個是 SPS(序列參數集);一個是 PPS(圖像參數集)。
SPS 主要包含的是圖像的寬、高、YUV 格式和位深等基本資訊;PPS 則主要包含熵編碼類型、基礎 QP 和最大參考幀數量等基本編碼資訊。
如果沒有 SPS、PPS 裡面的基礎資訊,之後的 I 幀、P 幀、B 幀就都沒辦法進行解碼。是以 SPS 和 PPS 是至關重要的。
這樣的話,H264碼流主要包含了 SPS、PPS、I 幀、P 幀和 B 幀。由于幀又可以劃分成一個或多個 Slice。是以,幀在碼流中實際上是以 Slice 的形式呈現的。是以,H264 的碼流主要是由 SPS、PPS、I Slice、P Slice和B Slice 組成的。如下圖所示:
3、NALU
上面說到H264碼流的組成部分,但是每個部分是如何區分開?為了解決這個問題,H264 設計了 NALU(網絡抽象層單元)。SPS 是一個 NALU、PPS 是一個 NALU、每一個 Slice 也是一個 NALU。每一個 NALU 又都是由一個 1 位元組的 NALU Header 和若幹位元組的 NALU Data 組成的。而對于每一個 Slice NALU,其 NALU Data 又是由 Slice Header 和 Slice Data 組成,并且 Slice Data 又是由一個個 MB Data 組成。其結構如下:
其中,NALU Header總共占用 1 個位元組,具體如下圖所示。
其中,
--> F:forbidden_zero_bit,占 1bit,禁止位,H264 碼流必須為 0;
--> NRI: nal_ref_idc,占 2bits,可以取 00~11,表示目前 NALU 的重要性。參考幀、SPS 和 PPS 對應的 NALU 必須要大于 0;
--> Type: nal_unit_type,占 5bits,表示 NALU 類型。其取值如下表所示。
有了這個,我們解析出 NALU Header 的 Type 字段,查詢表格就可以得到哪個 NALU 是 SPS,哪個是 PPS,以及哪個是 IDR 幀了。
不過NALU 類型隻區分了 IDR Slice 和非 IDR Slice,至于非 IDR Slice 是普通 I Slice、P Slice 還是 B Slice,則需要繼續解析 Slice Header 中的 Slice Type 字段得到。
下面我們通過兩個例子來看看常見的 NALU 裡的 NALU Header 是怎樣的:
下面我們再來看一個實際碼流的例子,看看在實際編碼出來的二進制資料中,各種 NALU 是怎麼“放置”在資料中的。下圖是用二進制檢視工具打開實際編碼後的碼流資料。我們可以看到在碼流的開始部分是一個起始碼,之後緊接着是一個 SPS 的 NALU。在 SPS 後面是一個 PPS 的 NALU。然後就是一個 IDR Slice 的 NALU 和一個非 IDR Slice NALU。
三、如何判斷哪幾個 Slice 是同一幀的
根據上面的分析,在H264 碼流中,幀是以 Slice 的方式呈現的,或者可以說在 H264 碼流裡是沒有“幀“這種資料的,隻有 Slice。
那麼有個問題,一幀有幾個 Slice 是不知道的。也就是說碼流中沒有字段表示一幀包含幾個 Slice。既然沒有辦法知道一幀有幾個 Slice,那我們如何知道多 Slice 編碼時一幀的開始和結束分别對應哪個 Slice 呢?
其實,Slice NALU 由 NALU Header 和 NALU Data 組成,其中 NALU Data 裡面就是 Slice 資料,而 Slice 資料又是由 Slice Header 和 Slice Data 組成。在 Slice Header 開始的地方有一個 first_mb_in_slice 的字段,表示目前 Slice 的第一個宏塊 MB 在目前編碼圖像中的序号。我們隻要解析出這個宏塊的序号出來。
--> 如果 first_mb_in_slice 的值等于 0,就代表了目前 Slice 的第一個宏塊是一幀的第一個宏塊,也就是說目前 Slice 就是一幀的第一個 Slice。
--> 如果 first_mb_in_slice 的值不等于 0,就代表了目前 Slice 不是一幀的第一個 Slice。并且,使用同樣的方式一直往下找,直到找到下一個 first_mb_in_slice 為 0 的 Slice,就代表新的一幀的開始,那麼其前一個 Slice 就是前一幀的最後一個 Slice 了。
其中,first_mb_in_slice 是以無符号指數哥倫布編碼的,需要使用對應的解碼方式才能解碼出來。但是有一個小技巧,如果隻是需要判斷 first_mb_in_slice 是不是等于 0,不需要計算出實際值的話,隻需要通過下面的方式計算就可以了。
以上便是對H264碼流結構的講解。