🎬 第八章 Live555搭建直播平台
📄 一句话概括

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 秒实验

  1. 把 MTU 改成 500,看包数和头开销怎么变——包数涨到三倍,RTP 头占比也跟着涨到三倍。
  2. 把分辨率换成 320x240,看还有几个 NAL 需要切——一个都没有,MTU 再小也不用 FU-A。
  3. 把 25 fps 改成 30 fps,看时间戳步长——90000 除以 30 是 3000,不再是 3600。

常见误区

  1. 「推流就是把文件读出来发」——要按帧率等,不然一秒发完。
  2. 「Live555 是多线程的」——单线程事件循环,没有锁。
  3. 「一个 NAL 一定是一个包」——关键帧几乎一定超 MTU。
自测三问(点开对答案) 1. 为什么不能把文件一口气读完就发? —— **播放器会按时间戳播放**,提前发只会把缓冲区撑爆。 2. FU-A 的 S 与 E 两位分别标什么? —— **S 标第一片,E 标最后一片。** 3. 25 fps 时 RTP 时间戳每帧加多少? —— **90000 除以 25 等于 3600。**

相关笔记