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

m3u8 就是一份 UTF-8 的纯文本清单:以 # 开头的是标签,其余行是 URI。 它不装媒体数据,只装「去哪儿取、取多长」。

📝 本节和前面几节的关系

5.1 讲清了「两层列表」的结构,这一节开始看文件本身。 5.2.2 讲这份清单怎么随直播滚动,5.2.3 讲清单里指的那些 .ts 是什么。

一、三个最基本的要求

① 必须是 UTF-8 编码的文本:不是二进制,用记事本能打开。 这四个字符就是名字的由来:m3u + 8 位编码。 ② 第一行必须是 #EXTM3U:不写就整个文件无效。 播放器看到前七个字节不对,会直接放弃解析。 ③ 以 # 开头的是标签,其余行是 URI:一个 URI 就是一段媒体。 空行和注释行会被忽略。

所以 m3u8 的心智模型非常简单: 它是一份「带指令的 URL 列表」,指令以 #EXT 开头。 除了这几个标签,剩下的全是要去取的地址。

二、四个必懂的标签

#EXT-X-TARGETDURATION:切片时长的上限,必须是整数秒。 全局只写一次,位置在第一个 #EXTINF 之前。 #EXT-X-MEDIA-SEQUENCE:第一个切片的序号。 全局一次,直播滚动时它会一直加。 #EXTINF:每个切片前一行,写这一段的时长,带小数。 后面跟的逗号可以带标题,一般留空。 #EXT-X-ENDLIST:写在末尾,表示列表结束。 它是「点播还是直播」的唯一判据。

最容易错的是 TARGETDURATION: 它必须是「所有切片时长的向上取整的最大值」。 如果有个切片实际 4.2 秒,这里写 4 就是违规,有些播放器会直接拒播。 写 5 才安全。

这条规则看着吹毛求疵,其实有实际用处: 客户端靠它决定「多久刷一次列表」,报小了就会刷得太晚,导致断流。

三、解析真的只要十几行

因为格式简单到极点:一行一行读,遇到 #EXTINF 记住时长, 下一行的非 # 内容就是切片地址。

实测一份带三个切片的列表:序号从 100 起,时长 4.2、4.0、3.8, 总时长 12.0 秒,声明的 TARGETDURATION 是 5 秒。 所有切片都不超过 5 秒,合规。

注意序号是从 MEDIA-SEQUENCE 起算的,与文件名无关。 所以「文件名不等于序号」是正常的——直播滚动时旧切片被删,序号却继续往前加。 这一点在排查「播放器跳片」时特别关键。

四、动手 Demo:m3u8——纯文本、逐行、以 # 开头的是标签

写一份最规范的 m3u8,再写十几行解析器把切片读出来。 顺手校验一件事:所有切片时长是不是都不超过 TARGETDURATION。

import io, os, tempfile

txt = ("#EXTM3U\n"
       "#EXT-X-VERSION:3\n"
       "#EXT-X-TARGETDURATION:5\n"
       "#EXT-X-MEDIA-SEQUENCE:100\n"
       "#EXTINF:4.200,\nseg100.ts\n"
       "#EXTINF:4.000,\nseg101.ts\n"
       "#EXTINF:3.800,\nseg102.ts\n"
       "#EXT-X-ENDLIST\n")
p = os.path.join(tempfile.mkdtemp(), "index.m3u8")
io.open(p, "w", encoding="utf-8", newline="\n").write(txt)

seq, dur, target, items = 0, None, 0, []
for ln in txt.splitlines():
    ln = ln.strip()
    if not ln:
        continue
    if ln.startswith("#EXT-X-TARGETDURATION:"):
        target = int(ln.split(":", 1)[1])
    elif ln.startswith("#EXT-X-MEDIA-SEQUENCE:"):
        seq = int(ln.split(":", 1)[1])
    elif ln.startswith("#EXTINF:"):
        dur = float(ln.split(":", 1)[1].split(",")[0])
    elif not ln.startswith("#"):
        items.append((seq + len(items), dur, ln))

for n, d, u in items:
    print("%d  %.1f 秒  %s" % (n, d, u))
print("共 %d 片、总 %.1f 秒;TARGETDURATION %d 秒"
      % (len(items), sum(x[1] for x in items), target))
print("全部不超上限:%s" % ("是" if all(x[1] <= target for x in items) else "否"))

实跑输出:

--------------------------------------------------------------------
m3u8——纯文本、逐行、以 # 开头的是标签
--------------------------------------------------------------------
**① 三个最基本的要求**

  **① UTF-8 编码的文本**:**不是二进制,用记事本能打开。**
  **② 第一行必须是 `#EXTM3U`**:**不写就整个文件无效。**
  **③ 以 `#` 开头的是标签,其余行是 URI**:**一个 URI 就是一段媒体。**

  **「m3u8」这个名字的含义是「m3u 的 UTF-8 版」。**
  **m3u 是老播放器的本地播放列表格式,HLS 沿用了它的外壳。**

**② 四个必懂的标签**

  标签                      位置           作用
  -------------------------------------------------------------------
  #EXT-X-TARGETDURATION     全局一次       切片时长的上限,整数秒
  #EXT-X-MEDIA-SEQUENCE     全局一次       第一个切片的序号
  #EXTINF                   每个切片前     这一段的时长,带小数
  #EXT-X-ENDLIST            末尾           列表结束,点播标志

  **最容易错的是 TARGETDURATION:它必须是「所有切片时长的向上取整的最大值」。**
  **如果有个切片实际 4.2 秒,这里写 4 就是违规,有些播放器会直接拒播。**
  **写 5 才安全。**

**③ 解析只要十几行**

  序号     时长     文件
  -------------------------------
  100      4.2      seg100.ts
  101      4.0      seg101.ts
  102      3.8      seg102.ts

  共 3 个切片、总时长 12.0 秒;TARGETDURATION 声明 5 秒
  所有切片都不超过 5 秒:是

  **序号从 MEDIA-SEQUENCE 起算,与文件名无关。**
  **所以「文件名不等于序号」是正常的**——**直播滚动时旧切片被删,序号继续往前加。**

序号从 100、101、102 起,不是我按文件名去猜的,是从 MEDIA-SEQUENCE 加偏移算的。 总时长 12.0 秒是三个 EXTINF 相加,m3u8 里从来不写总时长。 「全部不超上限」这一项要单独算,因为规范违反了也不报错,只是有些播放器拒播。

30 秒实验

  1. 把某个 #EXTINF 改成 6.0,看校验结果——变成「否」,超过 TARGETDURATION 5 秒就违规了。
  2. 删掉第一行 #EXTM3U,用播放器打开——大多数播放器直接不认,这是最省事的合规检查。
  3. 把 MEDIA-SEQUENCE 改成 0,看序号——变成 0、1、2,说明序号完全由这一行决定。

常见误区

  1. 「m3u8 里存了媒体数据」——没有,纯文本,只有地址和时长。
  2. 「TARGETDURATION 可以写小数」——必须整数秒,写 4.5 是违规。
  3. 「序号必须等于文件名里的数字」——无关,序号只由 MEDIA-SEQUENCE 决定。
自测三问(点开对答案) 1. m3u8 的三条硬要求是什么? —— **UTF-8 文本、首行 `#EXTM3U`、`#` 开头是标签**,其余行是 URI。 2. TARGETDURATION 怎么取? —— **所有切片时长的向上取整最大值**,4.2 秒的片就要写 5。 3. 为什么文件名常常不等于序号? —— **序号由 MEDIA-SEQUENCE 起算**,直播滚动删旧片时序号继续加。

相关笔记