# Demo MP4 recorder

Produces a shareable broadcast demo video — the live page (Cesium photoreal world, J/80 boats, HUD)
recorded headless, with the localized **commentary** and **race-clock beeps** muxed in — as
`demo_<lang>.mp4`.

```bash
# from v3/broadcast/
python demo/record/record_demo.py --lang en
python demo/record/record_demo.py --lang zh
python demo/record/record_demo.py --lang sv
```

One-time setup:

```bash
cd demo/record && npm install      # puppeteer-core (drives your installed Chrome)
```

Also needs **ffmpeg** on `PATH` and **Google Chrome** (auto-detected on Windows/macOS/Linux, or set
`BROADCAST_CHROME=/path/to/chrome`). The clips must exist for the language — run
`live/tts/synth.py` first if you've changed the feed (`demo/data/races/real.json`) or added a
language.

## What it does

1. **Audio** (`build_audio.py`) — lays the commentary on the real race-time line, replicating the
   page's speech queue (one line at a time, never overlapping, latest-2 backlog), then mixes the
   race-clock beeps (per-second countdown, a one-second gun tone, 220 ms on 2:00/1:00).
2. **Capture** — two modes:
   - **`frames`** (default, `capture_frames.mjs`) — drives the page **one frame at a time** via a
     `?capture=1` hook (`window.__cap.step()`), screenshots each frame to a JPEG, and **checkpoints**
     the director + camera state to disk every 30 frames. If the GPU-heavy headless Chrome crashes,
     it relaunches, restores the checkpoint, and **resumes from the next frame** — a crash costs ≤30
     frames, never the whole take. Robust under GPU contention; frames are real-time paced (no resync).
   - **`screencast`** (legacy, `capture.mjs`) — one continuous `page.screencast`; retries the *whole*
     run on a crash, and the fixed-fps stretch is undone in step 3.
3. **Assemble / resync + mux** — `frames`: ffmpeg concatenates the JPEGs at `--fps` and muxes the
   audio straight in. `screencast`: rescales the stretched timeline back to real time, then muxes —
   into `demo_<lang>.mp4`.

## Useful flags

| flag | default | notes |
|------|---------|-------|
| `--lang` | `en` | `en`, `sv`, `zh` (any language with clips + a UI block) |
| `--mode` | `frames` | `frames` = deterministic, resumable per-frame capture (robust under GPU load); `screencast` = legacy continuous capture |
| `--speed` | `1.0` | screencast only: capture faster than real time (output stays real-time); cuts crash exposure |
| `--keep-frames` | – | keep the intermediate JPEG frames (frames mode) |
| `--res` | `1920x1080` | output resolution |
| `--fps` | `30` | output frame rate |
| `--crf` | `18` | x264 quality (lower = better/bigger) |
| `--tail` | `24` | seconds kept past the winner call |
| `--use-webm PATH` | – | skip capture; just resync+mux an existing `.webm` (fast re-mux) |
| `--keep-webm` | – | keep the intermediate capture |
| `--out PATH` | `_cap/demo_<lang>.mp4` | output path |

Outputs land in `v3/broadcast/_cap/` (gitignored scratch). Resync is automatic to ≈±2 s; the audio's
key beats (gun, winner) are placed on the real race-time line.
