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帧判断)