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

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

  1. 把 N 改成 50:看 40 倍这个比例会不会变(应该基本不变)。
  2. 把 while 换成 time.sleep(N / 25.0) 一次性等:总量对了,但节奏是假的。
  3. 把 -g 50 换成 -g 5:FLV 体积会变大,关键帧多了。

常见误区

  1. 以为推流不用写 -re。不写就等于把整段视频几秒钟灌完。
  2. 以为字节一样就说明 -re 没起作用。它改的是时间,不是数据量。
  3. 用「进程正常退出」判断推流成功。连不上也正常退出。
自测三问(点开对答案) 1. `-re` 做的是什么? —— 按输入的原帧率发送,不改编码结果,只是「等」,把节奏拉成实时。 2. 为什么 FLV 第一个标签不是视频? —— 第一个是 0x12 脚本标签,装 onMetaData,播放器要先知道宽高帧率。 3. 推流地址写错会有什么现象? —— 完全没有报错,进程正常结束,但服务器一个字节都收不到。

相关笔记