2. RTP 头部结构:从比特到音画同步的基石
RTP 头部,说白了就是音视频数据的“快递面单”。我刚开始接触 RTP 时,总觉得这 12 字节的固定头部没什么了不起。直到有一次排查丢包问题,才发现序列号和时间戳的配合有多精妙。嗯,咱们今天就把它拆开揉碎了讲清楚。
核心认知:RTP 头部是每个数据包的开销,但它承载了排序、同步、负载识别等关键信息。没有它,接收端就是一盘散沙。
2.1 头部结构总览
RTP 固定头部 12 字节,后面可能跟着 CSRC 列表和扩展头部。我习惯把它画成一张图,这样看着更直观。
2.2 逐个字段拆解
2.2.1 版本号(V, 2 bit)
当前 RTP 版本号固定为 2。你可能会问,版本 1 去哪了?其实 RTPv1 是实验性协议,后来被 v2 完全取代。我个人习惯在抓包时先看一眼版本号,如果不是 2,那多半是解析器配置错了。
2.2.2 填充位(P, 1 bit)
填充位为 1 时,表示包尾有填充字节。填充的最后一个字节表示填充长度。我在做加密传输时遇到过这个字段——某些加密算法要求明文长度是块大小的整数倍,这时候就得靠填充位来对齐。
小技巧:如果你看到 P=1,记得把填充字节去掉再计算负载长度。否则解码器会吃到脏数据。
2.2.3 扩展位(X, 1 bit)
扩展位为 1 时,固定头部后面紧跟着一个扩展头部。扩展头部包含 16 位的扩展类型和 16 位的扩展长度。我记得有一次做视频会议系统,需要在 RTP 里携带自定义的 QoS 信息,就是用扩展头部实现的。
2.2.4 CSRC 计数(CC, 4 bit)
CC 表示 CSRC 标识符的数量,范围 0~15。每个 CSRC 占 32 位,所以 CSRC 列表总长度为 CC × 4 字节。这个字段在混音场景下特别重要——比如多方通话时,混音器会把多个源的 SSRC 填进 CSRC 列表。
2.2.5 标记位(M, 1 bit)
标记位的含义由负载类型决定。对于视频,M=1 通常表示一帧的结束;对于音频,M=1 表示一个 talkspurt 的开始。我曾经踩过一个坑:把 M 位当成帧边界,结果在 H.264 的 NAL 单元边界判断上出了 bug,画面花了好一阵。
注意:M 位的语义不是协议强制的,而是由具体的负载格式规范(如 RFC 6184 for H.264)定义的。不要想当然。
2.2.6 负载类型(PT, 7 bit)
PT 标识 RTP 负载的编码格式。常见的如 PT=96 表示 H.264,PT=0 表示 PCMU。7 位理论上支持 128 种类型,但 0~95 是静态分配,96~127 是动态分配。我建议你在做系统设计时,优先使用动态 PT 范围,避免和静态类型冲突。
| PT 值 | 编码格式 | 时钟频率 (Hz) | 说明 |
|---|---|---|---|
| 0 | PCMU | 8000 | G.711 μ-law |
| 8 | PCMA | 8000 | G.711 A-law |
| 96 | H.264 | 90000 | 动态分配,视频常用 |
| 97 | OPUS | 48000 | 动态分配,音频常用 |
2.3 序列号:丢包检测的利器
序列号是 16 位无符号整数,每个 RTP 包递增 1。接收端通过序列号可以检测丢包、乱序和重复包。我做过一个统计:在 Wi-Fi 环境下,RTP 序列号跳变超过 10 的情况并不罕见。
序列号初始值是随机的,不是从 0 开始。为什么?为了防止攻击者猜测后续序列号。我个人习惯在初始化时用 rand() 生成一个随机起点。
// 伪代码:序列号处理逻辑
uint16_t expected_seq = first_seq;
while (receive_packet(&pkt)) {
if (pkt.seq == expected_seq) {
// 正常顺序
process(pkt);
expected_seq++;
} else if (pkt.seq > expected_seq) {
// 丢包了
uint16_t lost = pkt.seq - expected_seq;
report_loss(lost);
expected_seq = pkt.seq + 1;
} else {
// 乱序或重复包
handle_out_of_order(pkt);
}
}
2.4 时间戳:音画同步的灵魂
时间戳是 32 位整数,表示 RTP 数据包中第一个字节的采样时刻。时钟频率由负载类型决定——视频通常用 90 kHz,音频用 8 kHz 或 48 kHz。时间戳的增量反映了媒体数据的真实时间流逝。
举个例子:一段 48 kHz 的音频,每帧 20 ms,那么每帧的采样点数为 48000 × 0.02 = 960。所以相邻 RTP 包的时间戳差值就是 960。
关键点:时间戳不是系统时钟,而是采样时钟。它用于播放时序控制,而不是用于计算 RTT。RTCP 的 SR 包会建立 RTP 时间戳和 NTP 时间戳的映射关系,这才是音画同步的桥梁。
2.5 SSRC 与 CSRC:谁在说话?
SSRC(同步源标识符)是 32 位随机数,唯一标识一个 RTP 会话中的同步源。每个参与者生成一个 SSRC,冲突时通过 RTCP 解决。CSRC(贡献源标识符)则用于混音场景,标识哪些源混合到了当前包中。
我记得有一次做多方通话测试,两个客户端偶然生成了相同的 SSRC,结果接收端把两个流当成一个来处理,音频全乱了。后来我们加上了 SSRC 冲突检测,才彻底解决。
| 字段 | 长度 | 作用 | 典型场景 |
|---|---|---|---|
| SSRC | 32 bit | 标识同步源 | 每个 RTP 流一个 |
| CSRC | 32 bit × N | 标识贡献源 | 混音器输出包 |
2.6 避坑指南
- 序列号回绕:16 位序列号最大 65535,回绕后从 0 开始。我曾经没处理回绕,导致丢包检测在回绕瞬间误报。解决方案是用有符号差或模运算。
- 时间戳回绕:32 位时间戳在 90 kHz 下约 13 小时回绕一次。长时间流媒体会话必须处理回绕,否则播放时序会乱。
- SSRC 冲突:随机生成 SSRC 有极小概率冲突。RTCP 的 BYE 和 SDES 包可以协助检测,但最好在应用层也做一层校验。
- 扩展头部解析:X=1 时,扩展头部紧跟在固定头部之后。解析时先读 16 位类型和 16 位长度,再跳过对应字节数。别把扩展数据当成负载。
2.7 小结
RTP 头部虽然只有 12 字节,但每个字段都有它的使命。序列号负责网络传输的可靠性,时间戳负责媒体播放的时序,SSRC/CSRC 负责源的标识。理解这些字段,是掌握 RTP 协议的第一步,也是实现音画同步的基础。