php自动加水印需定时轮询而非实时监听,用crontab每分钟执行脚本,通过filemtime()比对时间戳筛选新视频;加水印依赖ffmpeg命令行,须设绝对路径、权限、超时和flock锁防并发。

PHP 脚本无法直接监听文件系统变化,得靠轮询
PHP 本身没有原生的 inotify 或 fsevents 支持(除非手动编译扩展),fswatch、inotifywait 这类工具也不适合在 PHP 进程里长期驻留。所以“自动扫描”实际只能是定时轮询:用 crontab 每分钟跑一次 PHP 脚本,查比上次记录更新的视频文件。
关键不是“实时”,而是“低延迟可感知”。建议把上次扫描时间存进一个轻量存储里,比如:/tmp/last_scan_time.txt 或 MySQL 表里一个 last_run_at 字段。
- 每次脚本启动先读取上一次时间,然后用
filemtime()遍历目标目录下所有.mp4、.mov等后缀文件,筛选出filemtime() > 上次时间的新文件 - 别用
glob('*.mp4')直接扫全量——如果目录有几万文件,每次全扫太慢;加个is_file()和pathinfo()判断扩展名更稳妥 - 注意时区:PHP 的
filemtime()返回的是 Unix 时间戳,跟系统时区无关,但如果你用date('Y-m-d H:i:s')写日志,要确认date_default_timezone_set('Asia/Shanghai')已设好
加水印必须调用 FFmpeg,PHP 只负责调度和参数组装
PHP 的 exec() 或 shell_exec() 只是外壳,真正干活的是 ffmpeg 命令。别尝试用 PHP 图像库(GD / Imagick)处理视频——它们压根不支持帧序列读写。
一个典型加文字水印命令长这样:
ffmpeg -i input.mp4 -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:fontsize=24:fontcolor=white:x=10:y=10:text='©2024'" -c:a copy output.mp4
实操要点:
-
-c:a copy必须加上,否则音频会重编码,耗时翻倍且可能失真 -
drawtext的fontfile路径必须绝对路径,且 PHP 进程要有读权限;CentOS 常用/usr/share/fonts/dejavu/,Ubuntu 可能是/usr/share/fonts/truetype/dejavu/ - 如果水印图是 PNG,用
overlay滤镜,注意overlay=x=10:y=10的坐标是左上角,且 PNG 需带 alpha 通道,否则背景发黑 - 务必加超时控制:
exec("timeout 300 ffmpeg -i ... 2>&1", $output, $return_code),防止某个大视频卡死整个任务
crontab 权限和工作目录容易出错
很多人 cron 跑不起来,根本原因不是 PHP 写错了,而是环境不一致:cron 默认 $PATH 极简,找不到 ffmpeg;且当前目录是 root 用户家目录,不是你的项目目录。
解决方式不是“改 PATH”,而是显式指定:
- 在 crontab 里写完整路径:
* * * * * /usr/bin/php /var/www/watermark/scan.php >> /var/log/watermark.log 2>&1 - PHP 脚本开头立刻
chdir(__DIR__),确保后续file_put_contents()、exec()中的相对路径不出错 - 检查
www-data(或你 cron 使用的用户)对源视频目录、输出目录、字体文件、日志文件是否有读写权限;特别是 SELinux 开启的系统,ls -Z看上下文,可能需要chcon -t bin_t /usr/bin/ffmpeg - 测试 cron 环境:临时加一行
* * * * * env > /tmp/cron_env.txt,对比你手动执行env的输出差异
并发写入和重复处理必须加锁
如果扫描间隔短(比如 30 秒),而某个视频长达 10 分钟才加完水印,下一轮 cron 就可能再次选中它——导致两个 ffmpeg 进程同时读写同一文件,结果损坏或覆盖。
最轻量的防重方案是用 flock 文件锁,而不是数据库字段或 Redis:
- 在脚本开头打开一个锁文件:
$fp = fopen('/tmp/watermark.lock', 'c'); - 立即尝试非阻塞加锁:
if (!flock($fp, LOCK_EX | LOCK_NB)) { exit("Another instance is running.\n"); } - 锁只在当前 PHP 进程生命周期内有效,进程退出自动释放,不用手动
fclose()(但加上更好) - 别用
touch+file_exists()模拟锁——竞态条件严重,两个进程几乎同时判断,都会认为“没锁”
真正难搞的是 FFmpeg 进程崩溃后残留的半成品文件,建议输出路径加随机后缀,处理完成再 rename() 覆盖原文件,避免中间状态暴露。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











