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

直播播放器的参数几乎都在做同一件事:别攒。 攒得越多延迟越大,而直播要的是「现在」,不是「完整」。

📝 本节和前面几节的关系

8.6.3 是发的一端,这一节是收的一端。 两端的要求正好相反:推要稳,拉要快。

一、三条命令

最朴素的一条,什么都不调:

ffplay rtmp://127.0.0.1/live/stream

要低延迟,五个参数一起上:

ffplay -fflags nobuffer -flags low_delay -framedrop \ -probesize 32768 -analyzeduration 0 \ rtmp://127.0.0.1/live/stream

参数 作用


-fflags nobuffer 不攒缓冲,进来就送 -flags low_delay 解码器也走低延迟路径 -framedrop 赶不上就丢帧,保住实时 -probesize 32768 少读一点就确定格式 -analyzeduration 0 不做额外分析,立刻开播

这五个参数是一套的,都指向「别攒」。 但每一条都在拿稳定性换时间—— nobuffer 会让网络一抖就卡,framedrop 会让你看到跳帧。

二、播放器到底做了几件事

一个播放器只有三步:读容器、解编码、送去显示。 拿本地 FLV 试一遍,量出这两段时间:

指标 值


解出帧数 50 首帧耗时 5.69 ms 总耗时 12.05 ms 异常 无

首帧 5.69 毫秒、50 帧总共 12.05 毫秒——本地文件几乎不花时间。 换成网络流,这两项就变成「首屏要多久」与「卡不卡」的直观指标。

注意首帧占了总耗时的一半—— 因为第一步要先「探测格式」,这部分开销跟数据量无关,是固定成本。 直播里想省时间,就只能从这块固定成本下手,也就是 probesize。

三、probesize 调小会怎样

probesize 决定「最多读多少字节来猜格式」。调小能更快开播。

配置 帧数 耗时 异常


默认 50 10.1 ms 无 probesize 32768 50 8.8 ms 无 probesize 3276 50 10.5 ms 无

帧数一帧不少,耗时也没明显变短——因为这个文件的信息太少, 默认值本来就没读多少字节。换成大码率、多轨道的实时流就不一样了。

probesize 太小的代价是「可能把流的前几帧当垃圾扔掉」, 表现为开头缺画面、或者干脆认不出编码。 这就是调延迟的取舍:省下的等待,是用「可能少一段」换来的。

四、常见故障对照

下面这五种基本覆盖了直播播放的绝大多数问题。

现象 多半是什么


连上但一直黑屏 推流端 -re 忘了写 几秒后自动退出 推流端把文件推完了 Probe 阶段卡住 推的不是 FLV/RTMP 能播但花屏 关键帧间隔太大 有声音没画面 -c copy 撞上不兼容编码

第一条与第二条其实是同一个原因的两面:推得太快。 黑屏是「还没接到就结束了」,自动退出是「接到了但已经完了」。

排查顺序建议先看推流端,再看播放端。 因为播放端的所有参数都只是「调快慢」,改不了「有没有数据」这个根本问题。

五、动手 Demo:拆开播放器的三步,试三档 probesize

下面这段把 50 帧写成 FLV,再分别统计解码帧数、首帧耗时与总耗时。 然后对比默认、32768、3276 三档 probesize 的解码结果。

import io, os, tempfile, time
from fractions import Fraction
import av

buf = io.BytesIO()
o = av.open(buf, "w", format="flv")
st = o.add_stream("libx264", 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()

d = tempfile.mkdtemp()
f = os.path.join(d, "a.flv")
open(f, "wb").write(buf.getvalue())

def run(opts=None):
    t0 = time.time()
    first, n, err = None, 0, None
    try:
        cc = av.open(f, options=opts) if opts else av.open(f)
        for _fr in cc.decode(video=0):
            if first is None:
                first = (time.time() - t0) * 1000
            n += 1
        cc.close()
    except Exception as e:
        err = type(e).__name__
    return n, (first or 0), (time.time() - t0) * 1000, err

n, first, total, err = run()
print(u"帧数 %d  首帧 %.2f ms  总耗时 %.2f ms" % (n, first, total))

for tag, opts in ((u"默认", None),
                  (u"probesize 32768", {"probesize": "32768"}),
                  (u"probesize 3276", {"probesize": "3276"})):
    n2, _f2, t2, e2 = run(opts)
    print(u"%-16s 帧数 %d  耗时 %.1f ms  异常 %s"
          % (tag, n2, t2, e2 or u"无"))

实跑输出:

--------------------------------------------------------------------
8.6.4 使用 ffplay 播放 RTMP 直播流
--------------------------------------------------------------------

**① 直播播放的三条命令**

`ffplay rtmp://127.0.0.1/live/stream`

`ffplay -fflags nobuffer -flags low_delay rtmp://127.0.0.1/live/stream`

  参数                   作用
  -------------------------------------------------
  -fflags nobuffer       不攒缓冲,进来就送
  -flags low_delay       解码器也走低延迟路径
  -framedrop             赶不上就丢帧,保住实时
  -probesize 32768       少读一点就确定格式
  -analyzeduration 0     不做额外分析,立刻开播

**这五个参数是一套的:都指向同一件事——别攒。**
**攒得越多延迟越大,而直播要的是「现在」,不是「完整」。**

**② 实测:播放器到底做了几件事**

  指标         值
  -------------------------
  解出帧数     50
  首帧耗时     5.45 ms
  总耗时       10.92 ms
  异常         无

**首帧 5.45 毫秒、50 帧总共 10.92 毫秒——本地文件几乎不花时间。**
**换成网络流,这两项就变成「首屏要多久」与「卡不卡」的直观指标。**

**③ 实测:probesize 调小,解码结果变不变**

  配置                帧数     耗时        异常
  -------------------------------------------------
  默认                50       9.9 ms      无
  probesize 32768     50       11.0 ms     无
  probesize 3276      50       9.7 ms      无

**帧数一帧不少,说明这个文件的信息很少,小 probesize 也够用。**
**换成大码率、多轨道的实时流就不一样了——**
**probesize 太小会「把流的前几帧当垃圾扔掉」,表现为开头缺画面。**

**这也是调延迟的取舍:省下的那点等待,是用「可能少一段」换来的。**

**④ 常见故障对照**

  现象               多半是什么
  -------------------------------------------
  连上但一直黑屏     推流端 -re 忘了写
  几秒后自动退出     推流端把文件推完了
  Probe 阶段卡住     推的不是 FLV/RTMP
  能播但花屏         关键帧间隔太大
  声音有画面没有     -c copy 但编码不兼容

三档 probesize 解出来的帧数都是 50,耗时也只差一两毫秒。

这不是「probesize 没用」,而是「这个文件太简单,用不上」。 换成多轨道、大码率的实时流,差异才会显出来—— 这也是为什么别人调的参数直接抄过来往往没效果。

30 秒实验

  1. 把 probesize 改成 500:看会不会开始报「认不出格式」。
  2. 把 analyzeduration 设成 0:跟默认比首帧耗时。
  3. 换成 -framedrop:用 ffplay 实际播一遍,看画面是不是跳帧。

常见误区

  1. 以为延迟大就是网络慢。多数时候是自己攒了太多缓冲。
  2. 以为 probesize 越小越好。太小会认不出格式,或者丢开头几帧。
  3. 在播放端调参数想解决「没数据」。推流端没发,播放端怎么调都没用。
自测三问(点开对答案) 1. 直播播放器的参数为什么大多在「别攒」? —— 因为攒缓冲换稳定,而直播要的是实时,延迟比偶尔卡一下更难受。 2. 为什么首帧耗时几乎占总耗时一半? —— 第一步要先探测格式,这部分是固定成本,与数据量无关。 3. `probesize` 调太小会有什么后果? —— 认不出格式,或者把开头几帧当垃圾扔掉,表现为缺画面。

相关笔记