一条推流命令五段,漏了 -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 秒实验
- 推流命令漏掉
-re,猜服务器看到什么——一瞬间收到整段流,播放端只闪一下。 - 把
-f flv去掉、输出地址又没有扩展名,猜报什么——「无法为输出选择格式」,直接退出。 - 把
-c:v写成copy但输入是 H.265,猜推上去能不能播——推得上去,但多数播放器解不了。
常见误区
- 「推流要写很多参数」——核心就五段,其余是可选。
- 「本机测 1 毫秒,线上也差不多」——差的是三个 RTT。
- 「能打开就说明连上了」——libavformat 是惰性连接。
自测三问(点开对答案)
1. 推流命令里哪一段最容易被漏? —— **`-re`**,漏了就按 CPU 最快速度推完。 2. 为什么 `-f flv` 不能省? —— **RTMP 上跑的封装格式必须是 FLV**,不写就猜不出来。 3. 四步握手为什么要三个往返? —— **握手、connect、createStream 与 publish 各等一次回应**。- ☐ (本节完,下一节 14.4 使用 VLC 进行 RTMP 拉流并播放)