3、RTP协议详解:RTP头部结构、负载类型、序列号与时间戳的作用
聊到WebRTC的流控,RTP协议是绕不开的基础。说白了,所有音视频数据在网络上传输时,都得穿上RTP这件“外衣”。我刚开始接触WebRTC时,总觉得RTP就是个简单的数据封装,后来在排查一个视频卡顿问题时才发现——RTP头部里的每个字段,都可能成为流控的关键线索。
这一节,我们就把RTP协议拆开来看。重点聚焦三个核心要素:头部结构、负载类型(PT)、序列号与时间戳。理解了它们,你就能明白WebRTC是如何在不可靠的网络上,把音视频数据“拼”回来的。
3.1 RTP头部结构:12字节的“身份证”
RTP头部固定为12字节。别小看这12个字节,它包含了接收端重建媒体流所需的全部元信息。我习惯把它分成三块来看:
- 版本号(V, 2 bits):当前固定为2。如果收到非2的包,直接丢弃。
- 填充位(P, 1 bit):置1时,包尾有填充字节。加密场景下常见。
- 扩展位(X, 1 bit):置1时,头部后面紧跟着扩展头部。WebRTC的RTP头部扩展就用这个位。
- CSRC计数(CC, 4 bits):表示CSRC标识符的数量。混音场景下用,WebRTC点对点通信中通常为0。
- 标记位(M, 1 bit):应用自定义。视频中常用来标记帧边界,音频中标记讲话开始。
- 负载类型(PT, 7 bits):标识编码格式。后面细讲。
- 序列号(Sequence Number, 16 bits):每个RTP包唯一的递增序号。
- 时间戳(Timestamp, 32 bits):采样时刻,单位由编码器决定。
- SSRC(32 bits):同步源标识符。同一媒体流的所有包共享同一个SSRC。
核心要点:接收端通过SSRC区分不同的媒体流,通过序列号排序和丢包检测,通过时间戳实现播放同步。这三个字段是流控的“三驾马车”。
嗯,这里要注意:RTP头部扩展(X位)在WebRTC中非常常用。比如RTP头部扩展用于传输时间偏移、绝对时间等。我在做音画同步优化时,就靠扩展头部里的绝对捕获时间来对齐音视频时间线。
3.2 负载类型(PT):编码格式的“暗号”
PT字段占7位,取值范围0-127。它告诉接收端:“这个包里的数据是用什么编码器压缩的”。
PT值分为两类:
- 静态分配(0-95):由IANA预定义。比如PT=0是PCMU,PT=8是PCMA,PT=111是Opus。
- 动态分配(96-127):通过SDP协商确定。WebRTC几乎全用动态PT。
举个例子,在SDP中你会看到这样的行:
m=audio 5004 RTP/AVP 111
a=rtpmap:111 opus/48000/2
这表示:动态PT=111对应Opus编码,采样率48kHz,双声道。接收端收到PT=111的包,就知道用Opus解码器来处理。
个人经验:我在项目中遇到过一个问题——发送端和接收端的PT映射不一致。发送端把H264映射到PT=96,接收端却以为PT=96是VP8。结果画面全是花屏。排查了半天,最后发现是SDP协商时PT值冲突了。所以,PT映射必须在SDP中明确协商,不能硬编码。
另外,PT字段还影响流控策略。不同的编码器对丢包的敏感度不同。比如Opus有内置的PLC(丢包隐藏),丢个10%还能听;而H264的B帧丢了,画面可能直接卡住。流控模块需要根据PT值,动态调整丢包重传策略。
3.3 序列号:乱序与丢包的“侦察兵”
序列号是16位的无符号整数,初始值随机(防止攻击者猜测),每发送一个RTP包就加1。接收端用它来做三件事:
- 排序:网络可能乱序,接收端按序列号重排。
- 丢包检测:如果收到序列号100和102,没收到101,就认为101丢了。
- 包重复检测:如果收到两个序列号相同的包,丢弃后者。
你想想看,序列号只有16位,最大值65535。溢出怎么办?标准做法是回绕处理。比如当前序列号65535,下一个包应该是0(不是65536)。接收端需要能识别这种回绕。
避坑指南:我曾经在实现JitterBuffer时,忘了处理序列号回绕。结果在65535到0的边界上,JitterBuffer把新包当成了旧包直接丢弃,导致持续半秒的音频卡顿。修复方法很简单——用序列号算术比较,而不是直接比较数值大小。
在WebRTC的RTP解析代码中,序列号比较用的是这样的逻辑:
// 判断a是否在b之后(考虑回绕)
static bool IsNewerSequenceNumber(uint16_t a, uint16_t b) {
return static_cast<int16_t>(a - b) > 0;
}
这段代码把差值转成有符号16位整数。如果差值在1到32767之间,说明a比b新;如果差值在-32768到-1之间,说明a比b旧。这样就能正确处理回绕了。
3.4 时间戳:播放节奏的“指挥棒”
时间戳是32位,单位由编码器采样率决定。比如:
- 音频(Opus, 48kHz):每采样1个单位,1秒 = 48000个时间戳单位。
- 视频(通常90kHz):1秒 = 90000个时间戳单位。
时间戳的作用有两个:
- 播放时序:接收端根据时间戳决定什么时候播放这个包。
- 帧边界识别:同一帧的视频包,时间戳相同。
举个例子,一个视频帧被拆成3个RTP包,它们的时间戳都是123456。接收端收到后,知道这3个包属于同一帧,等全部收齐后再解码。
时间戳的初始值也是随机的。接收端不能直接拿时间戳的绝对值来算延迟,而是通过相对差值来推算播放时间。
流控中的关键作用:时间戳是JitterBuffer的核心输入。JitterBuffer通过统计时间戳的到达间隔,估算网络抖动,然后动态调整播放延迟。如果时间戳不准,JitterBuffer要么延迟太大(卡顿),要么延迟太小(抖动导致丢帧)。
3.5 序列号 vs 时间戳:别搞混了
很多初学者会把序列号和时间戳搞混。我画个对比表:
| 属性 | 序列号 | 时间戳 |
|---|---|---|
| 位数 | 16 bits | 32 bits |
| 递增规则 | 每个包+1 | 每个采样点递增 |
| 回绕周期 | 65535个包 | 约13小时(90kHz) |
| 主要用途 | 排序、丢包检测 | 播放时序、帧同步 |
| 同一帧的包 | 连续递增 | 相同 |
简单来说:序列号管“顺序”,时间戳管“时间”。网络乱序时,序列号帮你把包排好;播放时,时间戳告诉你什么时候放。
3.6 知识体系总览
下面这张图总结了RTP头部中与流控相关的核心字段及其关系:
3.7 小结
这一节我们深入了RTP头部。序列号和时间戳是流控的“双眼”——序列号看顺序,时间戳看节奏。负载类型则告诉流控模块“该用什么策略”。
我个人习惯在调试流控问题时,先抓包看这三个字段。如果序列号连续但时间戳跳跃异常,说明编码器有问题;如果时间戳正常但序列号有空洞,说明网络丢包了。定位问题会快很多。