概述
在上一篇文章中,我们详细介绍了使用RTP传输H264视频流的打包方法。接下来,我们继续介绍RTP传输H265视频流的打包方法。H265,正式名称为高效视频编码,英文全称为High Efficiency Video Coding(HEVC),是国际电信联盟视频编码专家组和国际标准化组织/国际电工委员会动态图像专家组共同开发的下一代视频编码标准。作为H264/MPEG-4 AVC的继任者,H265旨在提供更高的视频压缩效率,能够在保持相同视频质量的前提下大幅度减少视频文件的大小,或者在相同的比特率下提供显著提升的图像质量。当使用RTP传输H265视频流时,也需要遵循一定的打包和传输规则。
H265 NALU
H265 NALU是H265编码视频流的基本数据单元,用于承载编码后的视频数据,并提供网络传输的抽象。NALU的设计旨在使视频编码与底层网络传输协议分离,使得H265编码的视频内容能够适应各种网络环境和应用需求。
每个NALU都以一个固定长度的NAL Unit Header开始,NAL Unit Header占用两个字节,通常包含以下几个字段。
Forbidden Zero Bit (F): 占1位,这一位必须为0。如果为1,则表示语法错误,整个NALU将被丢弃。
NALU Type (Type): 占6位,定义了NALU所携带数据的类型。总共有64种可能的类型(范围是0-63),其中0-31是VCL(视频编码层)NAL单元,用于携带编码的视频数据;而32-63是非VCL NAL单元,用于携带控制信息或元数据。不同的NALU Type对应着不同的编码数据或控制信息,比如:P帧和B帧为1,IDR帧为19,VPS(Video Parameter Set)为32,SPS(Sequence Parameter Set)为33,PPS(Picture Parameter Set)为34,SEI(Supplemental Enhancement Information)为39等。
LayerId: 占6位,用于表示NAL所在的Access Unit所属的层,是为了HEVC的继续扩展而设置的。在当前的HEVC标准中,这个字段通常被设置为0,但在未来的扩展中可能会用到。
TID: 占3位,用于指定NAL单元的时间标识符,一般取值为1。它帮助解码器确定NAL单元在视频流中的时间位置,从而正确解码和播放视频。
紧跟在NAL Unit Header之后的是NAL Unit Payload,包含了编码视频流的核心数据和辅助信息,是视频解码和播放的基础。在实际的网络传输和存储中,NALU通常还需要进一步封装成以下格式中的一种。
Annex B格式:在Annex B格式中,每个NALU之前添加一个Start Code Prefix,可以是0x000001或0x00000001,用于标识NALU的起始位置。相邻NALU之间,以此方式明确分隔。
AVCC (Advanced Video Coding Container) 格式:AVCC格式常见于MP4容器中,NALU不再使用Start Code Prefix,而是通过Length字段来标识每个NALU的长度。SPS和PPS等参数以NALU形式封装,并在MP4文件的hvcC盒(Box)中以字节串的形式存储。
封装方法
H265 NALU在封装到 RTP包中时,需要遵循一定的规则和流程,以确保数据能够被正确地传输、接收和解码。根据NALU的大小和传输需求,可以选择以下三种常见的封装方法。
1、单NALU封装。对于小型的NALU,(比如:P帧、B帧),可以直接将整个NALU放入一个RTP包的Payload中,无需额外处理。此时,RTP包的结构如下。
+-----------------------------+
| RTP Header (12 Byte) |
| NALU Header (2 Byte) |
| NALU Data ... |
+-----------------------------+
2、FU-A分包。对于大型NALU(比如:某些关键帧),如果其大小超过了RTP包的最大有效载荷MTU,可以使用Fragmentation Unit A方式进行分片。原始NALU会被拆分成多个片段,每个片段作为一个独立的RTP包发送。此时,RTP包的结构如下。
+-----------------------------+
| RTP Header (12 Byte) |
| FU Indicator (2 Byte) |
| FU Header (1 Byte) |
| Fragmented NALU Data ... |
+-----------------------------+
可以看到,FU-A分包在12个字节的RTP Header后,有三个字节的分包头,分别为:FU Indicator和FU Header。
FU Indicator占用两个字节,由以下部分组成。
F (1 bit): 禁止位,与NALU Header的F位一致。
Type (6 bits): 分包类型,二进制固定为110001(对应十进制的49),表示FU-A类型。
LayerId (6 bits): 与NALU Header的LayerId一致。
TID (3 bits): 与NALU Header的TID一致。
FU Header占用一个字节,由以下部分组成。
S (1 bit): 分包起始位。如果该FU是原始NALU的第一个片段,S设为1。否则,设为0。
E (1 bit): 分包结束位。如果该FU是原始NALU的最后一个片段,E设为1。否则,设为0。
Type (6 bits): 原始NALU类型,与NALU Header的Type一致,用于在重组时恢复原始NALU Header。
3、STAP-A聚合
对于多个小尺寸NALU,如果它们具有相近的解码时间戳,且合并后总尺寸仍小于MTU,可以使用Single-Time Aggregation Packet A方式将多个NALU合并到一个RTP包中。此时,RTP包的结构如下。
+-----------------------------+
| RTP Header (12 Byte) |
| STAP-A Header (2 Byte) |
| NALU Payload1 Size (2 Byte) |
| NALU Payload1 |
| NALU Payload2 Size (2 Byte) |
| NALU Payload2 |
| ... |
+-----------------------------+
STAP-A Header紧跟在RTP Header之后,占用两个字节(与NALU Header结构类似),用于标识这是一个STAP-A包,其Type值固定为48。在每个聚合的NALU前,会有一个长度字段(通常为2个字节),表明后续NALU数据的长度。所有聚合在STAP-A包中的NALU都共享相同的时间戳,这是STAP-A包的一个重要特征。
注意:无论采用上面的哪种封装方法,NALU Data或NALU Payload中都不包括Annex B格式中的起始码(比如:0x000001或0x00000001),因为RTP包已经提供了足够的信息来标识NALU的边界。
FU-A分包及重组
在服务端,FU-A分包的大致步骤如下。
1、原NALU切割: 大型NALU被拆分成多个连续的片段。切割位置通常选择在NALU内部的编码块边界,以避免破坏编码结构。
2、片段标识: 每个片段(FU)在RTP Payload中添加一个FU Header,用于标识该片段属于哪个原始NALU,以及其在原始NALU中的位置。
3、独立传输: 每个FU作为一个独立的RTP包发送,每个RTP包的Payload仅包含一个FU。
客户端接收到FU-A分包的RTP包后,根据RTP Header解析出Payload Type,确认为H265 FU-A数据后,按照以下步骤处理。
1、FU分包头解析: 提取FU Indicator和FU Header中的信息。
2、片段重组: 将收到的FU片段按照RTP包的Sequence Number顺序重新组合,将所有片段的Fragmented NALU Data拼接在一起。
3、NALU还原: 在重组后的NALU数据前添加原始NALU Header(根据FU分包头中的信息恢复),形成完整的NALU结构。
4、解码处理: 将还原后的完整NALU提交给H265解码器进行解码。
使用FU-A封装方法进行分包和重组时,有以下几点需要特别注意。
1、顺序传输: FU-A分包的RTP包必须严格按照分包顺序发送和接收,以确保正确重组。
2、丢包处理: 如果中间某个FU片段丢失,可能导致原始NALU无法正确重组。接收端可以根据RTP包的序列号和确认机制检测丢包,并尝试通过重传请求(比如:RTCP的NACK)恢复丢失片段。
3、时间戳同步: 所有FU片段共享同一个解码时间戳,确保解码时的正确同步。