如何将 Playwright 打造成视频渲染引擎

当你平时需要做一些MG动画、图表演示、数据驱动的视频,例如B站数码区视频常见的能效曲线图,你会怎么做?使用AE?不,你已经受够了AE的卡顿,到了捏着鼻子也用不下去的程度,你需要一个更流畅的方案。

如果你是一名合格的前端,那你一定会感受到,浏览器的实时渲染比AE流畅数倍,如果能在浏览器里实现动画,再渲染成视频,会不会带来数倍的效率提升呢?这篇文章一起来探索一下。

Headless 无界面浏览器

Headless 无头浏览器,意思是没有界面的浏览器,一般用在自动化测试、爬虫等方向,本文使用使用的 Playwright,本质上是一个可以通过 Node.js 控制的完整浏览器。

思路

Playwright 有一个独有的 Clock API,可以控制网页的时间流逝,影响包括setTimeoutsetIntrevalrequestAnimationFrameDatepreformace.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.jsP5.jsThree.js,或者游戏引擎例如Phaser.js,配合动画库来达到你要的视觉效果。甚至可以用playwright的接口模拟键鼠交互。

类似方案

如果你觉得手动控制麻烦,这里有一个更方便库:Remotion,它用的也是类似方案,支持多线程渲染(同时启动多个浏览器实例),并且高度封装,只要关注写动画即可。缺点是没有手动来得灵活,多帧渲染也会带来一些资源加载闪屏、时间不匹配的问题,商用需要收费。