RTP 協議解包爲 H264裸流
一. 爲什麼使用 RTP 協議?
- TCP 傳輸流媒體數據由於其可靠性,會造成很大的網絡延時和卡頓。
- UDP 傳輸由於其不可靠性,會導致丟幀,如果是關鍵幀,則會花屏一個序列的時長。
- RTP 使用了 RTP 和 RTCP 兩個子協議來完成。
- RTP 使用 UDP 完成流媒體數據傳輸,保證其時效性。
- RTCP 也是使用 UDP,但只傳輸控制信息,佔帶寬很小,此時上層根據 RTCP 的信息,按照其重要性決定是否對丟失的流媒體數據進行重傳。
- RTP 在 1025-65535 之間選擇一個未使用的偶數端口號作爲其端口,RTCP 則使用下一個奇數端口號作爲其端口,進而組成一個 UDP 端口對,端口號 5004 和 5005 作爲 RTP 和 RTCP 的默認端口號。
二. H264 的封裝
-
拆解:H264 --> 序列(SPS.PPS.IPBBP…) --> Frame(幀) --> slice(切片) --> 宏塊 --> 子宏塊。
序列:一段 H264 序列是指從一個 I 幀開始到下一個 I 幀前的所有幀的集合。 -
NALU:H264 被封裝在一個個 NALU(Network Abstraction Layer Unit)中進行傳輸。
NALU 以 [00 00 00 01] 爲開始碼,之後是 NaluHeader,再之後是 NaluPayload。
eg: [00 00 00 01 67 2F A4 1E 23 59 1E 42 … ].
常見的幀頭數據:
00 00 00 01 67(SPS)
00 00 00 01 68(PPS)
00 00 00 01 65(IDR 幀)
00 00 00 01 61(P幀)
三. RTP 解包概念解析
-
RTP 封包時會將 [00 00 00 01] 的開始碼去除。(注:在收到 RTP 包時需要在每個 NALU 頭的位置拼接此開始碼)
eg:[RTP-Header] + [ 67 2F A4 1E 23 59 1E 42 … ]. -
NALU 封包策略
-
如果 NALU 長度比較小,則可以將其完整地裝在一個 RTP 包中。
此時,RTP 的結構爲 RtpHeader + RtpPayload(NaluHeader + NaluPayload). -
如果 NALU 長度超過 MTU(最大傳輸單元) 時,則需要對 NALU 進行分片封包。
此時,RTP 的結構爲 RtpHeader + RtpPayload(FuIndicator + FuHeader + NaluPayload).
會比完整包多一個字節的頭信息,下文會詳細解釋其含義。
-
什麼是 RtpHeader, NaluHeader, FuIndicator 和 FuHeader?
RtpHeader
-
結構體
/** * RtpHeader,普遍佔用12個字節 * * 由於 IP 協議採用大端序,這裏需要轉成小端序 (Java-Byte 是大端序,java 代碼中可以不用轉), * 所以這裏每一個字節內的各個屬性跟標準 rtp 協議頭剛好相反, * 並且在使用 "大於1bit" 的屬性時需要將網絡序轉成字節序. */ typedef struct rtp_header_t { // 1byte (0) unsigned int cc : 4; /* CSRC count */ unsigned int x : 1; /* header extension flag */ unsigned int p : 1; /* padding flag */ unsigned int version : 2; /* protocol version */ // 1byte (1) unsigned int pt : 7; /* payload type */ unsigned int m : 1; /* marker bit */ // 2bytes (2,3) unsigned int seq : 16; /* sequence number */ // 4bytes (4-7) uint32_t ts; /* timestamp */ // 4bytes (8-11) uint32_t ssrc; /* synchronization source */ // 4bytes csrc 可選位 // uint32_t csrc[1]; /* optional CSRC list */ };
-
屬性解析
a. 屬性 m:
表示是否到一幀的末尾。
java 中可以使用isFrameEnd = buf[1] & 0x80 == 0x80
來判斷。
& 0x80
: 獲取該 Byte 中第一個 bit 位. [ & 1000 0000 ].
== 0x80
: 該 Byte 第一個 bit 值爲1時,Byte值爲 0x80 ([1000 0000]).b. 屬性 seq:
表示當前消息的序列號,在一定範圍內每發出一個 RTP 包,seq 自增一次。
java 中可以使用seq = (((int) buf[2] & 0xff) << 8) + ((int) buf[3] & 0xff)
來獲得其值。
NaluHeader
-
結構體
/** * NaluHeader,佔用1個字節,在 RtpHeader 之後。 * RTP 包含完整包時,RtpPayload = NaluHeader + NaluPayload. */ typedef struct nalu_header_t { unsigned int nalu_head_3 : 3; /* 前三位填充 nalu-head 的前三位 */ unsigned int nalu_type : 5; /* 後五位表示 nalu-type */ };
-
屬性說明
a. 屬性 nalu_type:
表示 nal 單元的類型,1~12由H.264使用,24~31由其他應用使用,以下是具體定義。0 沒有定義 1-23 NAL單元 單個 NAL 單元包 1 不分區,非IDR圖像的片 2 片分區A 3 片分區B 4 片分區C 5 IDR圖像中的片 6 補充增強信息單元(SEI) 7 SPS 8 PPS 9 序列結束 10 序列結束 11 碼流借宿 12 填充 13-23 保留 24 STAP-A 單一時間的組合包 25 STAP-B 單一時間的組合包 26 MTAP16 多個時間的組合包 27 MTAP24 多個時間的組合包 28 FU-A 分片的單元 29 FU-B 分片的單元 30-31 沒有定義
FuIndicator + FuHeader
-
結構體
/** * FuIndicator + FuHeader. 各佔一個字節,在 RtpHeader 之後。 * NALU的長度超過 MTU 時,RTP 對其進行分片傳輸,稱爲 Fragmentation Unit(以下簡稱FU)。 * RTP 包含切片包時,RtpPayload = FuIndicator + FuHeade + NaluPayload. */ typedef struct fu_indicator_header_t { unsigned int nalu_head_3 : 3; /* FuIndicator, 前三位填充 nalu-header 的前三位 */ unsigned int nalu_type : 5; /* FuIndicator, 後五位表示 nalu-type */ unsigned int fu_flag : 3; /* FuHeader, 前三位表示 fu 的標誌位. (0x80/0x40/0x00) */ unsigned int nalu_head_5 : 5; /* FuHeader, 後五位填充 nalu-header 的後五位. */ };
-
屬性說明
a. 屬性 nalu_type:
佔5個bit,用於確定是否爲切片封裝。
如果候選值在 1-12 之間,則表示未切片,當前屬性屬於 NaluHeader,
如果候選值在 28-29 之間,則表示被切片,當前屬性屬於 FuIndicator,且後面會追加一個字節表示 FuHeader。
java 中可以使用isFU-A = buf[RTPHeader.len] & 0x1F == 0x1C
判斷是否屬於切片包。
buf[RTPHeader.len]
: 取 RTP-Head 後的第一個 Byte.
& 0x1F
: 獲取後5個 bit 位。[ & 0001 1111 ]
== 0x1C
: FU-A 的值爲 28 ([0x1C]).b. 屬性 fu_flag:
只有在確定爲切片包時有效,否則這個位置的字節已經屬於 NaluPayload.
佔用3個bit,SER,爲 FU 的標誌位,表示當前包爲 FU 包的開頭/結尾/內容。
S- StartFlag: 第1個 bit 位,爲1表示 NALU-Start,則 flag 值爲 0x80 ([1000 0000]);
E- EndFlag: 第2個 bit 位,爲1表示 NALU-End,則 flag 值爲 0x40 ([0100 0000]);
R- RemainFlag: 第3個 bit 位,保留位,恆爲0.
java 中可以使用Fu_Flag = buf[RTPHeader.len+1] & 0xE0
獲得當前 flag 值。c. nalu_head 拼接:
完整包時爲單獨字節,切片包時被切割封裝在 FuIndicator 的前3位和 FuHeader的後5位。
可以通過 nalu_header = (fu_indicator & 0xe0) | (fu_header & 0x1f) 獲得其值。
(候選值:0x67-SPS、0x68-PPS、0x65-IDR幀、0x61-P幀)
此時 nalu_header 的後五位仍然表示 nalu-type,但是取值範圍在1-12之間,表示當前切片包的類型是 I幀/P幀/SPS/PPS.
四. RTP 解包核心代碼
-
RTP 解包核心代碼
#define RTP_HEADER_LEN 12 #define NALU_STARTER_LEN 4 // [00 00 00 01] 的開始碼 #define NALU_HEADER_LEN 1 #define FU_INDICATOR_LEN 1 #define FU_HEADER_LEN 1 int rtp_buffer_unpack(unsigned char *write_buf, int write_size) { rtp_header_t *rtp_header = (rtp_header_t *)write_buf; unsigned char *rtp_payload = write_buf + RTP_HEADER_LEN; // 地址偏移 uint8_t nalu_type = rtp_payload[0] & 0x1F; // NaluHeader的後5位 或 FuIndicator的後5位 if (nalu_type == NALUType_FU_A) // 0x1C { uint8_t fua_type = rtp_payload[1] & 0xE0; // FuHeader 的前三位 int header_len = RTP_HEADER_LEN + FU_INDICATOR_LEN + FU_HEADER_LEN; unsigned char *nalu_payload = write_buf + header_len; // 地址偏移 if (fua_type == Fua_Start) // Fu包爲 Nalu的起始位置,需要寫入 NaluStarter + NaluHeader + NaluPayload. { uint8_t nalu_header = (rtp_payload[0] & 0xE0) | (rtp_payload[1] & 0x1F); // FuIndicator 的前3位和 FuHeader的後5位 int input_size = NALU_STARTER_LEN + NALU_HEADER_LEN + (write_size - header_len); unsigned char *input_buf = Hover_MemAlloc(input_size); memset(input_buf, 0, input_size); input_buf[NALU_STARTER_LEN - 1] = 1; // NaluStarter - [00, 00, 00, 01] memcpy(input_buf + NALU_STARTER_LEN, &nalu_header, NALU_HEADER_LEN); // NaluHeader memcpy(input_buf + NALU_STARTER_LEN + NALU_HEADER_LEN, nalu_payload, write_size - header_len); // NaluPayload return h264_buffer_input(input_buf, input_size); } else // Fu包爲 Nalu的其他位置,只需要寫入 NaluPayload. { int input_size = write_size - header_len; unsigned char *input_buf = Hover_MemAlloc(input_size); memset(input_buf, 0, input_size); memcpy(input_buf, nalu_payload, input_size); // NaluPayload return h264_buffer_input(input_buf, input_size); } } else // 完整 Nalu 包需要寫入 NaluStarter + NaluHeader + NaluPayload. { int input_size = NALU_STARTER_LEN + (write_size - RTP_HEADER_LEN); unsigned char *input_buf = Hover_MemAlloc(input_size); memset(input_buf, 0, input_size); input_buf[NALU_STARTER_LEN - 1] = 1; // NaluStarter - [00, 00, 00, 01] memcpy(input_buf + NALU_STARTER_LEN, rtp_payload, write_size - RTP_HEADER_LEN); // RtpPayload = NaluHeader + NaluPayload. return h264_buffer_input(input_buf, input_size); } }