一条直播链路可以拆成六个模块:采集、编码、封装、推流、分发、播放。每个模块之间都是「字节流」接口,所以可以自由替换——这正是整本书的组织方式。
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 秒实验
- 把
format="mpegts"改成"flv",再跑一次——编码那部分完全不用改,这就是模块边界清楚的好处。 - 把
libx264换成mpeg4,看码率和耗时怎么变——这就是「换编码器」的真实代价。 - 在
av.open前加一句print(rin.format.name),看 FFmpeg 是怎么自动识别容器格式的。
常见误区
- 「六个模块必须按顺序串行」——实际系统里编码和封装可能并行,分发和推流也可能合成一个进程。模块是逻辑划分,不是物理部署。
- 「自研系统更可控,所以不用 FFmpeg」——FFmpeg 的坑很多,但自己踩的坑只会更多。除非有硬需求,否则别重写。
- 「模块化就是拆成微服务」——模块化是「接口清楚」,拆不拆进程是另一回事。一个进程里也能有清楚的模块边界。
自测三问(点开对答案)
1. 六个模块之间传递的东西,本质上是什么? —— **字节流 + 时间戳。**正因为接口这么窄,每一层才能自由替换。 2. 想支持 H.265,需要改哪几个模块? —— **编码器和播放端。**封装、推流、分发都不关心编码格式——只要容器能标记 codec id 就行。 3. 一个最小可用的直播系统最少要几样东西? —— **三样:推流端、流媒体服务器、播放端。**- ☑ (第一章完;下一章 2.1.1 RTSP支持)