2. 网络基础回顾

RTP 协议看起来简单,但它的头部设计其实非常精巧。我刚开始接触时也觉得不就是个包头嘛,后来在排查一个视频花屏问题时才发现——每一个 bit 都可能影响整条链路的稳定性。今天咱们就把 RTP 头部从头到尾拆一遍。

一句话总结:RTP 固定头部只有 12 字节,但通过扩展机制可以承载非常丰富的控制信息。理解这 12 字节,你就掌握了 RTP 的命脉。

2.1 RTP 头部整体结构

先看一张总图。RTP 头部的前 12 个字节是固定的,后面跟着可选的 CSRC 列表和扩展头。我习惯把这 12 字节称为「铁打的营盘」,后面的都是「流水的兵」。

RTP 数据包头部结构(固定 12 字节 + 可选字段) V 2 bit P 1 bit X 1 bit CC 4 bit M 1 bit PT 7 bit Sequence Number 16 bit Timestamp(时间戳) 32 bit SSRC(同步源标识符) 32 bit CSRC List(0~15 项,每项 32 bit,可选) 字段速查 V:版本号,当前为 2 P:填充标志,1 表示尾部有填充 X:扩展标志,1 表示有扩展头 CC:CSRC 计数器,0~15 M:标记位,边界帧标识 PT:负载类型,标识编码格式 Seq Num:序列号,检测丢包 Timestamp:采样时间戳 SSRC:同步源唯一标识

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 时钟)
  • 时间戳也是随机初始化的,和序列号一样

时间戳的作用:

  1. 播放同步:接收端根据时间戳决定什么时候播放这个包
  2. 抖动消除:结合 RTCP 的 NTP 时间戳,可以计算网络抖动
  3. 音画同步:音频和视频流的时间戳可以关联起来

一个常见的误解:很多人以为时间戳就是发送时间。其实不是。时间戳是采样时间,发送时间由 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 包的。