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 秒实验
- 用 struct 造一个 RTP 包,把前两个字节打印成二进制,数一下各字段的位置。
- 把 PT 改成 97,看第一个字节变不变、第二个字节怎么变。
- 把序列号设成 65535,再加一看会不会变成 0。
常见误区
- 以为 RTP 头是变长的。 基础头固定 12 字节,只有带扩展(X=1)或 CSRC(CC>0)时才变长。
- 以为时间戳是毫秒。 是采样时钟,视频 90000 Hz、音频按采样率。用毫秒算会导致累积误差。
- 以为 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 位什么时候置一。
- ☐ (本节完,下一节 01.06.02 RTP的会话过程)