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 秒实验
- 把某个
#EXTINF改成 6.0,看校验结果——变成「否」,超过 TARGETDURATION 5 秒就违规了。 - 删掉第一行
#EXTM3U,用播放器打开——大多数播放器直接不认,这是最省事的合规检查。 - 把 MEDIA-SEQUENCE 改成 0,看序号——变成 0、1、2,说明序号完全由这一行决定。
常见误区
- 「m3u8 里存了媒体数据」——没有,纯文本,只有地址和时长。
- 「TARGETDURATION 可以写小数」——必须整数秒,写 4.5 是违规。
- 「序号必须等于文件名里的数字」——无关,序号只由 MEDIA-SEQUENCE 决定。
自测三问(点开对答案)
1. m3u8 的三条硬要求是什么? —— **UTF-8 文本、首行 `#EXTM3U`、`#` 开头是标签**,其余行是 URI。 2. TARGETDURATION 怎么取? —— **所有切片时长的向上取整最大值**,4.2 秒的片就要写 5。 3. 为什么文件名常常不等于序号? —— **序号由 MEDIA-SEQUENCE 起算**,直播滚动删旧片时序号继续加。- ☐ (本节完,下一节 5.2.2 HLS 播放模式)