播放 HLS 只需要读一份 m3u8,之后按清单一个个去要 ts。 请求数等于「1 加切片数」,这也是它适合 CDN 的原因。
8.7.2 读懂了清单,这一节把它播出来。 到这一节为止,第八章的协议链条就闭合了。
一、三条命令
最朴素的一条:
ffplay http://127.0.0.1:8080/hls/index.m3u8
要最实时的一条,从最后一片开始:
ffplay -live_start_index -1 \
http://127.0.0.1:8080/hls/index.m3u8
参数 作用
-live_start_index -1 从最后一片开始,最实时 -live_start_index 0 从第一片开始,最完整 -allowed_extensions 限定只下 ts,防跑偏 -reconnect 1 断了自动重连
-live_start_index 是 HLS 独有的参数:
m3u8 里有一串切片,你得选「从哪一片开始看」。
选 -1 是从尾巴进,画面最新但可能跳;选 0 是从头进,完整但慢。 这个选择没有对错,只看场景——监控要实时,课程回放要完整。
二、直接播本地 m3u8
播放器根本不关心 ts 有几个,它只读 m3u8 然后按清单取。
指标 值
切片数 2 解出帧数 50 首帧耗时 6.35 ms 总耗时 14.49 ms 异常 无
两片、50 帧、一帧不缺——跟直接播单个 ts 的结果完全一样。
这正是 HLS 最讨喜的地方:把「一条流」拆成「一堆文件」之后, 播放逻辑一点没变复杂,反而多出了「可缓存、可换源、可切码率」三样好处。
三、它到底请求了几次
起一个 HTTP 服务,把请求路径全记下来。
次序 请求路径 说明
1 /index.m3u8 清单 2 /seg000.ts 切片 3 /seg001.ts 切片
第一个请求永远是 m3u8,之后才按清单去要 ts——一共 3 次请求。 所以 HLS 的请求数等于「1 加切片数」,切片越小请求越密。
这也是 HLS 适合 CDN 的原因:全是普通 HTTP GET, 每个 ts 都能被缓存、被就近分发,服务器不用为每个观众保持一条长连接。
跟 RTMP 对比一下就明白了: RTMP 一个观众一条长连接,HLS 一个观众一串短请求—— 前者省握手、吃连接数;后者费请求、但能吃下百万并发。
四、三个调节钮
直播场景下想要的东西是互斥的,只能挑两个。
想要 调什么 代价
更低延迟 hls_time 调小、列表留短 请求变密、更易卡
更稳 列表留长、配多重连 延迟变大
省带宽 切片时长调大 起播更慢
三个目标互斥,所以同一场直播常常同时提供 HLS 与 HTTP-FLV 两路。 网页默认走 HLS(兼容性最好),要低延迟的客户端切到 HTTP-FLV。
这也是第八章一路讲下来的那条主线: 没有最好的协议,只有最合适的组合。
RTSP 求控制精细,RTP 求轻量,UDP 求快, RTMP 求实时推流的一条道,HLS 求可分发—— 每一种都在某一边让步,换另一边的收益。
五、动手 Demo:播本地 m3u8,再起服务看请求序列
下面这段先直接播本地 m3u8,量帧数与耗时。 再起一个 HTTP 服务把目录挂出去,记录 libav 请求了哪些路径。
import http.server, os, shutil, tempfile, threading, time
from fractions import Fraction
import av
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")})
st = o.add_stream("libx264", rate=Fraction(25, 1),
options={"g": "12"})
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()
def count(path):
n = 0
cc = av.open(path)
for _f in cc.decode(video=0):
n += 1
cc.close()
return n
print(u"本地 m3u8 解出 %d 帧" % count(m3u8))
log = []
class H(http.server.SimpleHTTPRequestHandler):
def __init__(self, *a, **kw):
http.server.SimpleHTTPRequestHandler.__init__(
self, *a, directory=d, **kw)
def log_message(self, *a):
pass
def do_GET(self):
log.append(self.path)
http.server.SimpleHTTPRequestHandler.do_GET(self)
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
n = count("http://127.0.0.1:%d/index.m3u8" % port)
time.sleep(0.2)
srv.shutdown()
srv.server_close()
print(u"HTTP 拉流解出 %d 帧,请求 %d 次" % (n, len(log)))
for p in log:
print(u" %s" % p)
shutil.rmtree(d, ignore_errors=True)
实跑输出:
--------------------------------------------------------------------
8.7.3 使用 ffplay 播放 HLS 直播流
--------------------------------------------------------------------
**① 三条命令**
`ffplay http://127.0.0.1:8080/hls/index.m3u8`
`ffplay -live_start_index -1 http://127.0.0.1:8080/hls/index.m3u8`
参数 作用
---------------------------------------------------
-live_start_index -1 从最后一片开始,最实时
-live_start_index 0 从第一片开始,最完整
-allowed_extensions 限定只下 ts,防跑偏
-reconnect 1 断了自动重连
**`-live_start_index` 是 HLS 独有的参数:**
**m3u8 里有一串切片,你得选「从哪一片开始看」。**
**② 实测:直接播本地 m3u8,能解出多少帧**
指标 值
-------------------------
切片数 2
解出帧数 50
首帧耗时 4.67 ms
总耗时 12.90 ms
异常 无
**播放器不需要知道 ts 有几个,它只读 m3u8,按清单一个个取。**
**所以「换个码率」只要换一份 m3u8,客户端代码一行都不用改。**
**③ 实测:起个 HTTP 服务,看它到底请求了几次**
次序 请求路径 说明
---------------------------------
1 /index.m3u8 清单
2 /seg000.ts 切片
3 /seg001.ts 切片
**第一个请求永远是 m3u8,之后才按清单去要 ts——一共 3 次请求。**
**所以 HLS 的请求数等于「1 加切片数」,切片越小请求越密。**
**这也是 HLS 适合 CDN 的原因:全是普通 HTTP GET,**
**每个 ts 都能被缓存、被就近分发,服务器不用为每个观众保持一条长连接。**
**④ 直播场景下的三个调节钮**
想要 调什么 代价
---------------------------------------------------------------
更低延迟 hls_time 调小、列表留短 请求变密、更容易卡
更稳 列表留长、多重连 延迟变大
省带宽 切片时长调大 起播更慢
**三个目标互斥,只能按场景挑两个。**
**这也是为什么同一场直播常常同时提供 HLS 与 HTTP-FLV 两路。**
本地播与 HTTP 播解出的帧数一模一样,说明容器这一层完全透明。
请求日志一定是「先 m3u8、后 ts」,而且 ts 的顺序跟清单一致。 如果看到重复请求同一片,多半是播放器在重连或者超时重试。
30 秒实验
- 把
g从 12 改成 25:切片变少,请求数跟着变少。 - 把
hls_time改成 2:看请求数剩下几次。 - 把地址改成不存在的文件:体会一下 404 是怎么冒出来的。
常见误区
- 以为 HLS 要一条长连接。它全是短请求,这正是它能上 CDN 的原因。
- 只提供一个地址就想兼顾实时与稳定。两者互斥,得给两路流。
- 用
-live_start_index播点播文件。参数会被忽略,白调。
自测三问(点开对答案)
1. 播放 HLS 一共要几次请求? —— 1 加切片数。先要 m3u8,再按清单逐个要 ts。 2. 为什么 HLS 适合 CDN 而 RTMP 不适合? —— HLS 全是普通 HTTP GET,可缓存可分发;RTMP 每人一条长连接,占连接数。 3. 想让延迟更低,代价是什么? —— `hls_time` 调小会让请求变密,网络一抖就更容易卡。- ☐ 8.7.3 完,第八章收官——下一章 9.1 FFmpeg 枚举设备