diff --git a/docs/internal/references/demo-recording.md b/docs/internal/references/demo-recording.md index a843ac40..ef39e760 100644 --- a/docs/internal/references/demo-recording.md +++ b/docs/internal/references/demo-recording.md @@ -1,6 +1,6 @@ --- created_on: 2026-10-06 14:15 -last_modified: 2026-10-21 19:40 +last_modified: 2026-10-22 15:30 status: current --- @@ -14,7 +14,7 @@ The recording workflow runs on Linux and macOS. Record the media the docs site p Install the repository dependencies with `bun install`. Recording uses the existing pinned Playwright dependency and Chromium installed by `just ui install-browser`. Also provide Git, `just`, Caddy, [VHS](https://github.com/charmbracelet/vhs), `ttyd`, Bash, `ln`, and FFmpeg/ffprobe on PATH. FFmpeg must support H.264 (`libx264`), WebP posters (`libwebp`), AAC, and subtitles (`libass`); every recording and render command, and the publication tests, check this first and name what is missing. Homebrew's regular `ffmpeg` formula, which other formulas install as a dependency, has neither `libwebp` nor `libass`: on macOS use `ffmpeg-full` and put `$(brew --prefix ffmpeg-full)/bin` ahead of other FFmpeg installs on PATH. Install the DejaVu fonts and Noto Color Emoji to render captions and the playground's terminal emoji; verify with `fc-match 'DejaVu Sans Mono'` and `fc-match 'Noto Color Emoji'`. On macOS, install them with Homebrew's `font-dejavu` and `font-noto-color-emoji` casks; Homebrew's `fontconfig` provides `fc-match`. A custom [Fontconfig configuration](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user.html) can be supplied through `FONTCONFIG_FILE` when fonts are kept in the checkout. -`just demo test` also requires Xvfb on Linux. The headed-window regression owns an automatically allocated display with TCP disabled and verifies the native 1280 × 720 content area and 2560 × 1440 capture. The recorder uses Chromium's `Browser.setContentsSize` command so browser decorations do not shrink the captured viewport. This test records only its fixture; it does not run an agent or record a demo film. +`just demo test` also requires Xvfb on Linux. The headed-window regression owns an automatically allocated display with TCP disabled, launches Chromium there with `--disable-gpu`, and verifies the native 1280 × 720 content area and 2560 × 1440 capture. The recorder uses Chromium's `Browser.setContentsSize` command so browser decorations do not shrink the captured viewport. Xvfb has no GPU: Chromium's first GPU process fails to start there, and on a cold CI runner a screenshot taken before its software replacement starts fails. This test records only its fixture; it does not run an agent or record a demo film. VHS may download its own Chromium on the first run. Where that Chromium reports "No usable sandbox", explicitly opt into VHS's sandbox override: diff --git a/packages/devhost-ui/AGENTS.md b/packages/devhost-ui/AGENTS.md index 0e8257b4..7a5153bd 100644 --- a/packages/devhost-ui/AGENTS.md +++ b/packages/devhost-ui/AGENTS.md @@ -12,7 +12,7 @@ Local React workspace for the injected `devhost` browser UI that gets embedded i - Record the real scenes and render the promo: `DEVHOST_DEMO_INTERACTIVE=1 just demo record`; render it again from an earlier recording: `just demo promo`; regenerate its ElevenLabs narration: `just demo audio`; recording tests: `just demo test`. Annotation recording requires a graphical browser and an operator for native tab sharing and permissions. See `../../docs/internal/references/demo-recording.md` for prerequisites, scene refreshes, promo editing, and artifact locations. - Refresh every public guide video: `DEVHOST_DEMO_INTERACTIVE=1 just demo guides` (or `just demo guides ` for guides without annotation capture); render one guide's film again from its latest recording: `just demo guide-film `; generate the guide films' ElevenLabs narration, keeping the take of each unchanged line: `just demo audio-guides`. Publication tests require FFmpeg/ffprobe; full rendering also needs VHS/ttyd and Node.js. The Docker guide needs Docker; source-highlight recording uses the running stack's native source-locator API. - Open the design reference in the default browser (from the repo root): `just design devtools` -- Linux recording regression tests require `Xvfb` on PATH. They create an owned display with `-displayfd`, disable TCP listening, and stop it after checking real headed-window geometry. CI installs Xvfb alongside FFmpeg and unzip. +- Linux recording regression tests require `Xvfb` on PATH. They create an owned display with `-displayfd`, disable TCP listening, launch Chromium on it with the GPU disabled because Xvfb has none, and stop it after checking real headed-window geometry. CI installs Xvfb alongside FFmpeg and unzip. ## Local conventions diff --git a/packages/devhost-ui/scripts/demo/__tests__/fixtures/captureAtScale.ts b/packages/devhost-ui/scripts/demo/__tests__/fixtures/captureAtScale.ts index 583e7b8f..c63cb82c 100644 --- a/packages/devhost-ui/scripts/demo/__tests__/fixtures/captureAtScale.ts +++ b/packages/devhost-ui/scripts/demo/__tests__/fixtures/captureAtScale.ts @@ -44,8 +44,8 @@ try { env.DISPLAY = `:${display}`; env.DEVHOST_DEMO_INTERACTIVE = "1"; } - // The test that runs this script gives it a working directory of its own. - browser = await launchDemoBrowser(env, captureScale); + // The test that runs this script gives it a working directory of its own. The Xvfb display has no GPU. + browser = await launchDemoBrowser(env, captureScale, displayServer === undefined); const page = await createDemoPage(browser, runtime, captureScale); const clips = await recordBrowserScene( page, diff --git a/packages/devhost-ui/scripts/demo/launchDemoBrowser.ts b/packages/devhost-ui/scripts/demo/launchDemoBrowser.ts index 65005eae..d3c09d56 100644 --- a/packages/devhost-ui/scripts/demo/launchDemoBrowser.ts +++ b/packages/devhost-ui/scripts/demo/launchDemoBrowser.ts @@ -1,17 +1,25 @@ import { chromium, type Browser } from "playwright"; import { viewport } from "./constants"; -export async function launchDemoBrowser(env: NodeJS.ProcessEnv, captureScale: number = 1): Promise { +export async function launchDemoBrowser( + env: NodeJS.ProcessEnv, + captureScale: number = 1, + hasGpu: boolean = true, +): Promise { return await chromium.launch({ env, headless: env.DEVHOST_DEMO_INTERACTIVE !== "1", // The recorder owns these signals so its finally block can stop the stack and remove routes. handleSIGINT: false, handleSIGTERM: false, - // The screencast records CSS pixels under an emulated device scale; only the window's own scale reaches it. - args: - captureScale === 1 + args: [ + // On a display without a GPU, such as Xvfb, Chromium's first GPU process fails to start and is replaced by a + // software one; a screenshot requested before that, which can take many seconds on a cold machine, fails. + ...(hasGpu ? [] : ["--disable-gpu"]), + // The screencast records CSS pixels under an emulated device scale; only the window's own scale reaches it. + ...(captureScale === 1 ? [] - : [`--force-device-scale-factor=${captureScale}`, `--window-size=${viewport.width},${viewport.height}`], + : [`--force-device-scale-factor=${captureScale}`, `--window-size=${viewport.width},${viewport.height}`]), + ], }); }