Launch video
Produces sideshow release/feature videos by driving the real viewer in a recording Chromium against a live server — 1920x1080 stage with window chrome, captions, and title cards, encoded with ffmpeg. Use for release roundups, single-feature demos, PR walkthroughs, and social announcements.From its SKILL.md
npx -y skills add modem-dev/sideshow --skill launch-videoAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
8.8 KB, as published. Nobody here has run it
Sideshow video pipeline
Maintainer-only: requires a sideshow source checkout (the pipeline lives in
scripts/launch-video/, which never ships to npm).
Generates product videos where every app frame is the real sideshow viewer
talking to a real server — no screen recording, no mockups. Unlike hunk's
keyframe-compositing pipeline (skills/launch-video in the hunk repo),
sideshow's product IS live motion — cards streaming in over SSE, sandboxed
iframes resizing, the comment loop — so this pipeline records one real-time
pass instead of compositing stills:
stage.html a 1920x1080 HTML stage: window chrome + caption lower-third +
full-screen title cards, with the live viewer in an iframe
record.mjs boots a fresh server, seeds demo content, drives the storyboard
with Playwright while recording video, prints the raw webm path
ffmpeg re-encodes the raw webm to mp4 (h264) and webm (vp9)
Two halves live in record.mjs: reusable machinery (boot, seed, CSP strip,
stage resolution, browser/recording setup) and the storyboard — the scene
sequence, captions, and title cards. The storyboard is editorial content for
one video; rewrite it per video. As of this writing the checked-in reference
storyboard is the 0.13.0 release video.
Creating a video
Expect a run to take roughly the video's length plus ~20s boot; encoding adds a couple of minutes. Total output should stay near 40–60s.
# 0. one-time work-dir setup (fonts are optional but much nicer than DejaVu)
mkdir -p .video-work && cd .video-work
printf '{"name":"sideshow-video-work","private":true}\n' > package.json
npm i @fontsource-variable/inter @fontsource/jetbrains-mono
cd ..
# 1. record (server boot + seed + storyboard, prints the raw webm path)
node scripts/launch-video/record.mjs
# 2. encode both deliverables
cd .video-work
RAW=$(ls page@*.webm)
ffmpeg -y -i "$RAW" -vf "fps=30,format=yuv420p" \
-c:v libx264 -preset slow -crf 18 -movflags +faststart sideshow-X.Y.Z.mp4
ffmpeg -y -i "$RAW" -vf "fps=30,format=yuv420p" \
-c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 sideshow-X.Y.Z.webm
Choosing a recipe
- Full release: distill 3–5 user-visible headlines from the release's
CHANGELOG.mdsection (per-PR entries are too granular to shoot; confirm a non-obvious shortlist with the user). Lead with what sideshow is (the publish → live render → comment → revise loop) before the release-specific scenes — a social audience hasn't seen it before. End on an install card. - Single feature / PR: intro card → one or two scenes demonstrating the
change → outro card. Name the output after the feature
(
sideshow-0.13-sidebar-rail.mp4), keep it 15–30s, and keep the canonical release storyboard inrecord.mjsintact — do the trim as a local edit and revert, or copyrecord.mjsto a scratch sibling (imports keep working) and delete it after. - Custom: any scene set works — tutorials, comparisons, announcements. Whatever runs in the viewer can be driven: publish over the API mid-recording and the cards stream in on camera; that's the money shot, use it.
Storyboard model (record.mjs + stage.html)
- The driver calls
window.stageon the stage page between Playwright actions:stage("caption", html)swaps the lower-third (slide out/in),stage("card", html)shows a full-screen title card,stage("hideCard")fades it out. Pacing is plainsleep(ms)between beats. - Caption vocabulary:
<span class="badge">NEW</span>amber pill,<span class="hl">amber highlight,<span class="dim">muted. A NEW badge is a claim about this release — drop or move badges as features age. - Card vocabulary:
badge,h1(+span.verfor the amber version),sub,cmds/cmd(+span.pfor the prompt$),foot. - Interact with the viewer through
page.frameLocator("#app"). Useful selectors:aside .sess-title(session rows),.sidebar-toggle(collapse/expand rail),.card:not(#whatsNew)(post cards — the update-notes card is also a.card),button.act.comment(the icon-only trigger that unfolds the composer; it has no text, sogetByTextwon't find it), then.composer input. - Publish content via the HTTP API while recording:
POST /api/sessions,POST /api/posts({session, title, surfaces: [{kind, …}]}),PUT /api/posts/:idto revise (bumps the version pill live), andPOST /api/comments({surface: postId, author, text}) for agent replies. Seed background sessions before opening the page; save the live publishes for on-camera. - Target pacing: money shots hold 2.5–4s after content settles, transitions
~1s, typing at
pressSequentially(..., { delay: 34 }). Intro card ~3s, outro ~4s.
Environment gotchas (each cost real debugging time)
- The viewer refuses to be framed. Since 0.12.0 the viewer document sends
Content-Security-Policy: frame-ancestors 'self', so the file:// stage's iframe would be blocked.record.mjsstrips that header with a Playwright route on exactly the viewer document URL. Never widen the route pattern: intercepting/api/eventsbuffers the SSE stream forever and live updates stop. - The title-card overlay must keep
pointer-events: none. Playwright's hit-target check runs in the top frame; an overlay that intercepts clicks aimed into the app iframe times every action out — even actions "behind" an intro card. - Wait for iframes after a session switch. Opening a session re-renders
each sandboxed surface from
/s/:id; cut away too early and the last shot shows empty card shells.waitFora.card iframeand hold ~2–3s before the outro (this is also why "publish on camera" shots need their settle time). - Park the mouse after sidebar clicks (
page.mouse.move(...)toward the content) or the hovered session row shows its delete "×" in every following frame. - Sandbox Chromium/driver mismatch. In the Anthropic sandbox the pinned
@playwright/testmay expect a newer browser build than/opt/pw-browsersprovides;record.mjsfalls back to/opt/pw-browsers/chromium(override withCHROMIUM_PATH). Driving a slightly older Chromium works. - mp4 needs a real ffmpeg with libx264 (
apt-get install ffmpeg;brew install ffmpegon macOS) — Playwright's bundled ffmpeg records the raw webm (VP8) but cannot produce h264. Verify withffmpeg -encoders | grep -E 'libx264|libvpx-vp9'. - Give
.video-work/its ownpackage.jsonbeforenpm ior the fonts land in the repo'spackage.json(revert withgit checkout package.json package-lock.jsonif that happens). - The recorder spawns
server/index.tswithPORT=0andSIDESHOW_DBin the work dir — every run is a fresh workspace, nothing touches~/.sideshow. The version being recorded matcheslateston npm, so the#whatsNewupdate card stays away on its own; scope card selectors with:not(#whatsNew)anyway. - A failed run can orphan the spawned server; kill it before rerunning
(
pkill -f "[s]erver/index.ts").
Content accuracy
- Verify install commands against reality:
npm view sideshow dist-tags. The outro card's commands arenpm i -g sideshow+sideshow serve --open(ornpx sideshow serve --open) — check they still match the README. - Perf/number claims must come from the changelog entry, phrased no stronger ("up to 95% lighter" for the 0.13.0 hydrate change, not "95% faster").
- The window chrome's URL pill shows the real server host:port — decorative
but it must not lie;
record.mjsfills it from the actual base URL. - Demo content is
bin/demoData.js(thesideshow demosessions) — label anything invented beyond it honestly, and keep agent names real (claude-code,pi). - The video is silent — never imply audio in the video or announcement copy.
Verification and delivery
- After encoding, extract spot frames at each scene boundary and mid-scene:
ffmpeg -y -ss <t> -i sideshow-X.Y.Z.mp4 -frames:v 1 check.png(Read renders PNGs). Look specifically for: blank surface iframes (cut too early), hover artifacts in the sidebar, captions overlapping scene changes, and the intro/outro cards fully faded. Check duration withffprobe -show_entries format=duration. - Outputs land in
.video-work/(gitignored — never commit videos or the raw recording). Send both files to the user (mp4: social/Slack; webm: web embeds), report duration and sizes, and flag if the mp4 exceeds ~10 MB (Slack) or ~15 MB (X). Copy elsewhere only if the user names a destination.