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

RTP 头固定 12 字节,九个字段,一个都不多。 时间戳按采样时钟走,不是毫秒——这一点决定了它怎么算时间。

📝 本节和前面几节的关系

上一节把 RTSP 收尾,这一节换协议:数据到底长什么样。 RTP 是整本书最底层的格式,ONVIF 和国标传视频最后都是它。 RTP 的负载里装的是什么,取决于编码格式, 那部分看 26 课 的 H.264 章节。

一、12 字节,九段

RTP 头是固定 12 字节,除非有扩展或 CSRC。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段 位 作用
V 2 版本,固定 2
P 1 末尾有没有填充字节
X 1 有没有扩展头
CC 4 后面跟几个 CSRC
M 1 这一帧的最后一个包
PT 7 负载类型,看 SDP 才知道含义
序列号 16 每包加一,会回绕
时间戳 32 采样计数,不是毫秒
SSRC 32 流的身份

九段加起来正好 12 字节(96 位):2+1+1+4 是第一字节,1+7 是第二字节,16 位序列号, 32 位时间戳,32 位 SSRC。

二、为什么要按位算

因为一个字节里塞了好几个字段,只能靠移位和掩码拆。 这是本节唯一需要动手的部分,但理解了就再也不会忘。

操作 代码 取到什么
取版本 b0 >> 6 最高两位
取填充位 (b0 >> 5) & 1 第 6 位
取扩展位 (b0 >> 4) & 1 第 5 位
取 CC b0 & 0x0F 低四位
取标记位 b1 >> 7 最高位
取负载类型 b1 & 0x7F 低七位

规律很一致:要取高位的就右移,要取低位的就掩码,两边都要就先移位再掩码。

装配的时候反过来用 | 和 <<,这就是 D15 那段代码在做的事。

三、时间戳为什么用采样时钟

如果用毫秒,25 fps 的帧间隔是 40 毫秒,30 fps 是 33.33 毫秒——后者除不尽,会累积误差。 用 90000 Hz 的采样时钟,25 fps 步长 3600、30 fps 步长 3000,都是整数。

帧率 毫秒步长 90000 Hz 步长
24 41.67(除不尽) 3750
25 40 3600
30 33.33(除不尽) 3000
60 16.67(除不尽) 1500

为什么偏偏是 90000:它能被 24、25、30、50、60 全部整除。 这不是随便选的,是为了让常见帧率都落成整数。

音频不一样:它按自己的采样率走,8000 Hz 的 G.711 一秒就是 8000 个刻度。 所以音视频两路的时间戳单位不同,换算要靠 RTCP 里的 NTP 时间对齐。

四、包头开销算一笔

RTP 头 12 字节,加上 UDP 8、IP 20,一共 40 字节。

负载大小 协议开销 占比
100 字节 40 29%
500 字节 40 7.4%
1000 字节 40 4.0%
1400 字节 40 2.8%

这就是 RTP 包不能切太碎的原因。典型值是 1200 到 1400 字节——既不超过 MTU(1500), 又能把开销压到 3% 左右。

低于 MTU 是为了避免 IP 分片:一个 UDP 包被切成两片,只要丢一片整个包就废了, 丢包率会被放大。

五、动手 Demo:RTP 头 12 个字节,逐位造一遍

这段用 struct 真造一个 RTP 头,再逐位拆回来。 关键是看前两个字节的十六进制:0x80 和 0x60 各自代表了什么。

import struct
seq, ts, ssrc = 0x1234, 90000, 0x1A2B3C4D
b0 = (2 << 6) | (0 << 5) | (0 << 4) | 0     # V=2 P=0 X=0 CC=0
b1 = (0 << 7) | 96                            # M=0 PT=96
hdr = struct.pack(u"!BBHII", b0, b1, seq, ts, ssrc)
print(u" ".join(u"%02X" % c for c in hdr))

# 拆回来
v = (hdr[0] >> 6) & 0x3
pt = hdr[1] & 0x7F
sq, tst, ss = struct.unpack(u"!HII", hdr[2:])

实跑输出:

--------------------------------------------------------------------
RTP 头 12 个字节,逐位造一遍
--------------------------------------------------------------------
**RTP 头固定 12 字节,一个字段都不多。真造一个包,再拆回来看。**

  struct.pack 造出来这 12 字节:
  80 60 12 34 00 01 5F 90 1A 2B 3C 4D

  字段            位数     值             意思
  ----------------------------------------------------------------------------
  V 版本          2        2              固定 2,别的值就是坏包
  P 填充          1        0              末尾有没有填充字节
  X 扩展          1        0              有没有扩展头
  CC 计数         4        0              后面跟几个 CSRC
  M 标记          1        0              这一帧的最后一个包置 1
  PT 负载类型     7        96             96 是动态类型,看 SDP 才知道是啥
  序列号          16       4660           每发一个包加 1,回绕到 0
  时间戳          32       90000          采样计数,不是毫秒
  SSRC            32       0x1A2B3C4D     这一路流的身份

**头 12 字节,加上 UDP 8 + IP 20 = 40 字节。**
  一个 1000 字节的包,**协议开销就是 4%**。
  但要是每包只装 100 字节负载,开销就变成 29%——这就是 RTP 包不能切太碎的原因。

**时间戳为什么不是毫秒:**
  它按「采样时钟」走。视频的时钟是 90000 Hz,音频按采样率(如 8000、48000)。
  这样一路流内的时间计算全是整数加法,不用碰浮点,也不会累积误差。

输出那 12 个字节是本节的核心:80 60 12 34 00 01 5F 90 1A 2B 3C 4D。 逐个数一遍就知道每个字段落在哪: 80 是 0b10000000,V=2、P=0、X=0、CC=0; 60 是 0b01100000,M=0、PT=96; 12 34 是序列号 4660; 00 01 5F 90 是 90000; 1A 2B 3C 4D 是 SSRC。

80 这个值可以背下来:所有正常的 RTP 包,第一个字节都是 0x80。 看到 0x80 就知道这是 RTP,抓包里这是最快的识别方式。

struct.pack 的 ! 是大端序,网络协议一律用大端。 少了这个感叹号,在 x86 上跑出来的包全是反的——这是写二进制协议最常见的错。

30 秒实验

  1. 用 struct 造一个 RTP 包,把前两个字节打印成二进制,数一下各字段的位置。
  2. 把 PT 改成 97,看第一个字节变不变、第二个字节怎么变。
  3. 把序列号设成 65535,再加一看会不会变成 0。

常见误区

  1. 以为 RTP 头是变长的。 基础头固定 12 字节,只有带扩展(X=1)或 CSRC(CC>0)时才变长。
  2. 以为时间戳是毫秒。 是采样时钟,视频 90000 Hz、音频按采样率。用毫秒算会导致累积误差。
  3. 以为 PT 96 就是 H.264。 96 以上是动态类型,含义由 SDP 里的 a=rtpmap 决定。
自测三问(点开对答案) 1. RTP 头为什么是 12 字节? —— 九段加起来正好 96 位:第一字节 8 位(V/P/X/CC)、第二字节 8 位(M/PT)、序列号 16 位、 时间戳 32 位、SSRC 32 位。 2. 视频为什么用 90000 Hz 做时钟? —— 因为它能被 24、25、30、50、60 全部整除,常见帧率的步长都是整数,不会累积误差。 3. 正常 RTP 包的第一个字节是什么? —— 0x80。版本 2 左移 6 位就是 128,其余三位全 0。抓包时看这个字节就能快速认出 RTP。

下一节让这些包动起来:序列号怎么递增、时间戳每帧加多少、M 位什么时候置一。

相关笔记