如何将 Playwright 打造成视频渲染引擎
当你平时需要做一些MG动画、图表演示、数据驱动的视频,例如B站数码区视频常见的能效曲线图,你会怎么做?使用AE?不,你已经受够了AE的卡顿,到了捏着鼻子也用不下去的程度,你需要一个更流畅的方案。
如果你是一名合格的前端,那你一定会感受到,浏览器的实时渲染比AE流畅数倍,如果能在浏览器里实现动画,再渲染成视频,会不会带来数倍的效率提升呢?这篇文章一起来探索一下。
Headless 无界面浏览器
Headless 无头浏览器,意思是没有界面的浏览器,一般用在自动化测试、爬虫等方向,本文使用使用的 Playwright,本质上是一个可以通过 Node.js 控制的完整浏览器。
思路
Playwright 有一个独有的 Clock API,可以控制网页的时间流逝,影响包括setTimeout、setIntreval、requestAnimationFrame、Date、preformace.now() 等几乎全部时间相关函数,而前端的动画库大多都依赖这些时间函数来计算动画。 我们只控制时间暂停,并改为手动推进,就可以精确控制每一帧动画进度,实现不丢帧的完美渲染。
需要注意的是 CSS 动画并不受时间控制,它是按真实时间流逝的,必须使用 js 控制动画,好在前端有很多动画库,可以非常方便的制作动画。例如 GSAP、Anime.js、Motion (Framer Motion),想用哪个都可以,我做了一个性能测试,可以供参考:React 动画框架性能简单对比,我最终选择的是 Motion。
Web框架方面,我选择的是前后端一体的Next.js,它同时带了Node和React环境,可以方便的把前端动画和后端渲染写在一个项目里。
代码实现
来看看代码怎么写:
动画
我们写一个小方块从左运动到右,渐显的入场动画,效果如下:
import {animate, useAnimation, motion} from "motion/react";
// 1.使用 hook 创建一个动画控制器
const controls = useAnimation();
// 2.创建动画触发函数
function trigger() {
// 动画内容:X 轴位置从 -100 到 100,不透明度从 0 到 1
controls.start({ translateX: [-100, 100], opacity: [0, 1] })
}
// 3.将控制器绑定到标签
<motion.div animate={controls} className="w-8 h-8 bg-blue-500" ></motion.div>
// 4.这样我们每次调用trigger(),动画就会从头执行一次
setInterval(() => trigger(), 1000)
渲染
我们将 Next.js 启动在localhost:3000,创建一个页面来承载动画,例如/anim。
然后我们用 playwright 创建一个浏览器实例,加载这个页面:
// 导入 playwright
import {chromium, firefox} from "playwright";
// 启动浏览器
const browser = await firefox.launch({})
const context = await browser.newContext({
// 设置分辨率
viewport: { width, height },
})
// 打开动画页面
const page = await context.newPage();
await page.goto("localhost:3000/anim")
然后我们开始渲染:
// 时间停止
await context.clock.install({time: 0})
await context.clock.pauseAt(0)
// 逐帧渲染
// 时长为 30帧 × 3秒
for(let i = 0; i < 30 * 3 ; i++) {
// 保存当前帧的画面
await page.screenshot({
type: "png",
path: `./screen/frames/frame${i}.png`
})
// 时间推进 1 帧(1/30 秒)
await context.clock.runFor(1000/30)
}
最后使用fluent-ffmpeg将帧序列合成为视频:
import ffmpeg from "fluent-ffmpeg";
// 传入图片路径,%d 代表文件名中递增的数字
ffmpeg(`./screen/frames/frame%d.png`)
.inputFPS(30) // 30帧每秒,和前面对上
.outputOption("-pix_fmt yuv420p")
.save(`./screen/output.mp4`)
.on("end", () => console.log("视频合成完成"))
.on("error", err => console.error(err));
大功告成,至此你已经可以将Web中的任意内容渲染成视频了!
思路拓展
可以使用创意引擎Pixi.js、P5.js、Three.js,或者游戏引擎例如Phaser.js,配合动画库来达到你要的视觉效果。甚至可以用playwright的接口模拟键鼠交互。
类似方案
如果你觉得手动控制麻烦,这里有一个更方便库:Remotion,它用的也是类似方案,支持多线程渲染(同时启动多个浏览器实例),并且高度封装,只要关注写动画即可。缺点是没有手动来得灵活,多帧渲染也会带来一些资源加载闪屏、时间不匹配的问题,商用需要收费。