2. 实时传输协议基础

好,咱们正式开始聊时间戳。说实话,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 时间戳是一个单调递增的计数器,从某个随机初始值开始,每发送一个媒体包就加上对应的增量。

生成逻辑其实就三步:

  1. 选初始值:会话开始时,随机选一个 32 位整数作为基准。别用 0,防止被攻击者猜到。
  2. 算增量:根据当前包的媒体数据量,算出采样周期数。音频就是样本数,视频就是 90000 / 帧率。
  3. 累加:上一个包的时间戳 + 增量 = 当前包的时间戳。

看个例子。假设视频 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 时间,再做比较。这样回绕问题自然就解决了。

RTP 时间戳与 NTP 映射关系 发送端 媒体采样 RTP 时间戳生成 NTP 时钟 SR 包构建 RTP + RTCP 接收端 解析 SR 包 建立 RTP↔NTP 映射 计算播放时间 音视频同步 关键说明 • RTP 时间戳是采样周期计数器,从随机初始值开始单调递增 • NTP 时间戳提供绝对时间参考,精度 232 皮秒 • SR 包同时携带 NTP 和 RTP 时间戳,建立映射关系 • 接收端通过映射将 RTP 时间戳转换为绝对播放时间