📹 第一章 流媒体与RTSP/RTP/RTCP简介
📄 一句话概括

序列号每包加一,16 位,跨过 65535 回绕到 0。 时间戳每帧加「时钟 ÷ 帧率」,同一帧切开的几个包共用一个时间戳。

📝 本节和前面几节的关系

上一节把静态的格式讲完了,这一节讲它怎么动起来。 序列号和时间戳是 RTCP 统计的全部原料——丢包率靠序列号算,抖动靠时间戳算。 播放器里这两个数怎么用, 看 34 课 的同步章节。

一、序列号:16 位,会回绕

每发一个 RTP 包,序列号加一,初值是随机的。 为什么要随机初值:防止同一对端之间前后两次会话的包被混淆。

它是 16 位,最大值 65535,加完就回绕到 0。 回绕不是错误,是设计。 但写统计代码时必须处理,否则「65535 之后变 0」会被算成丢了几万个包。

收到 上一包 差值 结论
100 99 1 正常
103 100 3 丢了 101、102
0 65535 1 回绕,没丢

第三行是关键:差值要用「模 65536 的无符号运算」算,(0 - 65535) & 0xFFFF 得到 1, 不是负六万多。

二、时间戳:每帧加多少

步长 = 时钟频率 ÷ 帧率,整数除法。

帧率 步长 一秒多少帧
25 3600 25
30 3000 30
50 1800 50
60 1500 60

注意「同一帧的几个包共用同一个时间戳」。一帧切成三个包发,这三个包的时间戳完全一样, 只有序列号在递增。

这样接收方就能靠时间戳判断「这几个包属于同一帧」,不用等 M 位。 两个信息是冗余的,冗余是为了容错。

三、M 位:一帧结束的标记

M(Marker)位只在「这一帧的最后一个包」上置 1。 接收方靠它判断:这一帧收齐了,可以送去解码了。

场景 一帧几个包 M 位在哪
小帧(I 帧的一部分) 1 唯一那个包
普通 P 帧 2~3 最后一个
大 I 帧 几十个 最后一个

为什么不能只靠时间戳判断帧结束:因为不知道这一帧该有几个包。 M 位是显式的「到此为止」,时间戳是隐含的「同属一帧」。

丢包时会发生什么:M 位那个包丢了,接收方就永远等不到帧结束,这一帧只能丢掉。 所以关键帧的最后一个包丢了特别伤。

四、抖动和丢包都是从这里算的

RTCP 的两个核心统计量,原料就是序列号和到达时间。

统计量 用哪些数 怎么算
丢包数 序列号 期望收到 − 实际收到
抖动 序列号 + 到达时间 到达间隔 − 发送间隔,取绝对值平滑

抖动的算法值得说一下:对每个包算「这一包和前一个包的到达时间差」减去「它们的时间戳差」, 取绝对值,再做指数平滑。 平滑系数是 1/16,所以它反映近期变化,不是全程平均。

这意味着网络刚变差,抖动值会很快爬上来;网络恢复了,它也要一段时间才降回去。 看这个数要记得它是有惯性的。

五、动手 Demo:30 帧的流,时间戳每包加 3000

这段模拟发 100 个包(每 3 个包一帧),看序列号和时间戳怎么走。 起点特意选在 65530,就是为了让回绕发生在中间。

fps, per_frame = 30, 3
step = 90000 // fps          # 3000
first = 65530
for i in range(100):
    s = (first + i) & 0xFFFF
    t = (1000000 + (i // per_frame) * step) & 0xFFFFFFFF
    m = 1 if (i + 1) % per_frame == 0 else 0
    T([u"%d" % i, u"%d" % s, u"%d" % t, u"%d" % m], [0, 0, 0, 0])

实跑输出:

--------------------------------------------------------------------
30 帧的流,时间戳每包加 3000
--------------------------------------------------------------------
**RTP 的时间戳步长 = 时钟频率 ÷ 帧率。**
  视频时钟固定 90000 Hz,所以:

  帧率       步长     一秒多少个包(一帧一包)
  ------------------------------------------------
  25 fps     3600     25
  30 fps     3000     30
  50 fps     1800     50
  60 fps     1500     60

  90000 这个数不是随便定的:它能被 24、25、30、50、60 整除,常见帧率算下来都是整数,不用四舍五入。

**真发 100 个包,看序列号和时间戳怎么走(每 3 个包一帧):**

  包序号     序列号     时间戳      M
  ---------------------------------------
  0          65530      1000000     0
  1          65531      1000000     0
  2          65532      1000000     1
  3          65533      1003000     0
  4          65534      1003000     0
  5          65535      1003000     1
  6          0          1006000     0
  7          1          1006000     0
  …          …          …           …
  62         56         1060000     1
  63         57         1063000     0
  64         58         1063000     0
  65         59         1063000     1
  66         60         1066000     0
  67         61         1066000     0
  68         62         1066000     1
  69         63         1069000     0

**两件事一眼能看出来:**
  一、序列号从 65530 开始,跨过 65535 后回绕到 0 继续——**16 位字段,回绕是正常的,不是错误。**
  二、时间戳三个包才加 3000,因为这三个包是同一帧切开的,**同一帧共用时间戳**。
  M 位只在每帧第 3 个包上置 1,接收端靠它判断「这一帧收齐了」。

**抖动和丢包也是用这两个数算的:**
  收到序列号 100、101、103,就知道 102 丢了;
  时间戳的到达间隔忽大忽小,就是抖动(jitter),RTCP 会统计它,见 D18。

输出里最值得看的是第 6 行:序列号从 65535 变成 0,时间戳正常往前走。 这一下就是回绕,而 & 0xFFFF 让它自动发生,不用写 if 判断。

时间戳那列的模式是「三行一样,然后跳 3000」:因为每三个包属于同一帧,共用时间戳。 M 位那列是「三个 0,然后一个 1」,正好对上。

这两个信息配合的好处:就算 M 位那个包丢了,接收方看到时间戳变了, 也知道上一帧该结束了——这就是「冗余设计」,代价是几乎为零,收益是容错。

实战里的一个陷阱:有的设备一帧切很多包(大 I 帧能到几十个),这时序列号在帧内快速递增, 丢一个包整帧就废。 所以关键帧的丢包代价远高于普通帧——这也是为什么不少方案会给关键帧加冗余(FEC)。

30 秒实验

  1. 算出 25 fps 的步长,和 30 fps 的比一下差多少。
  2. 把起点设成 65533,数到第几包时发生回绕。
  3. 把每帧包数从 3 改成 1,看 M 位变成什么样。

常见误区

  1. 以为序列号回绕是错误。 16 位字段必然回绕,统计时要用模 65536 的运算,不能直接相减。
  2. 以为时间戳是毫秒。 是采样时钟。30 fps 每帧加 3000,不是加 33。
  3. 以为每个包的时间戳都不同。 同一帧切开的几个包共用时间戳,只有序列号在递增。
自测三问(点开对答案) 1. 30 fps 的流,时间戳每帧加多少?为什么? —— 加 3000。视频时钟是 90000 Hz,90000 ÷ 30 = 3000。 2. 序列号从 65535 变成 0,是丢包吗? —— 不是,是正常的 16 位回绕。算差值要用 `(new - old) & 0xFFFF`,得到 1 而不是负六万多。 3. M 位和「时间戳相同」都表示同一帧,为什么要两个? —— 冗余容错。时间戳丢了还能看 M 位,M 位丢了还能看时间戳变化,两边互相兜底。

下一节进 RTCP:五种包类型,以及那个「length 字段写 6 代表 28 字节」的坑。

相关笔记