movie 不是串在链中间的滤镜,而是从旁边岔出来的一路「源」。 它的输出 pad 是运行时才建的,静态查出来是 0 进 0 出。
6.1 讲的链是「一个接一个」。这一节要引入第二种接法:两路并行。 图片水印的本质就是两路画面合到一起。
一、它为什么不像个滤镜
drawbox 静态查出来是 1 进 1 出,movie 却是 0 进 0 出:
滤镜 静态输入 pad 静态输出 pad
movie 0 0 drawbox 1 1
可它明明能出帧——因为它的输出 pad 是动态建的(dynamic outputs), 等真正配置时才按文件内容创建出来。
所以「数 pad」这个办法在这里会骗人。 判断一个滤镜是不是源,得真拉一帧才作数。
二、读视频与读图片,出来的是两种像素格式
同一个 movie,喂它 mp4 与喂它 png,出来的东西不一样:
喂进去的文件 出来什么 像素格式
2 秒 mp4 160×90 yuv420p 文字图 png 40×27 rgba
PNG 读出来是 rgba,四个通道都留着——这是水印能「透」的前提。 如果它被当成 rgb24 读,透明的地方会变成黑色,水印就成了黑方块。
这也是新手最常遇到的现象:水印加上了,但四周一圈黑边。 不是水印图做错了,是通道在读到内存时被丢了一个。
三、loop:一张静态图怎么撑住整段视频
主视频有 50 帧,水印图只有 1 帧。不解决这件事,第二帧就没水印了。
写法 连续拉了几帧 含义
不写 loop 1 取默认值 1,只放一遍 loop=0 5 以上 0 表示无限循环 loop=1 1 和默认值一样,还是放一遍
所以正确写法是 movie=水印.png:loop=0。
网上抄来的水印命令几乎都带这个参数,它不是可选项,是必需品。
留神 loop=0 与 loop=1 的方向是反的:
0 是「无限」,1 是「一次」。这种反直觉的约定在 FFmpeg 里不止一处。
四、命令行的写法与一个坑
ffmpeg -i in.mp4 -vf "movie=logo.png:loop=0[wm];[in][wm]overlay=10:10" out.mp4
movie 这一段自己带方括号标号,说明它是从旁边岔出来的一路, 必须靠 overlay 把两路合起来。
而 -vf 只能写一条链,写两路进去多半配不上。
所以真正带图片水印的命令,最后几乎都落到 -filter_complex 上。
五、动手 Demo:pad、像素格式与 loop
先比 movie 与 drawbox 的 pad 数,再真读一个 mp4 与一张 png,看像素格式。 最后试三种 loop 写法,看一张静图能连续拉出几帧。
import av
from PIL import Image
md = av.filter.Filter(u"movie")
db = av.filter.Filter(u"drawbox")
print(u"movie pad %d/%d" % (len(md.inputs), len(md.outputs)))
print(u"drawbox pad %d/%d" % (len(db.inputs), len(db.outputs)))
png = u"w40.png"
Image.new(u"RGBA", (40, 27), (255, 0, 0, 128)).save(png)
def esc(p):
return p.replace(u"\\", u"/").replace(u":", u"\\:")
for p in (u"s5.mp4", png):
g = av.filter.Graph()
m = g.add(u"movie", esc(p))
s = g.add(u"buffersink")
m.link_to(s)
g.configure()
fr = s.pull()
print(u"%s -> %dx%d %s" % (p, fr.width, fr.height, fr.format.name))
for extra in (u"", u":loop=0", u":loop=1"):
g = av.filter.Graph()
m = g.add(u"movie", esc(png) + extra)
s = g.add(u"buffersink")
m.link_to(s)
g.configure()
n = 0
while n < 5:
try:
s.pull()
n += 1
except Exception:
break
print(u"loop %-8s 拉到 %d 帧" % (extra or u"不写", n))
实跑输出:
--------------------------------------------------------------------
6.2.1 -vf 的 movie 滤镜——把一个文件变成「源」
--------------------------------------------------------------------
**① 先看 movie 与普通滤镜的差别**
滤镜 静态输入 pad 静态输出 pad
---------------------------------------------
movie 0 0
drawbox 1 1
**movie 静态查出来是 0 进 0 出,但它明明能出帧——**
**因为它的输出 pad 是运行时才建的(动态输出),静态表里看不到。**
**所以「数 pad」这个办法在这里会骗人,得真拉一帧才作数。**
**② 真读两种文件:视频与水印图**
喂给 movie 的文件 出来什么 像素格式
-----------------------------------------------
2 秒 mp4 160×90 yuv420p
文字图 png 40×23 rgba
**PNG 读出来是 rgba,四个通道都留着——这是水印能「透」的前提。**
**如果它被当成 rgb24 读,透明的地方会变成黑色,水印就成了一个黑方块。**
**③ loop:一张静态图怎么撑住整段视频**
写法 连续拉了几帧 含义
-------------------------------------------------------
不写 loop 1 不写就取默认值 1
loop=0 5 以上 0 表示无限循环
loop=1 1 1 是默认值,只放一遍
**中间那一行说明水印图的正确写法:`movie=水印.png:loop=0`。**
**不写 loop 的话,主视频放到第二帧,水印就没有下一帧可取了。**
**这也是为什么网上抄来的命令里几乎都带着 `:loop=0`——不是可选项。**
**④ 命令行长什么样**
**`ffmpeg -i in.mp4 -vf "movie=logo.png:loop=0[wm];[in][wm]overlay=10:10" out.mp4`**
**注意 `movie` 这一段自己就带方括号标号:它不是在链中间被「串」进去的,**
**而是从旁边岔出来的一路,必须靠 overlay 把两路合起来。**
**`-vf` 只能写单链,所以真正带水印的命令几乎都要用 `-filter_complex`。**
这段代码能用相对路径 s5.mp4,是因为工作目录里就有它。
换成绝对路径就得转义,下一节专门讲这件事。
esc() 只处理「反斜杠换正斜杠」与「冒号转义」两步。
两步缺一不可,后面 6.2.2 会把四种组合逐个试一遍。
pulled 到 5 帧就停,是我的测试上限,不是 movie 的上限。 loop=0 是无限循环,真要拉它自己能一直出帧。
30 秒实验
- 换成 gif:把 png 换成一个 gif,看 movie 读出来是几帧、什么格式。
- 加 seek:
movie=in.mp4:seek_point=1.0,看它从第几帧开始出。 - 去掉 loop:把
:loop=0删掉,观察第二帧之后水印还在不在。
常见误区
- 以为静态 pad 为 0 就不能当源。movie 是动态输出,pad 运行时才建。
- 水印不加 loop=0。第二帧就没水印了,而且不报错。
- 把 loop=1 当成无限循环。它是「放一遍」,与 0 的含义正好相反。
自测三问(点开对答案)
1. movie 的 pad 为什么静态查到 0 个? —— 它用的是动态输出,输出 pad 要到配置阶段才按文件内容创建。 2. 为什么水印图读出来必须是 rgba? —— 要留住 alpha 通道。丢了它就变成不透明,透明处会成黑块。 3. loop=0 与 loop=1 分别是什么? —— 0 是无限循环,1 是只放一遍(也是默认值)。- ☐ (本节完,下一节 6.2.2 -vf 的 movie 中的绝对路径)