一、引言
RTP封装H.264时,RTP对NALU Header的nal_unit_type附加了扩展含义。
由《音视频入门基础:H.264专题(4)——NALU Header:forbidden_zero_bit、nal_ref_idc、nal_unit_type简介》可以知道,nal_unit_type表示NALU的类型,在H.264官方文档《T-REC-H.264-202108-I!!PDF-E.pdf》第65页定义为:
二、NAL Unit Type
在RTP封包中,nal_unit_type被附加了扩展含义。根据《RFC 6184》第13页:
可以看到值为1 - 23的nal_unit_type为原H.264中规定的类型,24 - 31为RTP扩展的类型。扩展的类型包括STAP-A、STAP-B、MTAP16、MTAP24、FU-A、FU-B。
三、RTP payload header
由《音视频入门基础:H.264专题(3)——EBSP, RBSP和SODB》可以知道,对于AnnexB格式的H.264码流来讲,为了分割各个NALU ,编码时在每个NALU前面加上0x000001或0x00000001作为startcode(起始码)。通过RTP对H.264进行网络传输(RTP打包H.264码流)时,会去掉起始码,把起始码替换为RTP payload header。RTP payload header是每个RTP数据包有效载荷(payload)的第一个字节,接收方可通过这个RTP payload header来识别有效载荷结构。RTP payload header的结构始终是NALU Header(包含forbidden_zero_bit、nal_ref_idc、nal_unit_type)。
示例:
通过Wireshark抓包,可以看到RTP payload header固定占1字节,其结构就是H.264的NALU Header:
四、有效载荷结构
根据《RFC 6184》第12页,RTP的payload format(有效载荷格式)定义了三种不同的基本payload structures(有效载荷结构)。可能的结构如下:
(一)Single NAL Unit Packet
Single NAL Unit Packet(单个NAL单元数据包): RTP有效载荷中只包含一个NAL单元。NAL unit type等于原H.264中规定的类型,即NAL unit type范围在1至 23(含 23)之间。
这里定义的Single NAL Unit Packet必须只包含一个在H.264官方文档《T-REC-H.264-202108-I!!PDF-E》中定义的NAL单元。这意味着在Single NAL Unit Packet中,既不能使用aggregation packet(聚合数据包),也不能使用fragmentation unit(分片单元)。按 RTP 序列号(sequence number)顺序对single NAL unit packets进行去数据包化而组成的NAL单元流必须符合NAL单元解码顺序。single NAL unit packet的结构如下图所示。说明:NAL单元的第一个字节同时也是RTP payload header(RTP有效载荷标头):
示例:
通过Wireshark抓包,可以看到下面的RTP数据包的RTP payload header的NAL unit type的值为1,符合条件:“NAL unit type范围在1至 23(含 23)之间”。所以该RTP数据包的有效载荷结构就是Single NAL Unit Packet。该RTP数据包有效载荷中只包含一个NALU,NAL unit type值为1,表示携带的是Coded slice of a non-IDR picture这种NALU:
(二)Aggregation Packet
Aggregation Packet(聚合数据包): 用于将多个NAL单元聚合到单个RTP有效载荷中的数据包类型。这种数据包有四种版本:Single-Time Aggregation Packet type A(单时聚合数据包类型A ,STAP-A)、Single-Time Aggregation Packet type B(单时聚合数据包类型B,STAP-B)、带 16 位偏移的多时聚合数据包(MTAP16)和带24位偏移的多时聚合数据包(MTAP24)。为 STAP-A、STAP-B、MTAP16和MTAP24分配的NAL unit type分别为24、25、26和27。
Aggregation Packet是《RFC 6184》的NAL单元聚合方案。引入该方案是为了反映两个主要目标网络截然不同的 MTU 大小:有线IP网络(其 MTU 大小通常受限于以太网 MTU 大小,大约1500 字节)和基于IP或非基于IP(如ITU-T H.324/M)的无线通信系统,其首选传输单元大小为 254 字节或更小。为了防止这两种系统之间的媒体转码,并避免不必要的数据包开销,引入了一种NAL单元聚合方案。
《RFC 6184》定义了两种聚合数据包:
单时聚合数据包(STAP):聚合具有相同 NALU时间的NAL单元。定义了两种STAP,一种不含 DON(STAP-A),另一种包含 DON(STAP-B)。
多时间聚合包 (MTAP):聚合NALU 时间可能不同的NAL单元。定义了两种不同的MTAP,其 NAL 单位时间戳偏移的长度各不相同。
聚合数据包中传输的每个NAL单元都封装在一个聚合单元中。聚合数据包的RTP有效载荷格式结构下图所示:
MTAP和STAP 共享以下分组规则:
1.RTP时间戳必须设置为要聚合的所有NAL单元的最早NALU时间。
2.NAL单元类型八位位组的类型字段必须设置为下表所示的适当值。
3.如果聚合NAL单元的所有F位(forbidden_zero_bit)均为零,则必须清除 F 位;否则,必须设置 F位。
4.NRI(nal_ref_idc)的值必须是聚合数据包中所有NAL单位的最大值。
RTP 包头中的标记位(marker位)将被设置为聚合数据包最后一个 NAL单元的标记位在其自身的 RTP 数据包中传输时的值。
aggregation packet(聚合数据包)的有效载荷由一个或多个aggregation units(聚合单元)组成。聚合数据包可根据需要携带任意数量的聚合单元;但是,聚合数据包中的数据总量显然必须适合一个IP数据包,而且数据包大小的选择应使生成的IP数据包小于MTU 大小。聚合数据包不得包含fragmentation units(分片单元)。聚合数据包不得嵌套;也就是说,一个聚合数据包不得包含另一个聚合数据包。
(三)Fragmentation Unit
Fragmentation Unit(分片单元):用于将单个NAL单元分片到多个RTP数据包中。有FU-A和 FU-B两个版本,分别用值为28和29的NAL unit type标识。
说明:《RFC 6184》不限制封装在single NAL unit packets和fragmentation units中的NAL单元的大小。任何aggregation packet中封装的NAL单元的最大大小为65535 字节。
五、Decoding Order Number (DON)
根据《RFC 6184》第17页,在interleaved packetization mode(交错分组模式)中,允许NAL单元的传输顺序与NAL单元的解码顺序不同。Decoding Order Number(解码顺序号 ,DON)是有效载荷结构或派生变量中的一个字段,表示NAL单元的解码顺序。传输和解码顺序的耦合由可选的sprop-interleaving-depth媒体类型参数控制,具体如下:
当可选的sprop-interleaving-depth媒体类型参数的值等于 0(显式或默认值)时,NAL单元的传输顺序必须与 NAL 单元的解码顺序一致;当可选的sprop-interleaving-depth媒体类型参数的值大于0 时:MTAP16和MTAP24中NAL单元的顺序不需要是NAL单元解码顺序,通过在两个连续数据包中去包STAP-B、MTAP和FU生成的NAL单元的顺序不需要是NAL单元解码顺序:
六、Single-time aggregation packet(STAP)
根据《RFC 6184》第22页,当 NAL单元聚合在一起且共享相同的NALU时间时,应使用单时聚合数据包(STAP,Single-time aggregation packet)。
(一)STAP-A
STAP-A(STAP type A)的有效载荷不包括DON,至少由一个single-time aggregation units(单时聚合单元)组成。如下图所示:
single-time aggregation units(单时聚合单元)由16 位无符号大小信息(NAL unit size,按网络字节顺序排列)组成,以字节为单位表示后面NAL单元的大小(不包括这两个八位位组,即不包括NAL unit size,但包括NALU Header),后面是包括NAL unit type的NAL单元本身。单时聚合单元在 RTP 有效载荷内按字节对齐,但不一定按32位字边界对齐。下图显示了单时聚合单元的结构:
由《音视频入门基础:RTP专题(7)——RTP协议简介》可以知道:
一个RTP packet = RTP header + RTP layload + 填充字节(可选)
当有效载荷结构(即RTP layload)为STAP-A时,此时:
该RTP数据包(RTP packet) = RTP header + STAP-A
一个STAP-A = RTP payload header(此时为STAP-A NAL HDR) + 若干个single-time aggregation units
一个single-time aggregation units = NAL unit size(固定占2字节) + NAL unit(包含NALU Header)
下图展示了一个包含STAP-A的RTP数据包示例。STAP包含两个单时聚合单元,在图中分别标为1和2:
示例:
通过Wireshark抓包,可以看到下面的RTP数据包的RTP payload header的NAL unit type的值为24,所以该RTP数据包的有效载荷结构就是STAP-A。该RTP数据包有效载荷中可能包含多个NALU:
继续通过Wireshark展开该RTP数据包的信息,可以看到该RTP数据包有效载荷中包含了两个NALU。第一个NALU占772字节,为SEI。第二个NALU占213字节,为IDR SLICE:
(二)STAP-B
STAP-B(STAP type B)的有效载荷包括一个16位无符号解码顺序号 (DON)(按网络字节顺序排列)和至少一个single-time aggregation units(单时聚合单元):
DON 字段按传输顺序指定 STAP-B 中第一个 NAL 单元的DON值。对于 STAP-B 中按出现顺序连续出现的每个 NAL 单元,DON 值 =(STAP-B中前一个NAL单元的DON 值 + 1)% 65536,其中“%”表示模乘运算。
下图展示了一个包含 STAP-B 的 RTP 数据包示例。STAP 包含两个单时聚合单元,在图中分别标为 1 和 2:
七、Multi-Time Aggregation Packet(MTAP)
MTAP(Multi-Time Aggregation Packet,多时聚合包) 的NAL单元有效载荷包括一个16位无符号解码顺序号基 (DONB)(按网络字节顺序排列)和一个或多个多时间聚合单元,如下图所示。DONB必须包含MTAP NAL单元解码顺序中第一个NAL单元的DON值。说明:NAL 单元解码顺序中的第一个 NAL 单元不一定是 MTAP 中 NAL 单元封装顺序中的第一个 NAL 单元:
《RFC 6184》定义了两种不同的多时间聚合单元。它们都由以下 NAL 单元的 16 位无符号大小信息(按网络字节顺序)、8 位无符号解码顺序号差值(DOND)和该 NAL 单元的 n 位时间戳偏移(TS 偏移)(按网络字节顺序)组成,其中 n 可以是 16 位或 24 位。不同 MTAP 类型(MTAP16 和 MTAP24)之间的选择取决于应用:时间戳偏移越大,MTAP 的灵活性越高,但开销也越大。
MTAP16的多时间聚合单元结构如下图所示:
MTAP24的多时间聚合单元结构如下图所示:
数据包内聚合单元的起始或终止位置不要求在 32 位字边界上。多时间聚合单元中包含的 NAL 单元的 DON 等于 (DONB + DOND) % 65536,其中 % 表示取模运算。本备忘录未说明 MTAP 中的 NAL 单元如何排序,但在大多数情况下,应使用 NAL 单元解码顺序。
时间戳偏移字段必须设置为等于以下公式的值:如果 NALU 时间大于或等于数据包的 RTP 时间戳,则时间戳偏移 = (NAL 单元的 NALU 时间 - 数据包的 RTP 时间戳)。
如果 NALU 时间小于数据包的 RTP 时间戳,则时间戳偏移等于 NALU 时间 + (2^32 - 数据包的 RTP 时间戳)。
对于 MTAP 中的 “最早 ”多时间聚合单元,时间戳偏移必须为零。因此,MTAP 本身的 RTP 时间戳与最早的 NALU 时间相同。
参考说明:“最早 ”的多时间聚合单元是指如果聚合单元中包含的 NAL 单元封装在单个 NAL 单元数据包中,在 MTAP 的所有聚合单元中具有最小扩展 RTP 时间戳的单元。扩展时间戳是一种时间戳,它的位数超过 32 位,并能计算时间戳字段的缠绕,因此能确定时间戳缠绕时的最小值。这种 “最早 ”的聚合单元可能不是 MTAP 中聚合单元封装顺序中的第一个。最早 "的 NAL 单元也不必与 NAL 单元解码顺序中的第一个 NAL 单元相同。
下图展示了一个包含 MTAP16 类型的多时间聚合数据包的 RTP 数据包示例,该数据包包含两个多时间聚合单元,在图中标注为 1 和 2:
下图展示了一个包含 MTAP24 类型的多时间聚合数据包的 RTP 数据包示例,该数据包包含两个多时间聚合单元,在图中标注为 1 和 2:
八、Fragmentation Units (FUs)
根据《RFC 6184》第29页,这种有效载荷类型允许将一个NAL单元分割成多个RTP数据包。在应用层而不是依靠低层分片(如 IP)这样做有以下优点:
该有效载荷格式能够在IPv4网络上传输大于64 kbytes的NAL 单元,而这些 NAL单元可能存在于预先录制的视频中,尤其是高清格式中(每幅图像的切片数有限制,这导致每幅图像的 NAL 单元数有限制,从而可能产生较大的NAL单元)。
分片机制允许对单个NAL单元进行分片,并前向纠错。
片段只针对单个NAL单元,而不针对任何聚合数据包。一个NAL单元的片段由该NAL单元的整数个连续八位位组组成。NAL单元的每个八位字节必须恰好是该NAL单元的一个片段的一部分。
同一NAL单元的片段必须以升序RTP序列号连续发送(在第一个片段和最后一个片段之间不发送同一RTP数据包流中的其他RTP数据包)。同样,一个 NAL 单元必须按RTP序列号顺序重新组装。
当一个NAL 单元被分片并在分片单元 (FUs) 内传送时,它被称为分片NAL单元。STAP和MTAP不得分片。FU不得嵌套;也就是说,一个FU不得包含另一个FU。
携带FU的RTP 数据包的 RTP 时间戳被设置为分片NAL单元的NALU时间。下图显示了FU-A的 RTP 有效载荷格式。FU-A 由一个8位(1字节)的碎片单元指示符(FU indicator,又称FU identifier,其实就是RTP payload header)、一个8位的碎片单元报头(FU header)和一个碎片单元有效载荷(FU payload,又称fragmentation unit payload,H264 NAL Unit Payload)组成:
下图显示了FU-B的RTP 有效载荷格式。FU-B由一个8位(1字节)的片段单元指示符(FU indicator)、一个8位的片段单元头(FU header)、一个解码顺序号(DON)(按网络字节顺序)和一个片段单元有效载荷(FU payload)组成。换句话说,FU-B的结构与FU-A 的结构相同,只是多了一个DON字段:
在interleaved packetization mode(交错分组模式)中,NAL 单元类型 FU-B 必须用于分片NAL 单元的第一个分片单元。在任何其他情况下都不得使用 NAL 单元类型 FU-B。换句话说,在交错分组模式下,每个被分片的 NALU 的第一个分片都是FU-B,然后是一个或多个FU-A 分片。
FU indicator的格式如下:
FU indicator的Type字段28和29的值分别标识FU-A和FU-B。NRI字段的值必须根据分片NAL单元中 NRI 字段的值设置。
FU header的格式如下:
其中:
S:占1位,为起始位。当设置为1时,表示分片NAL单元的起始位置(即表示这是第一个分片)。当下面的FU有效载荷不是片段 NAL单元有效载荷的开始时,该位被设置为0(也就是说,如果不是第一个分片就被设置为0)。
E:占1位,为结束位。当设置为1时,表示分片NAL单元的结束(也就是说,如果分片结束了,该位被设置为0),即有效载荷的最后一个字节也是分片NAL单元的最后一个字节。当后续 FU 有效载荷不是片段 NAL单元的最后一个片段时,该位被置0。
R:占1位,为保留位。必须等于 0,且必须被接收器忽略。
Type:占5位。为下图中定义的 NAL 单元有效载荷类型:
说明:FU-Bs中的DON 字段允许网关将NAL单元分片到FU-Bs,而无需按照NAL单元解码顺序组织传入的NAL单元。
片段NAL 单元不得在一个FU中传输;也就是说,在同一个FU标头中,开始位和结束位不得同时置 1。
FU有效载荷由片段NAL单元有效载荷的片段组成,这样,如果连续 FU 的片段单元有效载荷按顺序连接起来,就可以重建片段NAL单元的有效载荷。碎片 NAL单元的NAL单元类型八位位组不包含在碎片单元有效载荷中,而是在碎片单元FU指示符八位位组的F和NRI字段以及FU标头的类型字段中传达碎片 NAL单元的NAL单元类型八位位组信息。FU有效载荷可以有任意数量的八位位组,也可以是空的。
说明:空 FU 允许在几乎无损的环境中减少某类发送方的延迟。这些发送方的特点是,他们在 NALU 完全生成之前,也就是在知道 NALU 大小之前,就将 NALU 片段打包。如果不允许使用零长度的 NALU 片段,发送者就必须在发送当前片段之前生成至少一个比特的下一个片段数据。由于 H.264 的特点,有时几个宏块占用的比特为零,这种情况并不可取,而且会增加延迟。不过,在使用零长度 NALU 片段(可能)时,应仔细权衡因传输时使用额外数据包而增加的至少部分 NALU 丢失风险。
如果一个分片单元丢失,接收方应按照传输顺序丢弃与同一分片 NAL 单元相对应的所有后续分片单元。端点或 MANE 中的接收方可将 NAL 单元的前 n 1 个片段聚合为一个(不完整的)NAL 单元,即使未收到该 NAL 单元的 n 个片段。在这种情况下,NAL 单元的 forbidden_zero_bit 必须设为 1,以表示语法违规。
示例:
通过Wireshark抓包,可以看到该RTP数据包的有效载荷结构就是FU-A。FU Header的S位值为1,表示这是某个Coded slice of a non-IDR picture的第一个分片:
通过Wireshark抓下一个包,可以看到该RTP数据包的有效载荷结构是FU-A。FU Header的S位值为0,E位值为0,表示这是该Coded slice of a non-IDR picture的处于中间的分片(既不是第一个分片,也不是最后一个分片):
通过Wireshark抓下一个包,可以看到该RTP数据包的有效载荷结构是FU-A。FU Header的S位值为0,E位值为1,表示这是该Coded slice of a non-IDR picture的最后一个分片:
从上面的Wireshark示例可以看出来,当某个NALU比较大时,就需要通过FU分片把一个NAL单元分割成多RTP数据包。