RTP 的包头固定 12 字节,剩下的全是负载。 时间戳不是帧号,是 90 kHz 时钟的刻度——25 帧一秒,每帧 3600 刻。
8.1 里 RTSP 只负责下指令,真正跑画面的是 RTP。 这一节把 RTP 的包头拆开看,8.2.2 与 8.2.3 再把两端接起来。
一、12 字节的包头
RTP 的头特别短,因为它的设计目标就是「每包只多加一点点开销」。 12 个字节里塞了九个字段:
字段 偏移 长度 本例取值
版本 V 0 2 位 2 填充 P 0 1 位 0 扩展 X 0 1 位 0 CSRC 数 0 4 位 0 标记 M 1 1 位 1 载荷类型 PT 1 7 位 33=MPEG-TS 序号 2 2 字节 从 1 往上加 时间戳 4 4 字节 90 kHz 刻度 同步源 SSRC 8 4 字节 0x55667788
前四个字段挤在第一个字节里,靠位数区分——这是省字节的经典做法。 版本必须是 2,写别的服务器直接丢包。
标记 M 表示「这是这一帧的最后一个包」。 一帧被拆成好几个 RTP 包时,接收端靠它判断帧的边界。
载荷类型 PT 决定「里面装的是什么」。33 是 MPEG-TS 的固定编号,
96 以上是动态编号,含义要由 SDP 里 a=rtpmap 那一行说明。
二、时间戳不是帧号
这是最容易想错的一处:RTP 的时间戳不是「第几帧」,是「第几个刻度」。 视频统一用 90 kHz 的时钟:
项 值
时钟频率 90000 Hz 25 帧一秒 每帧 3600 刻 一秒之后 加了 90000
3600 就是 90000 除以 25。换成 30 帧,每帧就是 3000 刻。 所以从时间戳能反推出帧率,也能反推出两帧之间隔了多少秒。
音频那边不用 90 kHz,一般直接用采样率当刻度(48 kHz 就是 48000 刻一秒)。 所以同一时刻,音视频两条流的时间戳数值完全不同——不能直接比大小。
要对齐得靠 RTCP 的 SR 包(Sender Report),它里面有「这一刻的 NTP 时间」 与「对应的 RTP 时间戳」,两条流各自换算到墙上时间才能同步。
三、一个包能装多少
RTP 头 12 字节,负载上限取 1400,加起来 1412。再加 IP/UDP 的 28 字节:
项 值
每包总长 1412 字节 其中 RTP 头 12 字节 其中负载 1400 字节 加 IP/UDP 头 1440 字节
1440 离 1500 还有 60 字节余量,所以不会触发 IP 分片。 这就是 1400 这个数的来历:不是标准规定的,是工程上算出来的安全值。
为什么怕分片:一个 IP 包被切开后,只要丢其中一片,整包就废了。 分片还让丢包概率变高——本来是「要么全到要么不到」,变成「可能只到一半」。
所以超过 1400 的帧不能硬塞,得在 RTP 这一层自己拆(8.1.3 讲的 FU-A)。 这种事交给协议自己做,比让 IP 层去分片可控得多。
四、RTP 不保证任何事
这句话要记住:RTP 本身不保证送达、不保证顺序、不保证不重复。 它跑在 UDP 上,UDP 什么样它就什么样。
担心的事 RTP 管不管 谁来管
丢包 不管 上层等关键帧 乱序 给序号,不重排 接收端按序号排 重复 给序号,不去重 接收端自己去重 延迟抖动 给时间戳 播放端缓冲
RTP 只做一件事:把「这是第几个包、属于哪一刻、是什么类型」写在头上。 剩下的全靠接收端自己扛——所以播放器的缓冲策略比协议本身更影响观感。
这也解释了为什么同样的流,换个播放器体验差很多: 协议给的信息是一样的,缓冲与丢包补偿的做法不一样。
五、动手 Demo:打包、拆包、真发一遍
下面这段代码先按字段拼一个 RTP 头,再拆回来验证; 然后用 UDP 真发五个包,量每包长度。
import socket, struct
def rtp_hdr(seq, ts, ssrc=0x55667788, pt=33, marker=False):
return struct.pack(">BBHII", 0x80,
(0x80 if marker else 0) | pt,
seq & 0xFFFF, ts & 0xFFFFFFFF, ssrc)
h = rtp_hdr(1, 90000)
seq, ts, ssrc = struct.unpack(">HII", h[2:12])
print(u"头 %d 字节 seq=%d ts=%d ssrc=0x%08X" % (len(h), seq, ts, ssrc))
port = 25000
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.bind(("127.0.0.1", port))
rx.settimeout(2.0)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(5):
pkt = rtp_hdr(i + 1, 90000 + i * 3600, marker=(i == 4)) + b"\x00" * 1400
tx.sendto(pkt, ("127.0.0.1", port))
for _ in range(5):
d, _a = rx.recvfrom(4096)
print(u"收到 %d 字节" % len(d))
实跑输出:
--------------------------------------------------------------------
8.2.1 RTP 简介
--------------------------------------------------------------------
**① RTP 的包头固定 12 字节,一个字段都不多**
字段 偏移 长度 本例取值
-----------------------------------------------------
版本 V 0 2 位 2
填充 P 0 1 位 0
扩展 X 0 1 位 0
CSRC 数 0 4 位 0
标记 M 1 1 位 1
载荷类型 PT 1 7 位 33(MPEG-TS)
序号 2 2 字节 从 1 往上加
时间戳 4 4 字节 按 90 kHz 走
同步源 SSRC 8 4 字节 0x55667788
**② 打包再拆包,一个字节都不差**
项 值
---------------------------------
包头长度 12 字节
解回来的序号 1
解回来的时间戳 90000
解回来的 SSRC 0x55667788
**③ 真正发出去:一个包 1412 字节**
项 值
----------------------------------------------
收到包数 5
每包总长 1412 字节
其中 RTP 头 12 字节
其中负载 1400 字节
加 IP/UDP 头后 1440 字节(没到 1500)
**负载 1400 是个工程约定:1400 + 12 + 28 = 1440,**
**给 IP 选项与隧道封装留了 60 字节余量,不会触发分片。**
**④ 时间戳不是「第几帧」,是「第几个 90 kHz 的刻度」**
项 值
------------------------------
时钟频率 90000 Hz
25 帧一秒 每帧 3600 刻
一秒之后 加了 90000
**90 kHz 是视频的统一时钟。音频那边常用 48 kHz 或采样率本身。**
**所以同一个时刻,音视频两条流的时间戳数值完全不同——**
**播放器要拿 RTCP 的 SR 包去对齐,不能直接比数字。**
头正好 12 字节,拆回来 seq=1、ts=90000、ssrc=0x55667788,一位不差。
五个包每个都是 1412 字节(12 加 1400 负载)。 这个数就是「RTP 头上限 + 一个安全负载」的结果,不是随便挑的。
注意时间戳每个包加了 3600——也就是 25 帧一秒的每一帧。 这一行是整个 RTP 时间轴的缩影:帧与帧之间差 3600 刻。
30 秒实验
- 改帧率:把 3600 换成 3000(30 帧),看时间戳推进变快。
- 把负载加到 2000:总长变 2012,超过 1500,就等着 IP 分片了。
- 改 PT:把 33 改成 96,观察字段位有没有被影响(7 位够放 96)。
常见误区
- 以为时间戳是帧号。它是刻度,3600 刻一帧。
- 以为 RTP 会重传。它只管标记,不管送达。
- 负载往 1500 上顶。加上头就超了,会触发分片。
自测三问(点开对答案)
1. RTP 包头多长? —— 12 字节,九个字段,前四个挤在同一字节里。 2. 25 帧一秒,每帧时间戳加多少? —— 3600 刻(90000 除以 25)。 3. RTP 保证送达吗? —— 不保证,它只给序号与时间戳,补偿是上层的事。- ☐ (本节完,下一节 8.2.2 VLC 作为 RTP 流媒体服务器)