🎬 第九章 FFmpeg命令行实现音视频设备采集
📄 一句话概括

采集加直播,就是把「设备吐帧」直接接到「网络送包」上,中间不落盘。 这一节最容易搞错的,是把推流端的 -re 也搬到采集端。

📝 本节和前面几节的关系

9.2 采完落了盘,这一节改成采完直接送出去。 8.6 讲过的 RTMP 推流,在这里换个上游。

一、采集加直播,只是换了个出口

落盘与直播的区别只有一个:末尾那个地址。

目的 末尾怎么写


存成文件 out.mp4 推成直播 -f flv rtmp://主机/live/流名 本地预览 -f flv udp://127.0.0.1:5004

前面那一大段采集参数、编码参数,一个字都不用改。 所以「先录下来确认没问题,再改成推流」是最稳的调试顺序。

实测里 50 帧封成 FLV 是 70512 字节、225 毫秒,折合 282 kbps。 这个数就是「上行带宽至少要有多少」—— 低于它,服务器那边就会开始丢,观众看到的是卡顿。

二、采集端千万别加 -re

这条最容易搞混,因为 8.6.3 反复强调过「推流一定要写 -re」。 但那说的是「推本地文件」——文件就在硬盘上,不限制的话几秒钟就推完了。

上游是什么 要不要 -re 为什么


本地文件 要 文件读得比实时快 采集设备 不要 设备本来就慢 另一路直播 不要 上游已经是实时的

判断标准只有一句话:上游是不是实时的。 是,就不用加;不是,就必须加。

加了也不会报错,只会让节奏变成两重限制—— 设备已经按自己的帧率给帧,再加一道等待,延迟会明显变大。

三、两路各自对齐才封得进去

视频是 25 fps,音频是 44100 Hz,两套时间基完全独立。

项 视频 音频


每秒多少包 25 帧 约 43 个 1024 点 时间基 1/25 1/44100 pts 谁算 自己补 编码器算

44100 除以 1024 是 43.07,不是整数—— 所以音频的 pts 不能靠「第几个包乘固定值」推出来, 必须让编码器自己按采样率算。这也是音视频处理最不对称的地方。

而视频这边,设备给的帧没有可靠时间戳,只能自己补 pts 与 time_base。 两边一个「自己算」一个「别人算」,这就是 9.2 那个坑的根源。

四、直播时要盯的三个数

这三个数任何一个不对,观众都会立刻感觉到。

指标 怎么看 不对劲说明什么


帧率 一秒内编了几帧 设备被占用或太慢 码率 每秒字节数乘 8 上行扛不住会卡 延迟 采集时刻到送出时刻 中间缓冲太多

三个数是互相拉扯的:码率降下来画质就掉, 延迟压下去缓冲就少、网络一抖就卡。

采集端能做的只有一件事:别在自己这一环多攒。 设备那一段的时间压不了,编码那一段可以省(比如换更快的预设), 而中间那些「顺手转一下格式」的活,往往才是真正的浪费。

五、动手 Demo:采一路,直接封成 FLV

下面这段采 50 帧,不落盘,直接封成 FLV 放在内存里。 再看字节数、耗时与折算码率,把采集链路的四段耗时拆开。

import io, time
from fractions import Fraction
import av, numpy as np

W, H, FPS = 320, 240, 25
t_open = time.time()
c = av.open("0", format="vfwcap")
st = c.streams[0]
open_ms = (time.time() - t_open) * 1000

t_cap = time.time()
frames = []
for f in c.decode(st):
    g = f.reformat(width=W, height=H, format="yuv420p")
    frames.append(np.array(g.to_ndarray(format="rgb24")))
    if len(frames) >= 50:
        break
c.close()
cap_ms = (time.time() - t_cap) * 1000

t0 = time.time()
buf = io.BytesIO()
o = av.open(buf, "w", format="flv")
v = o.add_stream("libx264", rate=Fraction(FPS, 1))
v.width, v.height, v.pix_fmt = W, H, "yuv420p"
v.bit_rate = 400000
for n, arr in enumerate(frames):
    f = av.VideoFrame.from_ndarray(arr, format="rgb24")
    g = f.reformat(width=W, height=H, format="yuv420p")
    g.pts = n
    g.time_base = Fraction(1, FPS)
    for p in v.encode(g):
        o.mux(p)
for p in v.encode(None):
    o.mux(p)
o.close()
enc_ms = (time.time() - t0) * 1000

sz = len(buf.getvalue())
print(u"打开设备 %.0f ms  采 %d 帧 %.0f ms" % (open_ms, len(frames), cap_ms))
print(u"编 + 封 %.0f ms  FLV %d 字节  折算 %.0f kbps"
      % (enc_ms, sz, sz * 8 / (len(frames) / float(FPS)) / 1000))

实跑输出:

--------------------------------------------------------------------
9.4 FFmpeg 采集摄像头与话筒获取的数据并直播
--------------------------------------------------------------------

**① 采集 + 直封 FLV,一步不落地**

  项           值
  -------------------------
  帧数         50
  FLV 字节     71446
  耗时         232 ms
  折算码率     286 kbps

**这一段跟 8.6.3 的 `-re` 不是一回事:**
**那里是「文件推得快,要人为放慢」,这里是「设备本来就慢,想快也快不了」。**
**所以采集端推流**千万别加 `-re`**——加了只会让节奏变成两重限制。**

**② 两路要各自对齐,才封得进去**

  项               视频              音频
  -------------------------------------------------------
  来源             vfwcap 索引 0     sine 440 Hz
  帧率/采样率     25 fps            44100 Hz
  每秒多少包       25 帧             约 43 个 1024 点
  时间基           1/25             1/44100

**44100 除以 1024 是 43.07,所以一秒不是整数个音频包——**
**这就注定音视频的 pts 不可能靠「第几个包 × 固定值」对齐,**
**必须各用各的 `time_base`,让封装器自己去换算。**

**③ 直播时真正要盯的三个数**

  指标     怎么看                 不对劲说明什么
  ----------------------------------------------------
  帧率     一秒内编了几帧         设备被占用或太慢
  码率     每秒字节数 × 8         网络扛不住会卡
  延迟     采集时刻到送出时刻     缓冲多了

**这三个数是互相拉扯的:码率降下来画质就掉,**
**延迟压下去缓冲就少、网络一抖就卡。**
**采集端能做的只有「别在自己这一环多攒」。**

**④ 采集链路的四段耗时**

  环节          本机实测
  ------------------------------
  打开设备      1799 ms
  采 120 帧     6812 ms
  编 50 帧      233 ms
  封装 FLV      含在上一行里

**采 120 帧要 6.8 秒,算下来一帧 57 毫秒。**
**而设备原生是 33 毫秒一帧(30 fps),多出来的 23 毫秒是**
**「缩放 + 转成 RGB 数组」的开销——不是设备慢,是我们自己加的活。**

**所以「采集」这一段的时间不是我们能压缩的,它就是现实世界的速度;**
**能优化的只有自己那一环——转格式这一步完全可以省掉,**
**直接把 `yuyv422` 交给编码器,让 libx264 自己去转。**

「采 50 帧」与「编 50 帧」是两段时间,差一个量级—— 前者等设备,后者吃 CPU。优化只能动后者。

FLV 放内存里不落盘,就是「推流」的等价物: 把 io.BytesIO() 换成 rtmp://… 或 udp://…,逻辑一行都不变。

30 秒实验

  1. 把 io.BytesIO() 换成 udp://127.0.0.1:5004:体会出口一换就成直播。
  2. 给编码器加 options={"preset": "ultrafast"}:看编 50 帧能快多少。
  3. 把 reformat 那一步去掉,直接把 yuyv422 交给编码器:省掉一次转换。

常见误区

  1. 采集端推流还加 -re。设备本来就慢,加了只会更慢。
  2. 以为「采得快就推得快」。设备的节奏压不了。
  3. 把 FLV 的字节数当成带宽需求。要加上封装与网络的额外开销。
自测三问(点开对答案) 1. 采集端推流为什么不该加 `-re`? —— 设备本身就按帧率给数据,上游已经是实时的,再加一道等待只会加大延迟。 2. 音视频的 pts 谁算?为什么不对称? —— 视频要自己补 pts 与 time_base,音频由编码器按采样率算。 3. 直播要盯哪三个数? —— 帧率、码率、延迟,三者互相拉扯,只能按场景取舍。

相关笔记