3. 时间戳核心概念
好,咱们进入第三章。这一章要聊的是RTP时间戳的核心玩法——同步机制。
说实话,我在刚接触RTP的时候,觉得时间戳不就是个计数器嘛,有什么好讲的?后来在做一个多路摄像头同步的项目时,被时间戳回绕坑得差点通宵加班。嗯,从那以后我再也不敢小看这个字段了。
3.1 RTP时间戳的线性增长
RTP时间戳的本质,是一个单调递增的计数器。它不代表绝对时间,而是代表采样时刻的“相对位置”。
你想想看,音频每秒钟采样8000次,那时间戳就每125微秒加1。视频如果是30fps,每帧的时间戳增量就是90000/30 = 3000。这个增量是固定的,只要采样率不变,时间戳就线性增长。
关键公式:
时间戳增量 = 时钟频率 / 帧率
例如:视频90000Hz时钟,30fps → 增量 = 90000/30 = 3000
我在项目中见过有人把时间戳写成随机数,结果播放器直接崩溃。记住:时间戳必须线性增长,不能跳跃,不能倒退,除非发生回绕。
| 媒体类型 | 典型时钟频率 | 典型帧率/采样率 | 每包增量 |
|---|---|---|---|
| PCM音频 | 8000 Hz | 8000 采样/秒 | 160 (20ms包) |
| H.264视频 | 90000 Hz | 30 fps | 3000 |
| Opus音频 | 48000 Hz | 48000 采样/秒 | 960 (20ms包) |
3.2 时间戳回绕处理
RTP时间戳是32位无符号整数。最大值是4294967295。到了这个值再加1,就变成0了。这就是回绕(wrap around)。
为什么会回绕?说白了就是计数器不够长。32位听着挺大,但90000Hz的时钟下,每秒钟跑90000个计数,4294967295 / 90000 ≈ 47721秒 ≈ 13.25小时。也就是说,连续跑13个多小时,时间戳就回绕一次。
我曾经踩过的坑:
有一次做视频录制,跑了12小时,突然播放器开始乱跳。查了半天,发现是时间戳回绕后,播放器用简单的“当前时间戳 > 上一个时间戳”来判断乱序,结果回绕后新时间戳比旧的小,被当成乱序包丢弃了。
正确的回绕处理,需要做模运算比较。我习惯用这个方式:
// 判断 a 是否在 b 之后(考虑回绕)
bool isLater(uint32_t a, uint32_t b) {
return (int32_t)(a - b) > 0;
}
// 计算差值(考虑回绕)
uint32_t delta(uint32_t a, uint32_t b) {
return (uint32_t)(a - b);
}
这里的关键是把uint32_t差值转成int32_t。如果差值在-2^31到2^31-1之间,就能正确判断先后顺序。说白了,就是假设两个时间戳的间隔不会超过2^31个计数。
我的建议:
在接收端维护一个“基准时间戳”。每次收到包,用当前时间戳减去基准,得到相对偏移。这样回绕只影响基准更新,不影响播放逻辑。
3.3 基于时间戳的播放同步
播放同步,说白了就是让接收端知道“这个包该什么时候播”。
RTP本身不提供绝对时间,只提供相对位置。接收端需要自己做映射:把RTP时间戳映射到本地时钟上。
我常用的方法是线性回归映射:
- 收到第一个包时,记录本地时间T0和RTP时间戳R0
- 后续每个包,用公式计算播放时间:T_play = T0 + (RTP - R0) / 时钟频率
- 如果发现偏差超过阈值,做微调
但这里有个问题:网络有抖动。包可能延迟到达。如果严格按照公式算,延迟的包一到就立刻播,那音频就会忽快忽慢。
所以需要抖动缓冲区(jitter buffer)。我一般设一个20-50ms的缓冲区,让播放器从缓冲区里匀速取数据,而不是直接从网络取。
同步误差校正的核心思路:
- 发送端:时间戳线性增长,反映采样时刻
- 接收端:建立RTP时间戳到本地时钟的映射
- 缓冲区:吸收网络抖动,平滑播放
- 回绕处理:用模运算比较,避免误判
下面这张图展示了整个同步流程:
实际项目中,我还会加一个时间戳漂移校正。因为发送端和接收端的时钟可能不同步,时间长了会有累积误差。我一般每30秒做一次重同步,把基准时间戳重新校准一下。
避坑指南:
- 不要用系统时间直接减RTP时间戳,要用相对偏移
- 回绕检测要放在所有时间戳比较之前
- 抖动缓冲区大小要动态调整,网络差时增大,好时减小
- 音频和视频的时间戳时钟频率不同,同步时要分别处理
嗯,这一章的内容就这些。时间戳同步说难不难,说简单也不简单。关键是把线性增长、回绕处理、播放映射这三个点串起来,形成一个完整的闭环。下次遇到播放不同步的问题,你就知道该从哪里入手了。