HLS 切片本质上就是把 TS 按时间切开,再配一份列表。 切两个参数:切多长、留几个。
5.1 到 5.6 把清单、TS、PS 全拆过一遍,这一节把它们拼回去。 第六章换一条路:HTTP-FLV,同样是「用 HTTP 搬流」,但不切片。
一、真切一次,看结果
用 PyAV 的 hls 封装器真切一次,参数是切片 2 秒、列表留 0(全留)。 实测生成 2 个切片:11656 字节与 5640 字节。 两个数字都能被 188 整除:11656 = 62 × 188,5640 = 30 × 188。 这就是「HLS 切片本质上就是 TS 文件」最直接的证据。
生成的 m3u8 长这样:
开头是 #EXTM3U 与 #EXT-X-VERSION:3,
然后 #EXT-X-TARGETDURATION:2、#EXT-X-MEDIA-SEQUENCE:0,
两个 #EXTINF:2.000000, 各跟一个切片名,末尾 #EXT-X-ENDLIST。
注意 #EXTINF 写的是 2.000000 与 1.000000,
第一个切片是完整的 2 秒,第二个只有 1 秒——因为素材总长就 3 秒。
真实直播里最后一个切片短、但仍留在列表里,会被播放器当成「还没切完」。
二、三个参数决定切片结果
① hls_time:目标切片时长。
实际会取「最近的整数个 GOP」——设 2 秒、GOP 1 秒,切出来就是 2 秒一段。
② hls_list_size:列表里保留几个切片。
0 表示全留(点播),直播常设 3 到 6。
③ 关键帧对齐:必须靠 force_key_frames 强制按时间插关键帧,否则切片边界会飘。
第 ③ 条是实践里最容易翻车的一条。
如果只设 hls_time=4 但 GOP 是 3 秒,实际切出来会是 3 秒或 6 秒,
永远到不了 4 秒——因为切片只能切在关键帧上。
正确做法是三个参数一起设:
-g 设成帧率乘切片秒数、-keyint_min 与它一致、-sc_threshold 0 关掉场景切换插入。
这样切出来的时长才稳定。
三、切片之后还剩什么事
切片只是第一步,一个能用的 HLS 服务还要做这些: ① 每切一片就更新 m3u8:这一步用「先写临时文件再改名」保证原子性。 直接覆盖会让正在读的客户端拿到半个文件。 ② 删除过期切片:直播时旧片要删,否则磁盘很快就满。 ③ 给 CDN 设正确的缓存头:切片可以长缓存,m3u8 必须短缓存或不缓存。 这条设反了会出现「观众永远看到同一个列表」的怪现象。
还有一条容易忽略的:切片文件名要单调递增且不重复。 用时间戳或序号都可以,但绝不能用「固定名字循环覆盖」, 否则 CDN 缓存会把新旧切片搅在一起,观众画面乱跳。
四、动手 Demo:切片——把一路流真切成 m3u8 加一堆 ts
真切一次 HLS:编码 3 秒素材、切片 2 秒、列表全留。 然后量每个切片的字节数,看它们是不是 188 的整数倍。
import io, os, tempfile
import av, numpy as np
out = tempfile.mkdtemp()
p = os.path.join(out, "index.m3u8")
c = av.open(p, "w", format="hls")
c.options = {"hls_time": "2", "hls_list_size": "0",
"hls_segment_filename": os.path.join(out, "s%d.ts"),
"hls_flags": "independent_segments"}
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(75):
f = av.VideoFrame(320, 240, "yuv420p")
y = np.zeros((240, 320), np.uint8)
y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
f.planes[0].update(y.tobytes())
for pi in (1, 2):
f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
f.pts = i
for pk in st.encode(f):
c.mux(pk)
for pk in st.encode(None):
c.mux(pk)
c.close()
segs = sorted(x for x in os.listdir(out) if x.endswith(".ts"))
tot = 0
for s in segs:
n = os.path.getsize(os.path.join(out, s))
tot += n
print("%s %d 字节 = %d × 188" % (s, n, n // 188))
print("共 %d 片、%d 字节" % (len(segs), tot))
print()
print(io.open(p, encoding="utf-8").read())
实跑输出:
--------------------------------------------------------------------
切片——把一路流真切成 m3u8 加一堆 ts
--------------------------------------------------------------------
**① 真切一次**
目录里生成了 2 个切片
文件 字节 188 的倍数
----------------------------------
s0.ts 11656 62
s1.ts 5640 30
**② 生成的 m3u8 长这样**
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2.000000,
s0.ts
#EXTINF:1.000000,
s1.ts
#EXT-X-ENDLIST
**③ 三个参数决定切片结果**
**① `hls_time`**:**目标切片时长,实际会取「最近的整数个 GOP」。**
**所以设 2 秒、GOP 1 秒,切出来就是 2 秒一段。**
**② `hls_list_size`**:**列表里保留几个切片,0 表示全留(点播)。**
**直播常设 3 到 6。**
**③ 关键帧对齐**:**必须靠 `force_key_frames` 强制按时间插关键帧,否则切片边界会飘。**
**实测 2 个切片共占 17296 字节**,**每个都是 188 的整数倍。**
**这就是「HLS 切片本质上就是 TS 文件」最直接的证据。**
每个切片的字节数都能整除 188——这不是巧合,切片就是 TS 文件。
hls_segment_filename 里的 %d 会被替换成递增序号,从 0 开始。
independent_segments 保证每片都能独立解码,靠的是每片开头重发关键帧与表。
30 秒实验
- 把
hls_time改成 1,看切片数——变多,但每个都更小,总字节基本不变。 - 把
hls_list_size改成 2,看 m3u8 里几行——只剩 2 个#EXTINF,旧的被滚掉了。 - 把帧数改成 250(10 秒),看切片时长——变成 5 个 2 秒的片,最后一片可能不足 2 秒。
常见误区
- 「切片就是重新编码一次」——不是,是转封装,不重编码。
- 「hls_time 设几就切几秒」——只能切在关键帧上,实际是 GOP 的整数倍。
- 「m3u8 和切片可以设一样的缓存时间」——不行,m3u8 必须短缓存。
自测三问(点开对答案)
1. 为什么切片字节数总能整除 188? —— **切片就是 TS 文件**,TS 是 188 定长包,长度必然整除。 2. hls_time 为什么不能精确控制? —— **切片只能切在关键帧上**,实际是 GOP 的整数倍。 3. m3u8 的缓存策略该怎么设? —— **短缓存或不缓存**;切片可以长缓存,设反了观众会一直看到旧列表。- ☐ (本章完,下一章 6.1 HTTP FLV 协议简介)