🎬 第七章 FFmpeg命令行实现音视频特效及复杂滤镜应用
📄 一句话概括

画中画只有两步:把小画面 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 秒实验

  1. 换位置:把 70:10 改成 0:0,看小画面是不是贴到左上角。
  2. 换大小:把 80:45 改成 40:22,看它是不是只剩四分之一大小。
  3. 故意反着接:把 b1 接到槽 1、b2 接到槽 0,看会不会报错。

常见误区

  1. 以为命令行也要读两遍文件。-i a -i b 就够了,movie 是 PyAV 的绕法。
  2. 忘了 scale。不缩直接贴,小画面跟主画面一样大,等于全覆盖。
  3. 两段时长不等。overlay 默认 repeatlast,短的那路会定格。
自测三问(点开对答案) 1. 画中画靠哪两个滤镜? —— `scale` 缩小画面,`overlay` 贴到指定坐标。 2. PyAV 里第二路为什么不能用 add_buffer? —— overlay 的槽 1 只吃源滤镜,接图输入端报 `returned 22`。 3. overlay 的默认行为里哪个最容易坑人? —— `repeatlast`:第二路放完会用最后一帧一直撑着。

相关笔记