HLS 只切在关键帧上,所以 hls_time 只是「最短」而不是「正好」。 mpeg4 切片能生出来却播不了——「切出来了」不等于「能播」。
8.6 讲的是「一条长连接」,这一节换成「一堆短请求」。 两者解决的问题完全不同:一个求实时,一个求可分发。
一、谁在切这两个字
切片不是 HLS 独有的动作,两个工具都能干。
做法 谁在切 典型配置
Nginx-rtmp 服务器边收边切 hls_path 指向目录
FFmpeg 命令行 离线或转码时切 -hls_time 1
两者切出来的东西完全一样:一个 m3u8 加一堆 ts 文件。 Nginx 的做法是等推流进来、按时间切;FFmpeg 的做法是边转码边切。
所以下面用 FFmpeg 的 hls 封装器演练,学到的规律对 Nginx 同样成立。 切片的边界由「关键帧」决定,这一点跟谁在切毫无关系。
二、切 50 帧看看生成了什么
50 帧等于 2 秒,hls_time 写 1,本以为能切两段。
文件 字节
index.m3u8 141 seg000.ts 36284
结果只有一片——hls_time 写 1 也没能切成两段。
原因在 8.7.2 会讲清楚:切片只能落在关键帧上。
注意 m3u8 只有 141 字节,比 ts 小了 250 倍。 它就是一张清单,播放器读它、按它去要 ts—— 所以换码率只要换一份 m3u8,客户端代码一行都不用动。
三、换编码器,切片能不能救
有人会想:是不是编码器不对,所以播不了?试一下就知道了。
编码器 切片数 解码帧数 结果
libx264 1 50 能播 mpeg4 2 0 ArgumentError
两个编码器都切出了文件,但 mpeg4 那份解不开
报的是 avcodec_send_packet() returned 22。
「切出来了」不等于「能播」,这是 HLS 排错最容易搞混的一步。
看切片数还能发现一件事:mpeg4 切成了两片,libx264 只有一片。 同样是 50 帧,关键帧间隔不同,切片结果就不同—— 所以「切片数不对」这个现象,先查 GOP,别怀疑切片器。
体积差得更明显:mpeg4 切片合计 118421 字节,libx264 只有 36425 字节。 同样的画质,码率差三倍——这就是为什么切 HLS 几乎都用 H.264。
四、三个参数决定切片的性格
切 HLS 只要三个参数,但每个都有坑。
参数 作用 写错会怎样
hls_time 每片最短多少秒 太小切片多、太大延迟高 hls_list_size 列表里留几片(0 全留) 直播留太少会丢开头 hls_playlist_type vod 或 event 直播写成 vod 就成点播
hls_time 只是「最短」——真切片会被拉到下一个关键帧,所以永远不小于它。
hls_list_size 是最容易忘的一个:写 0 表示「全留」,
直播场景一般写 5 到 10,让列表滚动起来。
要是留 0 又跑一整天,m3u8 会长到播放器都读不动。
hls_playlist_type 写 vod 会补一个 #EXT-X-ENDLIST,
于是直播变成点播——观众再也等不到新切片,看起来就是「卡住了」。
五、为什么 HLS 天生就慢
这不是实现问题,是模型决定的:想播就得先有一片,想有一片就得先等。
环节 要等多久
等一片切完 hls_time 秒 再等一片做缓冲 又 hls_time 秒 播放器留三片起播 合计三倍 hls_time
按 hls_time 等于 2 算,起播延迟 6 秒起步,工程上还要再加周转。
这就是为什么网页直播要么忍延迟用 HLS,要么换 HTTP-FLV 硬扛。
换个角度想:这 6 秒买来的是「能用普通 HTTP 分发」。 每个 ts 都是普通文件,能被 CDN 缓存、能被就近分发、能扛百万并发—— 直播平台绝大多数流量都是这么走的,延迟是他们主动付出的代价。
六、动手 Demo:切一遍 HLS,换编码器再切一遍
下面这段用 hls 封装器把 50 帧切成切片,列出生成的文件与字节数。 然后换 mpeg4 再切一遍,看「能不能播」跟「切没切出来」是两回事。
import os, shutil, tempfile
from fractions import Fraction
import av
def hls(codec, seg_sec="1"):
d = tempfile.mkdtemp()
m3u8 = os.path.join(d, "index.m3u8")
o = av.open(m3u8, "w", format="hls",
options={"hls_time": seg_sec, "hls_list_size": "0",
"hls_playlist_type": "vod",
"hls_segment_filename":
os.path.join(d, "seg%03d.ts")})
st = o.add_stream(codec, rate=Fraction(25, 1))
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()
return d, m3u8
for codec in ("libx264", "mpeg4"):
d, m3u8 = hls(codec)
files = sorted(os.listdir(d))
print(u"%s -> %s" % (codec, files))
try:
cc = av.open(m3u8)
n = sum(1 for _ in cc.decode(video=0))
cc.close()
print(u" 解码 %d 帧" % n)
except Exception as e:
print(u" 解码失败 %s" % type(e).__name__)
shutil.rmtree(d, ignore_errors=True)
实跑输出:
--------------------------------------------------------------------
8.7.1 Nginx-HTTP-FLV 生成 HLS 切片
--------------------------------------------------------------------
**① 切片这件事,两个工具都能干**
做法 谁在切 典型配置
---------------------------------------------------------
Nginx-rtmp 服务器边收边切 hls_path = 目录
FFmpeg 命令行 离线或转码时切 -hls_time 1
**两者切出来的东西完全一样:一个 m3u8 加一堆 ts。**
**所以下面用 FFmpeg 的 hls 封装器演练,学到的规律对 Nginx 同样成立。**
**② 实测:把 50 帧切成 HLS**
文件 字节
------------------------
index.m3u8 141
seg000.ts 36284
**注意只有一个 ts——50 帧总共 2 秒,`hls_time` 写 1 也没能切成两段。**
**原因在下一节 8.7.2 会讲清楚:切片只能落在关键帧上。**
**m3u8 全文只有 141 字节,逐行都会在 8.7.2 拆开看。**
**③ 实测:换编码器,切片能不能救**
编码器 切片数 解码帧数 结果
-----------------------------------------------------
libx264 1 50 能播
mpeg4 2 0 ArgumentError
**两个编码器都切出了文件,但 mpeg4 那份解不开——报的是 `ArgumentError`。**
**「切出来了」不等于「能播」,这是 HLS 排错时最容易搞混的一步。**
**而且 mpeg4 那份体积大得多:同是 50 帧,mpeg4 切片合计 118421 字节,**
**libx264 只有 36425 字节。编码效率差在这儿一目了然。**
**④ 三个参数决定切片的性格**
参数 作用 写错会怎样
---------------------------------------------------------------------------
hls_time 每片最短多少秒 太小切片多、太大延迟高
hls_list_size 列表里留几片(0 全留) 直播留太少会丢开头
hls_playlist_type vod 或 event 直播写成 vod 就成点播
**⑤ 为什么 HLS 天生就慢**
环节 要等多久
------------------------------------------
等一片切完 hls_time 秒
再等一片做缓冲 又 hls_time 秒
播放器留三片起播 合计三倍 hls_time
**按 `hls_time` 等于 2 算,起播延迟 6 秒起,实际工程里还要再加周转时间。**
**这就是为什么网页直播要么忍延迟用 HLS,要么换成 HTTP-FLV 硬扛。**
libx264 切出 1 片、解出 50 帧;mpeg4 切出 2 片、一帧都解不开。
「文件生成了」和「能播」是两件事, 排查 HLS 时必须先确认后者——拿播放器试一下,比数文件个数有用得多。
30 秒实验
- 把
hls_time改成 3:小于片长,看是不是只出一片。 - 把
hls_list_size改成 1:看 m3u8 里的切片是不是只剩最后一条。 - 把
hls_playlist_type改成event:看 ENDLIST 还在不在。
常见误区
- 以为切片数由
hls_time直接决定。真正的边界是关键帧。 - 看到 ts 文件生成了就以为能播。mpeg4 那份就播不了。
- 直播把
hls_list_size写成 0。列表无限长,久了播放器读不动。
自测三问(点开对答案)
1. 为什么 `hls_time` 写 1 却只切出一片? —— 切片只能落在关键帧上,这段 2 秒里没有第二个关键帧可用。 2. 「切出来了」和「能播」分别怎么看? —— 前者看 m3u8 与 ts 文件在不在,后者要用播放器或解码器真跑一遍。 3. HLS 的起播延迟为什么至少三倍 `hls_time`? —— 要等一片生成、再等一片缓冲、播放器还要留三片才敢起播。- ☐ 8.7.1 完,下一节 8.7.2 M3U8 简介