之前写过我们那个非遗记录站是怎么做 SEO 的,有人问:"你用户上传的图片视频,最后那个合成是怎么做的?"这篇就聊聊这块。
背景
我们站(supwai.cn)允许用户上传多张图片和一段音频,后台把它们合成为一个带配乐的短视频。听起来简单,真做起来坑不少。
为什么必须上队列
最开始偷懒,用户一提交就同步调用 ffmpeg 合成。结果很快出问题:
- 合成要几十秒到几分钟,HTTP 请求直接超时;
- 多个用户同时提交,ffmpeg 把 CPU 打满,整个站点卡死;
- 合成中途失败,用户只看到 500,没有重试入口。
正确做法:请求只负责把"任务"入队,立刻返回;真正的合成丢给后台 Worker。
选型:Redis + BullMQ
我们用 Node(Nitro 服务)+ Redis + BullMQ:
- 上传接口只把任务写进 Redis 队列,返回任务 ID;
- 独立的 Worker 进程消费队列,每个任务调 ffmpeg;
- 用 BullMQ 的
lockDuration、stalledInterval、attempts做容错。
几个真踩过的坑
- 损坏图片 +
-loop 1永不退出。ffmpeg 遇到坏图配合无限循环输入会卡死,Worker 被占死。解决办法是给任务加硬超时(SIGKILL),超时即失败重来。
- moov 原子在文件尾部。concat 出来的 mp4 如果 moov 在末尾,网页播放要下完整文件才能播。我们在合成后补一步
-movflags +faststart,并校验,不合格的拒绝入库而不是静默降级——这个降级曾经导致线上一堆"能下但不能播"的废文件。
- 1x1 的退化图。用户传了张 1 像素的占位图,ffmpeg 不报错但直接合成出黑屏。入口就按
MIN_IMAGE_DIM=32把太小的图挡在门外,返回 400,比事后排查省事。
- 宽高必须是偶数。libx264 要求
yuv420p,宽高偶数,否则合成失败。所有尺寸进 ffmpeg 前一律过一遍even2()取整。
超时预算怎么定
合成耗时 ≈ 时长 × 画布像素 ÷(编码预设 × 线程数),再叠加一个全局 deadline。这样既不会太松(用户等半天)也不会太紧(正常任务被误杀)。
成果
现在整套合成对用户是无感的:提交完页面显示"处理中",后台 Worker 跑完自动通知。站点已经上线,地址 supwai.cn ,微信小程序叫「我的拾遗手记」。
如果你也在做带媒体处理的内容站,强烈建议一开始就把"上传"和"处理"解耦成队列——别等线上卡死了才重构。