SR与RR报文:发送端报告与接收端报告的结构,NTP与RTP时间戳的映射

RTCP 报文里,SR 和 RR 是最核心的两个家伙。说白了,SR 是发送端在报自己的状态,RR 是接收端在反馈接收情况。音画同步能不能成,全靠它们俩配合。

我刚开始接触 WebRTC 时,总觉得 SR 和 RR 差不多,不就是报告嘛。后来踩了坑才发现——结构差远了,用途也完全不同。

4.1 SR 报文结构

SR 全称 Sender Report,发送端报告。谁在发媒体数据,谁就要定期发 SR。它告诉接收端:我发了多少包,时间戳对应什么时刻。

SR 报文的结构是这样的:

字段 长度 说明
V(版本) 2 bit 固定为 2
P(填充) 1 bit 是否有填充字节
RC(报告计数) 5 bit 包含的 RR 块数量
PT(负载类型) 8 bit SR 固定为 200
长度 16 bit 报文总长度(32 位字为单位减 1)
SSRC 32 bit 发送端的同步源标识
NTP 时间戳 64 bit 绝对时间,高 32 位秒,低 32 位小数
RTP 时间戳 32 bit 与 NTP 对应的 RTP 时间
发送包计数 32 bit 累计发送的 RTP 包数
发送字节计数 32 bit 累计发送的负载字节数
RR 块(可选) 可变 接收端报告块

注意看,SR 里有两个关键字段:NTP 时间戳和RTP 时间戳。它们之间的映射关系,就是音画同步的数学基础。

核心要点:SR 报文同时携带了绝对时间(NTP)和媒体时间(RTP)。接收端拿到这两个值,就能算出 RTP 时间对应的真实时刻。

4.2 RR 报文结构

RR 全称 Receiver Report,接收端报告。谁在收媒体数据,谁就要发 RR。它告诉发送端:我收到了多少包,丢了多少,抖动多大。

RR 的结构和 SR 的前半部分很像,但没有 NTP 和 RTP 时间戳字段:

字段 长度 说明
V(版本) 2 bit 固定为 2
P(填充) 1 bit 是否有填充字节
RC(报告计数) 5 bit 包含的 RR 块数量
PT(负载类型) 8 bit RR 固定为 201
长度 16 bit 报文总长度
SSRC 32 bit 报告发送者的 SSRC
RR 块 可变 每个块对应一个媒体源

每个 RR 块里包含:

  • 源的 SSRC
  • 丢包率
  • 累计丢包数
  • 最大序列号
  • 到达间隔抖动
  • 最后 SR 时间戳(LSR)
  • 自上次 SR 以来的延迟(DLSR)

我的经验:RR 里的抖动值特别有用。我曾经遇到一个场景,音频 RR 抖动正常,视频 RR 抖动飙升——后来发现是视频编码器在丢帧。RR 的抖动字段帮我快速定位了问题。

4.3 NTP 与 RTP 时间戳的映射

这是音画同步最核心的机制。我花了不少时间才真正理解它。

NTP 时间戳是绝对时间,64 位。高 32 位是秒数,低 32 位是小数部分。它代表的是「真实世界的时间」。

RTP 时间戳是媒体采样时间,32 位。它代表的是「媒体时钟的刻度」。音频的时钟频率通常是 48000 Hz 或 44100 Hz,视频通常是 90000 Hz。

SR 报文里同时携带了这两个值。接收端收到后,就能建立映射关系:

// 伪代码:从 SR 中提取映射关系
uint64_t ntp_timestamp = sr.ntp_seconds << 32 | sr.ntp_fraction;
uint32_t rtp_timestamp = sr.rtp_timestamp;

// 计算 RTP 时间对应的 NTP 时间
// 假设我们知道 RTP 时钟频率为 clock_rate
double ntp_time = ntp_timestamp + 
    (rtp_timestamp - sr.rtp_timestamp) / (double)clock_rate;

举个例子:

  • SR 报告:NTP = 1000.5 秒,RTP = 48000
  • 音频时钟频率 = 48000 Hz
  • 那么 RTP = 48000 对应 NTP = 1000.5 秒
  • RTP = 96000 对应 NTP = 1001.5 秒

接收端收到音频包和视频包后,分别用各自的 SR 映射关系,把 RTP 时间戳转成 NTP 时间。然后比较两个 NTP 时间,就知道音画是否同步了。

注意:音频和视频的 RTP 时钟频率不同。音频通常是 48 kHz,视频是 90 kHz。不能直接比较 RTP 时间戳,必须先转成 NTP 时间。

4.4 映射关系的维护

SR 报文不是每帧都发的。RTCP 的发送间隔通常是 5 秒左右。那中间的时间怎么办?

接收端会维护一个线性映射模型:

// 接收端维护的映射
struct NtpRtpMapping {
    uint64_t ntp_base;
    uint32_t rtp_base;
    uint32_t clock_rate;
};

// 将 RTP 时间戳转为 NTP 时间
double rtp_to_ntp(uint32_t rtp_ts, NtpRtpMapping mapping) {
    double offset = (rtp_ts - mapping.rtp_base) / (double)mapping.clock_rate;
    return mapping.ntp_base + offset;
}

每次收到新的 SR,就更新映射基准。两次 SR 之间,用线性插值推算。

关键点:这个映射是线性的。因为 RTP 时钟和 NTP 时钟都是线性递增的。只要时钟频率稳定,映射就准确。

4.5 我踩过的坑

嗯,这里要分享一个真实案例。有一次我做音画同步测试,发现视频总是比音频慢 200 毫秒。查了半天,最后发现是 SR 里的 NTP 时间戳有问题。

发送端在构造 SR 时,NTP 时间戳取的是「当前系统时间」,但 RTP 时间戳取的是「上一个已发送帧的时间」。这两个时间不匹配,差了 33 毫秒(一帧的间隔)。

正确的做法是:NTP 和 RTP 必须对应同一个时刻。要么都取当前时间,要么都取上一个帧的时间。

避坑指南:我曾经在代码里看到有人直接用 gettimeofday() 取 NTP,然后从编码器取 RTP。这两个时间源不同步,导致音画同步误差。建议在同一个函数里同时获取 NTP 和 RTP。

4.6 整体流程

下面这张图展示了 SR/RR 的交互流程,以及 NTP-RTP 映射在音画同步中的位置:

SR/RR 交互与 NTP-RTP 映射流程 发送端 接收端 SR(含 NTP + RTP) RR(含丢包、抖动) 获取 NTP 时间 获取 RTP 时间戳 构造 SR 报文 解析 SR 报文 建立 NTP-RTP 映射 计算音画同步偏差 关键说明 1. 发送端定期发送 SR,携带 NTP 和 RTP 时间戳 2. 接收端用 SR 建立 NTP-RTP 线性映射 3. 接收端发 RR 反馈接收质量(丢包、抖动) 4. 音画同步:将音频和视频的 RTP 分别转成 NTP 后比较

4.7 总结

SR 和 RR 是 RTCP 的基石。SR 提供时间映射,RR 提供质量反馈。NTP 和 RTP 的映射关系,是音画同步的数学基础。

我个人建议,在实现音画同步时,重点关注三点:

  • SR 里的 NTP 和 RTP 必须对应同一个时刻
  • 音频和视频的时钟频率不同,不能直接比较 RTP
  • RR 里的抖动值能帮你快速定位问题

记住一句话:没有 SR,音画同步就是空中楼阁。没有 RR,你就不知道网络出了什么问题。