參考:
1. 概述
h264 打包 rtp 在 rfc6184 中有詳細描述。
這里主要說明 Annex-B 格式的 264 碼流打包 rtp。
關於 h264 rtp 解包組幀,參看:https://www.cnblogs.com/moonwalk/p/15903766.html。
2. h264 碼流格式簡述(Annex-B 格式)
2.1 nal unit stream(Network Abstraction Layer Unit Stream)
h.264 編碼器把原始的 yuv 圖像文件編碼成碼流文件,生成的碼流文件稱為 NAL 單元流(NAL unit Stream),NALU stream 由一個個 NALU(nal 單元) 組成(https://www.cnblogs.com/TaigaCon/p/5215448.html):
2.2 nal 單元分割方式
多個 nalu 之間,通過分割字節,組成了 nalu stream,分隔符定義如下:
- zero_byte(0x00),一個字節。如果當前的 NAL 單元為 sps、pps 或者一個訪問單元(access unit)的第一個 NAL 單元,這個字節就會存在
- start_code_prefix_one_3bytes(0x000001),三個字節。固定存在的 NAL 單元起始碼,用來指示下面為一個 NAL 單元
所以我們常看到的 nalu 分隔符一般由 0x00000001 或則 0x000001 組成。
2.3 nal 單元結構
NALU 由 header + payload 組成(http://iphome.hhi.de/wiegand/assets/pdfs/DIC_H264_07.pdf):
2.3.1 nalu header
結構如下(rfc6184):
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
- F:1 bit,forbidden_zero_bit。固定為 0
- NRI:2 bit,nal_ref_idc。用於指示該 nalu 的重要性,實際應用層代碼一般不關心此值
- Type:5 bit,nal_unit_type。指定 nalu 的類型,如下所示:
我們通過讀取 nalu 的第一個字節,取得 nalu header,然后判斷 type,就能知道此 nalu 所屬的幀類型,常見的如 IDR/I 幀的 type=5,P/B 幀的 type=1,sps 的 type=7,pps 的 type=8 等。
在 rfc6184 中,又為 nal_unit_type 從類型值 24 開始進行了擴充:
Table 1. Summary of NAL unit types and the corresponding packet
types
NAL Unit Packet Packet Type Name Section
Type Type
-------------------------------------------------------------
0 reserved -
1-23 NAL unit Single NAL unit packet 5.6
24 STAP-A Single-time aggregation packet 5.7.1
25 STAP-B Single-time aggregation packet 5.7.1
26 MTAP16 Multi-time aggregation packet 5.7.2
27 MTAP24 Multi-time aggregation packet 5.7.2
28 FU-A Fragmentation unit 5.8
29 FU-B Fragmentation unit 5.8
30-31 reserved -
注意,擴充的 nal_unit_type 類型並不是編碼器輸出的類型,而是為了適應 rtp payload 打包而定義的打包類型。
2.3.2 nalu payload
nalu payload 由 rbsp 結構組成。
2.4 rbsp
rbsp 可以分為 video-codec-layer rbsp(如 I/P/B 幀等) 和 non-video-codec-layer rbsp(如 sps/pps/sei 等)。
2.4.1 防競爭字節
emulation_prevention_three_byte,1字節,固定0x03,在 rbsp 中出現連續的 0x0000 兩字節結構時,在后面添加 0x03 作為防競爭字節,避免與 nal 單元分割字節沖突:
0x000000 => 0x00000300
0x000001 => 0x00000301
0x000002 => 0x00000302
0x000003 => 0x00000303
.........
2.4.2 rbsp 尾部
- rbsp_stop_one_bit,1位,固定為1
- rbsp_alignment_zero_bit,用於字節對齊,可選
2.5 SPS/PPS
- SPS/PPS 不同於 slice,雖然也是編碼器輸出的內容,但是不包含任何原始碼流。他們中包含的是解碼器需要的解碼信息,例如圖像的寬高、一些編碼參數等。
- 在編碼的時候,可以通過 ffmpeg 設置 AV_CODEC_FLAG_GLOBAL_HEADER 參數,那么 sps、pps 會作為 extra data 出現在 AVCodecContext::extradata 變量中,而不是出現在每個 IDR 幀的前面。用戶發送數據的時候,最好每次發送 idr 幀時,都將 sps、pps 一起發送。
- sps、pps 會有一個 id 值,如 sps 中的 seq_parameter_set_id,用於標識 sps 版本。pps 中的 pic_parameter_set_id,用於標識 pps 版本(且 pps 也有一個 seq_parameter_set_id,用於標識參考的 sps)。idr 也會有一個 pic_parameter_set_id,用於標識參考的 pps id(然后通過 pps 的 seq_parameter_set_id 跟蹤參考的 sps)。當編碼參數發生變化時,這些值也會發生變化,所以發送給接收端的數據,一定要及時更新 sps、pps,否則會發生解碼錯誤。
2.6 slice(條帶)
編碼后原始碼流被保存到了稱為 slice 的結構中,編碼后的一幀圖像可以對應一個 slice。但是因為一些編碼器設置,例如設置了輸出 slice 的數量、進行了多線程並行編碼等,編碼后的一幀圖像,也會分為多個 slice,每個 slice 各自負責了圖像中某一塊的編碼。
slice 也分為 header 和 data 部分:
通過 header 部分,我們可以得到當前 slice 在一幀編碼圖像中的位置(第幾個)、當前編碼圖像是 I/P/B 幀等中的哪一種、當前編碼圖像參考的 SPS/PPS 等重要信息。
2.7 access unit(訪問單元)
訪問單元代表一張編碼圖像,不包含 sps、pps 等外部數據。由於一幀編碼后的圖像可能會生成多個 slice,所以,access unit(訪問單元)可以由屬於一幀編碼圖像的多個 slice(nalu) 組成。
2.8 idr 幀
idr 幀是立即刷新幀,意味着接收端收到 idr 幀時,前面的參考幀緩存都可以丟棄了(注意,僅針對 close-gop),且 idr 后面的幀不會參考 idr 前面的任何幀。
idr 與 I 幀不同,I 幀不具有刷新參考緩沖區的功能,使用 ffmpeg 編碼時,可以設置 AVCodecContext::gop_size 來指定多少幀產生一個 IDR 幀。
3. h264 rtp payload 格式
3.1 Packetization Modes(打包模式)
rfc6184 定義了三種打包模式,分別為:
- Single NAL Unit mode,單 nalu 模式
- Non-Interleaved mode,非交織模式
- Interleaved mode,交織模式
三種不同打包模式對不同 nal unit type 的支持如下:
Table 3. Summary of allowed NAL unit types for each packetization
mode (yes = allowed, no = disallowed, ig = ignore)
Payload Packet Single NAL Non-Interleaved Interleaved
Type Type Unit Mode Mode Mode
-------------------------------------------------------------
0 reserved ig ig ig
1-23 NAL unit yes yes no
24 STAP-A no yes no
25 STAP-B no no yes
26 MTAP16 no no yes
27 MTAP24 no no yes
28 FU-A no yes yes
29 FU-B no no yes
30-31 reserved ig ig ig
3.1.1 Single NAL Unit mode(單 nalu 模式)
單 nalu 模式即將編碼器輸出的 nalu stream 流直接通過分隔符拆分出來,一個一個 nalu 復制到 rtp payload 中進行發送:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| Type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 2. RTP payload format for single NAL unit packet
Single NAL Unit mode 有如下問題:
- 如果采用 udp 進行媒體傳輸,那么會有 ip 分片的問題
- 當 nalu 比較小時,網絡傳輸效率不高(ip 頭 + udp 頭 + rtp 頭會占用較多的帶寬)
3.1.2 Non-Interleaved mode(非交織模式)
非交織模式下不僅支持編碼器直接輸出的的 nalu 復制到 payload 的打包方式,也支持 stap-a 聚合包模式和 fu-a 拆分包模式。聚合包模式可以合並較小的 nalu 到一個 rtp payload 中,解決網絡傳輸效率不高的問題;拆分包模式可以拆分大 nalu 到多個 rtp payload 中,解決 ip 分片的問題。
3.1.3 Interleaved mode(交織模式)
沒有研究過,且 webrtc、sip 等都不支持此模式。
3.2 sdp 媒體協商
在 sdp 媒體協商中,有如下示例(來自 webrtc):
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
如上,packetization-mode=1,profile=42,level=e0,id=1f(固定,不做分析)。下面主要分析 packetization-mode、profile 和 level 三個字段。
3.2.1 packetization-mode(協商打包模式):
rfc6184 定義了 single nalu mode=0; non-interleaved mode=1; interleaved mode=2。一般 sdp 中只支持 packetization-mode=1 的打包模式,如果沒有此項,默認支持 Single NAL Unit mode。
3.2.2 profile
一般 sdp 中會出現三種 profile,分別為 42(baseline profile)、4d(main profile)、64(high profile),參考 wiki 的定義(https://en.wikipedia.org/wiki/Advanced_Video_Coding):
可以看到,三種 profile 都只支持 yuv420 一種源圖像格式。baseline profile 不支持 B 幀,其它兩種支持 B 幀。
但是實際項目中,即使協商的 profile 非 baseline,webrtc 等實時音視頻系統也不會出現 B 幀,因為 B 幀的解碼依賴於前后幀,解碼順序與顯示順序不一致,會造成顯示延遲。且發生丟包時,B 幀無法解碼的概率很大。還有一個項目上的問題是,與自己系統對接的其它音視頻系統也不一定支持 B 幀。
3.2.3 level
參考 wiki 的定義(https://en.wikipedia.org/wiki/Advanced_Video_Coding):
以上是一部分截圖,可以看到,level 主要影響的是支持的最高分辨率和幀率,如 3.1(16進制為1f),最高支持 1,280×720@30.0 的視頻。
3.3 packet type(rtp payload 打包類型)
在 h264 輸出碼流的 nalu 類型基礎上,為了適應 rtp payload 打包,又擴充了 nal_unit_type 的定義:
- nal_unit_type 從 1 到 23,是 h264 定義的 nalu 類型,直接一一對應 rtp payload 打包類型
- nal_unit_type 從 24 到 29,是 rfc6184 定義的 nalu 類型,主要適應於網絡傳輸要求
由於一般 webrtc、sip 等只支持 non-interleaved mode(非交織模式),所以下面只討論 stap-a 和 fu-a 打包方式(單包方式已在前面進行了討論)。
3.3.1 STAP-A(Single-Time Aggregation Packet A)
如果一個編碼器輸出的 h264 nalu 的大小太小,那么可以嘗試與下一個 nalu 合並后一起放到一個 rtp 包的 payload 中,只要總大小不超過 MTU(1500字節) 即可。基於此,定義了 stap-a 類型,用於將多個 nalu 組合到一個 rtp payload 中:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 7. An example of an RTP packet including an STAP-A
containing two single-time aggregation units
但是 STAP 聚合包有個規則是同一時間編碼出來的 nalu 才能聚合在一起,這意味着前后幀編碼后的 nalu 不能嘗試合並在一起。
- STAP-A NAL HDR,1 字節。依然由 F、NRI、Type 三項組成,其中 F 置 0(因為 h264 中所有 nalu 的 F 都置 0);NRI 為組合的 nalu 中 NRI 值最大的那個,實際上置 0 即可;Type=24
- NALU 1 size,2 字節。即第一個 nalu 的大小,包括 nalu header
- NALU 1 HDR,1 字節,nalu 的頭部,直接復制原始 nalu 的 header 即可
- NALU 1 DATA,(NALU 1 Size - 1) 字節大小,直接復制原始 nalu 的 payload 即可
- 后面是第二個 ... 第 n 個 相同的結構
- OPTIONAL RTP padding,rtp payload 可以進行 4 字節對齊(非強制要求)
3.3.2 FU-A(Fragmentation Units A)
fu-a 打包模式能夠將一個大的 nalu 拆分成多個放到 rtp payload 中,每個拆分包的大小以總數據包大小不超過 MTU 為准:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14. RTP payload format for FU-A
fu-a 的頭部有 2 個字節,分別為 FU indicator 和 FU header,各占一個字節。
FU indicator:
The FU indicator octet has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
其中,F 位、NRI 位都是復制自拆分的 nalu 的 header,Type=28。
FU header:
The FU header has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
其中,S 位表示此 payload 是否是 nalu 的第一個拆分包,如果是,則 S 位置 1,否則,置 0。
E 位表示此 payload 是否是 nalu 的最后一個拆分包,如果是,則 E 位置 1,否則,置 0。
R 位是保留位。Type 復制自 nalu 的 Type。
FU payload 賦值自拆分的 nalu body。
4. 打包步驟
打包時,不需要知道 nalu 的具體類型(sps/pps/IDR 還是非 IDR 等類型),只需要根據分隔符和 nalu 的大小選擇合適的打包方式。
同時,我們需要先預設一個與 MTU 相關的閾值,可以設置為 1400 byte,即一個 rtp payload 最大容量為 1400 字節。
4.1 識別 nalu stream 分隔符
編碼器輸出的是 nalu stream,通過前面說明的分隔符將每個 nalu 之間進行了分割,所以打包的第一步就是識別分隔符,得到每個 nalu 的起始地址和大小。
需要注意的是,分隔符可以是 0x00000001 或者 0x000001,進行分隔符識別時,代碼必須兼容這兩種分割方式。
4.2 Single NAL Unit mode(單 nalu 模式)
如果媒體協商 sdp 中沒有說明 packetization-mode=1,則只能打單包。前面說明了如果 nalu 大小超過 MTU,則有 ip 分片的風險,所以這就需要編碼器輸出的 nalu 大小不能超過 payload 閾值。在 x264 編碼器中,通過設置 slice-max-size 參數可以控制輸出的 slice(即 nalu)的最大字節數(http://www.chaneru.com/Roku/HLS/X264_Settings.htm#slice-max-size):
注意,設置 slice-max-size 后,設置的 slices 數量參數就無效了。
單 nalu 模式直接將根據分隔符識別的每個 nalu 原樣復制到 rtp payload 中即可。
在下一節中,會一起介紹判斷何時打 single 包的偽代碼。
4.3 STAP-A(聚合包模式)
如果媒體協商 sdp 中說明了 packetization-mode=1,則支持 stap-a 打包模式。
有如下偽代碼:
vector<NALU> nalus; // 存儲所有 nalu 的數組
int payload_threshold; // rtp payload 閾值
// 遍歷所有 nalus
while (i < nalus.size()) {
if (當前 nalu 的 size < payload_threshold) {
j = i + 1;
sum_len = nalus[i].len;
// 累加 n 個 nalu.len,直到累計大小大於閾值
while (j < nalus.size()) {
sum_len += nalus[j].len;
if (sum_len > payload_threshold) {
break; // 跳出第二個 while
}
}
if (j - i - 1 > 0) {
// 打包 [i, j-1] 內的 nalus 為一個 stap-a 包
do_package_stapa(nalus, i, j-1);
} else {
// 對於如下情況,只能將第 i 個包單獨打包:
// 1. 如果第 i+1 個包累加到第 i 個包后,總大小超過閾值
// 2. 如果第 i 個包已經是最后一個包
do_package_single(nalus, i);
}
// 更新 i,然后繼續從 while 循環開始檢測
i = i + (j-i);
}
}
4.4 FU-A(拆分包模式)
如果媒體協商 sdp 中說明了 packetization-mode=1,則支持 fu-a 打包模式。
有如下偽代碼:
vector<NALU> nalus; // 存儲所有 nalu 的數組
int payload_threshold; // rtp payload 閾值
// 遍歷所有 nalus
while (i < nalus.size()) {
if (當前 nalu 的 size > payload_threshold) {
// 得到 nalu 被拆分的個數
int count = nalus[i].size / payload_threshold;
if (nalu.size % GO_MAX_RTP_PACKET_SIZE) {
count += 1;
}
// 遍歷 count
for (j < count) {
// 標識 fu header 開始和結束位
bool start = (j == 0);
bool end = (j == (count-1));
// 得到每個拆分包的開始地址和大小
char* data = nalus[i].size + j * payload_threshold;
int size = payload_threshold;
if ((j + 1) * payload_threshold > nalus[i].size) {
size = nalus[i].size % payload_threshold;
}
// 開始構建 fu-a payload
// ...
}
}
// 更新 i,然后繼續從 while 循環開始檢測
i = i + 1;
}
4.5 sps、pps 和 idr
在 ffmpeg 中,如果沒有設置 AV_CODEC_FLAG_GLOBAL_HEADER,那么編碼器編碼出 idr 時,前面都會附帶 sps、pps、sei(可選),因為他們是同一時間編碼出來的,所以可以使用 stap-a 格式進行打包。且一般 sps 和 pps 的大小都很小,所以 sps 和 pps 常會打到同一個 stap-a payload 格式的包中。idr 一般都比較大,會拆分成多個 fu-a 包。
pps、idr 前面的分隔符,不一定都是 0x00000001 開頭,取決於編碼器的實際輸出,但是 sps 前面的分隔符一定是 0x00000001 開頭。
sps、pps、idr 可以看作是同一幀,標識 rtp header marker 時,只有 idr nalu 才會有可能被標識 marker,且他們的 rtp header timestamp 都一樣。
4.6 rtp header marker
rtp header marker 用於標識當前 rtp 包是否是一幀的最后一個 rtp 包:
- 打 stap-a 包時,如果聚合的最后一個 nalu 是 nalu stream 的最后一個 nalu,那么標識 marker
- 打 fu-a 包時,如果待拆分的 nalu 是 nalu stream 的最后一個 nalu,且 fu-a header end 位置 1 時,才能標識 marker
- 打 single 包時,如果當前 nalu 是 nalu stream 的最后一個 nalu,那么標識 marker
4.7 rtp header timestamp
rtp 打包 h264 不支持 B 幀,因為 pts、dts 不同的話,無法同時存儲兩個時間戳。
所以 rtp timestamp 直接使用 pts * clockrate 即可(注意 pts 時間單位為秒,應來自於送入編碼器前賦值的時間戳)。
如果發送 rtp 的是編碼端,參考編碼端打時間戳的考慮 https://www.cnblogs.com/moonwalk/p/16409385.html。