2. RTP 数据包格式详解:头部每个字节都有它的脾气
RTP 协议看起来简单,但它的头部设计其实非常精巧。我刚开始接触时也觉得不就是个包头嘛,后来在排查一个视频花屏问题时才发现——每一个 bit 都可能影响整条链路的稳定性。今天咱们就把 RTP 头部从头到尾拆一遍。
一句话总结:RTP 固定头部只有 12 字节,但通过扩展机制可以承载非常丰富的控制信息。理解这 12 字节,你就掌握了 RTP 的命脉。
2.1 RTP 头部整体结构
先看一张总图。RTP 头部的前 12 个字节是固定的,后面跟着可选的 CSRC 列表和扩展头。我习惯把这 12 字节称为「铁打的营盘」,后面的都是「流水的兵」。
2.2 版本号(V, 2 bit)—— 目前只有版本 2
版本号占 2 个 bit,当前 RTP 的版本是 2。为什么不是 1?因为 RTP 的第一个草案(版本 0)和第一个正式版(版本 1)早就被淘汰了。现在你抓到的任何 RTP 包,V 字段的值都是 2。
嗯,这里要注意:如果你在调试时看到 V 不是 2,那大概率是抓错了包,或者有人在搞恶作剧。我曾经在排查一个 VoIP 通话问题时,发现对端设备居然发了版本号为 0 的包——后来确认是固件 bug。
2.3 填充标志(P, 1 bit)—— 对齐强迫症患者的福音
P 位为 1 时,表示包尾有填充字节。填充的最后一个字节表示填充的总长度(包含它自己)。
什么场景会用?最常见的是加密场景。有些加密算法要求明文长度是块大小的整数倍,这时候就需要填充。另外,某些底层传输协议也要求包长对齐。
我的经验:如果你自己实现 RTP 打包,填充逻辑一定要小心。我曾经见过一个实现,填充字节数算错了,导致接收端解析时多读了几个字节,整个音频流都乱了。记住:填充长度字段包含自身,所以最小填充长度是 1 字节。
2.4 扩展标志(X, 1 bit)—— 给 RTP 加「外挂」
X 位为 1 时,表示固定头部后面紧跟着一个扩展头。扩展头的格式是:
- 前 2 字节:由具体应用定义的扩展类型(profile-specific)
- 后 2 字节:扩展长度(以 32 位字为单位,不包括这 4 字节头)
- 后面跟着扩展数据
说实话,我在实际项目中很少见到有人用 RTP 扩展头。大部分场景下,固定头部加负载就够了。但如果你在做视频会议系统,想传递一些额外的元数据(比如视频旋转角度、屏幕共享的坐标),扩展头是个不错的选择。
2.5 CSRC 计数(CC, 4 bit)—— 混音场景的「参会名单」
CC 字段表示后面跟着多少个 CSRC 标识符(每个 32 bit)。取值范围 0~15。
CSRC 是干嘛的?说白了,就是告诉接收端:「这个包里的数据是多个源的混合结果」。比如在电话会议中,MCU(多点控制单元)把三个人的声音混在一起发出来,CSRC 列表里就列出这三个人的 SSRC。
避坑指南:我曾经遇到过一个情况,MCU 在混音时忘记填写 CSRC 列表,导致接收端无法区分谁在说话。虽然音频能正常播放,但录音分析时完全分不清声道。所以,如果你在做混音相关开发,CC 和 CSRC 一定要填对。
2.6 标记位(M, 1 bit)—— 帧边界的「哨兵」
M 位只有 1 个 bit,但它的作用很关键。它用来标记一些「重要事件」的边界。
最常见的用法:
- 视频流:M=1 表示一个视频帧的结束(比如 H.264 的 NAL 单元边界)
- 音频流:M=1 表示通话的开始(talk spurt 的开始)
- DTMF(双音多频):M=1 表示按键事件的开始
我个人的习惯是:在视频打包时,每个帧的最后一个 RTP 包一定把 M 位置 1。这样接收端可以准确判断帧边界,不需要依赖时间戳来猜。
2.7 负载类型(PT, 7 bit)—— 告诉接收端「这是什么编码」
PT 字段占 7 位,取值范围 0~127。它标识了 RTP 负载的编码格式。
IANA(互联网号码分配局)定义了一些静态类型:
| PT 值 | 编码格式 | 时钟频率(Hz) | 说明 |
|---|---|---|---|
| 0 | PCMU(μ-law) | 8000 | G.711 语音编码 |
| 3 | GSM | 8000 | GSM 语音 |
| 8 | PCMA(A-law) | 8000 | G.711 另一种 |
| 96-127 | 动态类型 | 由 SDP 协商 | H.264、Opus 等 |
注意:96~127 是动态类型,需要通过 SDP 协议来协商。比如 H.264 视频通常用 96 或 97,Opus 音频用 111。但这不是绝对的,完全看双方怎么约定。
我的建议:如果你在开发中需要自定义负载类型,尽量用 96~127 这个范围。静态类型留给标准编码用,避免冲突。我曾经见过有人把 H.264 硬塞到 PT=8 上,结果接收端按 G.711 解码——那声音简直酸爽。
2.8 序列号(Sequence Number, 16 bit)—— 乱序和丢包的「照妖镜」
序列号占 16 位,每个 RTP 包递增 1。它的核心作用有两个:
- 检测丢包:如果收到包 100、101、103,就知道 102 丢了
- 恢复顺序:即使包乱序到达,也能按序列号重新排序
序列号是随机初始化的(不是从 0 开始),这是安全考虑——防止攻击者猜到序列号模式。初始值在 RTCP 的 SR(发送方报告)中会通告。
嗯,这里有个坑:序列号是 16 位的,最大值 65535,溢出后会回绕到 0。如果你的流持续时间很长(比如 24 小时监控视频),一定要处理好回绕逻辑。我见过一个播放器,序列号回绕后直接罢工了——因为它把 0 当成了比 65535 小的数,导致排序全乱。
2.9 时间戳(Timestamp, 32 bit)—— 播放节奏的「节拍器」
时间戳是 RTP 里最容易让人迷糊的字段。它记录的是采样时刻,而不是系统时钟的绝对时间。
关键点:
- 时间戳的增量取决于采样率和帧大小。比如 8kHz 的音频,每帧 20ms,那么每帧的时间戳增量就是 8000 × 0.02 = 160
- 视频的时间戳增量通常是 90000 的倍数(因为视频常用 90kHz 时钟)
- 时间戳也是随机初始化的,和序列号一样
时间戳的作用:
- 播放同步:接收端根据时间戳决定什么时候播放这个包
- 抖动消除:结合 RTCP 的 NTP 时间戳,可以计算网络抖动
- 音画同步:音频和视频流的时间戳可以关联起来
一个常见的误解:很多人以为时间戳就是发送时间。其实不是。时间戳是采样时间,发送时间由 RTCP 的 NTP 时间戳来记录。这两个概念一定要分开。
2.10 SSRC(同步源标识符, 32 bit)—— RTP 流的「身份证」
SSRC 是 32 位的随机数,用来唯一标识一个 RTP 流。同一个会话中,每个参与者的 SSRC 必须唯一。
SSRC 的生成算法在 RFC 3550 中有定义:基于源 IP、端口、时间等信息的哈希,碰撞概率很低。但万一碰撞了怎么办?RTCP 协议中有冲突检测机制,如果发现 SSRC 冲突,会发送 BYE 包并重新生成。
我遇到过最头疼的问题就是 SSRC 冲突。有一次在大型视频会议中,两个终端居然生成了相同的 SSRC,导致接收端把两个流混在一起。排查了半天才发现是其中一个终端的随机数生成器有 bug。
2.11 CSRC(贡献源标识符, 每项 32 bit)—— 混音场景的「贡献者名单」
CSRC 列表跟在固定头部后面,长度由 CC 字段决定。每个 CSRC 都是一个 32 位的标识符,对应一个贡献源的 SSRC。
CSRC 只在混音场景中使用。比如一个 MCU 把 3 个人的音频混在一起,CC=3,后面跟着 3 个 CSRC。接收端看到这个列表,就知道这个包里的数据来自哪几个人。
注意:CSRC 列表最多 15 项(因为 CC 只有 4 位)。如果混音源超过 15 个,MCU 需要做一些取舍——比如只列出音量最大的 15 个。
2.12 一个完整的 RTP 包解析示例
光说不练假把式。咱们抓一个真实的 RTP 包来拆解一下。假设我用 Wireshark 抓到这样一个 RTP 头部(十六进制):
80 08 00 01 00 02 00 03 12 34 56 78
逐字节解析:
| 字节偏移 | 十六进制 | 二进制 | 解析结果 |
|---|---|---|---|
| 0 | 0x80 | 1000 0000 | V=2, P=0, X=0, CC=0 |
| 1 | 0x08 | 0000 1000 | M=0, PT=8 (PCMA) |
| 2-3 | 0x0001 | 0000 0000 0000 0001 | 序列号 = 1 |
| 4-7 | 0x00020003 | — | 时间戳 = 131075 |
| 8-11 | 0x12345678 | — | SSRC = 0x12345678 |
这个包是一个 G.711 A-law 音频包,序列号 1,时间戳 131075,SSRC 为 0x12345678。没有扩展,没有填充,没有 CSRC——最标准的 12 字节头部。
调试小技巧:Wireshark 里可以直接看 RTP 解析结果。但我建议你偶尔也手动算一算,尤其是排查一些奇怪的偏移问题时。我当年就是靠手算发现了一个 RTP 头部对齐 bug——代码里少算了 4 字节的扩展头。
2.13 本章小结
RTP 头部虽然只有 12 字节,但每个字段都有明确的分工。我整理了一个速查表,方便你以后查阅:
| 字段 | 位数 | 核心作用 | 常见取值/说明 |
|---|---|---|---|
| V | 2 | 版本标识 | 固定为 2 |
| P | 1 | 填充标志 | 加密/对齐场景使用 |
| X | 1 | 扩展标志 | 自定义扩展数据 |
| CC | 4 | CSRC 数量 | 0~15 |
| M | 1 | 标记位 | 帧边界/事件边界 |
| PT | 7 | 负载类型 | 0~127,96+为动态 |
| Seq Num | 16 | 序列号 | 丢包检测、排序 |
| Timestamp | 32 | 采样时间戳 | 播放同步、抖动计算 |
| SSRC | 32 | 同步源标识 | 随机生成,全局唯一 |
| CSRC | 32×N | 贡献源列表 | 混音场景使用 |
记住这张表,RTP 头部你就吃透了。下一层咱们要深入负载部分——看看不同的编码格式是怎么塞进 RTP 包的。