Outputs
A single tape can write several artifacts from the same captured terminal session. This is useful when one run should produce a README GIF, a final-frame PNG for quick inspection, and state JSON for snapshot tests.
See the Tape reference for extension validation and the exact command syntax.
See Generated media storage for guidance on keeping rendered media out of source control and publishing stable documentation assets.
| Output | Command example | Written when | Encoder |
|---|---|---|---|
| WebP animation | Output demo.webp |
After the tape finishes | In process |
| WebP screenshot | Screenshot checkpoint.webp |
At that command | In process |
| GIF | Output demo.gif |
After the tape finishes | In process |
| PNG final frame | Output demo.png |
After the tape finishes | In process |
| State JSON | Output demo.json |
After the tape finishes | In process |
| Text snapshots | Output demo.txt or demo.ascii |
After the tape finishes | In process |
| MP4 | Output demo.mp4 |
After the tape finishes | ffmpeg |
| WebM | Output demo.webm |
After the tape finishes | ffmpeg |
| Frame directory | Output target/frames |
After the tape finishes | In process PNGs |
| Screenshot | Screenshot checkpoint.png |
At that command | In process |
| Checkpoint state | State checkpoint.json |
At that command | In process |
Choosing outputs
Section titled “Choosing outputs”Use GIF for documentation and release notes. Use PNG screenshots when a human needs to review a
single stable state. Use state JSON when a test should compare text,
scrollback, cursor metadata, and styles without doing pixel comparisons. Use .txt or .ascii to
keep VHS golden-file tests: they append the plain screen text after every shown command.
Use Presentation overlays when generated visual media should explain a workflow step or review checkpoint without changing terminal behavior. Captions render on visual outputs only; they do not appear in state JSON. Captions share a bottom presentation row with keyboard overlay chips when both are active.
Frame directories are useful when debugging capture timing or feeding rendered frames to custom
tooling. The path must not have an extension; Output target/frames writes numbered PNG files under
that directory.
Validation and review artifacts
Section titled “Validation and review artifacts”Use State checkpoints or final state JSON for automated assertions. Add screenshots to help
debug failures.
For review, pair a GIF or video with a screenshot so readers can inspect a frame without pausing the animation. Name the screenshot after the state it shows and capture it after a wait:
Wait+Screen "Profile saved"State target/snapshots/profile-saved.jsonScreenshot target/review/profile-saved.pngSleep 2sTo give viewers time to read, try 300-700 ms after simple transitions and 1.5-2.5 seconds for a screen of short output. Reserve 4-5 second pauses for the final screen. Allow more time for dense output.
What a recording captures
Section titled “What a recording captures”Betamax samples the terminal at the Framerate while the tape runs, the way a terminal redraws its
window, and each frame shows the screen as it is at that moment. A frame can catch a program partway
through a redraw: half the screen updated, or an empty image area between deleting an image and
sending its replacement. A terminal shows those frames too, so Betamax keeps them; they are how
flicker and tearing appear in a recording. Betamax never waits for a program to stop writing before
it captures a frame, so a program that animates continuously records at the full rate.
Programs avoid partial frames with synchronized output (DEC private mode 2026): they mark where each
update starts and ends, and the terminal keeps showing the previous frame until the update ends.
Recordings do the same and capture the new frame as soon as the update ends. As in Ghostty, an
update that never ends stops being held after one second. To record clean frames from your own
program, wrap each redraw in ESC [ ? 2026 h and ESC [ ? 2026 l.
GIF encoding
Section titled “GIF encoding”GIF stores each frame’s duration in hundredths of a second, and browsers play a frame of 10 ms or
less as 100 ms. Betamax therefore keeps every GIF frame at least 20 ms long, which caps GIF at 50
FPS, the default Framerate. When a higher Framerate or fast typing captures a state that would
show for less than 20 ms, the GIF drops it and shows the next state for the combined time. The
total duration still follows the capture, within 10 ms. WebP, video, and frame directories keep
every captured state.
WebP encoding
Section titled “WebP encoding”Use Output demo.webp for an infinitely looping lossless animation. Use Screenshot ready.webp
for a static image, including at the end of the tape for a final screenshot. CLI
betamax run demo.tape --output demo.webp adds an animated output.
Lossless WebP uses the original RGBA frames, preserving colors and transparency without GIF palette
quantization. After the first frame, each frame stores only the region that changed, so typing
costs a small rectangle per keystroke. No ffmpeg, cwebp, or system WebP library is needed.
Inline images, plots, and gradients make lossless frames large. Set WebpQuality 85 switches
animated WebP to lossy frames encoded by a bundled libwebp, which is usually several times smaller.
Set WebpQuality Lossless switches back; screenshots stay lossless. Keep the quality at 85 or
higher for terminal text: below that, libwebp treats small changes as unchanged, and erased dim text
can leave faint ghosts.
GitHub shows images up to 10 MB. A 23-second 1200×720 recording with an inline image animation
measured 22.7 MB as lossless WebP, 9.1 MB as GIF, and 5.2 MB with Set WebpQuality 85. To shrink
further, lower Framerate, reduce Width and Height, or shorten long holds.
Captured holds, including the final frame, are stored with millisecond precision. Cumulative
boundaries round to the nearest millisecond, with a minimum of 1 ms per frame. Viewers may lengthen
very short delays (especially 10 ms or less), so use normal presentation pacing for browser demos.
A single-frame animation retains its hold, and is written losslessly even when WebpQuality is set.
Dimensions must be 1–16384 pixels on each axis, each frame hold must fit in 16,777,215 ms (about
4.66 hours), and the file must be smaller than 4 GiB.
See examples/webp.tape for
animated and static output from the same capture, and
examples/webp-lossy.tape
for lossy output.
Video encoding
Section titled “Video encoding”Betamax encodes MP4 and WebM with ffmpeg. Terminal execution, terminal parsing, rendering,
GIF/WebP encoding, PNG encoding, and state JSON writing are in process. Betamax only requires
ffmpeg when the tape requests .mp4 or .webm.
Video uses the original raster frames and their captured hold times, including the final frame.
Framerate selects the constant output cadence: transitions and total duration are rounded to the
nearest frame interval, with at least one output frame. States shorter than one interval may be
omitted. Rates below 1 FPS are clamped to 1 FPS; video rates use three decimal places. GIF uses
hundredths of a second for each delay, so its duration can differ slightly from video.
Install it with:
# macOSbrew install ffmpeg
# Debian/Ubuntusudo apt-get updatesudo apt-get install ffmpegUse Require ffmpeg if the tape should fail
before doing any other work:
Output examples/output/demo.mp4Require ffmpegCheckpoints
Section titled “Checkpoints”Checkpoint outputs happen in the middle of a run. They are the right tool for tests and for documenting multi-step workflows.
Type "my-app --demo"EnterWait+Screen "Ready"Screenshot target/snapshots/ready.pngState target/snapshots/ready.jsonThe Screenshot preserves the decorated
visual frame. The State JSON preserves terminal
text and styles in a compact comparison format.
Checkpoints capture one frame after the command, whether or not the program has finished drawing, so put a wait before them when the screen must show something specific. See checkpoint timing.
Captions
Section titled “Captions”Captions annotate rendered media without changing the terminal session. They are useful when a GIF or screenshot needs to explain hidden setup, a review checkpoint, or a step in a TUI flow. See Presentation overlays for keyboard chips and placement tradeoffs. Use waits and state outputs for assertions. Captions only label the recording.
When a tape uses captions, Betamax reserves a bottom presentation row before deriving the terminal
grid. Captions render below the terminal canvas instead of covering terminal rows. They are
left-aligned with the terminal frame edge and remain single-line; if a caption needs to share space
with right-aligned keyboard overlay chips, Betamax truncates it with ... instead of wrapping.
Caption glyphs are clipped to their reserved width as a final guard for font fallback and unusually
wide characters.
Caption "Step 1: prepare the review frame"HideType "my-app --demo"EnterWait+Screen "Ready"ShowSleep 700ms
Caption "Step 2: capture the ready state"Screenshot target/snapshots/ready.pngState target/snapshots/ready.json
Caption ""Sleep 300msThe caption remains active until the next Caption command. Use Caption "" before later frames
that should not have an overlay. Quote caption text that contains spaces.
Caption has no duration and does not capture a frame by itself. It changes the caption used by
the next rendered visual frame: animation frames captured during later Sleep, Wait, typing,
keys, Show, or final-frame output, and checkpoint screenshots captured by Screenshot. If the
terminal is already stable and only the caption changed, add Sleep after Caption so GIF and
video outputs hold that caption for a visible amount of time.
Captions render on visual outputs: GIF, WebP, PNG, MP4, WebM, frame directories, and screenshots. They do not appear in state JSON or change final output dimensions.