一个 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 秒实验
- 把 BYE 的长度字段故意改成 2,看解析指针最后停在哪——会停在 56 而不是 52,这就是「走出来对不上」的样子。
- 在复合包最前面插一个 SR,看子包数变成 4,标准允许一个复合包里塞任意多个。
- 把抖动值从 3000 改成 900,算一下是多少毫秒——900÷90000 = 10 毫秒。
常见误区
- 「RTCP 一个 UDP 一个包」——是复合包,一包几个。
- 「抖动单位是毫秒」——是时间戳单位。视频要除 90000,音频除 48000。
- 「画质差就是编码器问题」——先看 RR 的丢包率。丢包 8% 时怎么调编码都没用。
自测三问(点开对答案)
1. 遍历复合包的四个步骤是什么? —— **读 4 字节取 PT 与长度 → 算 size=(ln+1)×4 → off+=size → 循环。** 2. 抖动的单位是什么? —— **时间戳单位。**视频除 90000、音频除 48000 才得到秒。 3. 客户说画质差,第一件事该看什么? —— **看 RR 里的丢包率。**高了就是链路问题,不用动编码器。- ☐ (本节完,下一节 3.3 RTP/RTCP与RTSP的关系)