从镜头到屏幕有八个环节,延迟是一段一段加出来的。编码只占其中百分之几,真正的大头是播放器「攒」的那几帧和网络上的两个往返。
1.2.3 把协议地图铺开了,这一节把「一条直播」从头走到尾。后面的第 2~13 章,都是在这条链路上挑一段深挖。
一、一条链路的八个环节
把一次直播拆开,从主播到观众要过八个环节:
① 采集:摄像头出 YUV/RGB 原始帧,声音出 PCM。这一层的数据量大得惊人——1080p30 一秒钟 180 MB。 ② 编码:压成 H.264/AAC。这是唯一能把数据量降到 1% 的一步。 ③ 封装:切成 FLV Tag 或 TS 包,加上时间戳与关键帧索引。 ④ 推流:主播端把包发到服务器。这一段走的是主播的【上行】带宽。 ⑤ 分发:服务器把一路流复制给 N 个观众。这是成本最高的一段。 ⑥ 拉流:观众从最近的节点取流。 ⑦ 解码:把 H.264 解回画面。 ⑧ 渲染:送到屏幕,按时间戳对齐音画。
注意 ① 和 ② 之间的落差:180 MB/秒 压到 2 MB/秒,缩了 90 倍。整条链路能成立,全靠这一次压缩。
二、延迟是一段一段加出来的
每一段要么在「攒」,要么在「等」,没有一段是零成本。
① 采集要攒满一帧:25fps 就是 40 毫秒一帧,摄像头再快也得等这一帧凑齐。 ② 编码要等:软编一帧几毫秒到几十毫秒。如果用了 B 帧,还要多等后面的参考帧。 ③ 推流与拉流各一个 RTT:跨省 30 毫秒,跨国 200 毫秒。 ④ 服务端转封装:转协议、切片,几毫秒到几十毫秒。 ⑤ 播放器缓冲:这是最大的一块。为了抗抖动,播放器总要攒几帧到几秒。
这些加起来,就是观众感知到的延迟。 而它们中间任何一段「省一点」,都会换来「更容易卡」。
一个常见的误解是「编码器越快延迟越低」。 实测下来编码只占百分之几——把编码优化到极致,也不如把播放器缓冲减一帧。
三、直播比点播多出来的三件事
点播是「文件已经从服务器上躺着」,直播是「内容正在产生」。这三件事只有直播才要处理:
① 实时性有硬要求。 点播慢 3 秒没人管,直播慢 3 秒就不同步了——弹幕、连麦、答题全都乱套。 ② 不可回看。 点播可以任意 seek,直播只能「从当前往前追一点」。所以直播的容器必须能「从中间开始解」,这对切片设计有要求。 ③ 突发流量。 一场直播开播时可能瞬间涌进几万人,点播的流量却相对平滑。 所以直播系统的扩容必须是「秒级」,而不是「小时级」。 所以直播系统的扩容必须是「秒级」,而不是「小时级」。
这三点决定了直播和点播的技术栈不一样: 点播用 HLS + CDN 就够,直播要考虑边缘回源、首帧优化、秒开、连麦。
四、想低延迟,只能动「攒」和「等」
把延迟拆开看,能动的其实只有两类:
① 少攒(缓冲):播放器缓冲从 3 秒降到 0.5 秒,延迟立降 2.5 秒。代价是网络一抖就卡。 所以低延迟播放器要做「自适应缓冲」——网好就少攒。 ② 少绕(RTT):把服务器搬到观众附近,用边缘节点、用 QUIC 减少握手。
而「编码更快」「封装更小」这些,收益都是毫秒级。 这不是说它们不重要——它们决定了「能不能跑得动」,而缓冲决定了「观众等多久」。
最后还有一招:把「攒」从播放器挪到服务端。 WebRTC 之所以能到 200 毫秒,就是因为它几乎不攒——用 UDP 传,丢了不重传,靠前向纠错和 PLC 补。 代价是弱网下画面会花,但不会卡。
五、动手 Demo:真编一段 720p,再把延迟一段一段加起来
先真测 720p 的编码速度,看它在 40 毫秒的帧预算里占多少; 再把一条链路的六段耗时列出来,看大头到底在哪。
import io, time, numpy as np, av
W, H, N, FPS = 1280, 720, 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 = 60 + (i * 20) % (W - 160)
y[H // 2 - 90:H // 2 + 90, cx:cx + 150] = 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": "28", "preset": "veryfast", "g": "25"}
t0 = time.perf_counter()
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()
enc_ms = (time.perf_counter() - t0) * 1000 / N
kbps = len(buf.getvalue()) * 8 / 1000 / (N / FPS)
print("每帧 %.1f 毫秒,码率 %.0f kbps" % (enc_ms, kbps))
# 把六段耗时加起来
segs = [("采集+编码", 40 + enc_ms), ("推流上行", 30), ("服务端", 5),
("分发下行", 30), ("播放缓冲", 120), ("解码显示", 16)]
tot = sum(x[1] for x in segs)
for name, ms in segs:
print("%-10s %6.1f 毫秒 %4.1f%%" % (name, ms, ms * 100 / tot))
print("合计 %.0f 毫秒" % tot)
实跑输出:
--------------------------------------------------------------------
直播链路——延迟是一段一段【加】出来的
--------------------------------------------------------------------
① 真测一段 720p 的编码速度
**50 帧 1280x720,veryfast 预设、CRF 28。**
**平均每帧 5.8 毫秒,码率 1095 kbps。**
**25 帧每秒的预算只有 40 毫秒,编码只花了 5.8 毫秒。**
**所以这一环不是瓶颈——CPU 软编也够。**
② 一段直播的延迟分给了谁
**从「镜头前的画面」到「观众屏幕」,中间有六段,每段都要么攒、要么等。**
环节 毫秒 钱花在哪
--------------------------------------------
① 采集 + 编码 45.8 凑满一帧
② 推流上行 30.0 一个 RTT
③ 服务端转封装 5.0 切片/转协议
④ 分发下行 30.0 一个 RTT
⑤ 播放器缓冲 120.0 攒 3 帧防抖
⑥ 解码 + 显示 16.0 一帧时间
合计 247 约 0.25 秒
**总延迟 247 毫秒,其中编码只占 5.8 毫秒(2%)。**
**真正的大头是「攒」和「等」:播放器缓冲 120 毫秒 + 两个 RTT 60 毫秒。**
③ 想再快,只能动这两项
**① 少攒**:播放器缓冲从 3 帧降到 1 帧,省 80 毫秒,但网一抖就卡。
**② 少绕**:把服务端和观众放到同一个机房,两个 RTT 从 60 毫秒降到 10 毫秒。
**至于编码——它已经从 40 毫秒的预算里省下 34 毫秒了,再优化也就是几十毫秒的量级。**
编码只占总延迟的百分之几,真正的大头是播放器缓冲(攒 3 帧)和两个网络往返(等)。 所以要降延迟,先减缓冲、再缩距离,最后才轮到优化编码器。 顺序搞反了会很冤:花大力气把编码提速一倍,总延迟只降了几毫秒。
30 秒实验
- 把
segs里的「播放缓冲」从 120 改成 40(攒 1 帧),看总延迟降到多少——这就是低延迟播放器的主要收益来源。 - 把 FPS 从 25 改成 60,看「采集+编码」这一段怎么变。帧率越高,单帧预算越小,但攒一帧的等待也越短。
- 把两个 RTT 从 30 改成 200(跨国),看总延迟翻了几倍——这就是「服务器要离观众近」的量化理由。
常见误区
- 「延迟主要是编码慢造成的」——实测编码只占几个百分点。延迟大头是缓冲和网络往返。
- 「帧率越高延迟越低」——帧率高让「攒一帧」更短,但码率也上去了,弱网下更容易卡。
- 「直播延迟 0 秒最好」——0 秒意味着不缓冲,网一抖就黑屏。工业界普遍接受 1~3 秒,因为观众感知不到这么短的差别。
自测三问(点开对答案)
1. 一条直播链路的八个环节,哪一段成本最高? —— **分发(把一路流复制给 N 个观众)。**它随观众数线性增长。 2. 想让延迟从 3 秒降到 1 秒,最先该动哪一项? —— **播放器缓冲。**它通常占了 1~2 秒,是所有环节里最大且最好调的一块。 3. 为什么 WebRTC 能做到 200 毫秒却不保证画质? —— **因为它用 UDP、不重传,靠前向纠错和丢包隐藏补。**丢得多了画面会花,但不会停下来等。- ☐ (本节完,下一节 1.3.2 通用的视频直播模型)