把第 2 章见过的 -vf 真正用起来:overlay 加水印、scale 缩放、drawtext 写字、-ss 截图——四个实战场景逐个跑通,每条命令都亲手验证
核心方法论:排除不可能
「水印没出来、尺寸不对、文字消失了——别急着怀疑 ffmpeg 坏了。把'不可能'一项项排除:滤镜参数格式、输入流编号、字体路径、-ss 放哪边……每个可疑点都亲手验证一遍。排除掉一切不可能,剩下的就是真相。这一章的三个滤镜,我们一条条跑给你看,跑出来的数字是什么就是什么。」
第 2 章我们用过 -vf scale=320:180 做缩放,这一章把滤镜真正用熟。先补概念:视频滤镜(video filter)说人话就是"画面的加工车间"——每一帧从车间入口进来,经过一台台机器(缩放、加字、叠加)再出去。滤镜有两条入口:-vf 只接收一个视频输入,多个滤镜用逗号 , 串成滤镜链(filter chain),像流水线逐台加工;-filter_complex 是滤镜图(filter graph),支持多输入、多输出、任意分支,滤镜之间用分号 ; 分隔。一条铁律:单输入用 -vf;多输入(水印 = 主视频 + logo 图)必须用 -filter_complex。
两条入口的写法差异很直观。滤镜链里每个滤镜的输出自动接下一个滤镜的输入,不用写名字;滤镜图里要给每个滤镜的输入输出贴标签([0:v] 是"第 0 个输入的视频流",[1:v] 是"第 1 个输入的视频流"),再用 [标签]滤镜名=参数[新标签] 显式连线。本章所有命令都在本机(ffmpeg 8.1.1)真实执行过,先看骨架:
# 滤镜链:单输入,逗号串联,逐个加工(不用写标签)
ffmpeg -i sample.mp4 -vf "scale=-2:180,drawtext=fontfile=...:text=..." -c:a copy out.mp4
# sample.mp4 ──> scale ──> drawtext ──> out.mp4(一条流水线)
# 滤镜图:多输入必须用 -filter_complex,标签显式连线
ffmpeg -i sample.mp4 -i logo.png -filter_complex "[0:v][1:v]overlay=10:10" -c:a copy out.mp4
# 第 0 路视频 + 第 1 路视频 ──> overlay 叠加 ──> out.mp4
福尔摩斯查案的第一个习惯:怀疑哪个滤镜,就把它的参数表调出来看。ffmpeg 每个滤镜都自带说明,ffmpeg -h filter=滤镜名 一行命令就能看到全部可配参数、默认值和取值范围——这是排查"参数写错"的标准动作,后面每节我们都会用到。以 scale 为例(真实输出节选):
# 查 scale 滤镜的参数表(标准动作,真实输出节选)
ffmpeg -hide_banner -h filter=scale
# Filter scale
# Scale the input video size and/or convert the image format.
# scale AVOptions:
# w / width <string> Output video width
# h / height <string> Output video height
# size / s <string> set video size
# force_original_aspect_ratio <int> ... (default disable)
# force_divisible_by <int> ... (default 1)
| 入口 | 输入路数 | 滤镜连接方式 | 典型场景 |
|---|---|---|---|
-vf 滤镜链 | 单输入 | 逗号 , 串联,自动接力 | 缩放、加字、裁剪等单路加工 |
-filter_complex 滤镜图 | 多输入 / 多输出 | 分号 ; 分行,[标签] 显式连线 | 水印叠加、画中画、多路合成 |
记不住滤镜参数?先跑 ffmpeg -h filter=名字。看到 w / width <string> 这种两栏格式,左栏是参数名,右栏是类型和默认值。'效果不对先查滤镜参数格式'——这是本章排错的第一原则,也是福尔摩斯排除法的起点。
第一个实战:给视频加水印。overlay 说人话就是"把一张图盖在视频画面上面",是最常用的多输入滤镜——主视频一路输入,logo 图一路输入,必须用 -filter_complex。先准备素材:本机没有现成 logo,用 ffmpeg 的 color 虚拟源(纯色画面发生器)生成 160x60 黛青底图,再用 drawtext 写上 "AILab"(drawtext 下一节细讲,先借用):
生成后先用 ffprobe 体检:png、160x60、像素格式 rgb24。这个 rgb24 意味着图片没有透明通道,叠加后是不透明矩形;真正的"透明水印"要用带 alpha 的 RGBA png,overlay 会尊重 alpha 让底下画面透出来。先用眼睛和 ffprobe 确认这个差别:
# 造一张 logo.png:160x60 的黛青底 + 白色 "AILab"(本机真实执行)
ffmpeg -y -f lavfi -i "color=c=0x3f7d76:size=160x60:duration=1:rate=1" \
-vf "drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='AILab':fontsize=28:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2" \
-frames:v 1 logo.png
# 实测产物:logo.png = 2091 字节,160x60,png
# ffprobe 体检:width,height,codec_name / pix_fmt
ffprobe -v error -show_entries stream=width,height,pix_fmt -of csv=p=0 logo.png
# 实测输出:160,60,rgb24 ← rgb24 表示无 alpha,水印将是不透明矩形
水印叠加命令:两个 -i 引入两路输入,滤镜图里 [0:v] 取主视频、[1:v] 取 logo,overlay 的 10:10 是"离左上角往右 10 像素、往下 10 像素"。x:y 默认 0:0(左上角贴齐),且支持表达式:main_w/main_h 是主画面宽高,overlay_w/overlay_h 是水印宽高,"右下角留 10 像素边距"写成 main_w-overlay_w-10:main_h-overlay_h-10。两条命令都实测跑过:
# 左上角水印:离边 10 像素(本机真实执行)
ffmpeg -y -i sample.mp4 -i logo.png \
-filter_complex "[0:v][1:v]overlay=10:10" -c:a copy overlay_10.mp4
# 实测产物:overlay_10.mp4 = 498070 字节,分辨率仍为 640x360(overlay 不改变尺寸)
# 右下角水印:main_w-overlay_w-10 表达式逐帧求值
ffmpeg -y -i sample.mp4 -i logo.png \
-filter_complex "[0:v][1:v]overlay=main_w-overlay_w-10:main_h-overlay_h-10" -c:a copy overlay_br.mp4
# 实测产物:overlay_br.mp4 = 482328 字节,水印出现在右下角
水印"没出现"时按排除法查三处:① 是不是用了 -vf 而没用 -filter_complex?多输入 + -vf 直接报错;② 标签顺序写反没有,[0:v] 是主视频、[1:v] 是 logo,写反就成了"视频盖在 logo 上";③ 位置表达式算错没有,main_w-overlay_w-10 少个减号水印就飞出画面了。
| 水印位置 | overlay 的 x:y 写法 |
|---|---|
| 左上角 | overlay=10:10 |
| 右上角 | overlay=main_w-overlay_w-10:10 |
| 左下角 | overlay=10:main_h-overlay_h-10 |
| 右下角 | overlay=main_w-overlay_w-10:main_h-overlay_h-10 |
注意:本章用 color 源造的 logo.png 是 rgb24——没有透明通道,盖上去就是一块不透明矩形。真实场景的透明水印(png 带 alpha)overlay 会正确透出底下画面;如果发现"水印把画面挡死了",先用 ffprobe -show_entries stream=pix_fmt 查图片有没有 alpha(rgba/png 带 a 就是有)。
第二个实战:缩放。scale 语法是 scale=宽:高,但工程里几乎不写死两个数字——等比缩放要保持宽高比,写死就变形了。答案是 -2:某一边写 -2,另一边按原始宽高比自动算,且保证结果是偶数(偶数约束来自编码:H.264 的 yuv420p 要求宽高都是偶数,第 2 章提过的 -pix_fmt yuv420p 就是它)。本机实测:640x360 素材 scale=-2:180 得 320x180;scale=180:-2 得 180x102——360 按 16:9 折算宽度 101.25,就近取偶成 102:
# 指定高度 180,宽度按 16:9 自动算(-2 = 自动且保证偶数)(本机真实执行)
ffmpeg -y -i sample.mp4 -vf "scale=-2:180" -c:a copy scaled.mp4
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 scaled.mp4
# 实测输出:320,180 ← 640:360 = 16:9,等比成立
# 指定宽度 180,高度自动算:101.25 就近取偶数 = 102
ffmpeg -y -i sample.mp4 -vf "scale=180:-2" -c:a copy s180.mp4
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 s180.mp4
# 实测输出:180,102
更复杂的场景是"塞进一个盒子":平台要求封面 200x200,但视频是 16:9——直接 scale=200:200 会拉变形。解法是 force_original_aspect_ratio=decrease(保持宽高比缩到能完整放进盒子,不拉伸),再配合 pad 滤镜(补黑边到指定尺寸)。这条命令第一次跑就报错了,福尔摩斯式排错现场:等比计算高度是 200÷16×9 = 112.5,scale 就近取整得到 200x113——113 是奇数,libx264 拒绝开工:
# 第一次跑:等比缩进 200x200 盒子,真实报错(本机完整复现)
ffmpeg -y -i sample.mp4 -vf "scale=200:200:force_original_aspect_ratio=decrease" -c:a copy far.mp4
# 实测报错(关键两行):
# [libx264] height not divisible by 2 (200x113)
# [enc:libx264] Error while opening encoder - maybe incorrect parameters ...
# 修复:force_divisible_by=2 强制结果可被 2 整除 → 200x112
ffmpeg -y -i sample.mp4 -vf "scale=200:200:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=200:200:(ow-iw)/2:(oh-ih)/2:color=black" -c:a copy pad2.mp4
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 pad2.mp4
# 实测输出:200,200 ← 黑边补满盒子,画面不变形(letterbox 信箱式)
注意:写死奇数尺寸同样翻车——scale=319:180 直接报 width not divisible by 2 (319x180),因为 yuv420p 要求宽高都是偶数。报错里的 "(200x113)" 就是福尔摩斯要的线索——看到 not divisible by 2,答案就是让尺寸变偶数(-2 或 force_divisible_by=2)。
| 需求 | scale 写法 | 本机实测结果 |
|---|---|---|
| 指定高度、宽度等比 | scale=-2:180 | 320x180 |
| 指定宽度、高度等比 | scale=180:-2 | 180x102(101.25 就近偶数) |
| 塞进盒子不拉伸 | scale=200:200:force_original_aspect_ratio=decrease | 200x113 → 报错,需 force_divisible_by=2 |
| 盒子 + 黑边补齐 | 上面写法 + ,pad=200:200:(ow-iw)/2:(oh-ih)/2:color=black | 200x200 |
pad 里 (ow-iw)/2 是"(输出宽 - 输入宽)/ 2",把画面水平居中;(oh-ih)/2 垂直居中。ow/iw、oh/ih 是 pad 内置的输出/输入尺寸变量,和 overlay 的 main_w/overlay_w 同一套表达式体系,记一组举一反三。
第三个实战:画面上写字。drawtext 滤镜(需 ffmpeg 编译时带 libfreetype,本机 ffmpeg-full 自带)说人话就是"往每一帧打印一行文字"。最关键参数是 fontfile——字体文件必须写绝对路径。macOS 系统字体在 /System/Library/Fonts/Supplemental/(英文 Arial.ttf,中文 Songti.ttc),Linux 一般在 /usr/share/fonts/。先写一行静态文字:白色 24 号、离左上角 10 像素。文字里若有冒号必须转义成 \:(冒号是滤镜参数的天然分隔符),本命令没有冒号所以直接写:
# 静态文字:白色 24px,左上角(本机真实执行)
ffmpeg -y -i sample.mp4 -vf "drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='AILab Tutorial':fontsize=24:fontcolor=white:x=10:y=10" -c:a copy texted.mp4
# 实测产物:texted.mp4 = 517863 字节;抽帧检查左上角确有白色文字
# 中文字体:宋体 Songti.ttc(说人话:ttc 是字体集合,drawtext 直接支持)
ffmpeg -y -i sample.mp4 -vf "drawtext=fontfile=/System/Library/Fonts/Supplemental/Songti.ttc:text='福尔摩斯排除不可能':fontsize=28:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2" -frames:v 1 -f image2 zh_text.png
# 实测:抽帧检查画面中央确有白色中文(x=(w-text_w)/2 是水平居中表达式)
drawtext 的杀手锏是动态文字:text 支持 %{...} 占位符,每渲染一帧求值一次。text='%{pts}' 显示当前帧时间戳(pts,presentation timestamp,显示时间戳——第 4 章讲过 B 帧重排的 pts/dts 两套时间轴),text='%{pts\:hms}' 格式化成"时:分:秒"。注意 \: 的转义:%{pts} 内部也要用冒号,不转义就会被滤镜当成参数分隔符,文字直接解析失败——drawtext 最经典的参数格式坑。两条都实测过,抽帧能看到画面底部黄色时间戳逐帧跳动:
# 动态时间戳:%{pts} 原始时间戳(单位随容器而定,这里是微秒级数值)(本机真实执行)
ffmpeg -y -i sample.mp4 -vf "drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='%{pts}':fontsize=24:fontcolor=yellow:x=10:y=h-40" -c:a copy texted_pts.mp4
# 实测产物:texted_pts.mp4 = 537921 字节;抽帧底部有黄色数字
# 人类可读时间戳:%{pts\:hms} 显示为 00:00:02 形式(冒号必须转义成 \:)
ffmpeg -y -i sample.mp4 -vf "drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='%{pts\:hms}':fontsize=24:fontcolor=yellow:x=10:y=h-40" -c:a copy texted_hms.mp4
# 实测产物:texted_hms.mp4;抽帧底部显示黄色 "00:00:02" 样式的动态时间
注意:fontfile 路径写错时,drawtext 不会报错——它静默回退到系统默认字体(本机实测回退到 Hiragino 角ゴシック),且 fontcolor 默认值是 black 黑色。"文字消失了"的真相往往是:字体路径拼错(用了回退字体)或没写 fontcolor(黑字压在深色画面上看不见)。排查顺序:先 fontfile 拼写,再 fontcolor——都排除掉文字还不在,才轮到怀疑滤镜本身。
查字体:Linux 用 fc-list,macOS 看 /System/Library/Fonts/ 目录。中文路径要写对:简体中文常用 /System/Library/Fonts/Supplemental/Songti.ttc(宋体)或苹方 /System/Library/Fonts/PingFang.ttc——写错不报错但回退字体,正是上一段 warning 的场景。
第四个实战:截图。核心是 -frames:v 1(只输出 1 帧就收工)加 -ss 跳转。第 2 章裁剪矩阵验证过 -ss 的位置决定快慢与精度,截图同理:-ss 2 -i sample.mp4(-ss 在 -i 前)是输入侧跳转——先跳关键帧再解码,快;-i sample.mp4 -ss 2 是输出侧跳转——从头解码丢掉前 2 秒,慢但帧级精确。截图要"快"和"差不多准",工程上默认 -ss 放 -i 前。本机实测:30 秒素材截第 28 秒,输入侧 0.09 秒,输出侧 0.16 秒,两图画面一模一样(同一关键帧):
# 截图:-ss 放 -i 前(输入侧快跳转)+ -frames:v 1 只出 1 帧(本机真实执行)
ffmpeg -y -ss 2 -i sample.mp4 -frames:v 1 -q:v 2 shot_ss2.png
# 实测产物:shot_ss2.png = 53138 字节,png,640x360(-q:v 2 控制质量,范围 2-31,越小越好)
# 截图 + 缩放一步到位:先 -ss 跳转,再 -vf scale 缩小输出
ffmpeg -y -ss 2 -i sample.mp4 -frames:v 1 -vf "scale=320:-2" shot_small.png
# 实测产物:shot_small.png = 20233 字节,png,320x180(缩略图比原图小一半多)
输出单帧还有个小坑:image2 图像封装器(muxer,把帧打包成图片文件)默认以为你要输出图片序列,碰到不带序号通配符的文件名会发一条提示——加 -frames:v 1 就能正常写单张图(实测不写 -f image2 也能靠 .png 后缀自动识别封装器)。要 jpg 就把输出名改成 .jpg,-q:v 同样管用。放一张"快与准"的对照:
# -ss 位置对照实测:30 秒素材,截第 28 秒(本机真实计时)
# 输入侧:-ss 28 在 -i 之前 —— 先跳关键帧再解码
ffmpeg -y -hide_banner -ss 28 -i long30.mp4 -frames:v 1 in_side.png # real 0m0.090s
# 输出侧:-ss 28 在 -i 之后 —— 从头解码丢掉前 28 秒
ffmpeg -y -hide_banner -i long30.mp4 -ss 28 -frames:v 1 out_side.png # real 0m0.159s
# 实测结论:输入侧 0.090s <- 输出侧 0.159s;两图字节数相同(54487 B),画面一致
注意:-ss 放 -i 后是"逐帧解码再丢弃",视频越长越慢,但帧级精确;放 -i 前是"先找关键帧",快,但落在关键帧上(第 2 章裁剪矩阵验证过:默认 GOP 素材全片只有第 0 帧是关键帧时,-ss 2 会跳到离 2 秒最近的关键帧)。截图省事用输入侧;要"精准到某一帧"再考虑输出侧。
截出来的图"模糊"或"不是想要的那一帧"时,按排除法:① 先确认 -ss 位置(输入侧落在关键帧上);② 再查 -frames:v 1 有没有写(不写就输出一整个视频文件,名字却是 .png);③ 最后看 -q:v(jpeg 质量 2-31,数字越小越好,png 无损不受它影响)。
三个滤镜都过关了,最后拧成一条命令——这也是滤镜图真正的威力:多输入 + 多步加工 + 中间产物命名。[0:v]scale=-2:180[base] 意思是"第 0 路视频先缩放,结果贴标签 [base]";[base][1:v]overlay=10:10[ov] 叠加 logo,结果贴标签 [ov];[ov]drawtext=... 最后加一行小字。三条滤镜用分号 ; 分行(滤镜图分隔符,区别于滤镜链的逗号),音频用 -c:a copy 原样搬运——滤镜只动视频流:
实测产物 320x180(缩放生效)、144678 字节(比原片 526497 字节小得多)、左上角有黛青 logo、底部有一行白色小字 "AILab"——每步效果都能用 ffprobe 和抽帧验证。这条命令就是"转码工程"里最典型的滤镜管线雏形:
# 组合拳:缩放 → 水印 → 加字,一条命令完成(本机真实执行)
ffmpeg -y -i sample.mp4 -i logo.png \
-filter_complex "[0:v]scale=-2:180[base];[base][1:v]overlay=10:10[ov];[ov]drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='AILab':fontsize=16:fontcolor=white:x=10:y=h-24" \
-c:a copy combo.mp4
# 实测产物:combo.mp4 = 144678 字节;x264 统计 kb/s:151.92
# 验收:ffprobe 确认尺寸,抽帧确认水印与文字都渲染了
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 combo.mp4
# 实测输出:320,180
ffmpeg -y -hide_banner -ss 2 -i combo.mp4 -frames:v 1 check_combo.png
# 实测抽帧检查:左上角黛青 logo(约 9053 像素命中品牌色),底部白色小字清晰可读
滤镜图里标签名自由取([base]、[ov] 只是名字),但每个标签只能被消费一次——一个中间结果想喂给两个下游滤镜,得先复制一份。新手最常见的报错是 "label base not found" 或 "Unknown filter",多半是分号 ; 写成逗号 ,,或标签名没配对。逐段检查标签的"出生"([0:v]... 定义处)和"死亡"(被下一个滤镜消费处),就能定位。
注意:滤镜链(-vf)用逗号 ,,滤镜图(-filter_complex)用分号 ; 分行——这是两条入口最容易搞混的语法点。scale=-2:180,pad=... 的逗号是"链内串联",;[base]... 的分号是"图内分行",混着写没问题,但方向写反(滤镜图里用逗号分行、滤镜链里写标签)必然报错。
到此,水印、缩放、加字、截图四件兵器都验过了。回看这一章:两条入口、overlay 的位置表达式、scale 的偶数约束与盒子缩放、drawtext 的字体路径与动态占位符、-ss 的快与准——每一个"效果不对"背后,都是一次"排除不可能"的推理。下一章也是最后一章,我们会把命令行、滤镜、探针这些零散环节串成一张转码工程全景:从拿到素材到交付成品,一条完整的生产管线长什么样,敬请期待。
主视频是 640x360,logo 是 160x60。要求水印出现在画面右上角、离右边缘和上边缘各 20 像素。overlay 的 x:y 表达式应该怎么写?如果想让水印水平居中(x 居中,y 仍为 20),又该怎么写?
回顾 15.2 的位置表达式表:右上角的关键是"主画面宽 - 水印宽 - 边距"。
右上角:overlay=main_w-overlay_w-20:20(x = 640-160-20 = 460,y = 20)。水平居中:overlay=(main_w-overlay_w)/2:20——注意居中不是 main_w/2,而是要减去水印自身宽度的一半。
16:9 的素材执行 scale=100:100:force_original_aspect_ratio=decrease:① 计算机会算出什么尺寸?② 直接拿这个尺寸喂给 libx264 会怎样?③ 怎么修复?
回顾 15.3 的排错现场:200:200 盒子算出 200x113 然后发生了什么?
① 高度 = 100÷16×9 = 56.25,取整得 100x56 或 100x57(取决于取整方向,奇数时即为 57)。② 奇数尺寸 + yuv420p 会被 libx264 拒绝:height not divisible by 2。③ 加 force_divisible_by=2 强制偶数(100x56),必要时再补 pad 填满盒子。
你写了 drawtext=fontfile=...:text='%{pts:hms}':fontsize=24:fontcolor=white,结果 ffmpeg 报参数解析错误。① 问题出在哪?② 修正后,%{pts} 与 %{pts\:hms} 显示的内容有何区别?③ 如果命令不报错但文字"消失了",第一步该查什么?
想想 15.4 里"冒号是滤镜参数的天然分隔符"这句话,再想想 %{pts\:hms} 为什么带反斜杠。
① %{pts:hms} 里的冒号没转义,被滤镜当成参数分隔符,text 的值被截断导致解析错误——应写成 %{pts\:hms}。② %{pts} 显示原始时间戳数值(如 13125),%{pts\:hms} 格式化成 00:00:02 这种人类可读形式。③ 先查 fontfile 路径拼写(写错会静默回退默认字体),再查 fontcolor(默认是黑色)。
用一条 ffmpeg 命令完成:把 sample.mp4 缩放成宽 480(等比)、叠加上自己造的 logo.png(右下角、边距 15 像素)、在画面顶部居中写一行白色文字 "Helios AILab"(字号 20),最后用输入侧 -ss 截第 3 秒的一帧存成 check_final.png。要求:① 写出完整命令;② 用 ffprobe 验证输出帧是 480x270;③ 说明音频流为什么全程不用管。
综合 15.2-15.5:滤镜图里标签链 [0:v]scale=-2:480[base] → overlay → drawtext;截图时 -ss 放 -i 前、-frames:v 1 收尾。居中表达式 x=(w-text_w)/2。
完整命令(本机已实测,输出帧 480x270,水印在右下角、顶部有居中白字):
ffmpeg -y -i sample.mp4 -i logo.png -filter_complex "[0:v]scale=-2:480[base];[base][1:v]overlay=main_w-overlay_w-15:main_h-overlay_h-15[ov];[ov]drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:text='Helios AILab':fontsize=20:fontcolor=white:x=(w-text_w)/2:y=10" -c:a copy combo2.mp4,再 ffmpeg -y -ss 3 -i combo2.mp4 -frames:v 1 check_final.png。
① 见上;② ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 check_final.png 输出 480,270;③ 滤镜(-vf/-filter_complex)只作用于视频流,音频流只要没有 -af/-an 之类的指令就原样通过,-c:a copy 显式声明"音频原样搬运"即可。