🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

播放 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 秒实验

  1. 把 g 从 12 改成 25:切片变少,请求数跟着变少。
  2. 把 hls_time 改成 2:看请求数剩下几次。
  3. 把地址改成不存在的文件:体会一下 404 是怎么冒出来的。

常见误区

  1. 以为 HLS 要一条长连接。它全是短请求,这正是它能上 CDN 的原因。
  2. 只提供一个地址就想兼顾实时与稳定。两者互斥,得给两路流。
  3. 用 -live_start_index 播点播文件。参数会被忽略,白调。
自测三问(点开对答案) 1. 播放 HLS 一共要几次请求? —— 1 加切片数。先要 m3u8,再按清单逐个要 ts。 2. 为什么 HLS 适合 CDN 而 RTMP 不适合? —— HLS 全是普通 HTTP GET,可缓存可分发;RTMP 每人一条长连接,占连接数。 3. 想让延迟更低,代价是什么? —— `hls_time` 调小会让请求变密,网络一抖就更容易卡。

相关笔记