画中画只有两步:把小画面 scale 下来,再用 overlay 贴到指定位置。 沙箱里第二路只能走 movie,两个 add_buffer 会直接报错。
7.4.3 已经拿 overlay 把裁出来的图贴回去过,这一节把它当画中画的主体。 它离真正的「一段视频叠另一段视频」只差一个 scale。
一、两句话说完
画中画=一张画里再放一张更小的画。技术上只有两步:
① 把第二路画面的尺寸缩下来(scale);② 把它贴到主画面的某个坐标(overlay)。
前面 7.2 讲透的 scale、7.4 讲透的 overlay,在这里合起来就是画中画。
ffmpeg -i main.mp4 -i small.mp4 -filter_complex "[1:v]scale=80:45[pip];
[0:v][pip]overlay=70:10" out.mp4
[1:v] 取第二个输入的视频流,scale 之后挂个标签 [pip],
再跟 [0:v] 一起送进 overlay。两个输入的顺序不能反:[0:v] 是底下的主画面。
overlay=70:10 说的是小画面左上角落在主画面的哪个像素。
写 70:10 就是距左边 70 列、距上边 10 行。
二、沙箱里为什么只能用 movie
命令行有 -i a -i b,两路输入天生就有;PyAV 里却是另一回事:
第二路怎么给 结果
两个 add_buffer returned 22 一路 buffer 加一路 movie 能跑
这跟 7.4.3 是同一件事:overlay 的槽 1 只吃「源滤镜」,
add_buffer 造出来的是图输入端,接上去当场报 Invalid argument returned 22。
movie 是个「读文件的源滤镜」,它自己就是源头,接槽 1 合法。
代价是同一个文件被读了两遍——这只是迁就 PyAV,命令行没有这一步。
三、三条硬规矩
做画中画之前先记住三条:
规矩 为什么
小画面必须真的小 大了就盖住主画面 位置要留边距 贴着边看着像裁坏了 两段时长要一致 短的放完会定格
第三条最容易被忽略。overlay 默认开着 repeatlast:第二路放完之后
它会一直拿最后一帧撑着,看着像画面「定住了」。
要避免这个,得让两段等长,或者给小画面加 -stream_loop -1 让它循环。
反过来,如果就是想要「定格在最后一帧」,那默认行为正合适。
四、动手 Demo:两路输入怎么给、小画面盖多大
下面这段代码先试「两个 add_buffer」能不能接 overlay,再试 buffer 加 movie, 最后算三档小画面各占主画面的多少。
import av
from fractions import Fraction
TB = Fraction(1, 25)
ESC = u"s7.mp4".replace(u"\\", u"/").replace(u":", u"\\:")
c0 = av.open(u"s7.mp4")
base = next(c0.decode(video=0)).to_ndarray(format=u"yuv420p")
c0.close()
print(u"--- 两个 add_buffer 接 overlay")
try:
g = av.filter.Graph()
b1 = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
b2 = g.add_buffer(width=80, height=45, format=u"yuv420p", time_base=TB)
ov = g.add(u"overlay", u"70:10")
sk = g.add(u"buffersink")
b1.link_to(ov, 0, 0)
b2.link_to(ov, 0, 1)
ov.link_to(sk)
g.configure()
print(u"通 建图成功")
except Exception as ex:
msg = str(ex)
if u"returned 22" in msg:
msg = u"Invalid argument returned 22"
print(u"断 %s %s" % (type(ex).__name__, msg))
print(u"--- 一路 buffer 加一路 movie")
g = av.filter.Graph()
b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
mv = g.add(u"movie", ESC)
ov = g.add(u"overlay", u"70:10")
sk = g.add(u"buffersink")
b.link_to(ov, 0, 0)
mv.link_to(ov, 0, 1)
ov.link_to(sk)
g.configure()
vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
vf.pts = 0
vf.time_base = TB
b.push(vf)
o = sk.pull().to_ndarray(format=u"rgb24")
print(u"通 输出 %dx%d" % (o.shape[1], o.shape[0]))
print(u"--- 小画面盖住多大一块")
for sw, sh in ((80, 45), (60, 34), (160, 90)):
print(u"%3dx%-3d 占主画面 %.2f" % (sw, sh, sw * sh / (160.0 * 90)))
实跑输出:
--------------------------------------------------------------------
7.9.1 画中画技术简介
--------------------------------------------------------------------
**① 画中画就是「一张画里再放一张画」**
**新闻直播右下角那个小窗、游戏实况左上角那个摄像头、教程视频的人脸框——**
**都是画中画。一个画面上叠一段更小的画面,两段各有各的内容。**
**技术上只有两步:把小画面的尺寸缩下来,把它贴到大画面的某个位置。**
**对应的两个滤镜就是 `scale` 与 `overlay`。**
**② 命令行长这样**
`ffmpeg -i main.mp4 -i small.mp4 -filter_complex "[1:v]scale=80:45[pip];`
`[0:v][pip]overlay=70:10" out.mp4`
**`[1:v]` 是第二个输入(小画面),先 scale 再取个标签 `[pip]`,**
**然后跟主画面一起送进 overlay。`overlay=70:10` 说的是小画面左上角的位置。**
**③ 沙箱里只能一路 buffer 加一路 movie**
两个输入怎么给 结果
--------------------------------------------
两个 add_buffer returned 22
一路 buffer 加一路 movie 能跑
**这跟 7.4.3 里说的一回事:overlay 的槽 1 只认源滤镜。**
**命令行没有这个限制,`-i a -i b` 就是两路输入,随便接。**
**④ 画中画的三条硬规矩**
规矩 为什么
-------------------------------------------
小画面必须真的小 大了就盖住主画面
位置要留边距 贴着边看着像裁坏了
两段时长要一致 短的放完会定格
**第三条最容易被忽略。`overlay` 默认在第二路放完之后保留最后一帧(repeatlast),**
**所以短的小画面会用最后一帧一直撑着,看着像「定住了」。**
第一段是本节那张小表的现场复现:两个 add_buffer 报 Invalid argument returned 22。
这个 22 是 EINVAL,意思是「参数不合法」,不是「没有这个滤镜」。
第二段换成 movie 就通了,输出仍是 160×90——overlay 不改变主画面尺寸。 贴上去的小画面只是盖在上面,画布一张都不多。
第三段是面积占比:80×45 占 0.25,160×90 占 1.00。 后者已经不是画中画,是把主画面整个换掉。
30 秒实验
- 换位置:把
70:10改成0:0,看小画面是不是贴到左上角。 - 换大小:把
80:45改成40:22,看它是不是只剩四分之一大小。 - 故意反着接:把
b1接到槽 1、b2接到槽 0,看会不会报错。
常见误区
- 以为命令行也要读两遍文件。
-i a -i b就够了,movie 是 PyAV 的绕法。 - 忘了 scale。不缩直接贴,小画面跟主画面一样大,等于全覆盖。
- 两段时长不等。overlay 默认 repeatlast,短的那路会定格。
自测三问(点开对答案)
1. 画中画靠哪两个滤镜? —— `scale` 缩小画面,`overlay` 贴到指定坐标。 2. PyAV 里第二路为什么不能用 add_buffer? —— overlay 的槽 1 只吃源滤镜,接图输入端报 `returned 22`。 3. overlay 的默认行为里哪个最容易坑人? —— `repeatlast`:第二路放完会用最后一帧一直撑着。- ☐ (本节完,下一节 7.9.2 使用 overlay 实现画中画)