Output & framing
Presentation is a compose-time decision, so all of this can change without going near your app again. Pick a kind first — it sets the posture — then tune.
Output kinds#
output.kind is the highest-leverage field in the spec. It is not a preset name; it changes what the video is for.
| Kind | Posture |
|---|---|
walkthrough | The default. Full craft: title card, camera moves, captions, chapters, end card. For docs and onboarding. |
demo | Tighter and more confident. Fewer explanatory beats, for a product tour. |
bug-repro | Literal screen evidence: a running clock and console feed on the footage, then a report card (not a brand slide) and bug-report.md with URL, steps and errors. Testimony, not marketing. |
before-after | Films the same flow against app.url and app.compareUrl. |
release | Release-note pacing — brisk, assumes the viewer knows the product. |
social | Built for autoplay without sound; pairs naturally with aspect: "9:16". |
Aspect ratios#
| aspect | Capture viewport | Output |
|---|---|---|
16:9 | 1280×720 | 1920×1080 |
9:16 | 720×1280 | 1080×1920 |
1:1 | 900×900 | 1080×1080 |
The capture viewport follows the aspect unless you set app.viewport explicitly, so a portrait video is filmed at a portrait viewport and your app reflows natively. Nothing is cropped to fit.
You can also render extra aspects from a capture you already have — the footage becomes a correctly-proportioned card on the new canvas:
playhead compose out/capture --aspects "9:16,1:1"
→ out-9x16.mp4, out-1x1.mp4Pacing#
Playhead does not play your capture back at real speed. It trims idle time, holds after each action long enough to register, and enforces a minimum step duration so nothing is too fast to follow — the pacing check proves the result.
| Setting | Effect |
|---|---|
pacing: relaxed | Longer holds, gentler trimming. Teaching material. |
pacing: normal | The default. |
pacing: brisk | Shorter holds, harder trimming. Release notes. |
dwellScale: 1.25 | A multiplier (0.5–4) on top of the preset. The finer knob when the preset is nearly right. |
narration: tts, pacing grows so every spoken line finishes inside its own step. A long narration line lengthens the video rather than being cut off — which is why a 47-second capture can become a 116-second walkthrough.The camera#
The camera path is computed from the bounding boxes measured during capture — never from image analysis. Zoom is quantized to a few named levels so the movement reads as intentional, and the planner will not choose a framing that crops the target it is meant to be showing.
The visible window is width-anchored to the output aspect, so a tall page pans instead of distorting.
| output.camera | Behaviour |
|---|---|
normal | Default. Frames each action, pushing in where it helps. |
tight | Pushes in harder. Good for dense UI and small controls. |
wide | Keeps the whole screen in view: context over close-up. This waives the close-up legibility floor (a clicked target rendering at least 28px tall) for the whole video. |
focus — the act-then-react shot#
By default the camera frames the element being acted on. Sometimes the interesting thing happens somewhere else: you click Create in a dialog and a toast appears in the corner. Naming that second place produces a shot that holds the action, then moves to the payoff.
- click: 'role=button[name="Create user"]'
focus: 'text="User added"' # act here, react there
caption: Create the account| Value | Meaning |
|---|---|
target | Frame the element being acted on. The implicit default. |
wide | Keep the whole screen for this one step — a navigation, a page-level change, or a dialog too large for any close-up. |
a locator | Frame that element as the payoff, once it appears. |
compose-manifest.json under diagnostics.cameraFallbacks. Repeated fallbacks exhaust its attempt budget and you will see “camera planner exhausted its attempt budget — all shots forced wide”. The fix is usually to say focus: wide explicitly on the step that cannot be framed, rather than making it discover that per frame.shot — where the cuts fall#
Scenes are narrative; shots are cinematography. shot: cut forces a step into its own shot and shot: continue merges it into the previous one, so you can keep a two-step sequence in a single continuous move without restructuring your scenes.
Captions and overflow#
Captions are numbered, laid out, and measured. If text will not fit its card at full size the compositor records the fact in diagnostics.overflows and the captions check reports it — so “my caption got clipped” is caught by the pipeline instead of by a viewer. Write the long version in narration, which has no size limit, and keep caption short.
The cursor#
The cursor is not filmed; it is re-synthesized at compose time from the event log, with eased movement and a ripple at each click beat. That is why it is always exactly where the click landed, and why the cursor-on-target check can verify it against the delivered pixels. How that is checked →
Extra deliverables#
playhead compose out/capture --gif 960 # embeddable GIF, the PR-comment format
playhead compose out/capture --watch # re-compose on every save
playhead compose out/capture --expect-sha 4623a8d7… # assert determinism