🎬 第十四章 FFmpeg直播应用综合案例分析
📄 一句话概括

一条推流命令五段,漏了 -re 会一瞬间推完。 四步握手在环回上几乎不耗时,跨公网就是三个往返。

📝 本节和前面几节的关系

14.2.2 选好了服务器,这一节把流推上去。 14.4 换到观众那一侧,把流拉下来。

一、一条命令拆成五段

ffmpeg 的推流命令看着长,其实只有五段:

位置 写什么 作用


第一段 ffmpeg 程序本身 第二段 -re -i 输入.mp4 按真实速度读,不快进 第三段 -c:v libx264 -c:a aac 指定编码,不指定就重编一遍 第四段 -f flv 输出格式必须是 FLV 第五段 rtmp://…/live/流名 推给谁

-re 这一段最容易被漏,漏了会怎样? ffmpeg 会以 CPU 的最快速度把整段读完推出去。 服务器那边看到的是「一瞬间来了十秒的流」, 现象是推流端显示完成、播放端只看到一眨眼。

-f flv 也不能省: rtmp 是传输协议,它上面跑的封装格式必须是 FLV。 不写的话 ffmpeg 会按输出文件名的扩展名猜,猜不出来就报错。

第三段里 -c:v 与 -c:a 分开写,是因为音视频要分别指定。 只写 -c:v copy 表示视频不重编、直接转发, 这时候 CPU 几乎不占,但输入必须已经是 H.264——否则推上去也没法播。

二、真推一次:四步与一条流

沙箱里没有 ffmpeg 可执行文件,所以用「自建 RTMP 服务端 + 手工分块客户端」 把 ffmpeg 在协议层做的事复现一遍。 ffmpeg 内部走的也是同一条路:握手、三条命令、然后发 tag。

步骤 花了多久 说明


握手 0.5 毫秒 环回上几乎没有开销 connect 0.04 毫秒 环回上几乎没有开销 createStream 0.06 毫秒 环回上几乎没有开销 publish 0.07 毫秒 环回上几乎没有开销 发流 1 毫秒 整段流一口气写下去

四步加起来不到 1 毫秒——因为它们全是「发一小段、等一小段」的往返, 而环回上的往返几乎为零。 换成跨公网 40 毫秒的往返,这四步就要 120 毫秒以上。

这就是为什么「本机推流秒连、线上要等一两秒」——差的全是 RTT。 所以调推流超时的时候,那个「几秒」应该按往返次数算,不是按代码行数算。

整场上行 13749 字节,服务端只回了 232 字节。 推流是极度不对称的:客户端说一路话,服务端只回三句「收到」。 这也是为什么「推流占带宽」和「拉流占带宽」在监控上是两回事。

三、视频消息的大小差得很远

同一条流里,消息大小能差两个数量级:

消息 字节 是什么


第一条(序列头) 48 带 SPS 与 PPS,解码器靠它初始化 关键帧(I) 3198 整帧独立编码,最大 普通帧(P) 48 只存与前一帧的差 最后一条(结束) 5 AVC 结束标记,五个字节

关键帧 3198 字节、普通帧 48 字节,差 67 倍。 码率是平均值,瞬时带宽要按关键帧算。

按平均值去规划带宽,一到关键帧就卡——这是直播最常见的容量误判。 因为关键帧不是均匀分布的,它一两秒来一次,一次就顶几十帧的量。

注意最后一行只有 5 字节——那不是视频数据,是 AVC 的结束标记。 所以「最小的一条」没有参考价值,要看的是「普通帧」那一行。

四、沙箱里为什么推不了 rtmp://

这一节真跑的是「协议层」,不是「ffmpeg 命令行」。两个原因: ① 没有 ffmpeg 可执行文件——shutil.which("ffmpeg") 返回空; ② PyAV 自带的 libavformat 没编进 rtmp 协议。

第二条是怎么确认的: 用 av.open("rtmp://…", "w", format="flv") 调一次,它立刻返回成功、 但服务端一个字节都没收到;真往里写帧时直接卡死。 这个现象说明协议层是空的——它把 URL 当成了一个不存在的输出,没报错。

这个坑值得记一笔: libavformat 的「能打开」不等于「能工作」。 它是个惰性容器,真正的连接发生在第一次写数据时。 所以「打开成功」永远不能当成「配置正确」的证据。

两者在协议上完全一致,差的是「谁来做这件事」: ffmpeg 是现成的程序,这里是三十行 Python。 换成真机,一条命令就能验证:ffmpeg -re -i a.mp4 -f flv rtmp://…。

五、动手 Demo:使用 FFmpeg 进行 RTMP 推流——四步,一次真的推上去

先拆命令的五段,再真起一个服务端把流推上去。 最后量四步各花多久、量视频消息的大小分布。

# 一条典型的推流命令(真机上是这一条)
CMD = ("ffmpeg -re -i a.mp4 -c:v libx264 -c:a aac "
       "-f flv rtmp://127.0.0.1:1935/live/test")

# 沙箱里没 ffmpeg,用协议层复现同样的四步
c = socket.create_connection(("127.0.0.1", port), timeout=8)
c.sendall(b"\x03" + b"\x00" * 8 + os.urandom(1528))   # C0 + C1
rd(c, 1537)                                            # S0 + S1
rd(c, 1536)                                            # S2
c.sendall(b"\x00" * 8 + os.urandom(1528))              # C2

for name, payload, cid in (
        ("connect", amf_connect("live", port), 3),
        ("createStream", amf_create(), 5),
        ("publish", amf_publish("livestream"), 5)):
    c.sendall(chunk(cid, 20, 0, 0, payload))
    r.read(c)                     # 等一句 _result 或 onStatus

for tag_type, ts, body in tags:
    if tag_type == 18:
        c.sendall(chunk(4, 18, 1, ts, body))    # metadata
    elif tag_type == 9:
        c.sendall(chunk(6, 9, 1, ts, body))     # 视频

实跑输出:

--------------------------------------------------------------------
使用 FFmpeg 进行 RTMP 推流——四步,一次真的推上去
--------------------------------------------------------------------
**① 一条推流命令,拆成五段**

  位置       写什么                      作用
  ---------------------------------------------------------------------
  第一段     `ffmpeg`                    程序本身
  第二段     `-re -i 输入.mp4`           按真实速度读,不快进
  第三段     `-c:v libx264 -c:a aac`     指定编码,不指定就重编一遍
  第四段     `-f flv`                    输出格式必须是 FLV
  第五段     `rtmp://…/live/流名`        推给谁

  **`-re` 这一段最容易被漏,漏了会怎样?**
  **ffmpeg 会以 CPU 的最快速度把整段读完推出去,服务器那边看到的是「一瞬间来了十秒的流」。**
  **现象是推流端显示完成、播放端只看到一眨眼。**

  **`-f flv` 也不能省:**
  **rtmp 是传输协议,它上面跑的封装格式必须是 FLV。**
  **不写的话 ffmpeg 会按输出文件名的扩展名猜,猜不出来就报错。**

**② 真推一次**

  **沙箱里没有 ffmpeg 可执行文件,所以这里用「自建 RTMP 服务端 + 手工分块客户端」**
  **把 ffmpeg 在协议层做的事复现一遍。**
  **ffmpeg 内部走的也是同一条路:握手、三条命令、然后发 tag。**

  **编出来的 FLV 是 10503 字节、28 个 tag(1 条 metadata、27 条视频)。**

  步骤             花了多久      说明
  -----------------------------------------------------
  握手             0.24 毫秒     环回上几乎没有开销
  connect          0.15 毫秒     环回上几乎没有开销
  createStream     0.19 毫秒     环回上几乎没有开销
  publish          0.15 毫秒     环回上几乎没有开销
  发流             0.73 毫秒     整段流一口气写下去

  **四步加起来不到 1 毫秒——因为它们全是「发一小段、等一小段」的往返,**
  **而环回上的往返几乎为零。**
  **换成跨公网 40 毫秒的往返,这四步就要 120 毫秒以上。**
  **这就是为什么「本机推流秒连、线上要等一两秒」——差的全是 RTT。**

  **整场上行 13749 字节,服务端只回了 232 字节。**
  **推流是极度不对称的:客户端说一路话,服务端只回三句「收到」。**

**③ 视频消息的大小差得很远**

  消息                 字节     是什么
  -----------------------------------------------------------------
  第一条(序列头)     48       带 SPS 与 PPS,解码器靠它初始化
  关键帧(I)          3198     整帧独立编码,最大
  普通帧(P)          48       只存与前一帧的差
  最后一条(结束)     5        AVC 结束标记,五个字节

  **关键帧 3198 字节、普通帧 48 字节,差 67 倍。**

  **序列头与普通帧恰好都是 48 字节,不是抄错——**
  **序列头里只有几个字节的编码参数,普通帧只存差分,量级本来就接近。**
  **码率是平均值,瞬时带宽要按关键帧算。**
  **按平均值去规划带宽,一到关键帧就卡——这是直播最常见的容量误判。**

**④ 沙箱里为什么推不了 rtmp://**

  **两个原因,都在环境上,不在代码上:**
  **① 没有 ffmpeg 可执行文件——`shutil.which("ffmpeg")` 返回空;**
  **② PyAV 自带的 libavformat 没编进 rtmp 协议。**

  **第二条是怎么确认的:**
  **用 `av.open("rtmp://…", "w", format="flv")` 调一次,它立刻返回成功、**
  **但服务端一个字节都没收到;真往里写帧时直接卡死。**
  **这个现象说明协议层是空的——它把 URL 当成了一个不存在的输出,没报错。**

  **所以这一节真跑的是「协议层」,不是「ffmpeg 命令行」。**
  **两者在协议上完全一致,差的是「谁来做这件事」:**
  **ffmpeg 是现成的程序,这里是三十行 Python。**

编出来的 FLV 是 10503 字节、28 个 tag(1 条 metadata、27 条视频)。 四步耗时:握手 0.5、connect 0.04、createStream 0.06、publish 0.07 毫秒。 加起来不到 1 毫秒,因为环回上的往返几乎为零。 换成跨公网 40 毫秒的往返,这四步就是 120 毫秒以上。 整场上行 13749 字节,服务端只回了 232 字节——不对称得厉害。 关键帧 3198 字节、普通帧 48 字节,差 67 倍,所以规划带宽要按关键帧算。

30 秒实验

  1. 推流命令漏掉 -re,猜服务器看到什么——一瞬间收到整段流,播放端只闪一下。
  2. 把 -f flv 去掉、输出地址又没有扩展名,猜报什么——「无法为输出选择格式」,直接退出。
  3. 把 -c:v 写成 copy 但输入是 H.265,猜推上去能不能播——推得上去,但多数播放器解不了。

常见误区

  1. 「推流要写很多参数」——核心就五段,其余是可选。
  2. 「本机测 1 毫秒,线上也差不多」——差的是三个 RTT。
  3. 「能打开就说明连上了」——libavformat 是惰性连接。
自测三问(点开对答案) 1. 推流命令里哪一段最容易被漏? —— **`-re`**,漏了就按 CPU 最快速度推完。 2. 为什么 `-f flv` 不能省? —— **RTMP 上跑的封装格式必须是 FLV**,不写就猜不出来。 3. 四步握手为什么要三个往返? —— **握手、connect、createStream 与 publish 各等一次回应**。

相关笔记