demo-reel 是做什麼的?
將網頁應用的腳本操作流程轉換為宛如專業螢幕錄製的影片:流暢的 60 fps 畫面、帶有光暈的可見游標、點擊波紋與字幕。它驅動真實應用於無頭 Chromium 中運行 — 影片內容就是產品本身,而非模擬畫面。
為何影片如此流暢
一般的螢幕錄製工具在瀏覽器自然繪製時才擷取畫面;在效能較差的機器(或使用軟體 WebGL)上,通常只有約 10–15 fps 且會卡頓。demo-reel 則透過注入虛擬時鐘(scripts/inject.js)來解決:requestAnimationFrame、performance.now 和 Date 僅在錄製器指示時才前進,同時 CSS 與網頁動畫透過 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: "篩選器" }));
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 元素 — 請傳入一個函數,向 UI 函式庫查詢螢幕座標。以下是 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
故障排除
| 症狀 | 解決方法 |
|---|---|
| 空白 / 「載入中…」幀 | 在 d.open 中延長 settle 時間,或在導航點擊後使用 await d.settle(1500)。 |
| 目標不可見 | 目標在畫面外或處於關閉選單內 — 先展開/滾動;對於 Canvas 項目,請使用點函數。 |
| WebGL/3D 渲染為黑色 | 保留預設啟動標誌(--use-angle=swiftshader);雖然慢(約 0.25 秒/幀)但正確。 |
| 影片比預期短或長 | 長度是精確的:所有 hold 和移動時間的總和。settle 時間從不被錄製。 |
| 在 PowerPoint 中顏色看起來偏淡 | 請勿在沒有 -pix_fmt yuv420p 的情況下重新編碼;demo-reel 的輸出已經是正確的。 |
| 文字稍微模糊 | 使用 --png 擷取無失真幀(較慢、檔案較大)。 |