🎬 第五章 HLS流媒体协议
📄 一句话概括

HLS 的文件不是一层,而是两层:主列表列码率,媒体列表列切片。 主列表里没有 #EXTINF,媒体列表里没有 BANDWIDTH。

📝 本节和前面几节的关系

前四章讲的都是「一条连接搬一路流」的思路。 HLS 完全换了个路子:把流切成文件,用普通 HTTP 一个个取。 5.1.2 接着讲这样一拆,服务端和客户端各自要做哪几件事。

一、两层各管什么

第一层是主列表(master playlist),文件名常叫 master.m3u8: 它只写三件事——有几个码率档、每档多少带宽、每档的分辨率。 它不写任何一个切片名。

第二层是媒体列表(media playlist),常叫 index.m3u8: 它写这一档的切片有哪些、每片多长、列表结束没有。 它不写任何别的档位。

实测一个只有两档的主列表,一共 150 字节、5 行。 两档分别写 800 kbps 与 2500 kbps,指向 360p/index.m3u8 与 720p/index.m3u8。 150 字节就是全部「目录」的成本,小到可以每秒重取一次。

二、为什么要分两层

因为一路直播经常同时提供好几个清晰度。 要是只有一层,每个档位就得各发一份完整列表, 客户端换档时还得重新知道「有哪些档」。

分两层之后的好处很具体: ① 换档不用重新协商:客户端手里已经有主列表,换档就是换一个 URI 去取。 ② 目录极小:主列表几 KB,可以频繁刷新,代价几乎为零。 ③ 结构可扩展:加一档 1080p 只是往主列表加三行,别的档位一个字都不用改。

一句话:主列表是目录,媒体列表才是正文。 目录要小、要稳定;正文要长、要常变。

三、客户端怎么挑档位

三条策略,优先级从高到低: ① 用户手动选:选了就锁死,不再自动切。 ② 按测速自动选:先按「实测带宽 × 0.7」挑一档,跑起来再微调。 ③ 兜底:网络很差时直接选最低档,保证能出画面。

为什么乘 0.7 而不是直接用实测带宽? 因为测速是「瞬时的」,而码率是「持续的」。 留三成余量,才不至于刚切上去就因为一次抖动而卡住。

切档时有一个硬要求:各档的时间轴必须对齐。 也就是第 N 个切片在每一档里都覆盖同一段时间。 不对齐的话,切换瞬间画面会跳一下——这是很多自制 HLS 服务的通病。

四、动手 Demo:写一个主列表,再解析回来

真写一个两档的主列表,再按行解析出带宽、分辨率、子列表路径。 解析只认 #EXT-X-STREAM-INF 与它下一行——这是主列表唯一的「结构」。

import io, os, re, tempfile

root = tempfile.mkdtemp()
master = (
    "#EXTM3U\n"
    "#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360\n"
    "360p/index.m3u8\n"
    "#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720\n"
    "720p/index.m3u8\n"
)
p = os.path.join(root, "master.m3u8")
io.open(p, "w", encoding="utf-8", newline="\n").write(master)

lines = master.splitlines()
print("主列表共 %d 字节、%d 行" % (len(master.encode()), len(lines)))

rows = []
for i, ln in enumerate(lines):
    if not ln.startswith("#EXT-X-STREAM-INF:"):
        continue
    attr = dict(
        kv.split("=", 1) for kv in ln.split(":", 1)[1].split(",")
    )
    rows.append((int(attr["BANDWIDTH"]) // 1000,
                 attr["RESOLUTION"], lines[i + 1]))

for kbps, res, uri in rows:
    print("%d kbps  %s  %s" % (kbps, res, uri))

实跑输出:

--------------------------------------------------------------------
索引文件的嵌套——主列表指向媒体列表
--------------------------------------------------------------------
**① HLS 的 m3u8 是两层结构**

  层级       文件名          里面装什么
  ---------------------------------------------------------------
  第一层     master.m3u8     码率档位列表,每档指向一个子列表
  第二层     index.m3u8      真正的切片列表,每行一个 .ts

  **为什么要分两层?**
  **因为同一路直播常有多档清晰度,观众可以手动切、也可以自动切。**
  **主列表就像一张目录,媒体列表才是正文。**

**② 真写一个主列表再解析**

  主列表共 150 字节、5 行

  带宽 kbps     分辨率       子列表
  ----------------------------------------------
  800           640x360      360p/index.m3u8
  2500          1280x720     720p/index.m3u8

**③ 客户端怎么选**

  **三条策略,按优先级:**
  **① 用户手动选**:**选了就锁死,不自动切。**
  **② 测速自动选**:**先按带宽的 0.7 倍挑一档,跑起来再调整。**
  **③ 兜底**:**网络很差时选最低档,保证能出画面。**
  **切换时要对齐时间轴**:**新档位的切片必须与旧档位同名同序,否则切换瞬间会跳一下。**
  **这就是为什么各档的切片时长必须完全一致。**

主列表只有 150 字节——小到可以放进一个 TCP 包,不用考虑分片。 BANDWIDTH 是「峰值」不是平均值,客户端按它挑档位时,实际占用通常只有七成左右。 子列表路径是相对的,所以换 CDN 时只要目录结构一样,主列表一个字都不用改。

30 秒实验

  1. 把 BANDWIDTH 改成一个很大的值,看客户端会怎么选——它会认为网络够好,直接挑最高档,然后卡住。
  2. 加一条 #EXT-X-I-FRAME-STREAM-INF,看主列表多出什么——这是「只取关键帧」的专用列表,拖进度条时用得上。
  3. 把子列表路径改成绝对 URL,看还能不能取到——能,HLS 允许跨域取子列表,这是 CDN 分流的基础。

常见误区

  1. 「主列表里也得写切片时长」——不用,时长在媒体列表的 #EXTINF 里。
  2. 「只有一档就不用写主列表」——不行,规范要求入口必须是主列表。
  3. 「切档时解码器要重建」——要,但如果时间轴对齐,画面不会跳。
自测三问(点开对答案) 1. 主列表和媒体列表各写什么? —— **主列表写码率档位,媒体列表写切片清单。**两者字段不重叠。 2. 为什么自动选档要乘 0.7? —— **测速是瞬时值、码率是持续值**,留三成余量防抖动。 3. 切档时不跳画面的前提是什么? —— **各档切片时长一致、时间轴对齐**,第 N 片覆盖同一段时间。

相关笔记