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

RTCP 有三条硬规矩:必须发复合包、间隔随人数变、带宽占比不超过 5%。 三条都是为了「不让控制流量挤掉数据流量」。

📝 本节和前面几节的关系

3.2.2 拆完了 SR 的字段,这一节讲「什么时候发、发多少」。 看完这一节,你就知道为什么自己写的 RTCP 一上线就被限速。

一、复合包是硬性规定

标准要求:每个 RTCP 复合包必须以 SR 或 RR 开头,并且必须带 SDES CNAME。

所以最小的合法复合包是「RR + SDES」,两个包拼在一个 UDP 里。 这也是为什么抓包时看到的 RTCP 包总是一包几个——那是一个复合包。

为什么定这条规矩?三个理由: ① 保证 CNAME 一定在,否则音画配不上对。 ② 保证接收统计一定在,否则发送方没法自适应码率。 ③ 减少包数,几个包拼一个 UDP,省掉重复的 IP/UDP 头。

违反这条会怎样? 大多数实现会直接丢弃,不报错。现象是「发了 RTCP 但对方毫无反应」——查半天以为是网络问题。

二、间隔随人数变

RTCP 的发送间隔不是固定的,它随「参与者数」动态调整。

原因是:如果每个人都固定 5 秒发一次,那 1000 人的会议每秒就有 200 个 RTCP 包,比数据还多。 所以要按人数摊:人越多,每个人发的间隔越长。

标准给的算法(简化版): ① 先算「平均间隔」= 固定最小间隔 ÷ 带宽占比。 ② 再乘一个 0.5~1.5 的随机数,避免所有人同时发。 ③ 再除以「参与者数开方」,人越多间隔越长。

第 ② 步的随机化很重要。 否则同一时刻加入的几百个客户端会同时发 RTCP,形成脉冲,把链路瞬时打满。 这个现象叫「同步自旋」,是早期视频会议系统的经典故障。

三、5% 带宽的两层含义

「RTCP 占 5%」这句话有两层意思,别只看第一层。

① 总量上限:整个会话里,所有 RTCP 加起来不超过总带宽的 5%。 ② 内部再分:这 5% 里,25% 给发送方(SR),75% 给接收方(RR)。

为什么接收方分得多? 因为会议场景里接收方人数是发送方的几十倍,按人头分的话发送方根本分不到多少。 而这个 25/75 的比例就是按「人数大致比例」定的经验值。

算一下 2 Mbps 的视频: RTCP 总量上限是 100 kbps,即每秒 12500 字节。一个复合包 48 字节,理论上一秒能发 260 个——远用不完。 所以 5% 在实践中很少成为瓶颈,它的意义是「兜底」。

四、还有三条容易忘的

① RTCP 也走 RTP 端口 +1,所以抓包要同时筛两个端口。 ② RTCP 的包也可能丢,所以统计要做平滑。 单次丢包率波动很大,通常取最近几个 RR 的加权平均。 ③ 会话结束时发 BYE 是礼貌,但不是必需。 不发也能靠超时发现,只是慢几秒。

最后一条值得单独说:BYE 里可以带「原因字符串」(如 camera unplugged)。 有些服务器会把它写进日志,排查时非常有用。 这个字段完全可选,但加了几乎不花成本。

五、动手 Demo:复合包、间隔、5% 带宽,一次算清

手工拼一个最小的合法复合包(RR + SDES),量它的字节数。 再按 5% 的额度算一下「一秒最多能发几个」。

import struct

# RR:头 4 + SSRC 4 + 一个报告块 24 = 32 字节,长度字段 7
rr = struct.pack("!BBHI", 0x81, 201, 7, 0x1A2B3C4D)
rr += struct.pack("!I", 0x0A0B0C0D) + b"\x00" * 20

# SDES:头 4 + SSRC 4 + (type,len) 2 + "ch30ab" 6 = 16 字节,长度 3
sdes = struct.pack("!BBH", 0x81, 202, 3)
sdes += struct.pack("!I", 0x1A2B3C4D)
sdes += struct.pack("!BB", 1, 6) + b"ch30ab"

comp = rr + sdes
print("RR %d + SDES %d = 复合包 %d 字节" % (len(rr), len(sdes), len(comp)))

# 逐个解析,验证长度字段
off = 0
while off + 4 <= len(comp):
    b0, pt, ln = struct.unpack("!BBH", comp[off:off + 4])
    size = (ln + 1) * 4
    print("  PT=%d 长度字段=%d -> %d 字节" % (pt, ln, size))
    off += size
print("走完指针停在 %d / 总长 %d" % (off, len(comp)))

# 5% 额度
for kbps in (500, 2000, 8000):
    lim = kbps * 1000 * 0.05 / 8
    print("%4d kbps -> RTCP 每秒最多 %.0f 字节,即 %d 个复合包"
          % (kbps, lim, lim // len(comp)))
    print("          按每个包 48 字节、5 秒一次估算,实际用量 %.0f 字节/秒"
                  % (len(comp) / 5.0))

实跑输出:

--------------------------------------------------------------------
RTCP 的注意事项——复合包、间隔、5% 带宽
--------------------------------------------------------------------
**① 复合包:一个 UDP 包里必须装至少两个 RTCP 包**

  RR 32 字节 + SDES 16 字节 = 复合包 48 字节

  **RTCP 标准要求:每个复合包必须以 SR 或 RR 开头,并且必须带一个 SDES CNAME。**
  **为什么?因为接收方收到一个陌生 SSRC 时,需要 CNAME 才能把它归到「同一个人」下面。**
  **没有 CNAME,多路混流时音视频会被当成两个不同的人。**

**② 间隔:不是想发就发**

  **标准建议 RTCP 的发送间隔随参与者数变化:人越多,间隔越长。**
  **小会话里通常 5 秒一次,几百人的会议可能几十秒一次。**
  **目的是「让控制流量占比恒定」,而不是「让每个人都发得勤」。**

**③ 带宽占比:5%**

  **RTCP 占会话总带宽的 5%,其中 25% 给发送方(SR),75% 给接收方(RR)。**
  **这个比例要让 RTP 占 95%——控制信息再多也不能抢数据的带宽。**

  算一下:
  2 Mbps 的视频,RTCP 最多分到 100 kbps,即每秒 12500 字节
  一个复合包约 48 字节,所以一秒最多发 260 个——远远用不完 5 秒一次的额度。

  **所以「5%」在实践中很少成为瓶颈,它的意义是「兜底」:**
  **防止某个有 bug 的客户端疯狂发 RR 把链路打满。**
  **服务器侧要做的是「收到就限速」,而不是「相信客户端会自觉」。**

复合包 48 字节 = RR 32 + SDES 16,两个包首尾相接,长度字段各自独立。 指针正好走到 48,说明两个长度字段都算对了。算错一个,后面的解析就全乱。 8 Mbps 时 RTCP 额度是每秒 50000 字节,而实际 5 秒才发 48 字节——额度用了不到千分之一。

30 秒实验

  1. 把 SDES 里的 "ch30ab" 换成更长的名字,注意要补到 4 字节对齐,否则长度字段算不出整数。
  2. 在复合包前面再加一个 SR,看总长和指针走位——标准允许一个复合包里塞多个。
  3. 把 kbps 改成 50,看额度降到每秒 312 字节,这时候 5 秒一个包就真的逼近上限了。

常见误区

  1. 「发一个 RR 就够了」——必须带 SDES CNAME,否则是非法复合包,很多实现直接丢。
  2. 「RTCP 固定 5 秒一次」——间隔随人数动态调整,还要加随机化避免同步自旋。
  3. 「5% 是每个包的额度」——是整个会话所有 RTCP 的总和。内部再按 25/75 分给发送方与接收方。
自测三问(点开对答案) 1. 最小的合法 RTCP 复合包由哪两个包组成? —— **RR + SDES。**必须以 SR 或 RR 开头,且必须带 CNAME。 2. 为什么发送间隔要加随机数? —— **避免所有人同时发形成脉冲**,那个现象叫同步自旋。 3. 5% 里 25/75 是怎么分的? —— **25% 给发送方(SR),75% 给接收方(RR)。**因为接收方人数远多于发送方。

相关笔记