不引一个 npm 包:用 Node + FFmpeg 自建游戏买量混剪工坊
一、为什么要自建
做游戏买量的人都懂那个循环:素材要得急、量要得大、单价要得低。市面上的路子无非几条——
云端 AI 视频工具:按积分计费,批量跑起来钱烧得肉疼;素材要传到别人服务器,合规心里没底;
剪映类手工工具:质量可控,但一条一条做,人就是产能瓶颈;
外包:更贵,也更慢。
而我的场景其实很「窄」:一个 Unity 小游戏,竖屏录屏是唯一的内容源,要做的是把这些录屏切成标准件、按模板拼成成片、烧上钩子大字 / 卖点 / CTA / 字幕,再派生出横屏版本。流程高度模板化——这正是自建性价比最高的场景。
于是有了这个「混剪工坊」:一个跑在本地的 Web 工具,双击启动、浏览器打开,九个页面走完「素材 → 切割 → 模板 → 编排 → 渲染 → 成品」全流程。
二、零依赖是执念吗
先交代技术栈,你可能会笑:
服务端:Node 内置 http 模块,没用 Express;
前端:原生 HTML / JS / CSS,没有框架、没有构建;
视频处理:FFmpeg 子进程;
存储:JSON 文件,没有数据库;
部署:双击 .cmd 启动,没有 Docker。
零 npm 依赖不是炫技,是三个很实际的理由:
生存周期。这类内部小工具的经典死法是「半年后想改,依赖装不上了」——依赖树烂掉了。零依赖意味着只要 Node 和 FFmpeg 还在,它就能跑。
审计成本。买量素材涉及字体授权、音频授权这类合规问题,供应链越短越好审。
改动信心。所有代码都是自己写的,没有黑盒,改坏了回归测试会告诉我。
代价当然有:路由自己写、静态资源自己伺候、JSON 并发写自己处理。这些活儿加起来几百行,对内部工具完全可接受。
三、几条核心设计
3.1 竖屏是唯一内容源
不搞双源录制。所有素材先归一成 1080×1920 @30fps 的「标准件」,横屏版本由竖屏成片级补边派生——存储不翻倍,标准化只跑一遍。
归一化的关键动作是「等比铺满 + 居中裁切」,而不是直接缩放到目标分辨率。我们吃过亏:某个供料方出的原片是 736×1280 @24fps,宽高比和标准 9:16 差 2.2%,肉眼根本看不出来,但直接缩放就会把变形永久烧进素材,后面怎么调都救不回来。
3.2 字体池与授权闸门
字体是买量素材最容易被忽视的合规雷。我们的字体池只收两类授权:OFL 开源(思源黑体 / 宋体,由可变字体离线切出静态字重)和明确「永久免费商用」的阿里系字体,目前 20 条、8 个字族。
关键在于:授权登记是物理防线而不是提醒。渲染前的闸门逐条检查,字体不在池内、或授权类型没登记,直接拒绝渲染——没有「跳过」这个选项。
3.3 输入指纹
素材换源但文件名没变,是最阴险的复用 bug:新素材顶着旧名字进了池,混出来的片子看着正常,内容却不是你以为的那个。解法是给每个入池片段按内容算指纹,指纹变了,就算同名也当新素材处理。
四、一个样式字段的三份镜像
字幕系统教我做人。
以「文字位置」为例,它在系统里有三份实现:成片渲染(FFmpeg drawtext 的 y 表达式)、字幕样式页的 CSS 实时预览、服务端的「渲一帧」预览接口。早期锚点硬编码在渲染代码里,改一个字段要人肉同步三处——漏一处,就是「预览动了、成片没动」,用户对着预览调半天,出的片子完全不是那回事。
后来定下三条规矩:
参数算式收进一个唯一真源模块,另外两处只做镜像;
回归测试里,前端镜像不写期望值,直接跟后端真源逐例对拍——镜像走样立刻变红,而不是等哪天我重新抄一遍期望值抄错了;
新增样式字段必须走完三步:默认值、归一化白名单、落到滤镜参数。少一步就是静默失效,UI 上能调、预览也像变了、成片纹丝不动——最坑的一类 bug。
五、两个预览不是重复
字幕样式页有两个预览,看着像功能重复,其实是分工:
CSS 实时预览负责「快」:拖字号、切字体、点样式胶囊,零延迟。调样式是高频动作,等一秒渲染都嫌久。
ffmpeg 渲一帧负责「准」:走成片同一套画字逻辑。描边往外扩多少、底色条多宽、字体回退长什么样,只有真渲能看出来。
「实时」的前提是一个硬约束:CSS 预览的位置和字号必须等比算准——预览框宽除以画布宽得到缩放系数,锚点照渲染代码的公式换算。「大概」只指观感细节,位置对不上的预览比没有预览更糟。
一个值得记下的 CSS 细节:用 -webkit-text-stroke 做描边时必须配 paint-order: stroke fill。CSS 描边以文字轮廓为中心往两边扩,而成片是往外扩的,预览里把描边宽度乘二抵消,再压一个「不超过字号一半」的上限——否则「重描边」这个胶囊渲染出来就是一坨黑块。这个 bug 截图上才能看出来,肉眼调样式时根本意识不到。
六、怎么证明「改了,但没改坏」
内部工具最怕的不是缺功能,是不敢改。这套验证组合拳是这个项目最值钱的遗产:
1. 测「发布的那份」。 前端回归测试从 app.js 源码里把真函数抽出来执行,而不是抄一份副本进测试用例——源码改坏了,测试会跟着红。为了验证这一点,我故意把一个已修复的 bug 注回去:113 项全绿变成 112 项 1 红,改回来即恢复。不做过这一步,你永远不知道自己的测试是不是假绿。
2. 两实例逐字节对拍。 证明「默认观感没变」最省事的手法:用户正在跑的旧实例不动,用新代码在另一个端口起一个实例,同一个请求打两边,比较返回 PNG 的 SHA256。比逐条读滤镜字符串可靠得多——那次改文字位置功能,五类文案的预览图逐字节相同,一句话证明老模板观感零变化。
3. CDP 截图看真面目。 前端 bug 最典型的表现是「按钮点了没反应」,这在截图上完全看不出来。所以截图脚本在导航之前就往页面注入错误收集器(onerror、unhandledrejection、console.error)——装晚了,第一波报错就丢了。实现上零依赖:Node 22 自带 WebSocket,直连 Chrome 的调试协议,截图、读 DOM、模拟点击全够用。
4. 破坏性实验前,先确认还原路径真的存在。 做过一次「先备份再注入 bug」的实验,备份到 /tmp 成功,还原时把路径写错,工作区一度停在 bug 复现态。教训:备份命令跑完,先 ls 一下那个文件真的在。
七、Windows 批处理的黑暗角落
工具靠双击 .cmd 启动,于是踩进批处理的经典泥潭。记两条最疼的:
编码。这个仓库的 .cmd 是 GBK + CRLF,脚本里调 chcp 切码页反而会把输出打成乱码;而我另一个仓库的惯例恰好相反(UTF-8 + chcp 65001)。同一台机器上两套惯例并存,规则只能靠文档钉死,否则每次改脚本都是一次赌运气。
失败可见。启动脚本必须做就绪轮询(探测健康检查接口,而不是固定 sleep 几秒),且失败分支要打印原因并 pause。否则错误信息随最小化的窗口一闪而过,用户只看到「启动不了」,而你永远收不到现场的报错截图。
八、下一步:口播配音的三层模型
配音能力接不接、怎么接,我想清楚分层再说。它其实分三层:
第一层:导入现成配音——已具备。 做好的音频放进人声池,标注来源与授权,渲染时整条混入成片,可调的只有音量。
第二层:选音色生成配音——未接。 也就是很多工具里那个「音色列表 + 试听 + 确认」的弹窗。需要 TTS 引擎,两条路:云端 API(用 Node 原生 https 直调就行,不破坏零依赖,代价是密钥、联网、按字计费)或本地模型(免费、离线,但要下模型,没 GPU 会很慢,音色数量和自然度通常不如云端)。还有一道合规关:AI 生成的配音属于 AI 合成内容,要亮标识;那些拟人化音色(方言、情感人设类)用于商业投放,各家的商用条款不一样,选型时得逐家核。
第三层:配音驱动分镜时长——未接,但这是关键层。 现在的成片节奏是模板定死的时间窗(钩子几秒 → 主体 → CTA,素材按时长切),配音只是贴上去的一条轨。如果配音是一句一句的,不改这一层就必然音画错位——接了 TTS 也只是多一条对不上的音轨。
很多工具做到第二层就宣传「支持配音」,用起来才发现,第三层才是体验的分水岭。
九、结语
自建不适用于所有场景:要炫酷特效、要复杂转场、要真人出镜,还是老老实实上专业工具。但当需求收窄到「模板化批量生产」时,一个用零散时间搭起来的本地工具,边际成本几乎为零,而且每一行都长在自己的合规要求上。
真正难的不是写代码,是敢改。验证体系,才是这类工具的核心资产。