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

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

  1. 把 hls_time 改成 3:小于片长,看是不是只出一片。
  2. 把 hls_list_size 改成 1:看 m3u8 里的切片是不是只剩最后一条。
  3. 把 hls_playlist_type 改成 event:看 ENDLIST 还在不在。

常见误区

  1. 以为切片数由 hls_time 直接决定。真正的边界是关键帧。
  2. 看到 ts 文件生成了就以为能播。mpeg4 那份就播不了。
  3. 直播把 hls_list_size 写成 0。列表无限长,久了播放器读不动。
自测三问(点开对答案) 1. 为什么 `hls_time` 写 1 却只切出一片? —— 切片只能落在关键帧上,这段 2 秒里没有第二个关键帧可用。 2. 「切出来了」和「能播」分别怎么看? —— 前者看 m3u8 与 ts 文件在不在,后者要用播放器或解码器真跑一遍。 3. HLS 的起播延迟为什么至少三倍 `hls_time`? —— 要等一片生成、再等一片缓冲、播放器还要留三片才敢起播。

相关笔记