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

编解码、封装、传输、会话是四层叠加,不是四个并列的选项。画质由编码器决定,协议只管怎么送——所以「哪个协议画质好」这个问题本身是错的。

📝 本节和前面几节的关系

1.2.1 讲了连接怎么建。但连接只是「路」,路上跑什么还得定义。这一节先把「跑的内容」分成四层,后面第 2~6 章再一层一层拆开。

一、四层叠加,不是并列

很多资料把 RTMP、HLS、H.264 并列成「几种视频格式」,这是把不同层次的东西混在一起了。

① 编码层:把画面压成码流。H.264、H.265、AV1。 ② 封装层:把码流切成包,加时间戳、加索引。TS、FLV、MP4、MKV。 ③ 传输层:包怎么在网络上走。TCP、UDP、QUIC。 ④ 会话层:怎么开始、暂停、停止、定位。RTSP、RTMP、HLS、HTTP-FLV。

四层是自由组合的:同一条 H.264 码流,装进 TS 走 RTSP 也行,装进 FLV 走 RTMP 也行。甚至同一个 HLS 里,也可以同时塞 H.264 和 H.265 两份。

理解这个分层,后面看协议就不会乱: RTMP 规定的只是第 ③④ 层,它完全不关心你编的是 H.264 还是 H.265。

二、I 帧、P 帧、B 帧:压缩靠什么

视频能压到几百分之一,靠的是「大部分画面没变」。

I 帧(帧内编码):只用自己的像素压缩,像一张 JPEG。它可以独立解码,所以是关键帧。 P 帧(前向预测):只存「和前一个参考帧的差」,加上运动矢量(这块内容往左移了 3 个像素)。 B 帧(双向预测):同时参考前后两帧,压得最狠,但要多等一帧才能解。

关键点:P 帧和 B 帧都依赖别的帧。 所以一旦丢了 I 帧,后面一串全废——这叫「花屏」。 流媒体的每个片段都必须从 I 帧开始,就是为了这个。

GOP(Group of Pictures)是两个 I 帧之间的间隔。 GOP 越短,越容易从丢包里恢复,但码率越高; GOP 越长,压缩率越好,但一丢就是一大片。 直播通常设 1~2 秒一个 I 帧,正好和 HLS 的切片对齐。

三、码率、质量、GOP 三者的拉扯

码率 = 每秒多少比特。 它是「花的钱」,画质是「买到的东西」。

同样码率下,画面越复杂(运动的树叶、雨滴、噪点)质量越差——因为编码器要把有限的比特分给更多「新信息」。 这就是为什么风景片能压得很小,体育直播压不动。

质量由三个旋钮决定: ① CRF/QP:恒定质量,码率浮动。质量优先时用它。 ② 目标码率(ABR):恒定码率,质量浮动。网络带宽固定时用它。 ③ preset:编码速度换压缩率。veryfast 比 medium 快两倍,但同画质码率高 20%。

直播必须用 ABR 或 CBR——因为你的上行带宽是固定的,码率一飙就卡。 点播才用 CRF,让码率随画面复杂度浮动。

四、协议不决定画质

经常听到「RTMP 画质不如 HLS」——这是错觉。 同一条码流,装进 RTMP 送和装进 HLS 送,解出来是一模一样的帧。

真实的差别来自别处: ① 码率上限:RTMP 常用于上行推流,主播的上行带宽决定了码率天花板;HLS 用于下行分发,天花板在服务端。 ② 转码:服务端经常把 RTMP 流转成多档 HLS,转码过程中降了码率——你看到的「差」,差在转码这一步。 ③ 丢包处理:UDP 丢了就花屏,TCP 丢了会等重传、卡一下。两种「差」不一样,但都不是编码器的问题。

所以判断画质问题,要沿着链路找: 编码器设的码率是多少 → 中间转码了吗 → 传输丢包了吗。 把「协议」当嫌疑人,基本是冤枉的。

五、动手 Demo:同一个画面,三种 CRF,码率与质量各是多少

用 libx264 真编 60 帧,CRF 取 18/28/38,再解回来和原始帧逐像素比,算 PSNR。 画面里特意加了会移动的细纹理——纯渐变的画面压得太狠,三档分不出差别。

import io, math, numpy as np, av

W, H, N, RNG = 640, 360, 60, np.random.default_rng(7)
NOISE = RNG.integers(-7, 8, size=(H, W)).astype(np.int16)

def frame(i):
    base = np.linspace(16, 190, W)[None, :] * np.ones((H, 1))
    gx = (np.arange(W)[None, :] + i * 3) // 4
    gy = np.arange(H)[:, None] // 4
    y = base + ((gx + gy) % 2) * 42 + NOISE
    cx = 40 + (i * 9) % (W - 80)
    y[H // 2 - 40:H // 2 + 40, cx:cx + 60] = 235
    f = av.VideoFrame(W, H, "yuv420p")
    f.planes[0].update(np.clip(y, 16, 235).astype(np.uint8).tobytes())
    for p in (1, 2):
        f.planes[p].update(bytes([128]) * f.planes[p].buffer_size)
    f.pts = i
    return f

src = [frame(i) for i in range(N)]
for crf in (18, 28, 38):
    buf = io.BytesIO()
    out = av.open(buf, "w", format="mpegts")
    st = out.add_stream("libx264", rate=25)
    st.width, st.height, st.pix_fmt = W, H, "yuv420p"
    st.options = {"crf": str(crf), "preset": "veryfast", "g": "25"}
    for fr in src:
        for pk in st.encode(fr):
            out.mux(pk)
    for pk in st.encode(None):
        out.mux(pk)
    out.close()
    blob = buf.getvalue()

    rin, se, cnt, k = av.open(io.BytesIO(blob), format="mpegts"), 0.0, 0, 0
    for fr in rin.decode(rin.streams.video[0]):
        if k >= N:
            break
        a = fr.to_ndarray(format="yuv420p")[:H].astype(float)
        b = src[k].to_ndarray(format="yuv420p")[:H].astype(float)
        se += ((a - b) ** 2).sum()
        cnt += a.size
        k += 1
    rin.close()
    psnr = 10 * math.log10(255 * 255 / (se / cnt))
    print("CRF %2d  %5.0f kbps  %5.1f dB  %4d KB"
          % (crf, len(blob) * 8 / 1000 / (N / 25), psnr, len(blob) // 1024))

实跑输出:

--------------------------------------------------------------------
编解码器——同一个画面,三种码率,质量差多少
--------------------------------------------------------------------
① 真编三种质量,看码率与 PSNR

  **CRF 是 x264 的「恒定质量」旋钮**:数字小 = 质量高 = 码率高。
  **PSNR 是拿解码后的画面和原始画面逐像素比,越高越像,40 dB 以上肉眼基本看不出。**

  CRF     码率          PSNR        60 帧字节     编码耗时
  ------------------------------------------------------------
  18      3594 kbps     35.7 dB     1052 KB       203 毫秒
  28      419 kbps      32.3 dB     122 KB        111 毫秒
  38      381 kbps      22.1 dB     111 KB        160 毫秒

  **CRF 从 18 放到 38,码率降到 11%,PSNR 掉 13.6 dB。**
  **但真正值得看的是 28 到 38 这一段:码率只降了 9%,PSNR 却掉了 10.2 dB。**
  **说明「降码率」与「掉质量」不是一条直线——38 那档是拿画质换来的数字好看。**
  **实际工程里直播用 CRF 23~28,点播才用 18~20。**

② 「视频流协议」到底约定了什么

  层         管什么                             谁负责
  --------------------------------------------------------------
  编码器     把画面压成码流(H.264/H.265)      libx264 等
  封装       把码流切成包、加上时间戳与索引     TS/FLV/MP4
  传输       包怎么在网络上走、怎么重传         TCP/UDP
  会话       怎么开始、怎么暂停、怎么停         RTSP/RTMP

  **四层是叠加的,不是并列的。**
  **同一条 H.264 码流,装进 TS 走 RTSP 也行,装进 FLV 走 RTMP 也行。**
  **所以「RTMP 与 HLS 哪个画质好」这个问题本身是错的——画质由编码器决定,协议只管怎么送。**

三档的码率差了几倍,PSNR 却没差那么多——这就是「边际收益递减」。 最有意思的是 28 到 38 这一段:码率几乎没降,PSNR 掉了一大截。 说明 38 那档是拿画质换的数字好看。 注意最高那档的 PSNR 也只有 35 dB 左右——因为画面里的细噪声本来就压不掉,这不是编码器不行。

30 秒实验

  1. 把 preset 从 veryfast 换成 medium,看码率降多少、耗时涨多少。这就是「用时间换码率」。
  2. 把 g 从 25 改成 250,看码率掉多少,再想想要是丢一个包会怎样。
  3. 把画面里的 NOISE 去掉再跑,你会发现三档差距突然变小——说明「画面复杂度」比「CRF 数字」更能决定码率。

常见误区

  1. 「H.265 一定比 H.264 好」——同画质省码率,但编码慢、老设备解不了。选型看终端,不看参数表。
  2. 「码率越高画质越好」——同一条码流,CRF 18 编出来 3.6 Mbps,换成 CRF 38 可能只有 0.4 Mbps,但把 3.6 Mbps 的再转一遍码,画质并不会变好。
  3. 「B 帧越多越好」——B 帧要等后面的帧才能解,直播用多了会增加编码延迟,通常只在前瞻里用 0~3 个。
自测三问(点开对答案) 1. RTMP 和 H.264 是同一层的东西吗? —— **不是。**H.264 是编码层,RTMP 是会话层,**RTMP 里可以装 H.264,也可以装 H.265。** 2. 为什么直播的 GOP 通常设 1~2 秒? —— **为了和切片对齐,也为了丢包后能快速恢复。**GOP 太长,一个新观众要等很久才能等到关键帧。 3. 同样的 CRF,为什么体育直播比风景片码率高很多? —— **因为画面变化大,可预测的部分少,编码器要花更多比特描述新信息。**

相关笔记