直播播放器的参数几乎都在做同一件事:别攒。 攒得越多延迟越大,而直播要的是「现在」,不是「完整」。
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 秒实验
- 把
probesize改成 500:看会不会开始报「认不出格式」。 - 把
analyzeduration设成 0:跟默认比首帧耗时。 - 换成
-framedrop:用 ffplay 实际播一遍,看画面是不是跳帧。
常见误区
- 以为延迟大就是网络慢。多数时候是自己攒了太多缓冲。
- 以为
probesize越小越好。太小会认不出格式,或者丢开头几帧。 - 在播放端调参数想解决「没数据」。推流端没发,播放端怎么调都没用。
自测三问(点开对答案)
1. 直播播放器的参数为什么大多在「别攒」? —— 因为攒缓冲换稳定,而直播要的是实时,延迟比偶尔卡一下更难受。 2. 为什么首帧耗时几乎占总耗时一半? —— 第一步要先探测格式,这部分是固定成本,与数据量无关。 3. `probesize` 调太小会有什么后果? —— 认不出格式,或者把开头几帧当垃圾扔掉,表现为缺画面。- ☐ 8.6.4 完,下一节 8.7.1 Nginx-HTTP-FLV 生成 HLS 切片