📹 第二章 VLC及FFplay流媒体播放器
📄 一句话概括

播放器和服务端是同一套流水线的两个方向:一个收,一个发。 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 秒实验

  1. 把 PT 改成 97,在 SDP 里也把 rtpmap 改成 97,看能不能对上——载荷类型不是固定的, 是两边谈出来的。
  2. 把时间戳改成每包 +33 再跑,然后用「差值 ÷ 90000」算出一秒该有多少帧,看差多少。
  3. 把序列号故意跳一个号(跳过 5),体会接收端怎么算出「丢了一个包」。

常见误区

  1. 以为时间戳单位是毫秒。 单位是采样点,H.264 下 1 秒 = 90000。按毫秒算,播放速度会差 90 倍。
  2. 以为推流只要发媒体包。 不发 RTCP 报告,对端几十秒后就判定流结束,而发端毫无察觉。
  3. 以为端口可以随便挑。 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 把整段交互录下来,逐包看清楚每条报文长什么样。

相关笔记