2、时间戳基础概念:采样率与时间单位、RTP时间戳的生成逻辑、时间戳与NTP的关系
好,咱们正式开始聊时间戳。说实话,RTP 里最容易让人栽跟头的就是时间戳。我见过不少团队,音视频能通,但一到同步就崩,十有八九是时间戳没搞对。
时间戳是什么?说白了,就是给每个媒体样本贴一个“生产时间”标签。接收端拿到这个标签,才知道该什么时候播放、怎么跟其他流对齐。没有它,音频和视频就是各说各话。
2.1 采样率与时间单位
先搞清楚一个基本概念:采样率。它决定了时间戳的“刻度”有多细。
音频里,采样率就是每秒采集多少个样本点。比如 48 kHz,就是每秒 48000 个样本。每个样本对应一个时间戳增量。视频呢?帧率就是它的“采样率”,30 fps 意味着每帧间隔约 33.33 毫秒。
RTP 时间戳的单位不是毫秒,而是采样周期。什么意思?
- 48 kHz 音频:1 个时间戳单位 = 1/48000 秒 ≈ 20.83 微秒
- 8 kHz 音频:1 个时间戳单位 = 1/8000 秒 = 125 微秒
- 视频 90 kHz:1 个时间戳单位 = 1/90000 秒 ≈ 11.11 微秒
为什么视频要用 90 kHz?这是历史遗留,但很实用。90 kHz 能被 24、25、30、50、60 这些常见帧率整除,算起来没有小数误差。我个人习惯在视频流里始终用 90 kHz,省心。
核心要点:RTP 时间戳的增量 = 每个样本占用的采样周期数。音频一帧 1024 个样本,48 kHz 下增量就是 1024。视频一帧,增量就是 90000 / 帧率。
| 媒体类型 | 典型采样率 | 时间戳单位 | 常见帧/包增量 |
|---|---|---|---|
| PCMU/PCMA 音频 | 8 kHz | 125 µs | 160(20 ms 包) |
| AAC 音频 | 48 kHz | 20.83 µs | 1024(21.33 ms) |
| H.264 视频 | 90 kHz | 11.11 µs | 3000(30 fps) |
| Opus 音频 | 48 kHz | 20.83 µs | 960(20 ms 包) |
避坑提示:我曾经遇到一个项目,音频用 48 kHz 采样,但时间戳增量写成了 480。结果播放器以为每包只有 10 ms 数据,缓冲区全乱套。记住:增量 = 每包的样本数,不是采样率除以包数。
2.2 RTP 时间戳的生成逻辑
时间戳怎么生成?不是拿系统时钟直接填。RTP 时间戳是一个单调递增的计数器,从某个随机初始值开始,每发送一个媒体包就加上对应的增量。
生成逻辑其实就三步:
- 选初始值:会话开始时,随机选一个 32 位整数作为基准。别用 0,防止被攻击者猜到。
- 算增量:根据当前包的媒体数据量,算出采样周期数。音频就是样本数,视频就是 90000 / 帧率。
- 累加:上一个包的时间戳 + 增量 = 当前包的时间戳。
看个例子。假设视频 30 fps,90 kHz 时钟:
初始时间戳 = 12345678(随机)
第 1 帧:时间戳 = 12345678
第 2 帧:时间戳 = 12345678 + 3000 = 12348678
第 3 帧:时间戳 = 12348678 + 3000 = 12351678
...
注意,这里有个关键点:时间戳反映的是采样时刻,不是发送时刻。如果网络延迟了,发送时间变了,但时间戳不变。接收端靠时间戳做播放调度,不是靠到达时间。
常见错误:有人把时间戳设成当前系统时间的毫秒值。这是错的。RTP 时间戳是采样周期计数,不是 Unix 时间戳。混用会导致接收端完全无法计算正确的播放时间。
还有一个细节:视频 B 帧。B 帧的采样时间可能比后面的 P 帧还早,但发送顺序是 IPB。时间戳必须按采样时间填,不能按发送顺序递增。我调试过一个播放器,画面一顿一顿的,查了半天发现是编码器把 B 帧的时间戳按发送顺序写了,解码器拿到后直接乱序播放。
2.3 时间戳与 NTP 的关系
RTP 时间戳是相对的,它只告诉你包之间的间隔,不告诉你绝对时间。那接收端怎么知道这个流对应的是几点几分?靠 NTP。
RTCP 的 Sender Report(SR)包里,同时携带了 NTP 时间戳和对应的 RTP 时间戳。这就像给 RTP 时间戳做了一个“绝对时间校准”。
举个例子:
SR 包内容:
NTP 时间戳 = 2024-01-15 10:30:00.000(即 3772696200 秒)
RTP 时间戳 = 12345678
采样率 = 90 kHz
有了这个映射关系,接收端就能算出任意 RTP 时间戳对应的绝对时间:
绝对时间 = NTP_base + (RTP - RTP_base) / 采样率
= 10:30:00.000 + (12351678 - 12345678) / 90000
= 10:30:00.000 + 6000 / 90000
= 10:30:00.000 + 0.0667 秒
= 10:30:00.067
这就是音视频同步的基础。音频流和视频流各自有独立的 RTP 时间戳,但通过 SR 包里的 NTP 映射,接收端能把它们对齐到同一个时间轴上。
关键公式:播放时间 = NTP_ref + (RTP_cur − RTP_ref) / 采样率。其中 NTP_ref 和 RTP_ref 来自最近的 SR 包。
这里有个坑:NTP 时间戳是 64 位的,高 32 位是秒数,低 32 位是小数秒。精度高达 232 皮秒。但 RTP 时间戳只有 32 位,以 90 kHz 算,大约 13 小时就会回绕一次。所以接收端必须处理回绕,不能简单比较大小。
我建议的做法是:维护一个 RTP 到 NTP 的线性映射,每次收到 SR 包就更新映射参数。播放时,用映射把 RTP 时间戳转成 NTP 时间,再做比较。这样回绕问题自然就解决了。