天天看点

RTP 协议解包为 H264 裸流

RTP 协议解包为 H264裸流

一. 为什么使用 RTP 协议?
  • TCP 传输流媒体数据由于其可靠性,会造成很大的网络延时和卡顿。
  • UDP 传输由于其不可靠性,会导致丢帧,如果是关键帧,则会花屏一个序列的时长。
  • RTP 使用了 RTP 和 RTCP 两个子协议来完成。
    • RTP 使用 UDP 完成流媒体数据传输,保证其时效性。
    • RTCP 也是使用 UDP,但只传输控制信息,占带宽很小,此时上层根据 RTCP 的信息,按照其重要性决定是否对丢失的流媒体数据进行重传。
    • RTP 在 1025-65535 之间选择一个未使用的偶数端口号作为其端口,RTCP 则使用下一个奇数端口号作为其端口,进而组成一个 UDP 端口对,端口号 5004 和 5005 作为 RTP 和 RTCP 的默认端口号。
  • RTP-Header 的流媒体特性,提供 帧边界、编码格式、序列号、时间戳等字段。
  • RTP 支持网络多播,而 TCP 不支持。
二. 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

RTP协议详解

h264基础及rtp分包解包

H264(NAL简介与I帧判断)

继续阅读