m3u8 就是一张清单,八行说完一条流。 切片边界由关键帧决定,hls_time 只保证「不早于」。
8.7.1 切出了 m3u8,这一节把它逐行读懂。 8.7.3 再用播放器把它放出来。
一、逐行拆开这份清单
拿一份真实生成的 m3u8,十行,一行一行看。
行 内容 含义
1 #EXTM3U 文件头,必须有 2 #EXT-X-VERSION:3 遵循的 HLS 版本 3 #EXT-X-TARGETDURATION:1 切片时长上限,取整 4 #EXT-X-MEDIA-SEQUENCE:0 第一个切片的序号 5 #EXT-X-PLAYLIST-TYPE:VOD VOD 点播,直播写 EVENT 6 #EXTINF:1.440000, 这一片多长(秒) 7 seg000.ts 切片文件名 8 #EXTINF:0.560000, 这一片多长(秒) 9 seg001.ts 切片文件名 10 #EXT-X-ENDLIST 到此为止
只有三种行:#EXTM3U 是文件头,#EXT-X- 开头的是标签,
剩下的就是切片文件名——别的什么都没有。
#EXTINF 与文件名是成对出现的:前者说「下一片多长」,后者说「去下哪个」。
所以读 m3u8 的循环很简单:遇到 EXTINF 记时长,遇到文件名就取。
二、关键帧才是真正的边界
两组实验都写 hls_time=1,切出来的时长却不一样,差别只在关键帧间隔。
关键帧间隔 切片数 每片时长
每 12 帧 2 1.440000 / 0.560000 每 25 帧 2 1.000000 / 1.000000
规则一句话说完:从当前位置往后,找第一个「不早于 hls_time」的关键帧。
关键帧每 12 帧一个,也就是每 0.48 秒一个。 第一个不早于 1 秒的关键帧在第 36 帧(1.44 秒),所以第一片是 1.44 秒。 剩下 50 减 36 等于 14 帧,凑成 0.56 秒的第二片。
关键帧每 25 帧一个,正好每 1.0 秒一个,跟 hls_time 严丝合缝。
所以两片各 1.0 秒,一点偏差都没有。
所以「切片时长不准」不是 bug,是「不重新编码」这个前提的必然结果。
想切得准,就得让关键帧间隔跟 hls_time 对齐——
hls_time=1 配 25 帧的 GOP,或者 hls_time=2 配 50 帧。
三、直播型与点播型差在哪
同一个格式,三种玩法,区别只在几个标签。
类型 有什么 没什么
点播 VOD ENDLIST、切片固定 列表不滚动 直播 EVENT 切片持续追加 没有 ENDLIST 直播滑动窗口 只留最近 N 片 老切片被移出列表
判断一份 m3u8 是直播还是点播,最快的办法就是找 #EXT-X-ENDLIST:
有就是点播(或直播已结束),没有就是还在播。
滑动窗口那份还要看 #EXT-X-MEDIA-SEQUENCE:它一直在涨,
涨到多少就说明已经扔掉了多少个老切片。
四、TARGETDURATION 怎么算
这一行最容易算错,因为它不是向上取整。
项 值
最长的切片 1.44 秒 四舍五入到整数 1 m3u8 里实际写 #EXT-X-TARGETDURATION:1
注意是「四舍五入」:1.44 秒算出来是 1,不是 2。 规范要求的是「每片时长四舍五入后都不超过它」,所以取的是最接近的整数。
播放器靠它决定「最多等多久必须来一片」。 写小了会频繁超时重试,写大了会白白多等——两边都难受。
顺带一提:#EXTINF 后面那个逗号不能省。
虽然多数解析器容错,但严格的校验器会直接判格式不合规。
五、动手 Demo:逐行打印 m3u8,再比两档关键帧
下面这段先切一份 HLS,把 m3u8 逐行打印出来并标注含义。 再换两档关键帧间隔切一遍,对比切片数与每片时长。
import os, shutil, tempfile
from fractions import Fraction
import av
def hls(gop=None):
d = tempfile.mkdtemp()
m3u8 = os.path.join(d, "index.m3u8")
o = av.open(m3u8, "w", format="hls",
options={"hls_time": "1", "hls_list_size": "0",
"hls_playlist_type": "vod",
"hls_segment_filename":
os.path.join(d, "seg%03d.ts")})
kw = {"options": {"g": str(gop)}} if gop else {}
st = o.add_stream("libx264", rate=Fraction(25, 1), **kw)
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
st.bit_rate = 400000
c, k = av.open("s7.mp4"), 0
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
k += 1
if k >= 50:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
txt = open(m3u8, encoding="utf-8").read()
return d, txt
d, txt = hls(gop=12)
for i, ln in enumerate([x for x in txt.split("\n") if x.strip()], 1):
print(u"%2d | %s" % (i, ln))
shutil.rmtree(d, ignore_errors=True)
for gop in (12, 25):
d, txt = hls(gop=gop)
ext = [x.split(":")[1].rstrip(",") for x in txt.split("\n")
if x.startswith("#EXTINF")]
tgt = [x.split(":")[1] for x in txt.split("\n")
if x.startswith("#EXT-X-TARGETDURATION")]
print(u"gop=%d 切片 %d 片 时长 %s TARGET %s"
% (gop, len(ext), " / ".join(ext), tgt[0]))
shutil.rmtree(d, ignore_errors=True)
实跑输出:
--------------------------------------------------------------------
8.7.2 M3U8 简介
--------------------------------------------------------------------
**① 实测:逐行拆一份 m3u8**
行 内容 含义
--------------------------------------------------------------
1 #EXTM3U 文件头,必须有
2 #EXT-X-VERSION:3 遵循的 HLS 版本
3 #EXT-X-TARGETDURATION:1 切片时长的上限,取整
4 #EXT-X-MEDIA-SEQUENCE:0 第一个切片的序号
5 #EXT-X-PLAYLIST-TYPE:VOD VOD 点播,直播写 EVENT
6 #EXTINF:1.440000, 这一片多长(秒)
7 seg000.ts 切片文件名
8 #EXTINF:0.560000, 这一片多长(秒)
9 seg001.ts 切片文件名
10 #EXT-X-ENDLIST 到此为止,后面没有了
**八行里有五行是 `#EXT-X-` 开头的标签,剩下的是一行文件名。**
**m3u8 本质就是「一张清单」:按顺序列出该去下哪些 ts 文件。**
**② 实测:关键帧才是切片的真正边界**
关键帧间隔 切片数 每片时长 TARGETDURATION
---------------------------------------------------------------------
每 12 帧 2 1.440000 / 0.560000 1
每 25 帧 2 1.000000 / 1.000000 1
**两边写的都是 `hls_time=1`,切出来的时长却不一样——差别只在关键帧间隔。**
**切片器不会为了凑整秒去重新编码,它只能切在「已有的关键帧」上。**
**规则可以一句话说完:从当前位置往后找第一个「不早于 hls_time」的关键帧。**
**关键帧每 12 帧一个,也就是每 0.48 秒一个。**
**第一个不早于 1 秒的关键帧在第 36 帧(1.44 秒),所以第一片是 1.44 秒。**
**剩下 50 减 36 等于 14 帧,凑成 0.56 秒的第二片。**
**关键帧每 25 帧一个,正好每 1.0 秒一个,跟 hls_time 严丝合缝。**
**所以两片各 1.0 秒,切片时长一点偏差都没有。**
**所以「切片时长不准」不是 bug,是「不重新编码」这个前提的必然结果。**
**想切得准,就得让编码器的关键帧间隔跟 hls_time 对齐——**
**比如 `hls_time=1` 配 25 帧的 GOP,或者 `hls_time=2` 配 50 帧。**
**③ 直播型与点播型差在哪**
类型 有什么 没什么
-----------------------------------------------------------
点播 VOD ENDLIST、切片固定 列表不滚动
直播 EVENT 切片持续追加 没有 ENDLIST
直播滑动窗口 只留最近 N 片 老切片被移出列表
**判断一份 m3u8 是直播还是点播,最快的办法就是看有没有 `#EXT-X-ENDLIST`:**
**有就是点播(或直播已结束),没有就是还在播。**
**④ TARGETDURATION 是怎么算出来的**
项 值
----------------------------------------------
最长的切片 1.44 秒
四舍五入到整数 1
m3u8 里实际写 #EXT-X-TARGETDURATION:1
**注意是「四舍五入」,不是向上取整:1.44 秒算出来是 1,不是 2。**
**规范要求的是「每片时长四舍五入后都不超过它」,所以取的是最接近的整数。**
**播放器靠它决定「最多等多久必须来一片」。**
**写小了会频繁超时重试,写大了会白白多等——两边都难受。**
gop=12 切出 1.44 加 0.56,gop=25 切出 1.0 加 1.0,hls_time 都是 1。
两组的 TARGETDURATION 都是 1——因为最长那片的四舍五入都是 1。
如果哪天看到 TARGETDURATION 比实际片长还小,那说明切片器有 bug。
30 秒实验
- 把
hls_list_size改成 1:看列表里剩几条,MEDIA-SEQUENCE 变没变。 - 把
hls_time改成 2:看 gop=12 时切片数会不会变少。 - 把
hls_playlist_type改成event:看 ENDLIST 消失、PLAYLIST-TYPE 变化。
常见误区
- 把
#EXT-X-TARGETDURATION当成向上取整。实际是四舍五入。 - 以为 m3u8 里写了几片就真有那么多。文件名要对得上,否则就是死链。
- 直播用点播型清单。带上 ENDLIST,播放器播完就停,再也等不到新片。
自测三问(点开对答案)
1. m3u8 里一共几种行? —— 三种:`#EXTM3U` 文件头、`#EXT-X-` 标签、切片文件名。 2. `#EXTINF` 与文件名是什么关系? —— 成对出现,前者说下一片多长,后者说去下哪个文件。 3. 怎么一眼看出这是直播还是点播? —— 看有没有 `#EXT-X-ENDLIST`:有是点播或已结束,没有是还在播。- ☐ 8.7.2 完,下一节 8.7.3 使用 ffplay 播放 HLS 直播流