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

一个 RTCP 抓包往往返回好几个包,那是一个复合包里的子包。 按长度字段一个个往下走,就能数清里面装了什么。

📝 本节和前面几节的关系

3.2.3 讲了规则,这一节到抓包里验证规则。 3.1.4 剥的是 RTP 的三层壳,这里换成了「复合包内部的多层结构」。

一、一个 UDP 里几个包

RTP 是「一个 UDP 一个包」,RTCP 不是。 RTCP 的复合包是「一个 UDP 里塞好几个包」,首尾相接。

所以抓包时会看到两种视图: ① 包列表里一行(一个 UDP 包)。 ② 展开后有多行(每个子包一行)。Wireshark 按长度字段自动分行。

如果展开的行数不对,说明长度字段有问题。 这是设备 bug 的一个典型症状:「明明有 SR 和 SDES,Wireshark 只显示 SR,后面一段显示成 Data」。 遇到这种情况,先手工按 (长度+1)×4 算一遍,对不上就是设备算错了。

二、怎么走完一个复合包

遍历的逻辑只有四行,但很容易写错:

① 在偏移 off 处读 4 字节,取出 PT 与长度字段 ln。 ② 算出这个子包的字节数 size = (ln + 1) × 4。 ③ 处理完这个子包,off += size。 ④ 直到 off 走到末尾。

写错的两个地方: 一是忘了加 1(ln × 4 就少了 4 字节,越走越偏);二是在 off 上加了 ln 而不是 size。 这两个错都会让第二个子包的 PT 读成垃圾值。

还有一个边界情况:复合包里可能有「填充」。 如果某个子包的 P 位(Padding)为 1,末尾那个字节是填充长度,而长度字段已经把填充算进去了。 所以 size 是对的,但「有效内容」要去掉填充。

三、三个必看的数

① 丢包率:在 RR 的报告块里。fraction lost 是 0~255,除以 256 得百分比;cumulative lost 是 24 位有符号的累计数。

② 抖动:也在报告块里,单位是时间戳单位。视频要除以 90000 才得到秒。 看到 3000 就以为是 3 秒是错的——那是 33 毫秒。

③ RTT:靠 LSR 和 DLSR 两个字段算。发送方收到 RR 后,用「当前时刻 − LSR − DLSR」得到往返时间。 这个值抓包时看不到直接字段,要自己算。

三个数里,抖动最容易被误读。 因为它的单位不是毫秒,而是「时间戳单位」。 音频的时间戳单位是采样率(48000),视频是 90000——同一个数值在音视频里含义完全不同。

四、排查「对方说我画质差」

这是一个非常具体的场景,值得单独说。

客户反馈画质差的流程应该是: ① 先要一份对方侧的抓包(或者让对方报 RR 里的丢包率)。 ② 丢包率高(>2%)就是链路问题,降码率或换链路。 ③ 丢包率低但抖动大,是网络拥塞但不丢包,要加深接收缓冲或开 FEC。 ④ 两者都正常,才轮到怀疑编码器。

顺序搞反了很常见:一上来就骂编码器,把码率调来调去,结果对方丢包 8%,怎么调都糊。 而 RTCP 的 RR 早就把真相写在里面了。

所以「会看 RTCP」比「会调编码参数」有用得多。 前者定位问题,后者只是试错。

五、动手 Demo:一个复合包里到底有几个包

手工拼一个 SR + SDES + BYE 的复合包,再按长度字段一个个走完。 看指针能不能正好停在末尾——这就是解析器要做的唯一一件事。

import struct

sr = struct.pack("!BBHIIIIII", 0x80, 200, 6, 0x1A2B3C4D,
                 3900000000, 0, 3600 * 700, 1500, 1800000)
sdes = struct.pack("!BBH", 0x81, 202, 3)
sdes += struct.pack("!I", 0x1A2B3C4D) + struct.pack("!BB", 1, 6) + b"ch30ab"
bye = struct.pack("!BBH", 0x81, 203, 1) + struct.pack("!I", 0x1A2B3C4D)
comp = sr + sdes + bye
print("SR %d + SDES %d + BYE %d = %d 字节"
      % (len(sr), len(sdes), len(bye), len(comp)))

NAME = {200: "SR", 201: "RR", 202: "SDES", 203: "BYE", 204: "APP"}
off, i = 0, 0
while off + 4 <= len(comp):
    b0, pt, ln = struct.unpack("!BBH", comp[off:off + 4])
    size = (ln + 1) * 4
    i += 1
    print("  %d. PT=%d %-4s 长度=%d -> %d 字节"
          % (i, pt, NAME.get(pt, "?"), ln, size))
    off += size
print("共 %d 个子包,指针停在 %d / 总长 %d" % (i, off, len(comp)))

# 常见误读:抖动单位
jit = 3000
print("抖动 %d 个时间戳单位 = 视频 %.1f 毫秒 / 音频 %.1f 毫秒"
      % (jit, jit * 1000.0 / 90000, jit * 1000.0 / 48000))

实跑输出:

--------------------------------------------------------------------
RTCP 的抓包分析——一个复合包里有几个包
--------------------------------------------------------------------
**① 造一个真实的复合包:SR + SDES + BYE**

  SR 28 + SDES 16 + BYE 8 = 52 字节

**② 按「长度字段」一个个往下走**

  次序     PT      名字     长度     实际字节
  -----------------------------------------------
  1        200     SR       6        28
  2        202     SDES     3        16
  3        203     BYE      1        8

  走完 3 个包,指针停在 52,总长 52 —— 正好对齐

**③ 抓包时看 RTCP 的三条经验**

  **① RTCP 端口是 RTP 端口加一。**
  **RTP 收在 6000,RTCP 就收在 6001。**
  **抓包时两个一起筛:`udp.port >= 6000 and udp.port <= 6001`。**
  **② Wireshark 会把复合包拆成多行显示**,一行一个子包。
  **分行是按长度字段算的——分错了说明设备把长度字段算错,那是它的 bug。**
  **③ 丢包率在 RR 的报告块里**,字段叫 `fraction lost` 和 `cumulative lost`。
  **前者是 0~255,除以 256 得百分比;后者是累计个数。**
  **这两个数比自己在客户端数包准得多,因为它是接收端汇报的对端视角。**

  **所以排查「为什么对方说我画质差」,第一件事不是看自己的编码器**,
  **而是看对方的 RR 里报了多少丢包。**
  **它说的是真相:路上丢了,不是你编得差。**

三个子包、三种长度字段(6/3/1),正好对应 28/16/8 字节。指针停在 52,与总长一致。 如果解析时忘了 +1,第一个包就会被判成 24 字节,第二个 PT 会读成垃圾。 抖动 3000 在视频里是 33 毫秒,在音频里是 62 毫秒——同一个数,含义差一倍。

30 秒实验

  1. 把 BYE 的长度字段故意改成 2,看解析指针最后停在哪——会停在 56 而不是 52,这就是「走出来对不上」的样子。
  2. 在复合包最前面插一个 SR,看子包数变成 4,标准允许一个复合包里塞任意多个。
  3. 把抖动值从 3000 改成 900,算一下是多少毫秒——900÷90000 = 10 毫秒。

常见误区

  1. 「RTCP 一个 UDP 一个包」——是复合包,一包几个。
  2. 「抖动单位是毫秒」——是时间戳单位。视频要除 90000,音频除 48000。
  3. 「画质差就是编码器问题」——先看 RR 的丢包率。丢包 8% 时怎么调编码都没用。
自测三问(点开对答案) 1. 遍历复合包的四个步骤是什么? —— **读 4 字节取 PT 与长度 → 算 size=(ln+1)×4 → off+=size → 循环。** 2. 抖动的单位是什么? —— **时间戳单位。**视频除 90000、音频除 48000 才得到秒。 3. 客户说画质差,第一件事该看什么? —— **看 RR 里的丢包率。**高了就是链路问题,不用动编码器。

相关笔记