3、RTP时间戳机制:采样率与时间戳增量、RTP时间与NTP时间映射、时间戳回绕处理

时间戳是RTP协议里最容易被低估的字段。很多人觉得它就是个单调递增的计数器,没什么好讲的。但我在实际项目中踩过不少坑——音画不同步、播放器卡死、回绕导致的计算错误,追根溯源都是时间戳处理不当。

说白了,RTP时间戳是接收端重建媒体时序的唯一依据。你发送端怎么打时间戳,接收端就怎么播放。如果这里出了问题,后面再好的流控算法也救不回来。

3.1 采样率与时间戳增量

RTP时间戳的单位不是毫秒,而是采样周期。每个媒体格式都有自己的采样率,时间戳每次增加的值就是「一个包携带的采样数」。

举个例子:

  • PCM音频,采样率8000Hz,每20ms发一包,每包160个样本。时间戳增量就是160。
  • H.264视频,时钟频率90000Hz,每33ms发一帧(30fps),时间戳增量就是 90000 / 30 = 3000。

为什么视频要用90000Hz?我刚开始做RTP时也觉得奇怪。后来查了标准才知道,这是为了兼容MPEG的时序精度。90000能被大多数帧率整除(24、25、30、50、60),算起来方便。

媒体类型 典型采样率 包间隔 时间戳增量
G.711 音频 8000 Hz 20 ms 160
Opus 音频 48000 Hz 20 ms 960
H.264 视频 90000 Hz 33 ms (30fps) 3000
H.265 视频 90000 Hz 40 ms (25fps) 3600

关键原则:同一媒体流的所有包,时间戳增量必须一致。如果发送端因为编码器变速导致包内样本数变化,必须在PT(Payload Type)中明确标识,否则接收端无法正确计算播放时长。

3.2 RTP时间与NTP时间映射

RTP时间戳是相对值,它只告诉接收端「这个包和上个包之间隔了多少个采样」。但接收端需要知道「这个包对应的绝对时间是什么」,才能做音画同步。

这就是RTCP SR(Sender Report)的作用。SR包里同时携带了两个时间:

  • NTP时间戳:发送端当前的绝对时间,64位,单位是秒的2^32分之一。
  • RTP时间戳:与NTP时间对应的RTP时间戳值。

有了这对映射关系,接收端就能算出任意RTP时间戳对应的绝对时间。

我记得有一次做多路摄像头同步,三路视频流各自独立发送,播放端总是对不齐。排查了半天,发现其中一路的RTCP SR里NTP和RTP的对应关系是错的——发送端在构造SR时用了不同的采样时刻。后来统一在编码器输出帧的时刻同时打上NTP和RTP戳,问题才解决。

我的习惯:在发送端维护一个「时间映射表」,每生成一个SR包就记录一次映射关系。接收端收到SR后,用线性插值计算中间任意RTP时间戳对应的NTP时间。公式很简单:

ntp_time = ntp_ref + (rtp_ts - rtp_ref) / sample_rate

注意单位换算,NTP的高32位是整数秒,低32位是小数秒。

3.3 时间戳回绕处理

RTP时间戳是32位无符号整数。对于90000Hz的视频,大约13小时就会回绕一次。对于48000Hz的音频,大约25小时。你以为系统不会跑这么久?我见过7×24小时的监控系统,回绕是必然发生的。

回绕带来的问题很直接:接收端无法通过简单比较时间戳大小来判断包的先后顺序。比如当前时间戳是0xFFFF0000,下一个包是0x00001000,按数值看后者更小,但实际后者更新。

处理回绕的标准做法是使用序列号来判断顺序,时间戳只用来计算相对间隔。但有些场景下你确实需要比较时间戳的大小,比如计算抖动时。

我曾经踩过的坑:在计算网络抖动时,直接拿两个时间戳相减。回绕发生后,差值变成了一个巨大的负数,抖动值瞬间爆表,触发流控误判。后来我改用带回绕补偿的差值计算:

int32_t diff = (int32_t)(ts2 - ts1);
if (diff < 0) {
    diff += 0x100000000;  // 补偿回绕
}

注意这里用int32_t来接收差值,利用C语言的溢出特性自动处理回绕。但前提是两个时间戳的间隔不能超过2^31个采样周期,否则无法区分是正向回绕还是反向回绕。

更稳妥的做法是维护一个扩展时间戳:在32位RTP时间戳之外,本地维护一个64位的高位计数器。每次收到新包时,如果RTP时间戳比上一个包小(且差值超过阈值),就认为发生了回绕,高位加1。这样你始终有一个单调递增的64位时间戳可用。

RTP时间戳回绕处理流程 发送端 RTP 包 32位时间戳 接收端收到包 比较与上一包时间戳 ts_new > ts_old → 正常 ts_new < ts_old → 回绕 扩展为64位时间戳 单调递增时间序列 回绕补偿条件 两个时间戳间隔必须小于 2^31 个采样周期,否则无法区分正向回绕与反向回绕

嗯,这里要注意:扩展时间戳的高位计数器只在本地维护,不随RTP包传输。接收端和发送端各自维护自己的扩展时间戳,它们之间没有同步关系。你只需要保证单个接收端内部的时间戳单调递增就够了。

避坑指南:如果你在做音画同步,千万别直接用RTP时间戳做跨流比较。音频和视频的采样率不同,时间戳增量也不同。正确的做法是:先把两路流的RTP时间戳通过RTCP SR映射到NTP时间,再用NTP时间做对齐。我曾经见过有人直接拿音频和视频的RTP时间戳做差值,结果音画不同步得一塌糊涂。

总结一下时间戳处理的三个核心点:采样率决定增量单位,RTCP SR提供绝对时间映射,回绕处理保证长时间运行的稳定性。这三个点搞清楚了,RTP时间戳这块基本就不会出大问题。