设备吐的是原始像素,一帧 640×480 就要 600 KB,必须自己编码。 而设备帧的时间戳要自己补,不补就报一个看着像路径错的问题。
9.1 摸清了有哪些接口,这一节真去采一路视频加一路音频。 9.3 再把「本地设备」换成「网络摄像头」。
一、摄像头这一路:真采 50 帧
vfwcap 吐的是 rawvideo,每一帧都要自己压。
采 50 帧、缩到 320×240,数据长这样:
项 值
采到帧数 50 原始字节 11520000 编码耗时 229 ms 成片字节 70673
11520000 字节压到 70673 字节,压了 163 倍。 原始字节的算法很直白:50 帧 × 320 × 240 × 3 通道。
注意「编码 229 毫秒」跟「采集用了多久」是两件事—— 编码是纯 CPU 的活,采集要等设备按帧率一帧帧给你。 后者快不了,因为它是现实世界的速度。
二、话筒这一路:沙箱里没有声卡
dshow 抓音频直接 OSError,因为沙箱里没有话筒。
所以这一路用 lavfi 的 sine 顶替——它是个虚拟音频源,
参数跟真话筒完全一样:采样率、声道数、每次给多少点。
尝试 结果
dshow 抓 audio OSError(没有话筒) sine 440 Hz 成功,44100 单声道 每帧采样点 1024
1024 这个数字记住:它是音频「一包」的常见大小。 44100 除以 1024 约等于 43,所以一秒大概 43 个包。 不是整数,这就注定了音视频的 pts 不能靠「第几个包」来对齐。
拿虚拟源练手是有价值的:采集 → 编码 → 封装这条链路,
逻辑跟真话筒一模一样,换设备只改开头那个 -f。
三、两路合成一个 mp4
视频 50 帧、音频 45 包,长度本来就不一样,照样能封进去。
项 值
成片字节 80623 音频包数 45 流数 2 时长 2.00 秒
视频 2 秒、音频约 1.05 秒,播放器以最长那条为准, 短的那条到点就静音——这不算错,是封装的正常行为。
有一个顺序上的硬规矩:音频流必须在第一次 mux 之前就建好。
先写完视频再 add_stream 音频,封装器会报
Cannot rebase to zero time——因为文件头已经写死了,加不进新流。
四、设备帧的时间戳必须自己补
这是本节最容易踩的坑,而且报错信息会把你带偏。
直接拿设备帧去编码,封装时抛 ArgumentError,
消息里写的是「输出文件名无效,returned 22」。
看起来像路径写错了,其实是时间戳不合法—— PyAV 的报错总是带着输出容器的名字,很容易误导。
要补的 含义 本例
pts 这是第几帧 0、1、2…… time_base 一帧占多少秒 1/25
两行就够:g.pts = n 与 g.time_base = Fraction(1, 25)。
不写的话,帧上带的是设备给的原生时间戳,
跟输出流的时间基对不上,封装器只能拒绝。
顺带一句:音频那边不用手动补, 因为音频编码器会按采样率自己算 pts——这也是音视频处理不对称的地方。
五、动手 Demo:采一路视频、配一路音频,封成 mp4
下面这段从 vfwcap 真采 50 帧,缩到 320×240 再编码成 mp4。 然后加一路 lavfi 正弦,把音视频合成一个文件,最后回读验证。
import os, tempfile, time
from fractions import Fraction
import av, numpy as np
W, H, FPS = 320, 240, 25
d = tempfile.mkdtemp()
frames = []
c = av.open("0", format="vfwcap")
st = c.streams[0]
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()
print(u"采到 %d 帧 原始 %d 字节" % (len(frames),
sum(a.size for a in frames)))
out = os.path.join(d, "av.mp4")
o = av.open(out, "w")
v = o.add_stream("libx264", rate=Fraction(FPS, 1))
v.width, v.height, v.pix_fmt = W, H, "yuv420p"
v.bit_rate = 400000
a = o.add_stream("aac", rate=44100)
a.layout, a.format = "mono", "fltp"
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)
ac = av.open("sine=frequency=440:sample_rate=44100", format="lavfi")
k = 0
for af in ac.decode(ac.streams[0]):
for p in a.encode(af):
o.mux(p)
k += 1
if k >= 45:
break
for p in a.encode(None):
o.mux(p)
ac.close()
o.close()
c2 = av.open(out)
print(u"成片 %d 字节 流 %s 时长 %.2f 秒"
% (os.path.getsize(out), [s.type for s in c2.streams],
(c2.duration or 0) / 1e6))
c2.close()
实跑输出:
--------------------------------------------------------------------
9.2 FFmpeg 采集本地话筒与摄像头数据
--------------------------------------------------------------------
**① 摄像头这一路:真采 50 帧**
项 值
--------------------------
采到帧数 50
缩到尺寸 320 x 240
原始字节 11520000
编码耗时 260 ms
成片字节 71607
**50 帧原始画面是 11520000 字节,编完只剩 71607 字节,压了 161 倍。**
**这就是「设备吐原始、自己压」的代价与收益:CPU 换带宽。**
**注意上面那 260 毫秒是「只编码」的时间,不含采集。**
**真采这 50 帧连读带转格式用了 2838 毫秒,平均一帧 57 毫秒。**
**设备原生的间隔是 33 毫秒(30 fps),多出来的部分是**
**「缩放 + 转成 RGB 数组」的开销——所以采集端永远快不过现实,**
**但慢多少,取决于你自己在中间加了多少活。**
**② 话筒这一路:沙箱没有声卡,用 lavfi 顶替**
尝试 结果
------------------------------------------
dshow 抓 audio OSError(没有话筒)
sine 440 Hz 成功,44100 单声道
每帧采样点 1024
**`sine` 是 lavfi 的虚拟音频源,参数跟真话筒一样:采样率、声道数。**
**所以拿它练「采集 → 编码 → 封装」这条链路,逻辑与真话筒完全相同。**
**③ 两路合成一个 mp4**
项 值
------------------------
成片 av9.mp4
字节 81557
音频包数 45
耗时 268 ms
**回读一遍:2 条流,时长 2.00 秒。**
**50 帧视频 2 秒,45 个音频包约 1.05 秒——两边长度不一样也能封装,**
**播放器会以「最长那条」为准,短的那条到点就静音。**
**④ 设备帧的时间戳必须自己整理**
**这是本节最容易踩的坑:直接拿设备帧去编码,封装时会报**
**`returned 22`,而且错误信息里写的是「输出文件名无效」,**
**看起来像路径写错了,其实是时间戳不合法。**
**所以每帧都要补两样东西:**
**`pts`(第几帧)与 `time_base`(每帧占多少秒)。**
**上面那段 `_mk_mp4` 里就两行:`g.pts = n` 与 `g.time_base = 1/25`。**
50 帧原始 11520000 字节,成片 80623 字节,压了 140 多倍。
g.pts 与 g.time_base 那两行删掉,立刻报 returned 22——
而报错里写的是输出文件名,看着像路径问题。这就是它坑的地方。
30 秒实验
- 把
g.pts那行注释掉:看报错到底说的是什么,体会一下误导性。 - 把音频流改到视频之后
add_stream:看Cannot rebase to zero time。 - 把
W, H改成 640, 480:原始字节翻四倍,成片字节只涨一点。
常见误区
- 以为编码耗时就是采集耗时。编码是 CPU 的活,采集要等设备。
- 以为音视频长度必须一样才能封装。不一样也能封,短的那条到点静音。
- 看到
returned 22就去查路径。它多半是时间戳没补。
自测三问(点开对答案)
1. 50 帧 320×240 的 RGB 画面占多少字节? —— 11520000 字节,等于 50 乘 320 乘 240 乘 3。 2. 设备帧编码前必须补哪两样东西? —— `pts`(第几帧)与 `time_base`(一帧占多少秒)。 3. 为什么音频流要先于第一次 mux 创建? —— 封装器写完文件头之后不能再加流,会报 Cannot rebase to zero time。- ☐ 9.2 完,下一节 9.3 FFmpeg 采集网络摄像头获取的数据并录制