-re 决定这条路是「实时」还是「尽快」,忘了它直播就成了闪一下。 而推流失败最坑的地方是——客户端全程不报错。
8.6.2 交代了三个角色,这一节只谈推流这一端的命令。 8.6.4 再换到播放端。
一、三条常用命令
第一条是不转码的直推,最省 CPU:
ffmpeg -re -i s7.mp4 -c copy -f flv rtmp://127.0.0.1/live/stream
第二条要转码,适合源文件编码不兼容的情况:
ffmpeg -re -i s7.mp4 -c:v libx264 -g 50 -f flv \
rtmp://127.0.0.1/live/stream
第三条是摄像头直推,现场直播就用它:
ffmpeg -f dshow -i video="摄像头名" -c:v libx264 \
-preset veryfast -tune zerolatency -f flv \
rtmp://127.0.0.1/live/stream
参数 作用
-re 按原帧率发,别一股脑灌过去 -c copy 不转码,直接搬,最省 CPU -f flv 封装成 FLV,RTMP 只吃这个 -g 50 每 50 帧一个关键帧,观众进得快 -tune zerolatency 关掉编码器内部的缓冲
-tune zerolatency 只对软件编码器有意义,硬编码器用不了。
它的作用是让编码器「编一帧吐一帧」,不攒 B 帧、不做前瞻。
二、-re 到底限住了什么
-re 是最容易被忘掉的一个参数。
下面这组数据是同一段 25 帧,一个不限速、一个按 25 fps 节流。
模式 耗时 平均每帧
不限速 25 ms 1.0 ms 按 25 fps 节流 1000 ms 40.0 ms
差了 40 倍。 这就是「实时」与「尽快」的区别:一个按片长走,一个按 CPU 速度走。
注意两份数据量完全一样(12960 与 12960 字节)。
说明 -re 只改时间节奏,不改编码结果——它不重新编码,只是等。
忘了写会怎样?本地文件几秒钟推完,服务器立刻判定「推流结束」, 观众那边只闪一下。这是新手最常见的「直播播不出来」原因。
三、FLV 里到底装了什么
推流出去的就是一串 FLV 标签。看懂头 13 个字节,就够排查大半问题。
偏移 字段 本例
0 到 2 签名 FLV 3 版本 0x01 4 标志位 0x01 5 到 8 数据偏移 9 9 到 12 PreviousTagSize0 0 13 起 第一个标签 类型 0x12
第一个标签是 0x12,也就是脚本标签,装的是 onMetaData。 宽高、帧率、时长这些信息都在里面,播放器靠它先知道「这是什么」。
标签类型只有三种:0x09 视频、0x08 音频、0x12 脚本。 标签头固定 11 字节:类型 1 + 长度 3 + 时间戳 3 + 扩展 1 + 流号 3。
每个标签后面还跟 4 字节 PreviousTagSize,等于「上一个标签的总长度」。
这是 FLV 唯一的结构校验手段,也是能随机定位的抓手。
四、服务器不在时,客户端一声不吭
把地址指向一个没人听的端口,整段推一遍:
项 值
异常 没报错 耗时 6 ms
一整段跑完,连一个字节都没送出去,却什么异常都没有。 这就是为什么「ffmpeg 推流成功了吗」不能只看进程退没退。
工程上要确认推上去了,只有两条路: 要么看服务器那边的收流日志,要么在本地抓一下 1935 端口有没有流量。 光看客户端不靠谱——它可能安静地把数据全丢了。
五、推流失败的五个常见原因
按排查顺序排,从上往下走基本能定位。
现象 多半是什么
Connection refused 服务器没起,或端口写错
推一下就没 忘了写 -re
完全没有报错但没画面 地址写错,字节被静默丢弃
服务器报 codec 不支持 -c copy 撞上了不兼容编码
能推但观众花屏 关键帧间隔太大
第三条最难查,因为它一点线索都不给。
所以养成习惯:推流前先用 nc -vz 主机 1935 确认端口通。
第二条也常见:推太快会让服务器以为「流结束了」。
判断依据很简单——如果推流只花了几秒,那一定是没写 -re。
六、动手 Demo:量 -re 的 40 倍差距,拆 FLV 标签
下面这段同一段画面编两遍:一遍不限速,一遍按 25 fps 节流。 然后把结果按 FLV 结构逐字段读出来,最后试一次「服务器不在」。
import io, struct, time
from fractions import Fraction
import av
def enc_flv(n=25):
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 >= n:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
return buf.getvalue()
N = 25
t0 = time.time()
d1 = enc_flv(N)
fast = (time.time() - t0) * 1000
t0 = time.time()
d2 = enc_flv(N)
while (time.time() - t0) < N / 25.0:
time.sleep(0.001) # 相当于 -re:按片长走
slow = (time.time() - t0) * 1000
print(u"不限速 %.0f ms,节流 %.0f ms,差 %.0f 倍"
% (fast, slow, slow / fast))
print(u"字节数一样吗:%d 与 %d" % (len(d1), len(d2)))
print(u"签名 %s 版本 0x%02X 标志 0x%02X"
% (d1[:3].decode("ascii"), d1[3], d1[4]))
print(u"偏移 %d Tag0 %d 首标签 0x%02X"
% (struct.unpack(">I", d1[5:9])[0],
struct.unpack(">I", d1[9:13])[0], d1[13]))
try:
o = av.open("rtmp://127.0.0.1:19998/live/x", "w", format="flv")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
c = av.open("s7.mp4")
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
break
o.close()
c.close()
print(u"服务器不在:没报错")
except Exception as e:
print(u"服务器不在:%s" % type(e).__name__)
实跑输出:
--------------------------------------------------------------------
8.6.3 使用 FFmpeg 实现 RTMP 直播推流
--------------------------------------------------------------------
**① 三条常用命令**
`ffmpeg -re -i s7.mp4 -c copy -f flv rtmp://127.0.0.1/live/stream`
参数 作用
--------------------------------------------------------
-re 按原帧率发,别一股脑灌过去
-c copy 不转码,直接搬,最省 CPU
-f flv 封装成 FLV,RTMP 只吃这个
-g 50 每 50 帧一个关键帧,观众进得快
-tune zerolatency 关掉编码器内部的缓冲
**`-re` 是最容易被忘掉的一个。忘了它,本地文件几秒钟就推完,**
**服务器那边看起来「推流结束」,观众只能看到一闪而过。**
**② 实测:`-re` 到底限住了什么**
模式 耗时 平均每帧
-------------------------------------------
不限速 26 ms 1.0 ms
按 25 fps 节流 1001 ms 40.0 ms
**同样 25 帧,不限速 26 毫秒就全吐出去了,节流之后要 1001 毫秒。**
**差了 38 倍——这就是「实时」与「尽快」的区别。**
**注意两个数据量一样(12960 与 12960 字节),**
**说明 `-re` 只改时间节奏,不改编码结果。**
**③ 实测:FLV 里到底装了什么**
偏移 字段 值
----------------------------------------------
0 到 2 签名 FLV
3 版本 0x01
4 标志位 0x01
5 到 8 数据偏移 9
9 到 12 PreviousTagSize0 0
13 起 第一个标签 类型 0x12
**第一个标签是 0x12,也就是脚本标签,装的是 onMetaData——**
**宽高、帧率、时长这些信息都在里面,播放器靠它先知道「这是什么」。**
**标签类型只有三种:0x09 视频、0x08 音频、0x12 脚本。**
**标签头固定 11 字节:类型 1 + 长度 3 + 时间戳 3 + 扩展 1 + 流号 3。**
**每个标签后面还跟 4 字节 PreviousTagSize,等于「上一个标签的总长度」。**
**这是 FLV 唯一的结构校验手段,也是能随机定位的抓手。**
**④ 实测:服务器不在时,推流会怎样**
项 值
-------------------
异常 没报错
耗时 6 ms
**一整段跑完,连一个字节都没送出去,却什么异常都没有。**
**这就是为什么「用 ffmpeg 推流成功了吗」不能只看进程退没退。**
**工程上要确认推上去了,只有两条路:**
**要么看服务器那边的收流日志,要么在本地抓一下 1935 端口有没有流量。**
**光看客户端不靠谱——它可能安静地把数据全丢了。**
两遍字节数完全相同,耗时差 40 倍——-re 只改节奏,不改内容。
最后那次「服务器不在」打印的是「没报错」, 这一行比前面所有数字都更值得记住:它意味着「成功」两个字完全不可信。
30 秒实验
- 把
N改成 50:看 40 倍这个比例会不会变(应该基本不变)。 - 把 while 换成
time.sleep(N / 25.0)一次性等:总量对了,但节奏是假的。 - 把
-g 50换成-g 5:FLV 体积会变大,关键帧多了。
常见误区
- 以为推流不用写
-re。不写就等于把整段视频几秒钟灌完。 - 以为字节一样就说明
-re没起作用。它改的是时间,不是数据量。 - 用「进程正常退出」判断推流成功。连不上也正常退出。
自测三问(点开对答案)
1. `-re` 做的是什么? —— 按输入的原帧率发送,不改编码结果,只是「等」,把节奏拉成实时。 2. 为什么 FLV 第一个标签不是视频? —— 第一个是 0x12 脚本标签,装 onMetaData,播放器要先知道宽高帧率。 3. 推流地址写错会有什么现象? —— 完全没有报错,进程正常结束,但服务器一个字节都收不到。- ☐ 8.6.3 完,下一节 8.6.4 使用 ffplay 播放 RTMP 直播流