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.

KindPosture
walkthroughThe default. Full craft: title card, camera moves, captions, chapters, end card. For docs and onboarding.
demoTighter and more confident. Fewer explanatory beats, for a product tour.
bug-reproLiteral 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-afterFilms the same flow against app.url and app.compareUrl.
releaseRelease-note pacing — brisk, assumes the viewer knows the product.
socialBuilt for autoplay without sound; pairs naturally with aspect: "9:16".

Aspect ratios#

aspectCapture viewportOutput
16:91280×7201920×1080
9:16720×12801080×1920
1:1900×9001080×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.mp4

Pacing#

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.

SettingEffect
pacing: relaxedLonger holds, gentler trimming. Teaching material.
pacing: normalThe default.
pacing: briskShorter holds, harder trimming. Release notes.
dwellScale: 1.25A multiplier (0.5–4) on top of the preset. The finer knob when the preset is nearly right.
Narration always wins. With 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.cameraBehaviour
normalDefault. Frames each action, pushing in where it helps.
tightPushes in harder. Good for dense UI and small controls.
wideKeeps 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
ValueMeaning
targetFrame the element being acted on. The implicit default.
wideKeep the whole screen for this one step — a navigation, a page-level change, or a dialog too large for any close-up.
a locatorFrame that element as the payoff, once it appears.
Conflicting hints make the planner give up. If it cannot satisfy a framing request it falls back to a wide shot and records the fallback in 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