RTP头部结构深度解析:从版本号到贡献源列表
RTP头部,说白了就是音视频数据包的“快递面单”。我调试过不下百次RTP流,每次出问题,90%的线索都藏在这12字节的固定头部里。今天咱们就把这12个字节掰开揉碎,一个一个讲清楚。
核心认知:RTP头部的前12字节是固定存在的,后面跟着的CSRC列表和扩展头是可选的。你只要掌握了这12字节,就能读懂任何RTP包。
2.1 RTP头部整体结构
先看一张总图,把各个字段的位置和长度搞清楚。
我的习惯:拿到一个RTP包,我第一件事就是看前两个字节(16bit)。V、P、X、CC、M、PT全在这两个字节里,一眼就能判断包的基本属性。
2.2 版本号(V)— 2bit
版本号占2个比特,当前RTP规范(RFC 3550)定义的版本号是2。为什么是2?因为版本0和版本1都是早期草案,早就没人用了。
我在项目中遇到过有人把版本号填成0,结果接收端直接丢弃了所有包。嗯,这种低级错误其实挺常见的,尤其是从旧代码迁移的时候。
| 版本号 | 含义 | 当前状态 |
|---|---|---|
| 0 | RTP草案版本(1989年) | 已废弃 |
| 1 | RTP草案版本(1991年) | 已废弃 |
| 2 | RFC 3550 标准版本 | 当前使用 |
| 3 | 保留 | 未定义 |
避坑指南:我曾经调试过一个诡异的问题——接收端偶尔丢包,但序列号是连续的。最后发现是发送端在某些条件下把版本号写成了3,接收端直接丢弃。检查版本号,永远是RTP调试的第一步。
2.3 填充位(P)— 1bit
填充位只有1个比特。当P=1时,表示RTP包尾部有填充字节。最后一个填充字节的值表示填充的总字节数。
你想想看,什么场景需要填充?最常见的是加密场景。某些加密算法要求明文长度是块大小的整数倍,这时候就需要填充。另外,有些底层传输协议也有对齐要求。
// 填充位示例:P=1,最后一个字节值为5
// 表示尾部有5个填充字节
+----------------+----------------+----------------+----------------+
| RTP头部 | 负载数据 | pad | pad | pad | pad=5 |
+----------------+----------------+----------------+----------------+
|<---- 5字节填充 ---->|
2.4 扩展位(X)— 1bit
扩展位标记头部之后是否有扩展头。X=1时,固定头部后面紧跟着一个扩展头,扩展头由profile-specific的扩展数据和长度字段组成。
我个人不太喜欢用扩展头。为什么?因为扩展头会增加解析复杂度,而且不是所有接收端都能正确处理。除非你的应用场景确实需要携带额外的元数据(比如视频的ROI区域信息),否则我建议保持X=0。
2.5 CSRC计数器(CC)— 4bit
CC字段表示CSRC列表中有多少个贡献源标识符。取值范围0-15。CC=0表示没有CSRC列表,头部就是标准的12字节。
这个字段在混音场景下特别重要。比如一个视频会议中有5个人在说话,SSRC标识混音器,CSRC列表标识那5个原始发言者。
2.6 标记位(M)— 1bit
标记位的含义由具体的负载类型(PT)定义。对于视频流,M通常标记一帧的结束;对于音频流,M通常标记一个通话突发(talk spurt)的开始。
我记得有一次调试视频卡顿问题,发现接收端把M位理解错了——发送端在每一帧的第一个包设置M=1,但接收端以为M=1表示帧结束。结果就是画面一直卡在第一帧。这种负载类型和标记位的语义对齐,是RTP联调时最容易出问题的地方。
| 负载类型 | M位典型含义 |
|---|---|
| PCMU / PCMA(音频) | 通话突发开始 |
| H.264(视频) | NAL单元边界 / 帧结束 |
| MPEG-TS(音视频) | 帧边界 |
2.7 负载类型(PT)— 7bit
PT字段标识RTP包中负载数据的编码格式。取值范围0-127,其中0-95是静态分配的(如PCMU=0,PCMA=8),96-127是动态分配的。
说白了,PT就是告诉接收端“这包数据是用什么编码的”。接收端根据PT值选择对应的解码器。如果PT对不上,解码出来的就是噪声或者花屏。
我的经验:动态PT(96-127)在实际项目中用得最多。比如H.264通常用96或97,Opus用98或99。但不同厂商的分配可能不一样,联调时一定要先交换PT映射表。
2.8 序列号(SN)— 16bit
序列号是RTP调试中最常用的字段。16bit无符号整数,初始值随机,每发送一个RTP包就加1。接收端通过序列号检测丢包、乱序和重复包。
序列号溢出怎么办?16bit最大65535,溢出后从0重新开始。接收端需要处理回绕(wrapping)的情况。
// 序列号回绕检测示例(C语言风格)
uint16_t last_seq = 65530;
uint16_t current_seq = 5;
// 判断是否回绕
if (current_seq < last_seq && (last_seq - current_seq) > 32768) {
// 发生了回绕,实际序列号 = current_seq + 65536
uint32_t real_seq = (uint32_t)current_seq + 65536;
}
避坑指南:我曾经遇到过一个bug——发送端在重启后序列号重新从随机值开始,接收端检测到序列号大幅跳变,判定为严重丢包,触发了关键帧请求。结果就是视频频繁卡顿。解决方案:发送端在RTCP的SR报告中携带序列号偏移信息,或者接收端在检测到SSRC变化时重置序列号状态。
2.9 时间戳(Timestamp)— 32bit
时间戳反映RTP包中第一个字节的采样时刻。32bit无符号整数,时钟频率由负载类型决定。比如音频PCMU的时钟频率是8000Hz,视频H.264通常是90000Hz。
为什么视频要用90000Hz?因为90000能被常见的视频帧率(24、25、30、50、60)整除,能精确表示帧边界。你想想看,如果用1000Hz,30fps的视频每帧33.33ms,时间戳增量就不是整数了。
| 负载类型 | 典型时钟频率 | 时间戳增量(一帧) |
|---|---|---|
| PCMU / PCMA | 8000 Hz | 160(20ms音频帧) |
| H.264 | 90000 Hz | 3000(30fps) / 3600(25fps) |
| Opus | 48000 Hz | 960(20ms音频帧) |
2.10 同步源标识(SSRC)— 32bit
SSRC是RTP会话中每个同步源的唯一标识。32bit随机数,在整个RTP会话中必须唯一。同一个SSRC的所有RTP包使用同一个时间戳和序列号空间。
SSRC冲突怎么办?RFC 3550定义了冲突检测和解决机制。当两个参与者发现SSRC冲突时,一方必须改变自己的SSRC并发送RTCP BYE包。
我的建议:SSRC的随机生成算法一定要用好的随机数源。我见过有人用时间戳的低32位做SSRC,结果两个客户端同时启动时SSRC冲突了。用/dev/urandom或者加密安全的随机数生成器,别偷懒。
2.11 贡献源列表(CSRC List)— 可选
CSRC列表跟在固定头部后面,长度由CC字段决定。每个CSRC占32bit,标识一个贡献源。CSRC列表只在混音器(Mixer)场景下使用。
举个例子:一个视频会议中有3个人(A、B、C)在说话,混音器生成一个RTP包,SSRC填混音器自己的标识,CSRC列表填A、B、C的SSRC。接收端就知道这个包是混音后的结果,原始贡献者是A、B、C。
// CSRC列表在RTP包中的位置
// 假设CC=3,表示有3个CSRC
+----------------+----------------+----------------+----------------+
| 固定头部(12B) | CSRC 1 (4B) | CSRC 2 (4B) | CSRC 3 (4B) |
+----------------+----------------+----------------+----------------+
| 负载数据... |
+--------------------------------------------------------------------+
关键总结:RTP头部12字节是调试的起点。V=2是硬性要求,P和X尽量保持0,CC根据混音场景设置,M和PT必须和负载类型语义对齐,SN和时间戳是丢包和抖动检测的核心,SSRC必须唯一,CSRC只在混音时使用。把这些字段吃透了,RTP调试就成功了一半。