RTP头部结构深度解析:从版本号到贡献源列表

RTP头部,说白了就是音视频数据包的“快递面单”。我调试过不下百次RTP流,每次出问题,90%的线索都藏在这12字节的固定头部里。今天咱们就把这12个字节掰开揉碎,一个一个讲清楚。

核心认知:RTP头部的前12字节是固定存在的,后面跟着的CSRC列表和扩展头是可选的。你只要掌握了这12字节,就能读懂任何RTP包。

2.1 RTP头部整体结构

先看一张总图,把各个字段的位置和长度搞清楚。

RTP固定头部结构(12字节) V 2bit P 1bit X 1bit CC 4bit M 1bit PT 7bit 序列号(Sequence Number) 16bit 时间戳(Timestamp) 32bit 同步源标识(SSRC) 32bit 贡献源列表(CSRC List)— 可选,长度由CC决定 每个CSRC 32bit,共CC个 字段速查表 V(2bit): 版本号,当前为2 P(1bit): 填充位,尾部有填充字节 X(1bit): 扩展位,头部后有扩展头 CC(4bit): CSRC计数器,0-15 M(1bit): 标记位,边界帧标识 PT(7bit): 负载类型,0-127 SN(16bit): 序列号,丢包检测 Timestamp(32bit): 采样时间 SSRC(32bit): 同步源唯一标识

我的习惯:拿到一个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调试就成功了一半。