demo-reel 是做什么的?
将脚本化的 Web 应用操作过程转换为一段精美的屏幕录制视频:流畅的 60fps 运动、可见的带光晕光标、点击涟漪和字幕。它驱动真实应用的无头 Chromium——画面即产品本身,而非模拟图。
为何视频如此流畅
普通屏幕录制在浏览器恰好绘制时捕获帧;在慢速机器(或软件 WebGL)上,帧率大约只有 10–15fps 并会出现卡顿。而 demo-reel 注入了一个虚拟时钟(scripts/inject.js):requestAnimationFrame、performance.now 和 Date 仅在录制器要求时前进,且 CSS/Web 动画通过 document.getAnimations() 逐步推进。每一帧精确前进 1/fps,截图后通过 ffmpeg 编码。无论渲染一帧需要多少实际时间,最终视频都完美流畅。
计时器(setTimeout/setInterval)故意保持真实——伪造它们会导致加载器和调度器中的零延迟计时器链无限循环(这就是 Playwright 自带的 page.clock 在许多 WebGL 页面上挂起的原因)。
要求
- Node 18+,ffmpeg 在 PATH 中。
- 在技能目录中执行一次:
npm install && npx playwright install chromium。
工作流程
- 启动应用,以用户看到的方式——最好是生产构建(
vite build && vite preview,next build && next start)配合本地或模拟数据。开发服务器也可用但较慢,且可能显示开发覆盖层。切勿录制真实或敏感数据——请使用演示数据。 - 编写场景(
*.scenario.mjs,参见下方 API)。从examples/tasks.scenario.mjs开始。将其规划为一个故事:20–60 秒,每个字幕一个想法,每个结果后停留 1–2 秒以便观众阅读。 - 录制:
node scripts/record.mjs my.scenario.mjs --out demo.mp4一段 60 秒的 1080p 片段大约需要 3–10 分钟(重型 WebGL 页面最慢)。在后台运行。 - 质量检查:
scripts/sheet.sh demo.mp4会生成一张联系表(每 2.5 秒一帧)。检查:字幕与屏幕内容匹配,无加载转圈,内容未被字幕遮挡,徽章/光标可见。通过提取连续几帧快速检查流畅度——在运动期间每帧应略有差异。 - 交付(参见“使用视频”)。
场景 API
export default {
size: "1920x1080", // 视口 = 视频尺寸
fps: 60,
theme: { captionPosition: "bottom", haloFill: "rgba(250,204,21,.30)" }, // 可选
async run(d) {
await d.open("http://localhost:4173/"); // 导航 + 稳定(不计入录制)
d.badge("NEW", "#15803d"); // 角落徽章(null 隐藏)
d.caption("新的仪表板"); // 字幕药丸(null 隐藏)
await d.hold(1500); // 录制 1.5 秒不变
await d.click(d.page.getByRole("button", { name: "Filters" }));
d.caption("筛选器集中在一个面板中"); // 在操作后设置字幕
await d.hold(1200);
await d.type("Stockholm"); // 模拟人类速度打字
await d.press("Enter");
await d.scroll(600); // 平滑滚轮滚动
await d.step("可选部分", async () => { /* 错误会被记录,录制继续 */ });
},
};
| 调用 | 作用 |
|---|---|
d.open(url, { settle }) | 导航,让页面稳定(不录制,默认 2500 毫秒) |
d.hold(ms) | 按当前状态录制页面 |
d.settle(ms) | 推进时间但不录制——跳过加载状态、延迟加载的代码块、不想显示的布局动画 |
d.move(target, { ms }) / d.hover | 平滑移动光标(缓动)到目标 |
d.click(target, { ms, button }) | 移动,点击并产生涟漪。button: "right" 用于上下文菜单 |
d.doubleClick(target) | 双击 |
d.type(text, { delay }) | 逐键输入(默认 90 毫秒/键) |
d.press(key) | 键盘快捷键,如 "Escape","Control+K" |
d.scroll(dy, { ms }) | 平滑滚轮滚动 |
d.caption(text) / d.badge(text, color) / d.cursor(bool) | 覆盖层状态;跨完整页面导航保持不变 |
d.step(name, fn) | 命名步骤;错误会被记录,不影响整体 |
d.page,d.point(target),d.width,d.height | 逃生舱 |
目标可以是 Playwright Locator、CSS 选择器字符串、{ x, y } 或异步函数 (page) => ({ x, y })。
Canvas 渲染的 UI(图形、地图、图表)没有每个项目的 DOM 元素——传递一个函数,让库返回屏幕坐标。Cytoscape.js 示例:
const node = (label) => (page) => page.evaluate((label) => {
const el = [...document.querySelectorAll("div")].find((e) => e._cyreg?.cy);
const n = el._cyreg.cy.nodes().filter((n) => n.data("label") === label)[0];
const r = el.getBoundingClientRect(), p = n.renderedPosition();
return { x: r.left + p.x, y: r.top + p.y };
}, label);
await d.click(node("Server A"));
编写优秀演示的技巧
- 字幕要真实。 只声称画面实际展示的内容。如果想展示的 bug 在录制中未复现,修改字幕——不要保留它。
- 在操作后设置字幕,而不是操作之前——否则下一屏的字幕会出现在上一屏上。
- 展示结果,而非点击。 在每个结果上停留 1–2 秒;用
d.settle跳过加载。 - 对比前后:用相同的故事情节和步骤录制两个场景,徽章分别为
BEFORE/AFTER(红色/绿色),保存为单独文件并可选拼接。 - 高光时刻(如落地页、动画):给予 5–10 秒——观众需要时间消化。
- 保持光标安静:每次移动 600–900 毫秒,在保持期间将其远离内容。
- 真实定时器在帧渲染期间持续运行(每帧约 50–300 毫秒实际时间),因此由定时器驱动的 UI——如提示、防抖搜索、自动关闭的工具提示——在视频中可能比现实中消失得更快。在需要它们显示之前触发,或减少保持时间。
使用视频
- PowerPoint:插入 → 视频 → 此设备。播放选项卡:开始:自动 并 循环播放直到停止。H.264
yuv420pMP4(demo-reel 输出的格式)在 Windows 和 macOS 上均可播放。 - Keynote:拖入;影片 → 重复 → 循环。
- 网页:
<video src="demo.mp4" autoplay loop muted playsinline></video>。 - GIF(README、聊天):
ffmpeg -i demo.mp4 -vf "fps=20,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" demo.gif - 拼接同尺寸/帧率的片段:
printf "file 'a.mp4'\nfile 'b.mp4'\n" > l.txt && ffmpeg -f concat -safe 0 -i l.txt -c copy both.mp4
故障排除
| 症状 | 解决方法 |
|---|---|
| 空白 / "Loading…" 帧 | 在 d.open 中延长 settle,或在导航点击后使用 await d.settle(1500) |
| 目标不可见 | 目标在屏幕外或关闭的菜单内——先展开/滚动;对于 Canvas 项目使用点函数 |
| WebGL/3D 渲染为黑色 | 保留默认启动标志(--use-angle=swiftshader);虽然慢(约 0.25 秒/帧)但正确 |
| 视频比计划短或长 | 持续时间精确:hold + 移动的总和。settle 时间从不计入录制 |
| 在 PowerPoint 中颜色显得暗淡 | 不要在没有 -pix_fmt yuv420p 的情况下重新编码;demo-reel 的输出已经正确 |
| 文字略微模糊 | 使用 --png 捕获无损帧(更慢,文件更大) |