🎬 第三章 RTP与RTCP流媒体协议
📄 一句话概括

SR 的前 28 字节是「我发了什么」,后面的报告块是「你收得怎么样」。 最值钱的是 NTP 与 RTP 时间戳的配对——它是音画同步的唯一依据。

📝 本节和前面几节的关系

3.2.1 认识了五种包的用途,这一节把 SR 拆到字段级。 3.2.4 会讲抓包里怎么看这些数。

一、前 28 字节:发送方信息

SR 的头 4 字节和其它 RTCP 一样:V/P/RC/PT/长度。 接着 4 字节是发送方 SSRC,然后才是 20 字节的发送统计。

20 字节里五个数: ① NTP 秒(4 字节):绝对时间,从 1900 年 1 月 1 日起算。 ② NTP 小数(4 字节):秒的小数部分,精度到 232 皮秒。 ③ RTP 时间戳(4 字节):对应 NTP 那一刻的 RTP 时间。 ④ 包计数(4 字节):累计发过多少个 RTP 包。 ⑤ 字节计数(4 字节):累计发过多少载荷字节。

注意 ④⑤ 是「累计」不是「本次」,所以接收方做差就能算出「这段时间发了多少」——丢了几个包,除以差就得到丢包率。

二、NTP 与 RTP 的映射

这一对数字是 RTCP 存在的最重要的理由。

为什么需要它?因为 RTP 时间戳是个相对值。 起点随机,你没法从「2520000」知道现实几点几分。 而 NTP 是绝对时间,能跨设备、跨机器比较。

有了映射就能算两件事: ① 音画对齐:视频的 RTP 时间戳换算成 NTP,音频的也换算成 NTP,两者一比就知道差多少毫秒。 ② 端到端延迟:把「采集时的 NTP」与「现在收到的 NTP」一减,就是这一帧走了多久。这需要收发两端的时钟同步(通常用 NTP 协议)。

所以没有 SR,就只能「按到达时间猜」——网络一抖,音画就错开,而且错开之后不会自己恢复。

三、报告块:接收质量

SR 可以带 RC 个报告块,每个 24 字节。RR 也带同样的块。 一个报告块描述「我对某一个 SSRC 的接收情况」。

24 字节里六个数: ① 源 SSRC(4):我在评价哪一路。 ② 丢包率(1)+ 累计丢包(3):前者是 0~255,除以 256 得百分比;后者是 24 位有符号数。 ③ 最高序号(4):我收到的最大的那个序号。 ④ 抖动(4):到达间隔的统计方差,单位是时间戳单位。 ⑤ 上次 SR 时刻(4):LSR,用来算 RTT。 ⑥ 距上次 SR 的延迟(4):DLSR,和 LSR 配合算 RTT。

⑤⑥ 这两个字段合起来就能算 RTT: 发送方看到 RR 里的 LSR 和 DLSR,用「现在 − LSR − DLSR」就是往返时间。 也就是说 RTCP 让发送方免费得到了 RTT,不需要额外探测。

四、一个容易忽略的细节:24 位有符号

累计丢包数是 24 位有符号整数,不是无符号。 为什么允许负的?

因为「重复包」会被算成负丢包。 网络里偶尔会有重复(重传、环路),收到比预期多的时候,累计值就会往负走。

所以判断「丢包严重」不能只看绝对值大,要看它是不是持续往正走。 负值说明链路有重复包,通常是一过性的,不用管。

拆这个字段的代码要注意: Python 里读 3 个字节再判符号位,别直接按无符号解析,否则 -1 会变成 16777215,看着像「丢了 1600 万个包」。

五、动手 Demo:造一个 SR,把前 28 字节拆开

按 RFC 3550 拼一个完整的 SR(含一个报告块),再逐字段解回来。 重点看那 20 字节发送统计里五个数的位置。

import struct

ssrc, ntp_s, ntp_f, rtp_ts = 0x1A2B3C4D, 3900000000, 0x80000000, 3600 * 700
sent, octet = 1500, 1500 * 1200

body = struct.pack("!IIIIII", ssrc, ntp_s, ntp_f, rtp_ts, sent, octet)
body += struct.pack("!I", 0x0A0B0C0D) + b"\x00" * 20   # 一个报告块
sr = struct.pack("!BBH", 0x81, 200, (4 + len(body)) // 4 - 1) + body
print("SR 总长 %d 字节" % len(sr))

b0, pt, ln = struct.unpack("!BBH", sr[:4])
print("V=%d RC=%d PT=%d 长度=%d" % (b0 >> 6, b0 & 31, pt, ln))

ss, ns, nf, ts, sc, oc = struct.unpack("!IIIIII", sr[4:28])
print("SSRC=0x%08X" % ss)
print("NTP 秒=%d 小数=0x%08X" % (ns, nf))
print("RTP 时间戳=%d" % ts)
print("发过 %d 个包、%d 字节" % (sc, oc))

# NTP 的秒数换算成「从 1900 年起的秒」
print("约合 1970 年之后 %.1f 年" % (ns / 86400 / 365.25 - 70))

# RTP 时间戳换算成秒
print("RTP 时间戳对应 %.1f 秒" % (ts / 90000.0))

实跑输出:

--------------------------------------------------------------------
RTCP 包结构——SR 里的 NTP 与 RTP 时间戳
--------------------------------------------------------------------
**① 造一个 SR(发送方报告)**

  SR 总长 36 字节

**② 前 28 字节是发送方信息**


  字段           字节     值             含义
  ----------------------------------------------------------------------
  版本           2        2              固定 2
  RC             2        1              报告块个数
  PT             2        200            200 = SR
  长度           2        8              以 4 字节为单位减一
  SSRC           4        0x1A2B3C4D     发送方标识
  NTP 秒         4        3900000000     绝对时间,1900 年起算
  NTP 小数       4        0x80000000     秒的小数部分,精度 232 皮秒
  RTP 时间戳     4        2520000        对应 NTP 那一刻的 RTP 时间
  包计数         4        1500           累计发过多少 RTP 包
  字节计数       4        1800000        累计发过多少载荷字节

**③ NTP 与 RTP 时间戳的配对是 RTCP 最值钱的地方**

  **RTP 的时间戳是个相对值,起点是随机的**——**所以你不知道「时间戳 3600 对应现实几点几分」。**
  **SR 里同时给出 NTP 绝对时间和当时的 RTP 时间戳,两者一比就建立了映射。**

  **有了这个映射,多路流才能对齐:**
  **A 路的 RTP 时间和 B 路的 RTP 时间各自换算成 NTP,再对齐到同一时间轴。**
  **音画同步、多机位切换,底层全靠这一对数字。**

  **报告块(RC 个)里装的是接收质量**:**丢包率、累计丢包数、抖动、上次 SR 的时间戳。**
  **它是「反向」的——发送方靠它知道链路好不好,从而决定降码率还是发关键帧。**

SR 长 36 字节 = 28 固定 + 8 字节报告块头(那个块只填了 SSRC)。完整报告块是 24 字节,这里简化了。 NTP 秒 3900000000 从 1900 年算起,减去 70 年的偏移大约就是 2023 年前后——这就是「绝对时间」的意义。 RTP 时间戳 2520000 ÷ 90000 = 28 秒,说明这一路流从开播到现在播了 28 秒。

30 秒实验

  1. 把 ntp_s 改成 0,看换算出来的年份变成多少——这就是「没同步时钟」时的样子。
  2. 把 sent 改成 0 再跑,想一下接收方做差得到什么——第一份 SR 的差值没有意义,必须等第二份。
  3. 把报告块的 SSRC 也填上,再补 20 字节把 24 字节填满,看总长变成 60 字节、长度字段变 14。

常见误区

  1. 「NTP 时间戳是秒」——是「秒 + 小数」两个 32 位字。第二个字的精度到 232 皮秒。
  2. 「累计丢包数一定是正数」——是 24 位有符号。重复包会让它变负。
  3. 「SR 只给发送方自己看」——SR 是发给对端的,它对端的价值就是那对 NTP/RTP 映射。
自测三问(点开对答案) 1. SR 的 20 字节发送统计里有哪五个数? —— **NTP 秒、NTP 小数、RTP 时间戳、包计数、字节计数。** 2. 为什么 RTP 时间戳必须和 NTP 配对? —— **因为 RTP 起点随机,无法单独换算成绝对时间。**配对之后才能做音画同步和延迟测量。 3. 累计丢包数为什么可以是负的? —— **重复包会被算成负丢包。**24 位有符号,解析时要注意符号位。

相关笔记