PHP本身不处理视频,只负责调度、验证、记录和兜底;真正转码由FFmpeg完成,稳定运行的关键是将转码从Web请求中彻底剥离,通过队列+独立Worker异步执行,避免同步阻塞、超时和资源雪崩。

直接上结论:PHP 本身不处理视频,它只负责调度、验证、记录和兜底;真正干活的是 FFmpeg,而稳定运行的关键在于「隔离」——把转码从 Web 请求生命周期里彻底剥离。
为什么不能用 exec() 直接在上传接口里调 FFmpeg
常见错误现象是:用户上传一个 500MB 视频,PHP 进程卡住 3 分钟,Nginx 报 504 Gateway Timeout,浏览器白屏,数据库里任务状态还是“pending”,没人知道它到底跑没跑、卡在哪、失败了没。
- PHP 默认执行时间限制(
max_execution_time)通常为 30 秒,远低于实际转码耗时 -
exec()同步阻塞,整个 FPM worker 被占住,无法响应其他请求 - FFmpeg 出错时 stderr 不一定被捕获,日志丢失,排查靠猜
- 并发高时 CPU 爆满,所有任务排队,形成雪崩
正确做法是:上传成功后,只往队列(Redis 或数据库)写一条任务记录,立刻返回 202 Accepted。由独立的 Worker 进程拉取任务、调用 ffmpeg、更新状态、发回调。
PHP-FFMpeg 库不是银弹,但能帮你避开 70% 的参数陷阱
直接拼接 shell 命令容易出安全问题(比如文件名带空格或 $()),也难统一控制编码参数。用 PHP-FFMpeg 可以结构化构建命令,但它底层仍是调 ffmpeg,不是魔法。
立即学习“PHP免费学习笔记(深入)”;
- 必须显式指定
FFMpeg::create(['ffmpeg.binaries' => '/usr/local/bin/ffmpeg']),否则可能调到旧版本或找不到二进制 -
clip->export()->toDisk('hls')这类链式调用看似简洁,但出错时堆栈深、定位难,建议每步加try/catch并记录原始ffmpeg命令 - 缩放分辨率时别写死
-s 1280x720,用-vf "scale=1280:-2"保持宽高比,否则黑边或拉伸 - 生成 HLS 时,
-hls_list_size 0表示保留全部分片,生产环境要配-hls_list_size 10防磁盘撑爆
转码任务状态必须持久化,且不能只靠数据库轮询
前端轮询 /api/task/123/status 每秒一次?这是反模式。用户等 3 分钟,你后台就多发 180 次请求,毫无必要。
- 状态字段至少包含:
queued→processing→completed/failed,失败时存ffmpeg的完整 stderr - 用 Redis Pub/Sub 或 WebSocket 主动推送完成事件,而不是让前端死等
- Worker 进程崩溃时,需有超时机制:比如状态卡在
processing超过 10 分钟,自动标为failed并重试(最多 2 次) - 不要在数据库里存绝对路径(如
/var/www/storage/videos/xxx.mp4),用相对路径 + CDN 域名前缀,方便迁移和多实例共享存储
HLS 切片和 MP4 输出不能共用同一套 FFmpeg 参数
同一个 ffmpeg 命令,输出 .mp4 和生成 .m3u8,表现差异极大——前者只要最终文件,后者依赖中间 .ts 文件的原子性写入和索引一致性。
- MP4 输出优先用
-movflags +faststart,把 moov box 提前,让浏览器边下边播 - HLS 必须加
-hls_flags +delete_segments,否则旧 .ts 文件越积越多 - 码率控制上,MP4 可用
-b:v 2M固定码率,HLS 推荐-b:v 1M -maxrate 1.5M -bufsize 2M保流畅 - 音频务必重采样:
-ar 44100 -ac 2,某些设备播不了 48kHz 单声道
最易被忽略的一点:FFmpeg 处理 HLS 时默认使用系统临时目录(/tmp),如果磁盘小或权限不对,切片会写失败且报错极隐蔽——务必用 -hls_segment_filename 显式指定输出路径,并确保该路径可写、有足够空间。



















