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

RTCP 只有五种包,PT 从 200 数到 204:SR、RR、SDES、BYE、APP。 其中 SR 和 RR 是主干,SDES 是必需的陪衬。

📝 本节和前面几节的关系

3.1 讲完 RTP 怎么搬数据,这一节开始讲 RTCP 怎么报账。 3.2.2 会把 SR 拆到字段级,3.2.3 讲发送规则。

一、五种包各干什么

① SR(PT=200,发送方报告) 告诉对方「我发了多少包多少字节」以及「NTP 与 RTP 时间戳的对应关系」。 ② RR(PT=201,接收方报告) 告诉对方「我丢了多少、抖动多大」。 它是反向的:让发送方知道链路好不好。 ③ SDES(PT=202,源描述) 带 CNAME 等文本,让接收方把 SSRC 归到「同一个人」。 ④ BYE(PT=203,告别) 我要走了。丢了也不影响,超时照样能发现。 ⑤ APP(PT=204,应用自定义) 厂商扩展用,标准不定义内容。

二、为什么 PT 从 200 开始

因为 0~127 被 RTP 的载荷类型占了。 RTCP 用 200~204 是为了「一眼分辨」:看第一个字节的高位就知道是 RTP 还是 RTCP。

具体怎么分辨?看 PT 的数值范围就行。 RTP 的 PT 只有 7 位(0~127),RTCP 的 PT 用满 8 位(200~204)。 所以同一个端口上收到包,PT ≥ 200 就是 RTCP。

但实际部署里它们很少共用端口。 标准约定 RTCP 端口 = RTP 端口 + 1,所以「按端口分」比「按 PT 分」更常见。 共用端口只在 RTP over TCP 时才出现(靠 interleaved 通道号分)。

三、SR 与 RR 的分工

这两个包的区别只有一个:谁在发数据。

① 发送方发 SR:因为我发了 RTP,所以我得告诉对方「我发了多少」。SR 里多出 20 字节的发送统计,以及 NTP/RTP 时间戳的配对。 ② 接收方发 RR:因为我只收,没什么可报的,就报「收得怎么样」。RR 就是 SR 去掉那 20 字节发送统计。

所以它们的头部结构完全一样,只差一个 20 字节的块。 这也解释了为什么标准说「复合包必须以 SR 或 RR 开头」——两者是同一种东西的两种形态。

双向通信时两边都既发又收,所以两边都发 SR,而 SR 里可以带报告块(RC>0),等于「SR 顺便干了 RR 的活」。

四、SDES 为什么是必需的

因为 SSRC 是个随机数,本身没有任何意义。 接收方看到 0x1A2B3C4D,不知道这是「摄像头」还是「麦克风」。

SDES 里的 CNAME 就是「稳定标识」。 同一个人的音视频两路流,SSRC 不同但 CNAME 相同——接收方靠 CNAME 把它们配对,才能做音画同步。

没有 CNAME 会怎样? 音视频被当成两个人,同步算法根本不会启动,现象是「画面和声音各走各的,越差越远」。 这是自制 RTP 实现最常见的坑之一:只发 RR 不发 SDES。

标准要求每个复合包都必须带 CNAME,就是为了堵这个洞。 所以「复合包」不是「可选的优化」,是硬性规定。

五、动手 Demo:真造五个包,验证 PT 与长度字段

按 RFC 3550 手工拼五种包,验证 PT 值、头字节与长度字段的换算。 重点是那个「以 4 字节为单位减一」的长度算法。

import struct

def mk(pt, body=b"", count=0):
    n = (4 + len(body)) // 4 - 1
    return struct.pack("!BBH", 0x80 | count, pt, n) + body

pk = [(200, mk(200, b"\x00" * 24, 0)),          # SR,无报告块
      (201, mk(201, b"\x00" * 24, 1)),          # RR,一个报告块
      (202, mk(202, b"\x01\x00\x00\x00", 1)),  # SDES
      (203, mk(203, b"\x1a\x2b\x3c\x4d" + b"\x00" * 4, 1)),
      (204, mk(204, b"\x63\x68\x33\x30" + b"\x00" * 8, 0))]

for pt, d in pk:
    b0, b1, ln = struct.unpack("!BBH", d[:4])
    print("PT=%d 头=0x%02X 长度字段=%d 实际=%d 字节"
          % (b1, b0, ln, len(d)))

# 长度字段的含义:(长度 + 1) * 4 = 总字节
for pt, d in pk:
    ln = struct.unpack("!H", d[2:4])[0]
    print("PT=%d 按长度字段推算总长 = %d" % (pt, (ln + 1) * 4))

实跑输出:

--------------------------------------------------------------------
RTCP 的 5 种分组类型——PT 从 200 数到 204
--------------------------------------------------------------------
**① 五种包的 PT 值**

  PT      名字     全称                    作用
  ---------------------------------------------------------------------------
  200     SR       Sender Report           发送方报告:发了多少、时钟对齐
  201     RR       Receiver Report         接收方报告:丢了多少、抖动多大
  202     SDES     Source Description      源描述:CNAME 等,用来认人
  203     BYE      Goodbye                 告别:我要走了
  204     APP      Application-defined     应用自定义

**② 真造五个包,验证 PT 与长度字段**


  PT      头字节     长度字段     实际字节
  --------------------------------------------
  200     0x80       6            28
  201     0x81       6            28
  202     0x81       1            8
  203     0x81       2            12
  204     0x80       3            16

  **长度字段的单位是「4 字节减一」,不是字节数。**
  **长度写 6 表示 7 个 32 位字,也就是 28 字节。**
  **这个减一是 RTCP 最容易算错的地方——它把「包含头部的总长」按 4 字节折算后再减 1。**

**③ 五种的用法差别**

  **SR 和 RR 是「必发」的**:发送方定期发 SR,接收方定期发 RR。**它们是 RTCP 的主干。**
  **SDES 必须跟着 SR/RR 一起发**(复合包里至少要有一个 CNAME)。
  **作用是让接收方知道「这个 SSRC 是谁」,否则多路混流没法区分。**
  **BYE 只在退出时发一次**,**APP 是给厂商扩展用的**,**大部分开源服务器的 APP 都留空。**

PT=200 的包 28 字节,长度字段是 6 ——因为 28÷4−1=6。不是 28,也不是 7。 头字节 0x80 与 0x81 差在最低位,那是 RC(报告块个数)。RC=1 就是带一个报告块。 第二段验证了换算公式:(长度+1)×4 正好等于实际字节,一个不差。

30 秒实验

  1. 把 SR 的 body 加到 40 字节,看长度字段变成多少——(44÷4)−1 = 10。
  2. 把 RR 的 RC 改成 2,body 要跟着加 24 字节,否则解析时会越界。
  3. 造一个 PT=199 的包,看你能不能一眼判断它不是标准 RTCP——标准只有 200~204。

常见误区

  1. 「RTCP 有很多种包」——标准只用五个 PT,其余都是扩展。
  2. 「SDES 可有可无」——复合包必须带 CNAME,否则音视频配不上对。
  3. 「长度字段就是字节数」——是「4 字节为单位再减一」。这个减一是最容易算错的地方。
自测三问(点开对答案) 1. 五种 RTCP 包的 PT 分别是多少? —— **SR=200、RR=201、SDES=202、BYE=203、APP=204。** 2. SR 和 RR 的结构差在哪? —— **SR 多 20 字节发送统计(NTP 秒/小数、RTP 时间戳、包数、字节数)。**头部结构完全一样。 3. 没有 CNAME 会出什么问题? —— **音视频被当成两个人,音画同步根本不启动,越播越不同步。**

相关笔记