4. RTP负载类型与封装格式:H.264/H.265的NAL单元拆分、AAC的ADTS头、opus的RTP封装

这一章,我们聊聊RTP的负载。说白了,就是你要传的视频、音频数据,怎么塞进RTP包里。

我刚开始做流媒体时,总觉得封装是个体力活。后来线上出过一次问题——H.264的NAL单元拆分不对,导致解码器疯狂报错。嗯,从那以后我再也不敢小看这一步了。

你想想看,RTP本身只是个运输工具。真正有价值的是里面的载荷。视频是H.264还是H.265?音频是AAC还是Opus?每种格式都有自己的脾气,封装方式完全不同。

4.1 H.264的NAL单元与RTP封装

H.264的码流,核心就是NAL单元。每个NAL单元都有一个头,告诉解码器「我是什么类型」。

NAL类型 值 说明
非IDR图像 1 普通帧,依赖前面的参考帧
IDR图像 5 关键帧,解码器从这里开始刷新
SPS 7 序列参数集,图像尺寸、编码配置
PPS 8 图像参数集,熵编码模式等

RTP封装H.264时,有三种模式。我个人习惯用单NAL单元模式,简单直接。但遇到大帧时,就得用FU-A分片了。

单NAL单元模式

一个RTP包只装一个NAL单元。适合小尺寸的帧,比如SPS、PPS。

RTP头 | NAL头(1字节) | NAL载荷

FU-A分片模式

大NAL单元拆成多个RTP包。每个分片都有FU指示器和FU头。

第一个分片: RTP头 | FU指示器(0x1C) | FU头(S=1,E=0) | 分片数据
中间分片: RTP头 | FU指示器(0x1C) | FU头(S=0,E=0) | 分片数据
最后分片: RTP头 | FU指示器(0x1C) | FU头(S=0,E=1) | 分片数据
我的经验:FU-A的起始位和结束位一定要设对。我曾经遇到过接收端一直等不到结束标记,导致解码器卡死。检查后发现是S位和E位写反了。

4.2 H.265的NAL单元与RTP封装

H.265和H.264的封装思路类似,但NAL头从1字节变成了2字节。多了个层ID和TID。

H.264 NAL头: [F(1) | NRI(2) | Type(5)]
H.265 NAL头: [F(1) | Type(6) | LayerID(6) | TID(3)]

H.265的RTP封装也支持单NAL和FU分片。但要注意,H.265的FU指示器是48,不是1C。

避坑:H.265的FU-A分片,FU头里没有NRI字段。NRI信息在FU指示器里。如果你直接把H.264的拆包逻辑搬过来,解码器会报「无效NAL类型」。

4.3 AAC的ADTS头与RTP封装

AAC的RTP封装,核心是ADTS头。每个AAC帧前面都要加这个头,解码器才能知道采样率、声道数、帧长度。

ADTS字段 位数 说明
syncword 12 固定0xFFF,同步头
ID 1 0=MPEG-4, 1=MPEG-2
profile 2 0=Main, 1=LC, 2=SSR, 3=LTP
sampling_frequency_index 4 采样率索引,查表得到实际值
channel_configuration 3 声道数配置
frame_length 13 ADTS头 + AAC数据的长度

RTP封装AAC时,我建议用AU Header模式。每个RTP包可以装多个AAC帧,用AU Header描述每个帧的长度。

RTP头 | AU Header(2字节) | AU Header长度(2字节) | AAC帧1 | AAC帧2 | ...
关键点:AU Header里的帧长度是「帧长度 - 1」。我第一次实现时忘了减1,结果解码器一直读错边界。排查了半天才发现是这里的问题。

4.4 Opus的RTP封装

Opus的RTP封装相对简单。它不需要像AAC那样加ADTS头,因为Opus帧本身就有自描述能力。

Opus的RTP载荷格式:

RTP头 | Opus帧1 | Opus帧2 | ...

每个Opus帧前面有一个TOC字节,描述了编码模式、带宽、帧长。

TOC字段 位数 说明
configuration 5 编码模式和带宽
stereo 1 0=单声道, 1=立体声
frame_count 2 帧数编码,00=1帧, 01=2帧, 10=3帧, 11=4帧
我的习惯:Opus的RTP包,我一般只装一帧。因为Opus帧本身很小,装多帧反而增加延迟。但如果你做直播,可以装2-3帧来减少包数量。

4.5 三种封装对比

编码 封装复杂度 头开销 典型应用
H.264 高(分片逻辑复杂) 1-2字节/包 视频会议、直播
H.265 高(2字节NAL头) 2字节/包 4K/8K视频
AAC 中(ADTS头7字节) 7字节/帧 音乐、广播
Opus 低(TOC字节) 1字节/帧 实时通信
RTP负载封装流程 原始音视频数据 判断编码类型 H.264/H.265 AAC Opus NAL拆分/FU-A分片 单NAL/FU-A/AP/STAP 添加ADTS头 AU Header模式 TOC字节封装 多帧/单帧模式 RTP载荷封装完成 发送到网络

这张图展示了三种编码的RTP封装流程。从原始数据开始,根据编码类型走不同的封装路径,最后统一成RTP载荷发送出去。

重要提醒:实际开发中,不要自己从头实现RTP封装。用FFmpeg的avformat库或者live555,它们已经处理好了各种边界情况。我见过太多人自己写拆包逻辑,结果遇到B帧顺序问题、PTS/DTS对齐问题,调试到崩溃。

好了,这一章的内容就到这里。RTP负载封装是流媒体开发的基础,理解了这些,后面讲传输优化时你才能明白为什么有些包会丢、有些包会乱序。