demo-video 是做什麼的?
bun run demo <feature>/<clip> 在租用的 iOS 模擬器上錄製一段影片,然後渲染成 1920x1080 的影片:左側是標題,一個終端機會逐字打出每個真實的 warden 命令,並在步驟結束時印出輸出;右側是手機畫面(圓角,3px 邊框)。工具包:scripts/demo/(demo.ts CLI、record.ts、render.py、timeline.ts、demo.types.ts)。定義檔:apps/example/demos/<feature>/<clip>.demo.ts(name 必須等於 <feature>/<clip>);靜態資源:apps/docs/public/demos/<feature>/<clip>.mp4|jpg。每個功能一個資料夾(biometrics、network、location、clock),小寫字母,連字號。內部使用:無需變更集(changeset)。
1. 何時製作
- 功能的效果在畫面上可見且在幾秒內發生(例如生物辨識、警示、手勢)。不適用於日誌、效能數字或任何用表格呈現更好的內容。
- 當 App UI 或某個命令的行為變更後:需要重新錄製,永遠不要編輯 mp4。
- 每個剪輯一個故事,長度 10-25 秒,包含 3-6 個附有說明的步驟。
需要 ffmpeg、ffprobe、uv(brew install ffmpeg uv)、Xcode 以及 node_modules/@expo-google-fonts 中的字型(bun install)。
2. 撰寫定義檔
導出 demo: DemoDefinition(scripts/demo/demo.types.ts)。複製 apps/example/demos/biometrics/faceid.demo.ts。
| 欄位 | 意義 |
|---|---|
title / subtitle / terminalTitle / footer | 靜態文字;title 是主張,subtitle 是一行說明 |
phoneCaption | 手機下方的說明文字,預設為 真實模擬器 · 非動畫;設為 "" 可隱藏 |
claim | { biometrics?, profile? } -> warden claim ios --json --label demo-<feature>-<clip> --ttl 30m [--biometrics] |
app | bundleId、kind(預設 simulator)、project(相對於儲存庫的 cwd)、deepLink(以 --settle 開啟)、appearance(light/dark,透過 simctl ui 在租用的 udid 上設定,錄製後恢復) |
setup | 在應用程式開啟後、deep link 之前離線執行的 warden 參數;["ui","network","set","online","--relaunch"] 會為 iOS dylib 啟動網路 / 時鐘步驟提供所需的環境。每個條目是一個 argv 或 { argv, optional: true }:可選步驟若以非零退出碼結束,僅被記錄(optional setup warden ... -> <code>)而不會導致錄製失敗(例如可能不會出現的權限提示) |
ready | 在 deep link 之後、影片開始之前必須通過的 warden ui wait 選擇器(例如 id=bio-screen);步驟絕不會按下未掛載的畫面 |
leadInMs / tailHoldMs | 步驟 1 之前的閒置時間 / 最後一幀的停留時間,由渲染器填充(錄製尾部為 1 秒) |
steps[] | argv(warden 之後的參數;錄製器會附加 --udid)、output(ok 繪製一個向量勾號,plain 為靜音模式,lines 印出多行靜音文字)、pauseAfterMs、shown?、hidden?、quote? |
outro | 最後一個輸出之後輸入的 # 註解 行 |
output: { kind: "lines", text: [...] } 會將每一行印出為一條靜音文字(大約保持 6 行;每增加一行會消耗版面上 0.4 個條目的間距),用於顯示檔案中的程式碼或文字。搭配 quote: { file }(路徑相對於 app.project):步驟完成後,錄製器會檢查每一行印出的文字(去除前後空白)是否存在於該檔案中,否則會拋出 quoted line not in <file>: <line> 錯誤,因此引用行絕不會是虛構的。這些行仍然會經過 maskSecrets 處理。
時間絕不是手動撰寫的:錄製器會根據影片時鐘測量每個步驟的開始和結束時間,輸出行會在步驟結束時出現。
3. 循環流程
bun run demo <feature>/<clip> --record-only # 聲明、9:41 狀態列、啟動、錄製、依 ID 釋放
bun run demo <feature>/<clip> --render-only --sheet # 建置 mp4 + jpg,寫入 <work>/sheet.jpg
bun run demo <feature>/<clip> # 兩者都執行
工作目錄:$TMPDIR/warden-demo/<feature>/<clip>(--out dir):take.mp4、steps.json、plan.json、sheet.jpg;若錄製失敗則有 failure-snapshot.txt(錯誤 + ui snapshot -i)。其中的資料都不會提交。
- 錄製。
- 讀取
sheet.jpg(4x4 格,每秒一格,時間點已標示)。與日誌比對:每個輸出行出現時機須與手機畫面上的反應一致;任何畫格不得顯示不相關的提示、鍵盤、紅框或錯誤的狀態列;最後一幀需顯示結束狀態。 - 如需更仔細檢查:
ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.png並「讀取」它。 - 在定義檔中修正(
pauseAfterMs、刪除/隱藏步驟、shown)並執行--render-only;僅在錄製內容本身有問題時才重新錄製。批次修正:每次循環只錄製一次。 - 將 mp4 + jpg 提交到
apps/docs/public/demos/<feature>/。
4. 誠實規則
- 每個印出的命令都實際執行過,且是在真實的應用程式上執行。
shown的意義不能偏離argv(它僅用於修剪--timeout等旗標)。 - 輸出文字是手機實際上做的事情,而非命令本身印出的內容。如果某個按壓操作沒有可見效果(例如在畫面的進入動畫期間被忽略、沒有任何改變),則不要為其加上說明:使用
hidden: true,或透過延長上一個pauseAfterMs來調整時機。 - 首次執行的權限提示只在全新安裝時顯示;聲明會重複使用已暖機的建置,因此除非影片中有顯示該提示,否則不要為其加上說明。
- 不得繞過應用程式端或使用模擬的提示:標題中的主張必須在影片中為真。
- 機密資訊:
maskSecrets會隱藏 key=value、ghp_/sk-風格的權杖以及印出文字中的長十六進位/base64 字串。但仍需檢查手機畫面中是否有權杖、電子郵件、裝置名稱。 - 真實模擬器,真實 iOS UI。絕不合成假的畫格。
5. 注意事項(內建於工具包中)
- simctl video 是 VFR 且會丟棄閒置的尾部:渲染器會進行
fps標準化並將最後一幀填補(tpad)至計畫長度。 - 影片時間與命令日誌的差異約在 0.5 秒內;錄製器在
video start之後立即啟動時鐘。 - Martian Mono 沒有檢查符號字形:
render.py將其繪製為向量。 - 畫面使用的是 docs Device Hub 深色調色板(Mona Sans + Martian Mono);設定
app.appearance: "dark"以讓手機畫面相符。錄製器會在之後恢復模擬器的外觀。 - 終端機溢出:
computeTerminalLayout會將間距和字型縮小為 17 px,然後將最早輸入的條目滾出畫面。 - 裝置影片限制為 3 分鐘;無論如何請將錄製保持在 40 秒以下。
- 狀態列:
xcrun simctl status_bar <udid> override允許在租用的 udid 上使用;錄製器會在之後清除它。
6. X 平台匯出
1920x1080,H.264 High,yuv420p,30 fps,+faststart,無音訊,長度不超過 2:20,檔案小(工具包目標是透過 CRF 階梯 24..36 控制在不超過 2 MB)。檢查:
ffprobe -v error -show_entries stream=codec_name,profile,width,height,r_frame_rate -show_entries format=duration,size apps/docs/public/demos/<feature>/<clip>.mp4
7. 靜態資源與文件嵌入
輸出:apps/docs/public/demos/<feature>/<clip>.mp4 + <clip>.jpg(海報 = 最後一幀),透過 /demos/<feature>/<clip>.mp4 提供服務(Vite public/)。使用 DemoClip MDX 元件(apps/docs/src/components/demo-clip.tsx)嵌入:自動播放、靜音、循環播放;在減少動作模式下會停留在海報畫面。src 不包含副檔名;label 說明剪輯的內容(與說明文字相符)。使用原始 <video> 標籤會導致 apps/docs/src/lib/source.test.ts 測試失敗。
<DemoClip src="/demos/<feature>/<clip>" label="warden ui ... 正在租用的模擬器上執行 ..." />
新增一個說明句子,命名所使用的命令。批次展示影片 apps/docs/public/demos/e2e/batch.*(使用 <DemoReel> 嵌入)是另一條不同的管線(bun run demo:compose)。
8. warden dev 展示影片(dev/hero)
第三條管線:warden 的真實開發儀表板(左側,在終端機卡片中)與模擬器 warden dev 自行聲明的顯示(右側)並排。沒有輸入或偽造的內容:dev-take.ts 在一個 PTY(Bun.spawn({ terminal }))中生成 warden dev ios --label demo-dev-<clip> --headless --no-shutdown,並將其內容鏡像到 @xterm/headless(用於等待的幀讀取),並將每個 PTY 區塊寫入 tui.cast;時鐘原點是生成時刻。它輪詢 warden ls --json 以取得已標記的裝置租約,然後設定深色模式 + 9:41,解除安裝應用程式並啟動 ui screen video(在此之前手機畫面是黑色)。開發子程序有自己的 WARDEN_SESSION_ID:warden dev 會接管呼叫者 session 所持有的裝置租約,而每個協作者代理共用一個 session,如果沒有這個設定,錄製可能會驅動到其他人的模擬器。
bun run demo:dev dev/hero --record-only # 聲明、解除安裝 app、9:41 + 深色模式、錄製、依 ID 釋放
bun run demo:dev dev/hero --render-only --sheet # 聚合 + ffmpeg -> apps/docs/public/demos/dev/hero.{mp4,webm,jpg},<work>/sheet.jpg
bun run demo:dev dev/hero # 兩者都執行
- 工具包:
dev-demo.ts(CLI)、dev-take.ts(錄製器,runBeats)、dev-render.ts(聚合 + 合成,DEV_LAYOUT)、dev-frame.py(卡片、邊框、遮罩)、dev-timeline.ts(投射文字、斜坡區段、說明文字)。型別:DevDemoDefinition/DevBeat,位於demo.types.ts。定義檔:apps/example/demos/dev/<clip>.demo.ts;工作目錄$TMPDIR/warden-demo/dev/<clip>(take.mp4、tui.cast、dev.json、sheet.jpg、failure-frame.txt)。 - 一個節拍 =
waitScreen(對 TUI 幀進行正則表達式比對)+waitPhone(ui wait選擇器)-> 動作(向 TUI 輸入按鍵,和/或對儲存庫檔案進行真實的edit)->afterPhone選擇器(熱重載的著陸點)->pauseAfterMs。dev.json保存每個節拍在影片時鐘上的開始 / 就緒 / 結束時間;說明文字從一個節拍的動作執行到間下一個節拍的動作。 - 編輯是真實的原始碼編輯(
welcome.tsx的字串內容),並在finally中逐位元組還原;錄製後檢查git diff apps/example/src是否為空。 uninstallFirst會從租用的模擬器中移除應用程式,以便warden dev從快取中安裝開發版本(在鏡頭前)。首次基於指紋的錄製會是本機建置(數分鐘,dev build行會編譯):先錄製一次以暖機快取,然後重新錄製。warden app ensure --kind dev不會暖機warden dev所讀取的那個快取。- 速度斜坡:
DevBeat.ramp: N會將該節拍的等待時間加快 N 倍(保留 1.2 秒的開頭和 1 秒的尾部以真實速度播放),並且展示影片會繪製一個× N 速度標籤。如果暖機錄製在開發用戶端等待時速度較慢,可以對其進行斜坡加速。按鍵輸入和熱重載始終是即時速度。 - 誠實性:
<DemoReel>說明文字中的事實來自錄製的 TUI 行(dev build 3.5s、Metro 3.9s、dev client 161ms)和dev.json(編輯 ->ui wait看到新文字:0.76 秒)。每次重新錄製後都重新讀取它們;它們可能會改變。 - 限制:儀表板沒有單獨的聲明行(它在
preparing the ios dev build內部);--headless所以不會開啟 Device Hub 視窗;100x26 PTY(100 列是 HUD 的最小值)。一個有stopVideoFirst的節拍會在其動作之前停止影片(q會結束租約,之後warden ui會拒絕)。在q之後,錄製會驗證warden ls沒有持有該 session 的任何租約(否則失敗),並且投射內容會在儀表板離開備用畫面處被截斷,以便展示影片以其最後一幀結束。開發啟動器的白色載入畫面和藍色的Refreshing...橫幅是真實的應用程式;Metro 重新載入也可能來自另一個正在編輯apps/example/src的代理。 - 斜坡:
ramp僅在等待時間減去其 1.2 秒開頭和 1 秒尾部後,至少還有 6 秒時(RAMP_MIN_S)才應用。英雄展示影片將其開發用戶端等待時間加速 3 倍,並帶有標籤。 devEnv會為warden dev子程序添加環境變數;英雄展示影片設定EXPO_NO_DEPENDENCY_VALIDATION=1(@expo/clicheckDependenciesOnStart)以移除 Expo 在 Metro 面板中顯示的「有可用更新」提示。僅用於減少雜訊,絕不改變行為。- 使用
<DemoReel src="/demos/dev/hero" command="warden dev ios" facts="..." label="..." />嵌入(登陸頁面英雄、sims/dev.mdx、getting-started/quickstart.mdx)。海報 =posterBeat動作幀 + 1 秒(HUD 已開啟,所有行已完成,應用程式正在執行)。
9. 終端機為主的剪輯(VID-6)
所有命令都是真實的;每個展示影片只顯示實際執行的內容。一次一個剪輯,每次按標籤釋放。
| 剪輯 | 管線 | 命令 |
|---|---|---|
e2e/parallel | batch-take.ts:warden e2e <suite> --count N --record --headless 進入 <work>/rec,在每個租用模擬器啟動時設定深色模式 + 9:41,然後 compose.ts --height 1080 | bun run demo:batch e2e/parallel --suite example --specs devtools,gestures,onboarding,sign-in,todo-detail --count 2(--record-only / --compose-only;標頭顯示規格數量;當規格與任務不同時,計數器顯示 N/總分片完成) |
dev/worktrees | LanesDemoDefinition:每個並發車道一個 recordDev,下方一個 shell 條(warden ls) | bun run demo:lanes dev/worktrees(lanes.ts、lanes-frame.py、lanes-demo.ts;車道透過 DevTake.startedAtS 共用同一個時鐘) |
golden/clone | DevDemoDefinition.script:一個 shell 腳本在 PTY 中執行,而非 warden dev | bun run demo:dev golden/clone |
debug/failure | script + rawVideo | bun run demo:dev debug/failure --sheet |
script模式:不解安裝,不檢查釋放;錄製以release --mine --label L --shutdown和sims delete <udid> --yes結束。一個帶有waitBoot的節拍會輪詢simctl io screenshot直到 PNG 超過 100 KB(HOME_SCREEN_MIN_BYTES):一個新克隆的無頭模擬器會在claim返回後 10 秒以上才顯示 Apple 標誌。對該等待進行斜坡加速(3 倍,帶標籤)。rawVideo:使用simctl io recordVideo(SIGINT 停止)錄製,因為 warden 的ui screen video輔助工具與warden e2e在同一裝置上衝突。--record僅記錄在開始時存在的裝置;延遲加入的彈性裝置沒有影片。主機容量閘門決定可以容納多少,因此要求啟動的模擬器數量不超過一開始已開機的數量(此處最多 2 個)。debug/failure的拋棄式規格寫入apps/example/e2e/debug-demo.e2e.ts,並由 shell 陷阱移除;之後檢查git status(中途被終止的錄製可能會留下它)。- 使用
<DemoReel src="/demos/<feature>/<clip>" command="..." facts="..." label="..." />嵌入;事實來自錄製,每次重新錄製後重新讀取。 demo:batch使用輪詢器(styleWhenBooted:針對標籤使用warden ls --json,針對 Booted 狀態使用simctl list,然後執行ui appearance dark+status_bar override 9:41)來設定模擬器樣式,因為warden e2e沒有每個裝置的鉤子;它在finally中釋放--mine --label demo-<feature>-<clip> --shutdown。使用能在乾淨模擬器上通過的規格(perf、settings、todos不可靠);合成指令的--height 1080旗標提供 16:9 佈局(終端機... 被截斷)