I was just to lazy to do it my self, so I let my Codex agent to build it for my Hermes agent, so I can capture walk-throughs remotely.
It works fine tbh.
Use it as you wish. But change your design template before installing it. There is no contribution expected. And of course this readme was not meant to be read by humans, let your agent read and explain it for you.
Reusable AI-agent skill for turning local HTML pages or browser apps into reliable rendered media, especially short MP4 walkthroughs, looped clips by default, and linear videos when requested.
The skill is built around a bundled Node renderer instead of ad hoc Playwright scripts. The intended workflow is:
- Identify the app entry point, state seeding needs, selectors for each interaction, and assertions that prove each step completed.
- Write a JSON capture config from
references/capture-config.md, usually in the target project undertools/. - Run
scripts/render-html-video.cjswith that config. - Verify the exported MP4 metadata and preview frames before considering the render complete.
render-html-video.cjs accepts one argument:
node scripts/render-html-video.cjs path/to/config.jsonAt runtime it:
- Resolves the config directory, static
root,entry, andoutput. - Installs or resolves
playwrightandffmpeg-staticin a temp dependency directory if local modules are unavailable. - Generates a temporary stage HTML file containing:
- a branded background and optional title
- a 16:9 iframe for the app
- semantic default, pointer, and text cursors with click feedback
- helper functions for cursor movement, clicks, waits, and iframe assertions
- Starts a local static server on
127.0.0.1and injects an early inline app background style into served HTML pages. - Opens the stage in headless Chromium at the configured capture size.
- Seeds
localStoragebefore loading the app iframe. - Waits for iframe load plus any configured initial selector/text assertion before revealing the cursor.
- Trims the final MP4 to begin after the app-ready paint by default, then holds for the configured pre-roll before running declarative actions such as
click,setCursor,waitForSelector,waitForText,waitForUrl, andnavigateFrame. - In looped mode, returns the cursor to the configured loop point before ending the recording.
- Records a source WebM using Playwright video recording.
- Transcodes the recording to MP4 with H.264,
yuv420p, Lanczos scaling, configured FPS, and+faststart. - Deletes the temporary WebM and generated stage file unless
keepIntermediateis enabled.
A minimal config looks like this:
{
"root": ".",
"entry": "/index.html",
"output": "renders/app-walkthrough.mp4",
"loop": true,
"title": "Product",
"appBackground": "#111211",
"initialWaitForSelector": ".tab[data-tab='home']",
"initialCursorSelector": ".tab[data-tab='home']",
"preRollMs": 500,
"actions": [
{ "type": "click", "selector": ".tab[data-tab='home']", "wait": 700 },
{ "type": "click", "selector": ".tab[data-tab='settings']", "waitForSelector": ".settings-panel" }
]
}Important fields:
root: Static web root. Relative paths resolve from the config file directory.entry: URL path loaded inside the staged iframe.output: Final MP4 path. Relative paths resolve fromroot.loop: Defaults totrue. Setfalsefor a normal linear walkthrough ending.storage: Optional localStorage seed entries usingvalue,json,jsonFile, ortextFile.appBackground: Background shown behind and inside the iframe before the app paints, injected into served HTML pages as an early inline style, defaulting to dark#111211.initialWaitForSelector/initialWaitForText: Optional required readiness assertions after iframe load and before cursor reveal/actions.preRollMs: Optional hold after readiness and cursor placement before actions begin, defaulting to500.initialWaitremains supported as a legacy alias.trimBeforeReady: Optional boolean, defaulting totrue. Removes frames before the app-ready paint from the final MP4 while keepingpreRollMs.trimSafetyMs: Optional milliseconds to back up the trim point, defaulting to0.initialCursorSelector: Initial cursor position. For loops, this is also the default final cursor return point.loopCursorSelector: Optional final cursor return point for looped output. Defaults toinitialCursorSelector, then stage center.loopHold: Optional milliseconds to hold after the loop cursor return, defaulting to500.capture: Recording size, defaulting to 3840x2160.outputSize: Delivery size, defaulting to 1920x1080.fps: Output frame rate, defaulting to 30.stage: Optional visual styling for title, background, frame, zoom, and glow.actions: Ordered declarative interaction list.
See references/capture-config.md for the complete schema and examples.
- Record larger than the final delivery size, then downscale for sharper text.
- Seed app state before iframe load rather than relying on manual setup.
- Keep the app frame dark before load with
appBackgroundorstage.appBackground; the renderer injects this into served HTML pages and masks the frame until ready. - Preserve the intended title font. Leave the default JetBrains Mono
stage.titleFontUrland matchingstage.titleFontFamilyin place unless the user asks for a different font; use"titleFontUrl": falseonly for explicit offline/no-network rendering. - The renderer chooses the pointer cursor for scripted actions, the text cursor for editable targets, and the default cursor elsewhere.
- Require an initial readiness assertion with
initialWaitForSelectororinitialWaitForTextbefore the first visible cursor motion. - Leave
trimBeforeReadyenabled unless you intentionally need to show the app loading sequence. - Use assertions after interactions:
waitForSelector,waitForText, orwaitForUrlContains. - Use
fallbackNavigateonly after a visible click when app routing is flaky. - Configs are looped by default. Use
"loop": falsewhen the requested output should finish on a distinct final state. - For looped clips, make the first visible app state match the final app state. The renderer returns the cursor to
loopCursorSelector,initialCursorSelector, or stage center before ending. - Prefer bundled
ffmpeg-staticor explicitFFMPEG_PATH; do not rely on system ffmpeg. - Do not keep raw recordings or preview images unless they are needed for debugging or requested.
Before publishing a render, check:
- The MP4 exists and is meaningfully sized.
- The codec is H.264 in an MP4 container, not a renamed WebM.
- Resolution, aspect ratio, and FPS match the request.
- Preview frames show the intended staging and important UI destinations.
- For loops, first and last frames align visually, including cursor position. For
"loop": false, the last frame should show the intended linear ending.
Useful commands:
ffmpeg -hide_banner -i renders/app-walkthrough.mp4
ffmpeg -hide_banner -ss 00:00:00.5 -i renders/app-walkthrough.mp4 -frames:v 1 -update 1 renders/first.png
ffmpeg -hide_banner -ss 00:00:13.0 -i renders/app-walkthrough.mp4 -frames:v 1 -update 1 renders/last.png
node scripts/verify-no-white-start.cjsBefore publishing the skill itself, run the cross-harness compatibility proof:
node scripts/verify-harness-compat.cjsThe proof validates the portable skill structure, checks OpenAI/Codex metadata, stages the skill in temporary Codex and Hermes homes, and validates a generated Claude plugin wrapper with claude plugin validate. It does not make model/API calls.
- Node.js
- npm network access on first use, unless
playwrightandffmpeg-staticare already locally available - A local filesystem
- Chromium installed by Playwright, which the script attempts to install automatically
Optional environment variables:
HTML_RENDERER_DEPS_DIR: Override the temp dependency install directory.FFMPEG_PATH: Use a specific ffmpeg binary.
MIT. This skill is free to use, copy, edit, and redistribute.