# Rendering a Demo Movie (browser-composited) Turn a real, running app into a short titled/captioned demo `.mp4` whose frames are genuine screenshots of the product — not mockups — and verify the output is actually correct before handing it over. Needs a running instance of the app, a browser-automation tool that can navigate, run JS (`eval`), set a viewport, and screenshot to a path, plus `ffmpeg`/`ffprobe`, and a scratch dir such as `/tmp/app-movie/`. ## Step 1 — capture real scene frames from the live app Set a fixed viewport, then per scene: navigate/interact via JS to compose the shot, screenshot to `frame-NN.png`, and **read the PNG back to confirm** the shot is what you intended. No fixed fps — one deliberate screenshot per scene beat. ``` use_browser: {"action":"navigate","payload":"http://localhost:/"} use_browser: {"action":"screenshot","payload":{"path":"/tmp/app-movie/frame-01.png"}} # ...navigate/eval to set up each subsequent scene, screenshot frame-02..frame-NN ``` ## Step 2 — composite title/caption/end cards in the browser Prefer this over ffmpeg `drawtext`, which is fragile: on macOS-under-sandbox, `textfile=` reliably fails with `Either text, a valid file, a timecode or text source must be provided` (even with absolute paths), while a trivial inline `text=Foo` may work. Don't fight it. Render cards as HTML and screenshot them — you also get real fonts, `` accents, and CSS layout for free. `card.html` (param-driven: title / end / image+caption-bar): ```html ``` Drive it (name cards so a lexical glob orders them title → scenes → end: `card-00` … `card-07` … `card-99`): ``` use_browser: {"action":"set_viewport","payload":{"width":1400,"height":960}} use_browser: {"action":"navigate","payload":"file:///tmp/app-movie/card.html?mode=title"} use_browser: {"action":"screenshot","payload":{"path":"/tmp/app-movie/card-00.png"}} # per scene: define a helper once, then swap innerHTML and screenshot: use_browser: {"action":"eval","payload":"window.__setCard=(img,cap)=>{document.body.innerHTML='
'+cap+'
';return img;}; __setCard('frame-01.png','The scene resolves — it lands in New state')"} use_browser: {"action":"screenshot","payload":{"path":"/tmp/app-movie/card-01.png"}} # ...repeat __setCard + screenshot for frame-02..frame-07 -> card-02..card-07 use_browser: {"action":"navigate","payload":"file:///tmp/app-movie/card.html?mode=end"} use_browser: {"action":"screenshot","payload":{"path":"/tmp/app-movie/card-99.png"}} ``` ## Step 3 — concatenate the cards Pure image concat, no drawtext. `-framerate 1/3` holds each card 3 seconds; the `card-*` glob orders them. ```bash cd /tmp/app-movie && \ ffmpeg -y -loglevel error -framerate 1/3 -pattern_type glob -i 'card-*.png' \ -vf "scale=1400:960" -r 30 -pix_fmt yuv420p ~/Desktop/app-demo.mp4 && \ ffprobe -v error -show_entries format=duration -of csv=p=0 ~/Desktop/app-demo.mp4 # 9 cards -> 27.000000 ``` ## Step 4 — verify the artifact (do not skip) Extract a mid-movie frame and actually look at it; duration/size are necessary but not sufficient. This is the step that catches a scene screenshotted mid-scroll (half-blank) before it ships. ```bash ffmpeg -y -loglevel error -ss 13 -i ~/Desktop/app-demo.mp4 -frames:v 1 /tmp/app-movie/check.png # then Read check.png; if a scene is wrong, re-capture just that frame-NN, # recompose its card-NN.png, and re-run Step 3. ``` ## If you must use ffmpeg drawtext (failed under sandbox — kept for reference) This is the approach that **FAILED** under macOS sandbox (`textfile=` unreadable). Inline `text=` may still work for short labels; per-scene captions letterbox the shot and draw text into the padding: ```bash FONT=/System/Library/Fonts/Helvetica.ttc # title card (lavfi solid color + two inline drawtext) ffmpeg -y -loglevel error -f lavfi -i "color=c=0xfaf8f4:s=1400x960:d=3" \ -vf "drawtext=fontfile=$FONT:text='App Name':fontsize=110:fontcolor=0xb3422f:x=(w-text_w)/2:y=360,drawtext=fontfile=$FONT:text='one-line tagline':fontsize=42:fontcolor=0x44403a:x=(w-text_w)/2:y=510" \ -r 30 -pix_fmt yuv420p seg-00.mp4 # a captioned scene: scale to 1400x900, pad 60px dark bar, caption in the bar ffmpeg -y -loglevel error -loop 1 -i frame-01.png -t 3 \ -vf "scale=1400:900,pad=1400:960:0:0:color=0x2a2722,drawtext=fontfile=$FONT:text='caption text':fontsize=30:fontcolor=0xfaf8f4:x=(w-text_w)/2:y=918" \ -r 30 -pix_fmt yuv420p seg-01.mp4 # concat demuxer for f in seg-*.mp4; do echo "file '$f'"; done > list.txt ffmpeg -y -loglevel error -f concat -safe 0 -i list.txt -c copy ~/Desktop/app-demo.mp4 ``` ## Why the browser-composited path wins - Real product screenshots as scenes are unfakeable — an honest "show it off." - No dependency on ffmpeg font rendering, the flaky part; cards get real fonts, rich markup (`` accents), and CSS layout. - Deterministic ordering via zero-padded `card-NN.png` filenames plus glob. - The extract-a-frame-and-read-it check in Step 4 is the honesty gate: it is how a bad frame gets caught instead of shipped.