PHP怎么调用ffmpeg给视频加水印不偏移
前言用overlay滤镜加水印命令不报错、输出也能播但打开一看水印偏了跑到画面外只露一角或本该贴右下角却悬在中间或被拉宽变形。这类偏移不是 FFmpeg 的 bug几乎都来自四个原因尺寸变量写混。overlay里大写W/H指主视频小写w/h指叠加图。xW-w-10是主视频宽 - 水印宽 - 10正确xw-w-10恒等于-10水印跑到左边界外。SARSample Aspect Ratio采样宽高比不一致。水印图和视频 SAR 不同时播放器为对齐显示比例会整体缩放看起来像位置变了。旋转元数据没处理。手机竖屏视频带rotate90你按看到的画面量坐标FFmpeg 按存储像素去贴差了一次旋转。坐标算的是缩放后尺寸实际用的是缩放前尺寸。本文按overlay 坐标系 → SAR 与旋转 → 完整 PHP 封装的顺序讲命令和代码都可直接跑。示例基于 PHP 8.1 与 FFmpeg 4.0overlay很早就有但参数表达式 4.x 后才稳定。一、overlay 的坐标系大写和小写的区别最核心也最容易记错的一点变量指代举例主视频 1920x1080水印 200x80W主视频宽度1920H主视频高度1080w叠加图宽度200h叠加图高度80x/y叠加图左上角在主视频里的坐标位置参数四角写法overlayx10:y10 # 左上 overlayxW-w-10:y10 # 右上 overlayx10:yH-h-10 # 左下 overlayxW-w-10:yH-h-10 # 右下最常用心算校验把 W/H/w/h 代进具体数字看结果是否落在 [0, W-w] 与 [0, H-h] 内。上例x1920-200-101710而W-w1720在范围内正确。overlay的几个有用选项选项默认作用x/y0位置表达式支持t时间秒做动态移动formatyuv420覆盖层像素格式颜色发灰/半透明发黑先怀疑它eof_actionrepeat水印图先播完时行为可选repeat/endall/passshortest0设为1时任一输入结束就结束alphastraightalpha 通道处理premultiplied用于预乘 alpha 的素材enable无时间表达式控制水印出现的时段shortest1与eof_action的组合是偏移的另一个隐蔽来源水印是单帧 PNG 时会被repeat重复到尾没问题但水印是短视频且没设shortest1时输出时长会被水印视频拉长或截短你会觉得整段视频时间轴歪了。二、偏移的三大真凶2.1 SAR 不一致看起来位置偏了其实是比例偏了每个视频流都带 SAR 字段显示宽高比DAR 存储宽高比 × SAR视频1920x1080、SAR1:1→DAR16:9正常。某些 DVD/老素材转来的视频720x480、SAR32:27→DAR≈16:9。存储是 720x480显示成 16:9。若水印SAR1:1把它overlay到SAR32:27的视频上FFmpeg 需统一比例。不同版本策略不同结果是水印被横向拉长、或整幅画面被缩放你量好的坐标全部失效。统一解法是在 filter 链最后强制setsar1缩放时显式指定尺寸。ffmpeg -i input.mp4 -i logo.png \ -filter_complex \ [0:v]scale1920:1080,setsar1[base]; \ [1:v]scale200:-1,setsar1[wm]; \ [base][wm]overlayxW-w-10:yH-h-10[out] \ -map [out] -map 0:a? \ -c:v libx264 -crf 20 -preset medium -pix_fmt yuv420p -c:a copy \ output.mp4要点scale200:-1宽定 200高按比例自动算。-1表示自动算但必须整除比它更严格的是-2算出奇数高度会报错推荐一律用-2。setsar1加在每一路缩放之后所有支路 SAR 归一化overlay时无比例歧义。想保留原始分辨率就不写scale只写setsar1[0:v]setsar1[base]; [1:v]setsar1[wm]; [base][wm]overlay...。2.2 旋转元数据坐标系差了一次旋转手机竖屏视频通常存成横向的1920x1080再加rotate90或rotate-90元数据。播放器看到元数据会自动旋转显示所以你看到的是1080x1920竖屏画面。ffprobe读出的width/height是存储尺寸不是显示尺寸。于是你按竖屏画面量出水印在右下角坐标约 (900, 1700)FFmpeg 拿到的画面是1920x1080y1700直接越界要么把水印丢到画面外要么报错。两种正确做法# 做法 A把旋转固化掉再处理推荐后续坐标都按看到的方向量 ffmpeg -i input.mp4 -c copy -metadata:s:v:0 rotate0 -c:v libx264 -crf 20 \ -pix_fmt yuv420p normalized.mp4 # 做法 B在滤镜链里自己 transpose并告诉 ffmpeg 别自动旋转 ffmpeg -noautorotate -i input.mp4 -i logo.png \ -filter_complex [0:v]transpose1[base];[1:v]scale200:-2[wm];[base][wm]overlayW-w-10:H-h-10[out] \ -map [out] -map 0:a? -c:v libx264 -crf 20 -pix_fmt yuv420p -c:a copy output.mp4transpose参数1顺时针 90 度2逆时针 90 度0再翻转一次。对rotate90用transpose1对rotate-90/rotate270用transpose2——错了视频就是倒的比位置偏移更显眼但一眼能看出来。2.3 位置表达式写错overlayxw-w-10:yh-h-10 # ❌ 恒等于 -10水印跑到左边界外 overlayxW-w:yH-h # ❌ 少了边距很多播放器会裁掉最外圈像素 overlayxW-w-10:yH-h-10 # ✅ overlayx(W-w)/2:y(H-h)/2 # ✅ 居中还有个更隐蔽的错误表达式里的引号被 shell 吃掉了。在 PHP 里拼命令时若没正确转义(W-w)/2的括号会被 bash 当成子 shell 语法命令直接报错或被替换成空串。三、PHP 封装一次把三件事都做对下面代码完整可运行。设计原则在 PHP 侧把分辨率、SAR、旋转全读出来算清参数再交给 FFmpeg而不是把表达式丢给 FFmpeg 现场算。?php declare(strict_types1); // 视频加水印不偏移版PHP 8.1 / FFmpeg 4.0 final class VideoWatermark { public function __construct( private readonly string $ffmpeg ffmpeg, private readonly string $ffprobe ffprobe, ) {} /** return array{width:int, height:int, sar:string, rotation:int} */ public function probe(string $file): array { $out $this-exec([ $this-ffprobe, -v, error, -select_streams, v:0, -show_entries, streamwidth,height,sample_aspect_ratio:stream_side_datarotation, -of, json, $file, ]); $stream json_decode($out, true, 512, JSON_THROW_ON_ERROR)[streams][0] ?? []; $rotation 0; foreach ($stream[side_data_list] ?? [] as $side) { if (isset($side[rotation])) { $rotation (int) $side[rotation]; } } return [ width (int) ($stream[width] ?? 0), height (int) ($stream[height] ?? 0), sar (string) ($stream[sample_aspect_ratio] ?? 1:1), rotation $rotation, ]; } /** * param string $position top-left/top-center/top-right/middle-left/center/ * middle-right/bottom-left/bottom-center/bottom-right * param int $scaleWidth 水印缩放后宽度0 表示不缩放 */ public function apply( string $input, string $logo, string $output, string $position bottom-right, int $margin 10, int $scaleWidth 0, int $opacity 100, int $crf 20, ): void { $vmeta $this-probe($input); $lmeta $this-probe($logo); // 1) 旋转 90/270 度时实际画面是 w/h 互换的 $rotated in_array(abs($vmeta[rotation]), [90, 270], true); $baseW $rotated ? $vmeta[height] : $vmeta[width]; $baseH $rotated ? $vmeta[width] : $vmeta[height]; // 2) 水印尺寸 $wmW $scaleWidth 0 ? $scaleWidth : $lmeta[width]; if ($wmW $baseW) { throw new InvalidArgumentException( 水印宽度 {$wmW} 不小于视频宽度 {$baseW}请调小 scaleWidth ); } // 高度由 FFmpeg 按比例算这里只做保守估算用于边界校验 $ratio $lmeta[width] 0 ? $lmeta[height] / $lmeta[width] : 0.0; $wmH (int) ceil($wmW * $ratio); // 3) 坐标在 PHP 里算好不留表达式给 FFmpeg [$x, $y] $this-positionToXY($position, $baseW, $baseH, $wmW, $wmH, $margin); if ($x 0 || $y 0 || $x $wmW $baseW || $y $wmH $baseH) { throw new InvalidArgumentException( 坐标越界: 水印 {$wmW}x{$wmH} 位置 ({$x},{$y}) 画布 {$baseW}x{$baseH} ); } // 4) filter_complex主视频按需旋转 强制 SAR1水印缩放/透明处理 $baseFilters []; if ($rotated) { $baseFilters[] $vmeta[rotation] 90 ? transpose1 : transpose2; } $baseFilters[] setsar1; $logoFilters []; if ($scaleWidth 0) { $logoFilters[] scale{$scaleWidth}:-2; // -2 保证偶数高度 } $logoFilters[] setsar1; if ($opacity 100) { $alpha round($opacity / 100, 2); $logoFilters[] formatrgba,colorchannelmixeraa{$alpha}; } $filter [0:v] . implode(,, $baseFilters) . [base]; . [1:v] . implode(,, $logoFilters) . [wm]; . sprintf([base][wm]overlayx%d:y%d:formatauto:shortest1[out], $x, $y); $this-exec([ $this-ffmpeg, -y, -i, $input, -i, $logo, -filter_complex, $filter, -map, [out], -map, 0:a?, // 有音频就带上没有也不报错 -c:v, libx264, -crf, (string) $crf, -preset, medium, -pix_fmt, yuv420p, -c:a, copy, -movflags, faststart, $output, ]); } /** return array{0:int, 1:int} */ private function positionToXY( string $position, int $baseW, int $baseH, int $wmW, int $wmH, int $margin, ): array { $x $margin; // 水平默认贴左 if ($position center || str_ends_with($position, -center)) { $x (int) round(($baseW - $wmW) / 2); } elseif (str_ends_with($position, -right)) { $x $baseW - $wmW - $margin; } $y $margin; // 垂直默认贴上 if ($position center || str_starts_with($position, middle-)) { $y (int) round(($baseH - $wmH) / 2); } elseif (str_starts_with($position, bottom-)) { $y $baseH - $wmH - $margin; } return [$x, $y]; } /** param liststring $args */ private function exec(array $args): string { $cmd implode( , array_map( static fn (string $a): string escapeshellarg($a), $args )); $process proc_open($cmd, [1 [pipe, w], 2 [pipe, w]], $pipes); if (!is_resource($process)) { throw new RuntimeException(无法启动 ffmpeg/ffprobe); } $stdout stream_get_contents($pipes[1]) ?: ; $stderr stream_get_contents($pipes[2]) ?: ; fclose($pipes[1]); fclose($pipes[2]); $code proc_close($process); if ($code ! 0) { $tail implode(\n, array_slice(explode(\n, trim($stderr)), -20)); throw new RuntimeException(ffmpeg 失败(exit {$code}):\n{$tail}); } return $stdout; } } // ------------------------------------------------------------------ $wm new VideoWatermark(); $wm-apply( input: __DIR__ . /input.mp4, logo: __DIR__ . /logo.png, output: __DIR__ . /output.mp4, position: bottom-right, margin: 20, scaleWidth: 160, opacity: 80, ); echo 水印添加完成\n;这段代码把偏移的三个来源都提前掐掉旋转PHP 里判断画面实际方向决定transpose1还是transpose2并用交换后的基准尺寸算坐标。SAR每路都加setsar1overlay两侧比例归一。坐标系坐标在 PHP 里用具体数字算好$baseW - $wmW - $margin传进overlay的是x1710这样的常量而非W-w-10表达式从根本上不存在大小写写混的可能。注意overlay...:formatauto。默认的formatyuv420会把带 alpha 的水印强行转成不带 alpha 的格式透明背景变成黑色方块。水印 logo 一定要用带透明通道的 PNG并把format设为auto。常见坑点1.w/h和W/H写反❌overlayxw-w-10:yh-h-10恒等于(-10,-10)水印跑到左上角外输出里什么都看不到。 ✅ 记住大写是主视频、小写是叠加图xW-w-10:yH-h-10。2. 水印 PNG 没有透明通道变成黑块❌ 用 JPG 当水印或截图工具存的看起来背景是白的PNG —— 白色背景会实打实盖住画面。 ✅ 用绘图工具导出带 alpha 通道的 PNG并在overlay里加formatauto。判断方法ffprobe看pix_fmtrgba/pal8带 alphargb24不带。3. 忘记setsar1❌ 视频SAR32:27、水印SAR1:1直接overlay—— 整幅画面被横向拉伸水印跟着变宽看起来位置偏了实际是被拉伸了。 ✅ 每个缩放分支后都加setsar1。4. 竖屏视频坐标全错❌ 直接按看到的竖屏画面量坐标喂给 FFmpeg —— 存储坐标系是旋转前的y值会越界。 ✅ 先ffprobe读side_data_list的rotation在 PHP 里交换基准宽高再算坐标并在滤镜链里transpose。5. 同一条命令里混用-vf和-filter_complex❌-vf scale1280:-2与-filter_complex ...[out]同时出现 —— 两个滤镜图打架行为不确定常见结果是-vf被忽略你以为的缩放根本没发生坐标自然全错。 ✅ 只要涉及两个输入就统一只用-filter_complex把缩放写进链里[0:v]scale1280:-2,setsar1[base]。6.-c:v copy与滤镜同用 /-ss放错位置❌-vf overlay... -c:v copy报Filtering and streamcopy cannot be used together因为滤镜必须解码再编码。加滤镜就必须重新编码libx264等。 ✅-ss放在-i之后是解码到该秒再输出很慢但精确追求速度应放-i之前关键帧定位可能不精确到帧。用滤镜时推荐用输出侧的-ss/-to做时间裁剪语义更清晰。总结偏移症状真正原因修复水印跑到画面外 / 完全看不见W/w大小写写混大写主视频、小写叠加图水印整体被拉伸变宽SAR 不一致每个分支后加setsar1坐标怎么调都对不上旋转元数据没处理读rotation交换宽高 transpose水印背景变黑色方块水印无 alpha 或formatyuv420用带 alpha 的 PNG formatauto位置看起来偏了几像素算的是缩放前尺寸把计算挪到 PHP 里用缩放后的数值视频时长异常水印先结束且未设shortest加shortest1避免水印偏移的根本办法只有一个不要在 FFmpeg 表达式里做算术在 PHP 里把最终坐标算成确定的整数再传进去。表达式写在命令行里既无法断点调试也无法提交前校验写在 PHP 里越界、比例、旋转三类问题都能在调用 FFmpeg 之前抛异常拦下来报错明确排查从半小时缩短到一眼。

相关新闻

OpenShell:开源Shell前端与工作流管理工具,统一终端体验

OpenShell:开源Shell前端与工作流管理工具,统一终端体验

1. 为什么需要OpenShell:终端场景下的真实痛点不管是写代码、管理服务器,还是跑数据流水线,只要你的工作离不开命令行,就一定经历过这种别扭:系统自带的终端窗口又丑又难用,开几个会话就要开好几个窗口&…

2026/10/3 9:43:44 阅读更多 →
生物酶牙膏是智商税吗?从作用机制到配方实践全解析

生物酶牙膏是智商税吗?从作用机制到配方实践全解析

我做了七八年口腔护理方向的配方开发,这两年被问得最多的问题就是:生物酶牙膏到底是不是智商税?每次听到这种问题,我都挺感慨的。前几年说起抑菌,大家的条件反射就是氯己定、酒精这类化学杀菌剂;现在风向明…

2026/10/3 9:44:02 阅读更多 →
STM32F407嵌入式智能垃圾分类系统实战

STM32F407嵌入式智能垃圾分类系统实战

1. 项目概述:这不是一个“会动的垃圾桶”,而是一套可落地的嵌入式智能决策系统“基于STM32的智能垃圾分类机器人”——光看标题,很多人第一反应是:又一个学生课设?堆几个传感器、加个舵机、贴个“可回收”标签就叫智能…

2026/10/2 7:54:31 阅读更多 →

最新新闻

TypeScript深入浅出:函数参数为何逆变?协变与逆变完全解析

TypeScript深入浅出:函数参数为何逆变?协变与逆变完全解析

最近有同事跑过来问我一个类型报错:他给一个事件处理器写了(e: MouseEvent) > void,正要赋给一个声明为(e: Event) > void的回调,结果 TypeScript 直接把他拦住了。我解释了两句,他冒出一句:所以函数参数到底是协…

2026/10/3 9:44:14 阅读更多 →
无人机芯片模块设计实战:飞控选型、接口调试与可靠性优化

无人机芯片模块设计实战:飞控选型、接口调试与可靠性优化

无人机这行干久了就会发现,真正把飞机调稳、把功能做扎实,拼的往往不是某一个酷炫的算法,而是藏在机身里那一颗颗芯片和围绕它们展开的模块设计。飞控选什么主控、通信走什么接口、电源怎么监测、视觉算力够不够,这些看似枯燥的技…

2026/10/3 9:44:13 阅读更多 →
Coze低代码Agent开发框架实操指南:五个核心模块与工程落地

Coze低代码Agent开发框架实操指南:五个核心模块与工程落地

这篇是系列第二篇,接着上一篇聊。上篇我讲了为什么低代码Agent开发框架会在2025年之后成为企业落地AI的主流选择,以及Agent开发与传统软件开发的本质区别。这一篇直接落到实操工具上,把Coze平台的核心功能彻底拆开揉碎,给出一份“…

2026/10/3 9:44:13 阅读更多 →
七自由度车辆动力学模型:从方程到仿真实战解析

七自由度车辆动力学模型:从方程到仿真实战解析

方向盘刚转到30度,轮胎和地面摩擦发出的尖啸声就穿透了车窗。这熟悉的失控感让我突然想到——搞车辆仿真的那会儿,被七自由度模型折腾的日日夜夜,可不比现在轻松多少。很多人以为车辆仿真就是把坐标和速度扔给计算机,跑一个动画就…

2026/10/3 9:44:13 阅读更多 →
Java为什么能跨平台?JVM、字节码与一次编译到处运行的底层原理

Java为什么能跨平台?JVM、字节码与一次编译到处运行的底层原理

经常有初学者问我:Java为什么可以跨平台?在准备面试的人嘴里,这更是一道高频题。我先给一个最短的答案——因为有JVM。但如果你只答这一句,面试基本就凉了,因为紧接着就会追问:JVM是怎么实现跨平台的&#…

2026/10/3 9:44:11 阅读更多 →
数据库行式存储详解:原理、优势、调优与场景选型

数据库行式存储详解:原理、优势、调优与场景选型

1. 行式存储是什么:先搞清楚“一行数据”到底存在哪 1.1 从表到页到行:存储引擎的最小组织单元 很多人一听到“行式存储”,第一反应就是“这不就是普通的数据库表吗”。这个理解没有错,但不完整。行式存储的准确含义是&#xff1…

2026/10/3 9:43:10 阅读更多 →

日新闻

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 前任.skill 是一个运行在 Claude Code 上的开源 Skill:导入微信、iMessage、短信、…

2026/10/3 0:00:27 阅读更多 →
45个经典Linux面试题:从命令到网络排障的完整考点解析

45个经典Linux面试题:从命令到网络排障的完整考点解析

刚开始带应届生的时候,我最头疼的就是他们拿着一摞Linux面试题背得滚瓜烂熟,一上机全露馅。后来自己从被面的人变成面别人的人,才慢慢摸清楚:Linux面试题考的根本不是答案本身,而是你面对一个不确定的系统问题时&#…

2026/10/3 0:01:28 阅读更多 →
SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

简介:本资源是一份面向SAP ABAP开发人员、生产计划专员及ERP实施顾问的实操型操作指南,聚焦SAP生产预留核心业务场景,系统解决物料预留创建、查询、校验与批量处理等高频问题。文档以结构化方式覆盖预留背景原理、OMC2编码规则、工厂级参数配…

2026/10/3 0:01:28 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/3 9:14:33 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/1 19:41:40 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/3 9:42:31 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →