🎬 第七章 流媒体开源库简介
📄 一句话概括

WebRTC 的视频链路有六段,延迟大头在抖动缓冲上。 帧大小极不均匀,是必须有缓冲的根本原因。

📝 本节和前面几节的关系

7.7.1 讲了整体架构,这一节钻进视频这一条链路。 7.7.3 讲音频,两者在缓冲策略上正好相反。

一、六段链路

① 采集:摄像头出原始帧,常见 YUV420。 ② 预处理:缩放、去噪、降帧率。这一步决定编码难度。 ③ 编码:VP8/VP9/H.264/AV1,WebRTC 全支持。 ④ 打包传输:切成 RTP 包,走 SRTP 加密。 ⑤ 抖动缓冲:把乱序与抖动抹平,这是延迟的主要来源。 ⑥ 解码呈现:解出帧送到屏幕。

六段里只有第 ⑤ 段是「故意引入延迟」的: 其它五段都想越快越好,只有它必须等一等。 因为网络到达时间天然不均匀,不等就会卡。

二、帧大小为什么极不均匀

实测一段 50 帧的视频:平均 65 字节、标准差 134 字节。 标准差比平均值还大一倍,说明分布极不均匀。 关键帧 2 个,最大帧 968 字节,而最小帧只有 37 字节。

这种不均匀在真实场景里更夸张: 静止画面一帧可能只有几百字节,镜头一晃就是几十 KB。 所以「按平均码率预留带宽」一定会出问题—— 必须按峰值留余量,或者让编码器做码率控制。

顺带一个观测技巧: 看「最大帧/最小帧」的比值就能粗判码率模式。 比值大是变码率(CRF),接近 1 说明可能开了 CBR。

三、抖动缓冲要多大

缓冲太小会卡顿,太大延迟就上去了。 常见做法是按「网络抖动的 2 到 4 倍」自适应。

实测这组换算:抖动 10 毫秒时缓冲 20 到 40 毫秒, 抖动 100 毫秒时缓冲 200 到 400 毫秒。 所以 WebRTC 的端到端延迟通常在 100 到 300 毫秒—— 一半是编码与网络,另一半就是这个缓冲。

为什么不能取 1 倍? 因为抖动是统计量,1 倍意味着「有一半的包会晚到」,必然卡。 取 2 倍能覆盖大多数情况,取 4 倍更稳但延迟翻倍。 所以这是「卡顿率」与「延迟」之间的一条曲线,没有最优只有取舍。

四、动手 Demo:视频分析——延迟花在抖动缓冲上

真编 50 帧,量帧大小的平均值、标准差与极值。 再算抖动缓冲该给多大,看延迟是怎么堆到 300 毫秒的。

import av, numpy as np, tempfile, os

p = os.path.join(tempfile.mkdtemp(), "a.mp4")
c = av.open(p, "w", format="mp4")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(50):
    f = av.VideoFrame(320, 240, "yuv420p")
    y = np.zeros((240, 320), np.uint8)
    y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
    f.planes[0].update(y.tobytes())
    for pi in (1, 2):
        f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
    f.pts = i
    for pk in st.encode(f):
        c.mux(pk)
for pk in st.encode(None):
    c.mux(pk)
c.close()

sizes, keys = [], 0
with av.open(p) as c2:
    for pk in c2.demux(video=0):
        if not pk.size:
            continue
        sizes.append(pk.size)
        if pk.is_keyframe:
            keys += 1
avg = sum(sizes) / len(sizes)
sd = (sum((x - avg) ** 2 for x in sizes) / len(sizes)) ** 0.5
print("%d 帧,平均 %.0f、标准差 %.0f、最大 %d"
      % (len(sizes), avg, sd, max(sizes)))
print("关键帧 %d 个" % keys)

for j in (10, 20, 50, 100):
    print("抖动 %3d 毫秒 -> 缓冲 %d~%d 毫秒" % (j, j * 2, j * 4))

实跑输出:

--------------------------------------------------------------------
视频分析——延迟花在抖动缓冲上
--------------------------------------------------------------------
**① 真编一段,看帧大小的抖动**

  **50 帧、平均 65 字节、标准差 134 字节。**
  **关键帧 2 个,最大帧 968 字节。**

  **标准差比平均值还大一倍,说明帧大小极不均匀。**
  **这就是必须要有抖动缓冲的原因:网络到达速率会跟着帧大小一起抖。**

**② 抖动缓冲要多大**

  **缓冲太小会卡顿,太大延迟就上去了。**
  **常见做法是按「网络抖动的 2 到 4 倍」自适应。**

  网络抖动     缓冲 2 倍     缓冲 4 倍
  ----------------------------------------
  10 毫秒      20 毫秒       40 毫秒
  20 毫秒      40 毫秒       80 毫秒
  50 毫秒      100 毫秒      200 毫秒
  100 毫秒     200 毫秒      400 毫秒

  **所以 WebRTC 的端到端延迟通常在 100 到 300 毫秒——**
  **一半是编码与网络,另一半就是这个缓冲。**

标准差 134 大于平均值 65,这条流的大小分布非常散。 关键帧 2 个对应 GOP 25,50 帧正好两个关键帧。 缓冲按抖动的 2 到 4 倍给,这个倍数不是理论最优,是工程折中。

30 秒实验

  1. 把 crf 从 30 改成 18,看标准差——会变小,高质量编码下帧大小更均匀。
  2. 把 GOP 从 25 改成 5,看关键帧数与平均值——关键帧变多、平均值变大,因为关键帧本身大。
  3. 把抖动设成 200 毫秒,看缓冲——400 到 800 毫秒,这已经不适合交互场景了。

常见误区

  1. 「缓冲越大越好」——会稳但延迟翻倍,交互场景不能这么干。
  2. 「按平均码率留带宽就够」——帧大小波动极大,要按峰值。
  3. 「WebRTC 延迟主要在网络」——大头在抖动缓冲。
自测三问(点开对答案) 1. WebRTC 视频链路的六段是什么? —— **采集、预处理、编码、打包传输、抖动缓冲、解码呈现。** 2. 为什么必须有抖动缓冲? —— **网络到达时间不均匀**,不等就会因为乱序与抖动卡顿。 3. 缓冲取抖动的几倍? —— **2 到 4 倍**,1 倍必然卡,4 倍更稳但延迟更高。

相关笔记