第15章:视频滤镜实战:水印、缩放与截图

把第 2 章见过的 -vf 真正用起来:overlay 加水印、scale 缩放、drawtext 写字、-ss 截图——四个实战场景逐个跑通,每条命令都亲手验证

🎩

本章导师:福尔摩斯

核心方法论:排除不可能

「水印没出来、尺寸不对、文字消失了——别急着怀疑 ffmpeg 坏了。把'不可能'一项项排除:滤镜参数格式、输入流编号、字体路径、-ss 放哪边……每个可疑点都亲手验证一遍。排除掉一切不可能,剩下的就是真相。这一章的三个滤镜,我们一条条跑给你看,跑出来的数字是什么就是什么。」

15.1 滤镜的入口:-vf 滤镜链与 -filter_complex 滤镜图

第 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> 这种两栏格式,左栏是参数名,右栏是类型和默认值。'效果不对先查滤镜参数格式'——这是本章排错的第一原则,也是福尔摩斯排除法的起点。

15.2 overlay 水印:两路输入的第一课

第一个实战:给视频加水印。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 就是有)。

15.3 scale 缩放:尺寸计算的语法

第二个实战:缩放。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,答案就是让尺寸变偶数(-2force_divisible_by=2)。

需求scale 写法本机实测结果
指定高度、宽度等比scale=-2:180320x180
指定宽度、高度等比scale=180:-2180x102(101.25 就近偶数)
塞进盒子不拉伸scale=200:200:force_original_aspect_ratio=decrease200x113 → 报错,需 force_divisible_by=2
盒子 + 黑边补齐上面写法 + ,pad=200:200:(ow-iw)/2:(oh-ih)/2:color=black200x200
福尔摩斯提示

pad(ow-iw)/2 是"(输出宽 - 输入宽)/ 2",把画面水平居中;(oh-ih)/2 垂直居中。ow/iw、oh/ih 是 pad 内置的输出/输入尺寸变量,和 overlay 的 main_w/overlay_w 同一套表达式体系,记一组举一反三。

15.4 drawtext 文字:字体路径与动态时间戳

第三个实战:画面上写字。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 的场景。

15.5 截图:-ss 快准 + 单帧输出

第四个实战:截图。核心是 -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 无损不受它影响)。

15.6 组合拳:一条命令同时水印、缩放、加字

三个滤镜都过关了,最后拧成一条命令——这也是滤镜图真正的威力:多输入 + 多步加工 + 中间产物命名[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 的快与准——每一个"效果不对"背后,都是一次"排除不可能"的推理。下一章也是最后一章,我们会把命令行、滤镜、探针这些零散环节串成一张转码工程全景:从拿到素材到交付成品,一条完整的生产管线长什么样,敬请期待。

章末练习

练习 1:水印位置换算 入门

主视频是 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,而是要减去水印自身宽度的一半。

练习 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 填满盒子。

练习 3:drawtext 时间戳排错 进阶

你写了 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(默认是黑色)。

练习 4:滤镜管线挑战 挑战

用一条 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 显式声明"音频原样搬运"即可。