Live555 把本地文件当直播流发,靠的不是「读得快」,而是「按帧率等」。 它读一帧、发一帧、再睡到下一帧该发的时间。
第 7 章讲了 Live555 的定位,这一节讲它怎么用。 8.1.2 是它的客户端 openRTSP,8.2 讲编译,8.3 拆它的点播流程。
一、它到底是个什么东西
一句话: 一套 C++ 写的库,帮你把 RTSP 服务端和客户端搭起来。
它不是「一个服务器软件」,而是「做服务器用的零件」。
官方网站给的是源码包,里面除了库还有几个示例程序:
testOnDemandRTSPServer 是点播服务端,openRTSP 是客户端。
这几个示例程序本身就是能用的:
编译完直接跑 testOnDemandRTSPServer,
它就会在 8554 端口上开一个服务,把几个样例文件挂上去。
再用 VLC 打开 rtsp://本机IP:8554/h264ESVideoTest 就能看。
它的设计有个很鲜明的特点:单线程。 整个服务器跑在一个事件循环里, 所有连接、所有会话都在这一个线程上排队处理。 好处是没有锁、行为可预测;坏处是一个慢操作会拖住所有人。
二、本地文件怎么变成「实时流」
这是本节最容易想错的地方。 直觉上「推流」就是把文件读出来赶紧发出去—— 但那样一秒钟就能把十分钟的片子发完,播放器直接卡死。
真实做法是「按时间发」: 读一帧、算它该在第几毫秒出现、看现在到没到, 没到就等,到了才发。 这样一小时的片子就正好发一小时。
所以「本地视频推流」这个名字其实有点误导: 它推的不是「文件内容」,而是「文件内容配上一条时间轴」。 播放器看到的是「第 0 秒、第 0.04 秒、第 0.08 秒各有一帧」, 而不是「这里有 140 KB 数据」。
这条时间轴的载体就是 RTP 头里的时间戳。 H.264 用 90000 的时钟,25 fps 就是每帧加 3600。 时间戳错一位,播放器就会把画面放快或放慢。
三、四个角色接力
读文件、切帧、打包、发走是四件独立的事,各有各的类。
① 会话层:OnDemandServerMediaSubsession 管「有人点播才建会话」。
没人看就什么都不做。
② 读文件:H264VideoFileServerMediaSubsession 按帧率吐数据。
它不知道网络的事。
③ 切 NAL:H264VideoStreamFramer 找出每个 NAL 的边界。
关键帧在哪、SPS 在哪,都是它标出来的。
④ 打包:H264VideoRTPSink 加 RTP 头,超 MTU 就切 FU-A。
它是唯一碰网络的环节。
这种拆法最大的好处是「换一个不牵动一片」: 换编码格式只改 ②③④,换传输协议只改 ④, 要改成读摄像头而不是读文件,只改 ②。
四、动手 Demo:本地视频推流——把文件变成一串 RTP 包
先看 NAL 有多大,再真打包、真走 UDP、真收回来重组。 最后算清 RTP 头占了多少。
import socket, struct, threading, zlib
MTU, SSRC = 1400, 0x12345678
def rtp_hdr(seq, ts, marker, pt=96):
return struct.pack("!BBHII", 0x80,
(0x80 if marker else 0) | pt,
seq & 0xFFFF, ts & 0xFFFFFFFF, SSRC)
def pack(nals, ts, seq0):
out, seq = [], seq0
for i, nal in enumerate(nals):
last = (i == len(nals) - 1)
if len(nal) <= MTU:
out.append((seq, ts, False, nal))
seq += 1
continue
body, chunk = nal[1:], MTU - 2
k = (len(body) + chunk - 1) // chunk
for j in range(k):
ind = (nal[0] & 0xE0) | 28
fu = (0x80 if j == 0 else 0) | (0x40 if j == k - 1 else 0)
fu |= (nal[0] & 0x1F)
out.append((seq, ts, last and j == k - 1,
bytes([ind, fu]) + body[j*chunk:(j+1)*chunk]))
seq += 1
return out
def unpack(items):
out, cur = [], None
for _seq, _ts, _mk, pl in items:
if (pl[0] & 0x1F) == 28:
if pl[1] & 0x80:
cur = bytearray([(pl[0] & 0xE0) | (pl[1] & 0x1F)])
cur += pl[2:]
elif cur is not None:
cur += pl[2:]
if (pl[1] & 0x40) and cur is not None:
out.append(bytes(cur))
cur = None
else:
out.append(pl)
return out
def crc(nals):
buf = b"".join(len(x).to_bytes(4, "big") + x for x in nals)
return zlib.crc32(buf) & 0xFFFFFFFF
实跑输出:
--------------------------------------------------------------------
本地视频推流——把文件变成一串 RTP 包
--------------------------------------------------------------------
**① Live555 里,这件事由四个角色接力**
**读文件、切帧、打包、发走,是四件独立的事,各有各的类。**
角色 Live555 里的类 干什么
---------------------------------------------------------------------------------
会话 OnDemandServerMediaSubsession 有人点播才建会话
读文件 H264VideoFileServerMediaSubsession 按帧率把文件当实时流吐
切 NAL H264VideoStreamFramer 找出每个 NAL 的边界
打包 H264VideoRTPSink 加 RTP 头,超 MTU 就切 FU-A
**② 先看 NAL 到底有多大**
**同一个编码器、同样的内容,分辨率一换,包的大小就变了性质。**
分辨率 文件字节 NAL 个数 最大 NAL 超 1400 的
------------------------------------------------------------------
320x240 10672 76 837 0
1280x720 142021 201 3935 25
**320x240 一个都不用切,1280x720 有 25 个必须切。**
**这就是 FU-A 存在的理由:**
**以太网一个帧最多 1500 字节,扣掉 IP 头 20 与 UDP 头 8,**
**真正能装的载荷只剩 1472,工程上再留点余量取 1400。**
**画面一大,关键帧必然超,不切就发不出去。**
**③ 真打包装、真走 UDP、真收回来重组**
**这一遍用 720p 的码流,按 MTU 1400 切,发到本机环回。**
**收端只按序号拼,别的什么都不知道——和真实播放器一样。**
项 数值
------------------------------
原始 NAL 201 个
原始字节 140252
RTP 包数 242
发出去 143263 字节
收到 242 包
丢掉 0 包
重组出 NAL 201 个
原始 CRC32 0x7051AD8B
重组 CRC32 0x7051AD8B
一致 是
**④ 这笔账要算清楚:RTP 头到底占多少**
**载荷 140252 字节,RTP 头 2904 字节,占 2.1%。**
**要是把 UDP 与 IP 头也算上,再加 6776 字节。**
**这一点开销换来的是「一个包丢了不影响别的包」——**
**对直播来说,这个交换永远划算。**
**注意 RTP 头是「每包 12 字节」而不是「每帧 12 字节」:**
**切得越碎,头越多。所以 MTU 不能调得太小。**
**调到 500 的话,包头比例会翻三倍——这也是个成本。**
关键在 28 这个数字,它是 FU-A 的类型号,代表「这一包是碎片」。 S 位标第一片、E 位标最后一片,中间那些两位置都是 0。 重组时只要按 S 开头、E 收尾,碎片就自己接回原来的 NAL。 CRC 对上了说明整条链路一个字节都没错,这是最硬的证据。
30 秒实验
- 把 MTU 改成 500,看包数和头开销怎么变——包数涨到三倍,RTP 头占比也跟着涨到三倍。
- 把分辨率换成 320x240,看还有几个 NAL 需要切——一个都没有,MTU 再小也不用 FU-A。
- 把 25 fps 改成 30 fps,看时间戳步长——90000 除以 30 是 3000,不再是 3600。
常见误区
- 「推流就是把文件读出来发」——要按帧率等,不然一秒发完。
- 「Live555 是多线程的」——单线程事件循环,没有锁。
- 「一个 NAL 一定是一个包」——关键帧几乎一定超 MTU。
自测三问(点开对答案)
1. 为什么不能把文件一口气读完就发? —— **播放器会按时间戳播放**,提前发只会把缓冲区撑爆。 2. FU-A 的 S 与 E 两位分别标什么? —— **S 标第一片,E 标最后一片。** 3. 25 fps 时 RTP 时间戳每帧加多少? —— **90000 除以 25 等于 3600。**- ☐ (本节完,下一节 8.1.2 openRTSP 客户端流程)