播放器和服务端是同一套流水线的两个方向:一个收,一个发。
VLC 两种身份都能当,区别只是在 --sout 那一行参数。
上一节是「客户端拉流」,这一节反过来——自己造一路流出去。 实战价值很高:设备没到货、要联调多路、要压测,都靠这一步。 RTP 包的逐字段结构在第一章 1.6,这一节管「怎么把包发对」。
一、三种输出方式,选哪个
VLC 往外送流有三大类输出,用错会浪费一天。
| 方式 | 地址写法 | 适合什么 |
|---|---|---|
| UDP 裸流 | udp://@:1234 |
局域网调试,谁都能收 |
| RTP 封装 | rtp://239.0.0.1:5004 |
标准 RTP,能抓包分析 |
| RTSP 服务 | --sout-rtsp 开一个内置服务端 |
让别的播放器来拉 |
调试期最省事的是第一行:一句 --sout udp://@:1234,另一台机器 vlc udp://@:1234 就能看到,
不用任何握手。
代价是没有会话管理:谁都能收、什么时候开始收都行,也就没法暂停、没法拖进度。
要验 RTP 就得用第二行,因为裸 UDP 里没有 RTP 头,抓包只能看到一堆 H.264 字节, 看不到序列号和 SSRC。
二、RTP 的三个字段,一个都不能错
D22 自己造包发出去再收回来,验的就是这三个字段。
| 字段 | 位宽 | 错了会怎样 |
|---|---|---|
| 序列号 | 16 位 | 乱序可重排,丢了能算出来 |
| 时间戳 | 32 位 | 同步全靠它,错了就音画错位 |
| SSRC | 32 位 | 多路混一起就分不开 |
时间戳的增量是最容易算错的一处:H.264 的时钟是 90 kHz,30 fps 每帧加 3000(90000÷30)。 不是加 33——33 是毫秒,可 RTP 时间戳的单位是采样点,不是毫秒。
音频又是另一套账:48 kHz 时钟,一个包装 1024 个采样,那增量就是 1024。 两路时钟频率不同,所以时间戳不能直接比大小,要各自换算成秒再比。
三、端口是怎么分的
一对端口管一路流,RTP 占偶数、RTCP 占下一个奇数。
| 端口 | 干什么 | 谁分配 |
|---|---|---|
| 5004(偶) | 收 RTP 媒体包 | 客户端在 SETUP 里提 |
| 5005(奇) | 收 RTCP 报告 | 跟着 RTP 自动 +1 |
| 554 | RTSP 信令,固定 | 服务端 |
「偶数是 RTP、奇数是 RTCP」是约定,不是协议强制,但所有实现都这么干,
破坏它会在某些设备上直接不通。
所以客户端在 SETUP 里写 client_port=8000-8001,一次说一对,服务端照着回一对。
四、推流这一侧最容易忽略的
发端不发保活,收端就以为你走了。 这一条坑过很多人。
| 问题 | 表现 | 原因 |
|---|---|---|
| 没发 RTCP | 几十秒后对方断开 | 收端按 RTCP 判断存活 |
| 序列号不连续 | 收端算出一堆丢包 | 跳号被当成丢包 |
| 时间戳跳变 | 画面卡一下再追上 | 收端做了时钟校正 |
第一行在自研推流时最常踩:只发媒体包、不发 RTCP,对端过了超时时间就判定流结束, 而发端一点异常都看不到。 这就是 D22 那种「收十个包就够」的实验和真业务的差别:真业务里包要一直发,报告也要一直发。
五、动手 Demo:自己造 RTP 包打出去,再拆回来验
用两个 UDP socket,一端按 RTP 头格式造包发出去,另一端拆开逐字段验。 头部十二字节全是按位拼的,拼错一位,抓包软件就显示成畸形包。
SSRC = 0x0A0B0C0D
PT = 96 # 载荷类型,SDP 里 rtpmap 对应的那个数
ts0 = 90000 # 起始时间戳,随便挑
for i in range(10):
b0 = (2 << 6) | (0 << 5) | (0 << 4) | 0 # V=2 P=0 X=0 CC=0
b1 = (0 << 7) | PT # M=0 PT=96
hdr = struct.pack("!BBHII", b0, b1,
0x1000 + i, # 序列号
ts0 + i * 3000, # 时间戳,每包 +3000
SSRC)
tx.sendto(hdr + bytes(80), ("127.0.0.1", rport))
# 收端
b0, b1, seq, ts, ssrc = struct.unpack("!BBHII", data[:12])
ver = b0 >> 6 # 版本必须是 2
pt = b1 & 0x7F # 载荷类型
实跑输出:
--------------------------------------------------------------------
自己造 RTP 包打出去,验端口、SSRC、序列号
--------------------------------------------------------------------
**播放器拉流是「别人发我收」,推流是「我发别人收」——RTP 的格式完全一样。**
这一节在本机 UDP 上把包真打过去,接收端拆开验。
发端:SSRC=0x0A0B0C0D,载荷类型 96,时间戳每包加 3000
收端:**127.0.0.1:57754**,共收到 10 个包
序号 序列号 时间戳 SSRC 版本 字节
-------------------------------------------------------------
1 4096 90000 0A0B0C0D 2 92
2 4097 93000 0A0B0C0D 2 92
3 4098 96000 0A0B0C0D 2 92
4 4099 99000 0A0B0C0D 2 92
5 4100 102000 0A0B0C0D 2 92
6 4101 105000 0A0B0C0D 2 92
**三处自洽**:序列号从 4096 连到 4105,差 9,中间没断;
时间戳从 90000 到 117000,每包稳定加 3000;SSRC 十个包全一样。
**三个字段各管一件事,缺一不可:**
字段 管什么 乱了会怎样
-----------------------------------------------------------------
序列号 包序号,16 位循环 乱序能重排,丢了能发现
时间戳 这一帧的播放时刻 同步全靠它,错了就音画不同步
SSRC 这一路流的编号 多路混在一起时分不开
**注意时间戳加 3000 不是加 33**:H.264 的时钟是 90 kHz,30 fps 每帧 = 90000 ÷ 30 = **3000**。
起始值 90000 是随便挑的(RTP 建议随机起,防止被猜),接收端只看相邻差值。
换成音频就另一套账:48 kHz 时钟,一个包装 1024 个采样,那增量就是 **1024**,不是 3000。
**时钟频率不同,增量就不同——这是 RTP 里最容易算错的一处。**
序列号从 4096 连到 4105,时间戳每包 +3000,SSRC 十个全一样——三处自洽,说明头部拼对了。 起始序列号是 0x1000 随便挑的,RTP 建议随机起,防止外人猜出流的结构。 接收端只看相邻差值,不看绝对值。
这一节最该记住的是「时间戳增量 = 时钟频率 ÷ 帧率」这个公式。90 kHz ÷ 30 fps = 3000,一步算完。 换成 25 fps 就是 3600,换成音频 48 kHz、每包 1024 采样就是 1024。公式一样,代入的数不同。
还有一处容易忽略:载荷那 80 个字节是假的。真流里这里应该是 H.264 的 NAL 单元, 而且一个 NAL 大于 MTU 时还要切成 FU-A 分片——那是 1.6.1 的内容。
30 秒实验
- 把
PT改成 97,在 SDP 里也把 rtpmap 改成 97,看能不能对上——载荷类型不是固定的, 是两边谈出来的。 - 把时间戳改成每包 +33 再跑,然后用「差值 ÷ 90000」算出一秒该有多少帧,看差多少。
- 把序列号故意跳一个号(跳过 5),体会接收端怎么算出「丢了一个包」。
常见误区
- 以为时间戳单位是毫秒。 单位是采样点,H.264 下 1 秒 = 90000。按毫秒算,播放速度会差 90 倍。
- 以为推流只要发媒体包。 不发 RTCP 报告,对端几十秒后就判定流结束,而发端毫无察觉。
- 以为端口可以随便挑。 RTP 与 RTCP 成对出现,破坏「偶 RTP、 奇 RTCP」的约定会在部分设备上直接不通。
自测三问(点开对答案)
1. 30 fps 的 H.264,RTP 时间戳每帧加多少? —— 90000 ÷ 30 = 3000。90000 是 H.264 的时钟频率,不是毫秒。 2. 客户端 SETUP 写了 `client_port=8000-8001`,两个端口分别干嘛? —— 8000 收 RTP 媒体包,8001 收 RTCP 报告。成对分配是约定。 3. 为什么自研推流要定期发 RTCP? —— 接收端靠 RTCP 判断发送端还活着。 只发媒体不发报告,对端超时后会主动断开,而发端看不到任何异常。下一节回到「看」这一侧:用 pcap 把整段交互录下来,逐包看清楚每条报文长什么样。
- ☐ (本节完,下一节 02.01.05 使用Wireshark抓包分析RTSP交互流程)