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. 何时制作
- 效果在屏幕上可见且几秒内发生(如生物识别、提示、手势)。不适用于日志、性能数据或表格更佳的内容。
- 当应用 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 | 静态文本;标题是主张,副标题一行 |
phoneCaption | 手机下方的说明文字,默认为 real simulator · not an animation;"" 隐藏它 |
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 | 应用打开后、深度链接前运行的 warden 参数(在镜头外);["ui","network","set","online","--relaunch"] 为 iOS dylib 启动网络/时钟步骤提供所需。每个条目是一个参数数组或 { argv, optional: true }:可选的步骤退出非零时会被记录(optional setup warden … -> <code>)而不是导致录制失败(例如权限提示,可能不会显示) |
ready | 深度链接后、视频开始前必须通过的 warden ui wait 选择器(例如 id=bio-screen);步骤不会按下未挂载的屏幕 |
leadInMs / tailHoldMs | 步骤 1 之前的空闲时间/最后一帧保持时间,由渲染器填充(录制尾部为 1 秒) |
steps[] | argv(warden 之后;录制器附加 --udid)、output(ok 绘制矢量勾选,plain 静默,lines 打印多条静默行)、pauseAfterMs、shown?、hidden?、quote? |
outro | 最后输出后键入的 # comment 行 |
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 网格,每秒一张,pts 烧印)。对照日志检查:每个输出行出现在手机可见反应时;没有帧显示无关的提示、键盘、红框或错误状态栏;最后一帧显示结束状态。 - 如需更仔细查看:
ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.png并Read它。 - 在定义中修复(
pauseAfterMs、删除/隐藏步骤、shown)并--render-only;仅在录制本身出错时重新录制。批量修复:每轮一次录制。 - 将 mp4 + jpg 提交到
apps/docs/public/demos/<feature>/。
4. 诚实规则
- 每个打印的命令都实际运行过,针对真实应用。
shown与argv含义不同(它只修剪了诸如--timeout的标志)。 - 输出文本是手机实际做的,而不是命令打印的内容。如果一次按下没有可见效果(例如在 sheet 的进入动画期间丢失,或者什么也没变),不要为其添加说明:使用
hidden: true,或者通过更长的前一个pauseAfterMs调整时机。 - 首次运行的权限提示只在新安装时显示;声明会复用热构建,因此除非录制确实显示了提示,否则不要为其添加说明。
- 没有应用侧绕过或模拟提示:标题中的声明必须在录制中真实成立。
- 秘密信息:
maskSecrets隐藏 key=value、ghp_/sk-风格令牌和打印行中的长 hex/base64。仍要检查手机上是否有令牌、邮箱、设备名称。 - 真实模拟器,真实 iOS UI。绝不合成虚假帧。
5. 注意事项(已编码在工具包中)
- simctl 视频是 VFR 且会丢弃空闲尾部:渲染器进行
fps归一化并填充最后一帧(tpad)以达到计划长度。 - 视频时间与命令日志的时间差在约 0.5 秒内;录制器在
video start后立即启动时钟。 - Martian Mono 没有勾选字形:
render.py将其绘制为矢量。 - 帧使用文档 Device Hub 深色配色(Mona Sans + Martian Mono);设置
app.appearance: "dark"以使手机匹配。录制器之后会恢复模拟器的外观。 - 终端溢出:
computeTerminalLayout将字号和间距缩小到 17 px,然后滚动移除最早的条目。 - 设备视频限制 3 分钟;无论如何请保持录制在 40 秒以内。
- 状态栏:允许在托管的 udid 上使用
xcrun simctl status_bar <udid> override;录制器之后会清除它。
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 真实的 DEV 仪表盘(左侧,在终端卡片内)旁边是模拟器,warden dev 自己进行声明(右侧)。没有任何内容被输入或伪造:dev-take.ts 在 PTY 中生成 warden dev ios --label demo-dev-<clip> --headless --no-shutdown(Bun.spawn({ terminal }))并镜像到 @xterm/headless(帧读取用于等待),并将每个 PTY 块写入 tui.cast;时钟起点是生成时刻。它轮询 warden ls --json 以获取标记的设备租约,然后设置深色 + 9:41,卸载应用并启动 ui screen video(手机在此之前是黑屏)。dev 子进程有自己的 WARDEN_SESSION_ID:warden dev 采用调用者会话持有的设备租约,并且每个合作代理共享一个,因此没有它的话录制可能会驱动别人的模拟器。
bun run demo:dev dev/hero --record-only # 声明、卸载应用、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)。 - 一个 beat =
waitScreen(TUI 帧上的正则) +waitPhone(ui wait选择器) -> 动作(向 TUI 输入按键,和/或对仓库文件进行真实的edit) ->afterPhone选择器(热重新加载落地) ->pauseAfterMs。dev.json保存每个 beat 在视频时钟上的开始/就绪/结束时间;说明文字从 beat 的动作运行到下一个 beat 的动作。 - 编辑是对源文件的真实编辑(
welcome.tsx的 body 字符串),在finally中逐字节还原;录制后检查git diff apps/example/src是否为空。 uninstallFirst从托管的模拟器中移除应用,以便warden dev在镜头前从缓存安装开发构建。第一次录制是本地构建(几分钟,dev build行编译):先录制一次以预热缓存,然后重新录制。warden app ensure --kind dev不会预热warden dev读取的缓存。- 速度斜坡:
DevBeat.ramp: N将该 beat 的等待加速 N 倍(保持 1.2 秒头部和 1 秒尾部以原始速度),轮播会绘制一个× N speed标签。如果预热录制在开发客户端等待中速度较慢,则进行斜坡加速。按键和热重新加载始终是实时。 - 诚实:
<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的 beat 会先停止视频再执行动作(q结束租约,然后warden ui会拒绝)。在q之后,录制验证warden ls是否持有该会话的租约(否则失败),并且投射在仪表盘离开备选屏幕的地方被截断,以便轮播以其最后一帧结束。开发启动器的白色加载屏幕和蓝色的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 的“有更新可用”通知。仅用于噪音,绝不用于行为。- 使用
<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/total shards done) |
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的 beat 会轮询simctl io screenshot直到 PNG 超过 100 KB(HOME_SCREEN_MIN_BYTES):一个刚刚克隆的无头模拟器在claim返回后 10 多秒才显示苹果标志。对该等待进行斜坡加速(3 倍,带标签)。rawVideo:使用simctl io recordVideo录制(通过 SIGINT 停止),因为 warden 的ui screen video助手与warden e2e在一个设备上冲突。--record只录制开始时存在的设备;弹性后期加入者没有视频。主机容量门决定有多少个可以容纳,因此要求不超过启动时启动的模拟器数量(这里最多 2 个)。debug/failure的临时规格写入apps/example/e2e/debug-demo.e2e.ts,并由 shell trap 移除;之后检查git status(被杀的录制可能留下它)。- 使用
<DemoReel src="/demos/<feature>/<clip>" command="…" facts="…" label="…" />嵌入;事实来自录制,每次重新录制后重新读取。 demo:batch使用轮询器设置模拟器样式(styleWhenBooted:通过标签warden ls --json,simctl list查找 Booted,然后ui appearance dark+status_bar override 9:41),因为warden e2e没有按设备钩子;它在finally中释放--mine --label demo-<feature>-<clip> --shutdown。使用在干净模拟器上通过的规格(perf、settings、todos不可靠);合成--height 1080标志给出 16:9 布局(终端
…
[已针对翻译截断]