Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/internal/references/demo-recording.md
Original file line number Diff line number Diff line change
@@ -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
---

Expand All @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion packages/devhost-ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <guide-slug>` for guides without annotation capture); render one guide's film again from its latest recording: `just demo guide-film <guide-slug>`; 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 13 additions & 5 deletions packages/devhost-ui/scripts/demo/launchDemoBrowser.ts
Original file line number Diff line number Diff line change
@@ -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<Browser> {
export async function launchDemoBrowser(
env: NodeJS.ProcessEnv,
captureScale: number = 1,
hasGpu: boolean = true,
): Promise<Browser> {
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}`]),
],
});
}
Loading