Terrain Viewer
Dev

Demo videos

How the demo videos are made by agents — takes recorded with agent-browser and a DevTools screencast, cut in Remotion with Remocn components, and the commands that run the pipeline

The demo videos are made the same way as the slide decks: by an agent, from the real app. The pipeline lives in video/, a Remotion workspace with its own install, outside the pnpm workspace, like slides/.

Recording

scripts/record.sh drives headless Chrome with agent-browser: it opens a URL that carries the app state, clicks the real controls and waits. Chrome is launched on the real GPU (--use-angle=d3d11, --ignore-gpu-blocklist); the default headless launch renders WebGL on SwiftShader.

The frames are captured by scripts/capture.mjs, not by agent-browser's own record, which dropped every map take ("encoder fell more than 500 ms behind capture") whatever the frame rate or size. capture.mjs attaches to the same Chrome over the DevTools protocol (agent-browser get cdp-url), runs Page.startScreencast, writes the frames with their timestamps and assembles them afterwards with FFmpeg's concat demuxer. Chrome only sends a frame when the page changes, so a take is 20 to 70 frames, which is right for an interface demo. Takes land in recordings/.

What works for a take (October 2026)

Five takes of the same basic feature, recorded by delegated agents, settled the recipe. The scripts of the latest one (take-v4.sh, take-v4.mjs, under .cache/recordings/, not tracked) are the base to copy.

  • Record the production site, not a dev server: no dev widgets in frame, and no reload mid-take when Vite bundles a dependency on a mode's first toggle (the shims are pre-bundled now, but a dev server still reloads under other agents' edits).
  • 1920×1080 at 30 fps. set viewport 1920 1080 gives device pixel ratio 1 and full-size screencast frames; capture.mjs needs no change.
  • A real cursor. The page is captured without a pointer. Inject an <img> at the top layer, pointer-events: none, moved on mousemove, holding the Windows cursor itself: the 32 px frame cut out of C:\Windows\Cursors\aero_arrow.cur and saved as PNG (Pillow only opens the 128 px frame, so slice the directory entry by hand); hotspot at the tip, the arrow 12×19 px inside the frame, as Windows draws it. A hand-drawn arrow always looks slightly off.
  • Natural motion. Drive the pointer over the DevTools protocol in about 60 steps a second along eased, slightly curved paths with varied speed, never straight constant-speed lines. Something moves at every moment: the camera, the pointer or a control. Tooltips may show; they do not distract.
  • Keep the hillshade on under slope or any other mode: slope alone reads wrong. End a slope sequence with the maximum around 60 degrees, not 90, where the tint fades; then bring the hypsometric tint in and change its ramp and bounds.
  • Traps. Close the extra chrome://newtab tab before capture or capture.mjs records nothing; send wheel events without awaiting each one (awaiting made a 2 s zoom take 6.6 s); each element-position read mid-take costs about 0.3 s of a still pointer, so read what you need before the take; Chrome sends no frame for up to 0.3 s on a mode toggle or a slider press; dragging a slider makes the side panel see-through, which is the app's own transparent-panel behaviour. Load the app's state through the URL; localStorage must be written from a same-origin non-app URL (/@vite/client on dev) because the app overwrites it on first load.
  • The report of a recording agent starts with the absolute path of the MP4; a middle frame is optional.
  • A known gap: the injected cursor is always the arrow. The pointer never shows the hand over links and buttons or the resize shapes, because the capture has no pointer at all and the image is one static arrow. Reading the computed cursor style of the element under the pointer and swapping the image would fix it; not done yet.

The reference take, a 29 s FHD tour of slope and hypsometric tint over Mont Blanc (October 2026), is kept in two places: its scripts under video/takes/basic-modes-v5/ (the agent-browser driver, the rehearsal, the DevTools pointer script, the cursor image) and the MP4 on the demo-takes release.

Cutting

src/Demo.tsx is the cut: a title, the takes with captions on the clicks, focus-pull transitions, an outro. Each take's in-point, speed and click times are constants at the top of the file; change them after a new recording.

The motion components under src/components/remocn/ are Remocn components, copied in with the shadcn registry (npx shadcn@latest add @remocn/<name>, registry in components.json) and edited locally: they ship as full-frame scenes with their own background and a per-character text split, so StaggeredFadeUp and FocusBlurResolve gained an inline mode and a word split.

The cut's look

The cuts follow the slide decks' kit (slides/slides/*/index.tsx): white cards, Geist, hairline rules, soft panels, one accent (#1A237E), a mono eyebrow for labels. Captions are a white panel with dark text in the lower third of the map, clear of the sidebar and the timeline; title and outro cards are white with a plain fade; transitions are cross-fades or cuts. No frosted or blurred backdrops, no shader backgrounds. Demo4 is the current cut (October 2026, 142 s: take v7 and the historical take v4 paced for narrator subtitles, cards of about 4 s; the render is on the demo-takes release); Demo3 and Demo2 are the earlier ones, kept for reference. Geist is loaded from video/public/fonts/, Geist Mono from the machine, where the whole Geist family is installed for the user.

Commands

cd video
pnpm install --ignore-workspace   # once
pnpm record     # agent-browser drives the app, takes land in recordings/
pnpm clips      # scale the takes to 1080p/30 fps into public/, write public/clips.json
pnpm studio     # Remotion Studio, to edit and preview
pnpm render     # out/terrain-viewer-demo.mp4

Tried and rejected

  • agent-browser's record: works for simple pages, drops map scenes (above). The FFmpeg call is baked in, already -preset ultrafast, so it is not fixable from outside.
  • fframes (Rust, GPU, no browser) for title and end cards: not tried, because on Windows it needs a shared FFmpeg 9 build, LLVM and Vulkan.

Notes

.claude/memory/agent-browser-recording.md has the details: the CDP screencast capture, the GPU flags, and the traps (localStorage written from a same-origin non-app URL because the app overwrites it on first load, Base UI checkboxes clicked through label[for=…], Git Bash path rewriting and MSYS_NO_PATHCONV=1, never piping agent-browser output, slow IIIF hosts). video/PROCESS.md is the trace of the first video and what to do differently next time.

On this page