RTP 協議解包爲 H264 裸流

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);
       }
    }
    

參考文章:
RTP協議詳解
h264基礎及rtp分包解包
H264(NAL簡介與I幀判斷)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章