🎬 第十章 FFmpeg命令行在Linux系统中的应用
📄 一句话概括

drawtext 在 Linux 上的坑全在两处:字体路径,和表达式里的逗号。 跑马灯的位移是一条 mod 公式,走完 w+tw 个像素就绕回起点。

📝 本节和前面几节的关系

第六章已经用 drawtext 做过水印和跑马灯, 这一章补两件当时没细讲的事:\, 到底为什么必须转义, 以及 text_w 量的到底是字框还是墨迹。

一、Linux 上那条 drawtext 命令

同一条命令,Linux 上写法是这样(注意三处不一样):

ffmpeg -i in.mp4 \
  -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: \
       text='Hello':x=10:y=10:fontsize=28" \
  out.mp4

哪一处 Linux 上怎么写 坑在哪


字体路径 /usr/share/fonts/... Linux 没有 C 盘那套路径 路径里的冒号 不用转义 写 C 盘路径必须写成 C\: 文本里的空格 text='…' 不加引号会被断开 表达式里的逗号 要写成 \, 滤镜串自己用逗号分层

字体路径是 Linux 上第一个坑:C:\Windows\Fonts 那套在这儿不存在。 常见字体在 /usr/share/fonts/truetype/ 下按家族分目录, Debian 系一般有 dejavu/DejaVuSans.ttf,要中文字体得先装 fonts-noto-cjk。 找不到字体时 FFmpeg 报的是 Cannot find a valid font,不会告诉你该装哪个包。

二、表达式里的逗号为什么必须转义

因为滤镜串本身就是用逗号分层的:-vf "hflip,vflip,scale=320:240"。 而 drawtext 的 x 是个表达式,里面常有逗号—— mod(t*100,w+tw) 就有一个。不转义,这一刀就切在表达式中间。

滤镜串 切成几层 切出来是什么


x=w-mod(t100\,w+tw) 1 整条不动 x=w-mod(t100,w+tw) 2 后半截 w+tw) 成了另一个滤镜 hflip,vflip,scale=320:240 3 三个滤镜,本来就要三份

第二行就是没转义的下场。 FFmpeg 会拿 w+tw) 去找一个同名的滤镜,找不到就报错; 报错里指的位置往往跟真正的原因差着十万八千里, 这就是「明明只想改个坐标,却报 No such filter」的来历。

两种解法都行:写 \,,或者把整个表达式用单引号包起来—— x='w-mod(t*100,w+tw)',单引号里的逗号对滤镜解析器不算分隔符。 后者更好读,代价是多一层引号,shell 那边要写对。

三、跑马灯的位移公式

就一条:x = w - mod(t*speed, w+tw)。 w 是屏宽,tw 是文字宽,speed 是每秒走多少像素。

为什么是 w+tw 而不是 w? 因为文字完全走出左边(x 到 -tw)之后才开始下一轮, 整条轨迹的长度是「屏宽 + 文字宽」,除以速度就是走一圈的秒数。

时刻 公式 x 实际左边界 屏上看见几列


0.0 秒 320 -1 0 0.5 秒 270 272 42 1.0 秒 220 222 84 2.0 秒 120 122 173 3.0 秒 20 22 173 5.0 秒 -180 0 20

屏宽 320、文字宽 200、速度 100,走一圈是 520 像素,也就是 5.2 秒。 所以 t=0 和 t=5.2 秒时 x 都是 320——整条字在屏幕右边外面,一列都看不见。

t=5.0 秒那一行有点意外:公式给的 x 是 -180,可屏上还能看见 20 列。 因为文字宽 200,从 -180 起算还剩 20 像素露在屏内。 「看不见」的准确条件是 x <= -tw,不是 x < 0—— 这一点直接决定了循环该怎么写才不会出现「闪一下」。

四、text_w 量的是字框,不是墨迹

表里第三列比第二列普遍大 2 像素,除了看得见的那几行。 不是公式错了,是字形左边自带一点留白(bearing)。

getbbox 返回的是「字框」,text_w 也一样—— 它们量的是排版用的方框,不是墨迹真正覆盖的范围。 所以「让字刚好贴住右边缘」这种要求,靠 w-tw 是做不到严丝合缝的。

实用结论:留两三个像素余量反而稳。 水印那种要紧贴边缘的场合,宁可写 x=w-tw-4,也别指望 x=w-tw 刚好。

五、硬字幕、软字幕与 Linux 字体

drawtext 画出来的是硬字幕:像素烤进画面,播放器关不掉。 软字幕是另一条流(-c:s mov_text 之类),播放器可以选、可以关。

Linux 上做硬字幕,有两处依赖容易缺: drawtext 要 fontconfig 找一个字体文件; subtitles 滤镜要 libass,缺了会在配置滤镜时就报 No such filter。 这两个都不是「参数写错」,是编译时就没带,报错信息也完全不像。

所以第六章在沙箱里绕开了 drawtext,改用 Pillow 画字再 overlay—— 那条路不依赖 fontconfig,只要有一个字体文件就能走通, 这也是「本机没有某个滤镜」时最实际的一条退路。

六、动手 Demo:把滤镜串切开看看,再逐时刻验一遍位移

下面这段干两件事:先用二十行代码模拟 FFmpeg 切滤镜串的规则, 证明 \, 到底是干什么用的;再把跑马灯的公式逐时刻算一遍。

def split_chain(s):
    """按「未转义的逗号」切滤镜串——ffmpeg 自己就是这么切的。"""
    out, cur, i = [], [], 0
    while i < len(s):
        if s[i] == "\\" and i + 1 < len(s):
            cur.append(s[i:i + 2])
            i += 2
            continue
        if s[i] == ",":
            out.append("".join(cur))
            cur = []
            i += 1
            continue
        cur.append(s[i])
        i += 1
    out.append("".join(cur))
    return out


for s in ("x=w-mod(t*100\\,w+tw)",
          "x=w-mod(t*100,w+tw)",
          "hflip,vflip,scale=320:240"):
    print("%d 层  %s" % (len(split_chain(s)), split_chain(s)))

W, TW, SPEED = 320, 200, 100
PERIOD = W + TW
print("走一圈 %d 像素 = %.2f 秒" % (PERIOD, PERIOD / float(SPEED)))
for t in (0.0, 0.5, 1.0, 2.0, 3.0, 5.0):
    x = int(W - (t * SPEED) % PERIOD)
    print("t=%.1f  x=%4d  完全看不见吗:%s" %
          (t, x, x >= W or x <= -TW))

实跑输出:

--------------------------------------------------------------------
10.3 使用 FFmpeg 实现文字水印及跑马灯
--------------------------------------------------------------------

**① Linux 上的 drawtext,三处跟 Windows 不一样**

    ffmpeg -i in.mp4 \
      -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: \
           text='Hello':x=10:y=10:fontsize=28" \
      out.mp4

  哪一处          Linux 上怎么写            坑在哪
  --------------------------------------------------------------------
  字体路径        /usr/share/fonts/...      Linux 没有 C 盘那套路径
  路径里的冒号    不用转义                  写 C 盘路径必须写成 C\:
  文本里的空格    text='…'                  不加引号会被断开
  表达式里的逗号  要写成 \,                 滤镜串自己用逗号分层

**最后一条是这三处里最难缠的:`drawtext` 的 `x` 是个表达式,**
**而表达式里常有逗号(`mod(t*100,w+tw)` 就有),逗号又是滤镜串的分隔符。**
**所以要么写 `\,`,要么把整个表达式用单引号包起来。**

**② 用一段切分代码把这件事证明出来**

  滤镜串                      切成几层  切出来是什么
  ------------------------------------------------------------------------
  x=w-mod(t*100\,w+tw)        1         x=w-mod(t*100\,w+tw)
  x=w-mod(t*100,w+tw)         2         x=w-mod(t*100 | w+tw)
  hflip,vflip,scale=320:240   3         hflip | vflip | scale=320:240

**第二行就是没转义的下场:一条 `drawtext` 被硬生生劈成两条,**
**后半截 `w+tw)` 会被当成另一个滤镜名,报「找不到这个滤镜」。**
**而报错里指的位置往往跟真正的原因差着十万八千里。**

**③ 跑马灯的位移公式,逐时刻验一遍**

**文字量出来 200×30 像素,屏宽 320,速度 100 像素每秒。**
**公式是 `x = w - mod(t*speed, w+tw)`,走到 `w+tw` 个像素就绕回起点,**
**也就是 520 像素一个来回,折算 5.20 秒。**

  时刻      公式 x      实际左边界    差值      屏上看见几列
  ------------------------------------------------------------
  0.0 秒    320         -1            -321      0
  0.5 秒    270         272           +2        42
  1.0 秒    220         222           +2        84
  2.0 秒    120         122           +2        173
  3.0 秒    20          22            +2        173
  5.0 秒    -180        0             +180      20

**两点值得盯住:**

**第一,t=0 时公式给的是 x=320,也就是整条字正好在屏幕右边外面,**
**一列都看不见;t=5.20 秒时又回到同一个位置——这就是「走完一圈」。**

**第二,除了看得见的那几行,实际左边界都比公式算的大 2 像素。**
**不是公式错了,是字形左边自带一点留白(bearing),**
**`getbbox` 量的是字框不是墨迹——`text_w` 也一样,它量的是字框。**
**所以想「刚好贴边」就别指望数值严丝合缝,留两像素余量反而稳。**

第一行切出 1 层,第二行切出 2 层——这一刀就是 \, 挡住的那一刀。 第三行切出 3 层是应该的,它本来就是三段的滤镜链。

最后那个布尔值把「看不见」的两个区间都写进去了: 右边还没进来是 x >= w,左边已经出去是 x <= -tw。 只判 x < 0 是不够的——左边刚出屏时右边可能还露着一大截。

30 秒实验

  1. 把速度改成 200:算一下走一圈要几秒,再想想「太快看不清」该怎么折中。
  2. 把 mod 换成 abs:看轨迹会变成什么样,理解 mod 在这里的唯一作用。
  3. 在 Linux 上跑一次 fc-list:找出本机到底装了哪些字体,别再瞎猜路径。

常见误区

  1. 表达式里的逗号不转义。报错是「找不到滤镜」,跟逗号一点关系看不出来。
  2. 以为循环周期是屏宽。是「屏宽 + 文字宽」,差着一个字宽。
  3. 以为 x < 0 就看不见了。文字还有一半挂着,屏上照样露着。
自测三问(点开对答案) 1. `x=w-mod(t*100,w+tw)` 为什么要写 `\,`? —— 滤镜串用逗号分层,不转义就会把表达式切成两段,后半截被当成另一个滤镜。 2. 跑马灯走一圈是多久? —— (屏宽 + 文字宽)除以速度。320 + 200 再除 100,5.2 秒。 3. `text_w` 和墨迹宽度差在哪? —— `text_w` 是排版字框,墨迹左右各有 bearing 留白,所以框比墨迹宽一两个像素。

相关笔记