ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP8.5怎么用ffmpeg给视频加动态贴纸

PHP8.5怎么用ffmpeg给视频加动态贴纸 前言给视频加动态贴纸很多人第一次尝试就会撞上这几类问题贴纸的透明背景变成了黑方块贴纸本该在第 3 秒出现结果从头到尾都在想做上下浮动的动画命令写完贴纸纹丝不动加了个-shortest想让输出及时结束结果整个视频被截成了一帧。这些问题的成因各不相同但都集中在同一个地方filter_complex滤镜图filtergraph的语法与时间表达式。贴纸功能不是某一个「加贴纸」的开关而是overlay滤镜加上基于时间变量t的表达式组合出来的效果而滤镜图有它自己的一套转义规则和 shell 的转义规则是两层弄混了就报「Unable to parse option value」。关于版本先说明「动态贴纸」是 ffmpeg 的能力不是 PHP 的特性与 PHP 版本没有关系。标题里的 PHP 8.5 指的是运行环境版本。PHP 8.5 新增的管道操作符|、array_first()/array_last()、clone with能让配套的脚本写得更短例如从贴纸配置列表里取首尾一项、克隆配置对象后只改时间窗但它们不参与视频处理本身本文不会夸大它们的作用。文中代码使用 PHP 8.0 起就有的语法在 8.5 上直接可跑。一、动态贴纸的四个组成部分一个「动态」贴纸其实是四件事的叠加缺一个效果就不对效果由谁实现关键写法只在一段时间内出现overlay的enable选项enablebetween(t,1,5)位置固定或跟随移动overlay的x/y表达式xW-w-40、yH-h-40-18*sin(2*PI*t/2.5)透明背景正确合成formatrgba 带 alpha 的源图[1:v]formatrgba,...淡入淡出fade滤镜带 alpha 选项fadetin:st0.5:d0.4:alpha1其中最容易漏掉的是formatrgba。贴纸通常是带透明通道的 PNG但解码后如果不在滤镜链里显式转成带 alpha 的像素格式叠加时透明区域会按不透明处理结果就是一块黑或白的方块盖在画面上。这个现象的迷惑性在于贴纸文件本身用图片查看器打开是完全正常的。另一个要点是evalframe。overlay的x/y表达式如果只在初始化时求值一次里面用到t时间戳单位秒的动画就完全不会动。显式写上evalframe能让它每帧重新计算。二、时间表达式的作用范围理解t的含义很关键在overlay的表达式里t是输出时间轴上的时间戳不是原始素材的时间戳。所以enablebetween(t,3,8)表示「在成片的第 3 到第 8 秒显示」与源文件的时间戳处理方式无关。常用的时间表达式写法目标表达式固定时间窗between(t,1,5)从某个时间点之后一直显示gte(t,2)周期性显示lt(mod(t,10),5)匀速横向移动x(W-w)*(t-1)/4正弦上下浮动yH-h-40-18*sin(2*PI*t/2.5)条件切换位置yif(lt(t,3),H-h-40,100)注意表达式里用的W/H是主画面overlay 的底图的宽高w/h是贴纸的宽高。写xW-w-40就是把贴纸贴到右上角并且留 40 像素边距——这是自适应分辨率的标准写法比写死像素坐标更适合多分辨率输出。三、转义三层规则叠在一起这是动态贴纸最容易翻车的地方。你的命令要穿过三层解析PHP 数组参数 → ffmpeg 命令行参数 → 滤镜图解析器 → 表达式解析器好消息是用 PHP 的proc_open数组形式PHP 7.4 起支持传参可以完全跳过 shell 这一层不需要escapeshellarg路径里的空格和引号都不会出问题。麻烦的是后两层。逗号在滤镜图里逗号是「同一链上多个滤镜」的分隔符。所以时间表达式between(t,1,5)里的那两个逗号必须被保护起来——用单引号包住整个表达式即可enablebetween(t,1,5) ✅ 逗号在引号内不会被当成滤镜分隔符 enablebetween(t,1,5) ❌ 滤镜图被拆成三段报解析错误冒号冒号是「滤镜内不同选项」的分隔符。如果表达式里出现了字面的冒号需要写成\:。表达式里出现冒号的场景不多一旦报「Unable to parse option value」而表达式里恰好有冒号先怀疑这里。分号分号分隔滤镜图中的不同链。这一层是必须保留的不要转义。掌握了这三条绝大多数「命令拼对了却报语法错」的问题都能自己解决。四、完整可运行的贴纸脚本下面这份代码可以直接保存为sticker.php运行需要系统里有 ffmpegPHP 8.0 及以上均可?php declare(strict_types1); // sticker.php —— PHP 8.0 CLI // 用法: php sticker.php input.mp4 sticker.png output.mp4 final class Sticker { public function __construct( public readonly string $path, public readonly float $startAt 0.5, // 出现时间输出时间轴秒 public readonly float $endAt 5.0, // 消失时间 public readonly int $width 240, // 贴纸宽度高度按比例 public readonly int $marginX 40, public readonly int $marginY 40, public readonly float $fade 0.4, // 淡入淡出时长 public readonly bool $float true, // 是否上下浮动 ) {} } final class StickerComposer { public function __construct(private string $ffmpegBin ffmpeg) {} /** * 生成 filter_complex。 * 分成两条链第一条处理贴纸第 2 个输入第二条把它叠到主画面上。 */ public function buildFilter(Sticker $s, string $outLabel vout): string { // 链 1贴纸 - 带 alpha 的像素格式 - 缩放 - 淡入 - 淡出 $fadeOutStart $s-endAt - $s-fade; $stickerChain sprintf( [1:v]formatrgba,scale%d:-1, . fadetin:st%.2f:d%.2f:alpha1, . fadetout:st%.2f:d%.2f:alpha1[st], $s-width, $s-startAt, $s-fade, $fadeOutStart, $s-fade ); // 链 2叠加。x/y 里的 t 是输出时间戳 $x sprintf(W-w-%d, $s-marginX); $y $s-float ? sprintf(H-h-%d-18*sin(2*PI*(t-%.2f)/2.5), $s-marginY, $s-startAt) : sprintf(H-h-%d, $s-marginY); $overlayChain sprintf( [0:v][st]overlayx%s:y%s:evalframe . :enablebetween(t,%.2f,%.2f)[%s], $x, $y, $s-startAt, $s-endAt, $outLabel ); return $stickerChain . ; . $overlayChain; } public function run(string $src, Sticker $sticker, string $dst): void { $cmd [ $this-ffmpegBin, -y, -i, $src, // 贴纸作为第二个输入而不是用 movie 滤镜后者在新版 ffmpeg 中不推荐 -i, $sticker-path, -filter_complex, $this-buildFilter($sticker), -map, [vout], -map, 0:a?, // 有音频就带上? 表示没有也不报错 -c:v, libx264, -preset, medium, -crf, 20, -c:a, aac, -b:a, 128k, -movflags, faststart, $dst, ]; // 数组形式传参不经 shell路径含空格也安全 $descriptors [1 [pipe, w], 2 [pipe, w]]; $proc proc_open($cmd, $descriptors, $pipes); if (!is_resource($proc)) { throw new RuntimeException(无法启动 ffmpeg); } stream_get_contents($pipes[1]); $stderr stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); $code proc_close($proc); if ($code ! 0) { // 滤镜图语法错误都会在这里完整打印出来是排查的第一手材料 throw new RuntimeException(ffmpeg 执行失败退出码 {$code}:\n . $stderr); } printf(完成: %s\n, $dst); } } // ---------- 使用示例 ---------- $in $argv[1] ?? input.mp4; $png $argv[2] ?? sticker.png; $out $argv[3] ?? output.mp4; $composer new StickerComposer(getenv(FFMPEG_BIN) ?: ffmpeg); // 单个贴纸 $composer-run($in, new Sticker( path: $png, startAt: 1.0, endAt: 6.0, width: 240, ), $out); // 想加第二个贴纸把两条 overlay 链接起来即可 // $a new Sticker($png, 0.5, 3.0, 180); // $b new Sticker($png2, 3.0, 6.0, 180); // 在 buildFilter 的基础上再追加一条 [vout][st2]overlay...[vfinal] 的链 // 并把 -map 改成 [vfinal]。运行方式php sticker.php input.mp4 sticker.png output.mp4脚本会把完整的滤镜图拼出来传给 ffmpeg。如果报错stderr里会带上滤镜图的解析位置这也是为什么代码里没有把 stderr 丢掉——滤镜图的语法错误必须看原始输出才能定位。想先确认参数再执行可以直接把滤镜图打印出来用命令行单独试# 先看生成的滤镜图在 PHP 里 var_dump($composer-buildFilter(...)) 即可 # 再手工试跑一次把 -v verbose 加上看详细解析 ffmpeg -v verbose -i input.mp4 -i sticker.png \ -filter_complex [1:v]formatrgba,scale240:-1,fadetin:st1.00:d0.40:alpha1[st];[0:v][st]overlayxW-w-40:yH-h-40:evalframe:enablebetween(t,1.00,6.00)[vout] \ -map [vout] -c:v libx264 -crf 20 -an check.mp4五、滚动跑马灯与旋转贴纸overlay之外还有两个常用的动画滤镜它们的表达式变量和overlay不完全一样不要凭记忆写用-h filter名字查当前版本支持的变量ffmpeg -h filterrotate ffmpeg -h filterzoompan ffmpeg -h filteroverlayrotate滤镜的a选项接受角度表达式配合t可以做持续旋转c选项是填充色需要保持背景透明时设为none否则旋转后贴纸四周会出现实色边角[1:v]formatrgba,scale200:-1,rotatea0.4*t:owrotw(a):ohroth(a):cnone[st]zoompan用于贴纸本身的缩放动画比如心跳式的放大缩小它的变量集合比较特殊务必用上面那条-h命令确认之后再写。如果贴纸是动画 GIF需要让它在整片范围内循环播放用输入侧的循环选项即可GIF 解复用器另有控制播放次数的开关同样以ffmpeg -h demuxergif的输出为准-stream_loop -1 -i sticker.gif常见坑点1. 加了-shortest导致正片被截断❌ffmpeg -i in.mp4 -i logo.png -filter_complex ... -shortest out.mp4✅ 用enablebetween(t,1,6)控制显示窗口不要加-shortest。静态 PNG 输入只有一帧。-shortest的含义是「以最短的输入为准结束输出」于是输出的长度可能只有一帧——整个视频被截成一张图。这个错误的症状非常戏剧化但报错却是空的。2. 忘记formatrgba❌[1:v]scale240:-1[st]✅[1:v]formatrgba,scale240:-1[st]透明区域会变成黑块或白块。贴纸文件本身没问题用图片查看器打开是正常的透明 PNG所以很容易怀疑到错误的方向上去。3. 用 JPEG 做贴纸❌ 把 logo 存成logo.jpg当作贴纸。 ✅ 用 PNG或 WebP、APNG等支持 alpha 通道的格式。JPEG 格式根本没有透明通道无论怎么配置滤镜都会得到一个带底色的方块。4. 时间表达式里的逗号没有引号保护❌enablebetween(t,1,5)✅enablebetween(t,1,5)滤镜图解析器会把那两个逗号当成滤镜分隔符于是between(t、1、5)被当成三个滤镜去解析报出与真实原因完全不相干的错误信息。5. 忘记evalframe❌overlayxW-w-40:yH-h-40-18*sin(2*PI*t/2.5)贴纸纹丝不动。 ✅ 加上evalframe。表达式默认可能只在初始化时求值一次所有含t的动画都会失效——而这个失败是完全静默的不报任何错。6. 用-vf而不是-filter_complex❌-vf [1:v]overlay...报错说不认识[1:v]。 ✅ 用-filter_complex。-vf只能处理一路输入流无法引用第二个输入文件。要叠加两路输入必须用-filter_complex并显式-map输出标签。7. 写死像素坐标❌overlayx1400:y800✅overlayxW-w-40:yH-h-40写死坐标的贴纸在 4K 源上会跑到画面中间在竖屏输出上可能直接跑到画外。用W/H/w/h的相对表达式才能自适应。8. 贴纸宽度写死成像素而不看输出分辨率❌ 输出缩放成 720p贴纸仍是按 4K 调好的 480 像素宽。 ✅ 用相对宽度例如按W的比例或在缩放输出后再调贴纸尺寸。贴纸与画面的比例会完全失调看起来像贴纸突然变大了两倍。总结需求实现方式关键点定时出现overlay的enablebetween(t,1,6)逗号要引号保护位置自适应x/y用W/H/w/h表达式不要写死像素浮动 / 移动 / 旋转表达式里用t必须evalframe透明背景formatrgba PNG 素材JPEG 无 alpha不能用淡入淡出fade的alpha1时间点按输出时间轴多贴纸叠加多条overlay链首尾相接记得改-map的目标标签循环动画贴纸输入侧-stream_loop -1GIF 的循环开关查 demuxer 帮助不要截断正片用enable而不是-shortest静态图片输入只有一帧给视频加动态贴纸的技术核心只有三件事用overlay把两路画面叠起来、用带t的表达式让参数随时间变化、用正确的转义让滤镜图不被解析器拆坏。剩下的是参数调优而所有滤镜的可用变量与默认值都应该用ffmpeg -h filter名字查当前版本文档而不是照抄别处的写法——ffmpeg 的滤镜参数在不同版本之间确实会调整照抄是这类问题最常见的来源。
返回列表