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 映射在音画同步中的位置:
4.7 总结
SR 和 RR 是 RTCP 的基石。SR 提供时间映射,RR 提供质量反馈。NTP 和 RTP 的映射关系,是音画同步的数学基础。
我个人建议,在实现音画同步时,重点关注三点:
- SR 里的 NTP 和 RTP 必须对应同一个时刻
- 音频和视频的时钟频率不同,不能直接比较 RTP
- RR 里的抖动值能帮你快速定位问题
记住一句话:没有 SR,音画同步就是空中楼阁。没有 RR,你就不知道网络出了什么问题。