🎬 第一章 流媒体与直播基础理论
📄 一句话概括

一条直播链路可以拆成六个模块:采集、编码、封装、推流、分发、播放。每个模块之间都是「字节流」接口,所以可以自由替换——这正是整本书的组织方式。

📝 本节和前面几节的关系

1.3.2 讲了系统的形状(一进多出),这一节讲它的内部怎么切块。切完你会发现,后面每一章都能对上一个模块。

一、六个模块各自的接口

模块能替换的前提,是模块之间的接口足够窄。 这条链路上每一处的接口都只是「字节流 + 时间戳」。

采集 → 编码:原始帧(YUV/PCM)+ pts。 编码 → 封装:压缩后的码流(H.264 NAL/AAC 帧)+ pts。 封装 → 推流:容器包(FLV Tag/TS 包)+ 时间戳。 推流 → 分发:网络字节流。 分发 → 播放:同上,反向。

只要接口不变,任何一层都能换。 这就是为什么「换编码器不用动服务器,换协议不用动编码器」。

反过来说,如果某个系统「换一个编码器就要改一遍服务端」, 那说明它的模块边界没划对——这在自研系统里非常常见。

二、每换一块要付什么

模块能换,不代表换了没代价。每一块都有自己的账:

① 采集:换摄像头/换采集卡,代价最小,但分辨率帧率一改,后面全都得跟着调。 ② 编码:H.264 → H.265 省 30~50% 码率,但编码慢、老设备解不了。换成硬件编码又快又省 CPU,但同码率画质差一点。 ③ 封装:TS → FLV 延迟从 2 秒降到亚秒,但 FLV 不能被 CDN 当静态文件缓存。 ④ 推流:RTMP → SRT/WebRTC,抗弱网更好,但服务器要支持新协议。 ⑤ 分发:单机 → 集群 → CDN,能力上去了,成本和运维复杂度也上去了。 ⑥ 播放:换播放器内核,要处理各浏览器的兼容——iOS Safari 只认 HLS,安卓浏览器要 flv.js。

所以工程上的选型从来不是「哪个技术先进」,而是「哪一层的代价我付得起」。

三、最小可用系统长什么样

如果只想跑通一条直播,最少需要三样东西:

① 一个推流端:OBS 或 FFmpeg 命令行,推 RTMP 到服务器。 ② 一个流媒体服务器:Nginx-rtmp、SRS、ZLMediaKit 任选,负责收流、转封装、分发。 ③ 一个播放端:VLC(拉 RTSP/RTMP)、浏览器(HLS 用 video 标签,FLV 用 flv.js)。

三样凑齐,一条直播就成立了。 第 14 章的综合案例干的就是这件事,只是每一步都讲细了。

而这本书的结构,正好是把这个最小系统逐块放大: 协议(第 2~6 章)讲「③ 服务器跟谁说话」,服务器(第 7~13 章)讲「② 怎么搭」,案例(第 14 章)讲「三样怎么拼起来」。

四、动手 Demo:真跑一条最小链路:编码 → TS → HTTP → 拉流解码

没有摄像头,用合成帧代替采集;没有 Nginx/SRS,用本地 HTTP 服务代替分发。 中间三段(编码、封装、拉流解码)是真的,用的就是 FFmpeg 的库。

import http.server, io, socketserver, threading, numpy as np, av

W, H, N, FPS = 320, 240, 50, 25

def frame(i):
    f = av.VideoFrame(W, H, "yuv420p")
    y = np.zeros((H, W), np.uint8)
    y[:, :] = np.linspace(16, 200, W, dtype=np.uint8)[None, :]
    cx = 20 + (i * 5) % (W - 60)
    y[H // 2 - 30:H // 2 + 30, cx:cx + 40] = 235
    f.planes[0].update(y.tobytes())
    for p in (1, 2):
        f.planes[p].update(bytes([128]) * f.planes[p].buffer_size)
    f.pts = i
    return f

# ① 编码 + 封装
buf = io.BytesIO()
out = av.open(buf, "w", format="mpegts")
st = out.add_stream("libx264", rate=FPS)
st.width, st.height, st.pix_fmt = W, H, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(N):
    for pk in st.encode(frame(i)):
        out.mux(pk)
for pk in st.encode(None):
    out.mux(pk)
out.close()
BLOB = buf.getvalue()

# ② 分发:本地 HTTP
class H(http.server.BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Length", str(len(BLOB)))
        self.end_headers()
        self.wfile.write(BLOB)

class S(socketserver.ThreadingTCPServer):
    daemon_threads = True
    allow_reuse_address = True

srv = S(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()

# ③ 拉流 + 解码
rin = av.open("http://127.0.0.1:%d/live.ts" % srv.server_address[1])
n = sum(1 for _ in rin.decode(rin.streams.video[0]))
rin.close()
srv.shutdown()
print("编了 %d 帧、%d KB;拉回来解出 %d 帧" % (N, len(BLOB) // 1024, n))

实跑输出:

--------------------------------------------------------------------
直播系统的模块——一条真跑的链路有多长
--------------------------------------------------------------------
① 先把六个模块各自做什么写清楚

  模块     输入             输出
  ------------------------------------------
  采集     摄像头/屏幕     原始帧 YUV
  编码     原始帧           H.264 码流
  封装     H.264 码流       TS/FLV 包
  推流     封装好的包       网络字节
  分发     一路输入         N 路输出
  播放     网络字节         屏幕上的画面

② 真跑一条最小链路:编码 → TS → HTTP → 拉流解码

  **这里没有摄像头,用合成帧代替采集;没有 Nginx/SRS,用本地 HTTP 服务代替分发。**
  **中间三段(编码、封装、拉流解码)是真的,用的就是 FFmpeg 的库。**

③ 实跑数字

  环节            结果                说明
  -------------------------------------------------------------
  编码 + 封装     50 帧 / 0.21 秒     42 KB,174 kbps
  拉流 + 解码     50 帧 / 0.03 秒     从 http:// 拉回来再解
  帧数对不对      50 / 50             一帧不多一帧不少

  **一条直播链路就这么长:六段,每段都能单独替换。**
  **把编码器换成 H.265、封装换成 FLV、分发换成 SRS——链路还是这六段。**

④ 每一段「换掉」都会带来什么

  换什么                 换来                    付出
  ------------------------------------------------------------------
  编码 H.264 → H.265     同画质码率省 30~50%     老设备解不了
  封装 TS → FLV          延迟从 2 秒降到亚秒     不能走 CDN 缓存
  传输 RTMP → WebRTC     延迟降到 200 毫秒       并发能力差

  **整本书接下来就是在拆这六段:第 2~6 章拆协议,第 7~13 章拆服务器,**
  **第 14 章把它们拼回一条完整的直播系统。**

解出来的帧数和编进去的一模一样——这就是「封装 + 传输 + 解封装」没丢东西的证据。 这条链路的六段里,编码和封装用的是 FFmpeg 的库,分发用的是 Python 的 HTTP 服务。 换成 Nginx、换成 SRS,接口不变——变的只是「分发」这一段的实现。

30 秒实验

  1. 把 format="mpegts" 改成 "flv",再跑一次——编码那部分完全不用改,这就是模块边界清楚的好处。
  2. 把 libx264 换成 mpeg4,看码率和耗时怎么变——这就是「换编码器」的真实代价。
  3. 在 av.open 前加一句 print(rin.format.name),看 FFmpeg 是怎么自动识别容器格式的。

常见误区

  1. 「六个模块必须按顺序串行」——实际系统里编码和封装可能并行,分发和推流也可能合成一个进程。模块是逻辑划分,不是物理部署。
  2. 「自研系统更可控,所以不用 FFmpeg」——FFmpeg 的坑很多,但自己踩的坑只会更多。除非有硬需求,否则别重写。
  3. 「模块化就是拆成微服务」——模块化是「接口清楚」,拆不拆进程是另一回事。一个进程里也能有清楚的模块边界。
自测三问(点开对答案) 1. 六个模块之间传递的东西,本质上是什么? —— **字节流 + 时间戳。**正因为接口这么窄,每一层才能自由替换。 2. 想支持 H.265,需要改哪几个模块? —— **编码器和播放端。**封装、推流、分发都不关心编码格式——只要容器能标记 codec id 就行。 3. 一个最小可用的直播系统最少要几样东西? —— **三样:推流端、流媒体服务器、播放端。**

相关笔记