🎬 第六章 FFmpeg命令行实现图片水印及文字跑马灯
📄 一句话概括

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 秒实验

  1. 换成 gif:把 png 换成一个 gif,看 movie 读出来是几帧、什么格式。
  2. 加 seek:movie=in.mp4:seek_point=1.0,看它从第几帧开始出。
  3. 去掉 loop:把 :loop=0 删掉,观察第二帧之后水印还在不在。

常见误区

  1. 以为静态 pad 为 0 就不能当源。movie 是动态输出,pad 运行时才建。
  2. 水印不加 loop=0。第二帧就没水印了,而且不报错。
  3. 把 loop=1 当成无限循环。它是「放一遍」,与 0 的含义正好相反。
自测三问(点开对答案) 1. movie 的 pad 为什么静态查到 0 个? —— 它用的是动态输出,输出 pad 要到配置阶段才按文件内容创建。 2. 为什么水印图读出来必须是 rgba? —— 要留住 alpha 通道。丢了它就变成不透明,透明处会成黑块。 3. loop=0 与 loop=1 分别是什么? —— 0 是无限循环,1 是只放一遍(也是默认值)。

相关笔记