Your demo video is a build artifact.
It changes when your product does, or the build fails.
Demos go stale.
The UI moves and the video does not. Nobody re-records it, because re-recording means blocking out an afternoon, finding a quiet room and getting the mouse path right on the fourth take. So the demo quietly lies about the product, and it keeps lying until someone is embarrassed enough to book that afternoon.
How it works
Write the take down. Run it in CI.
A take is a newline-delimited JSON script of ops. It lives in the repo, next to the code it films, so it is reviewed and versioned like the code it films.
examples/demo.jsonl
{"op":"navigate","url":"http://127.0.0.1:8099/"}
{"op":"wait","selector":"#q","timeout_ms":30000}
{"op":"start_recording"}
{"op":"wait","ms":2200}
{"op":"mark","label":"start"}
{"op":"type","selector":"#q","text":"14 Rue Lafayette, Paris"}
{"op":"wait","ms":500}
{"op":"click","selector":"#go"}
{"op":"wait","selector":"li","timeout_ms":15000}
{"op":"wait","ms":1400}
{"op":"mark","label":"tracked"}
{"op":"type","selector":"#q","text":"Kigali Heights, KG 7 Ave"}
{"op":"wait","ms":500}
{"op":"click","selector":"#go"}
{"op":"wait","ms":1800}
{"op":"mark","label":"second"}
{"op":"wait","ms":1600}
{"op":"stop_recording"}
Every op carries a timestamp and a bounding box, and the zoom is derived from them. You do not describe camera moves. You describe what happens.
.github/workflows/demo.yml
name: demo
on:
push:
branches: [main]
paths:
- src/**
- examples/**
- .github/workflows/demo.yml
jobs:
record:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Serve the app
run: |
python3 -m http.server 8099 --directory examples &
for _ in $(seq 1 40); do
curl -sf http://127.0.0.1:8099/ >/dev/null && break
sleep 0.25
done
- name: Record
uses: thisisisheanesu/kaviri@v1
with:
script: examples/demo.jsonl
out: demo.mp4
preset: readme
artifact-name: demo
The action installs ffmpeg and a Chromium if the runner has none, builds the recorder and
uploads the MP4 as a workflow artifact. preset sets the viewport, the capture
scale and the output size together: readme, tiktok,
square, landscape, phone.
or locally, with no CI at all
cargo build --release
./target/release/kaviri record --script examples/demo.jsonl --out demo.mp4
The part that matters
The build fails if the take filmed nothing.
A recorder that writes a valid MP4 of a Chrome error page has not failed in any way your CI can see. Thirty seconds of a frozen dev-server timeout is still thirty seconds of video, and it will sail past a duration check. So kaviri's own workflow compares the first frame against the last one. A script that navigates, clicks and types cannot produce the same picture twice.
.github/workflows/demo.yml, the check that turns the job red
ffmpeg -nostdin -v error -y -ss 0.5 -i "$OUT" -frames:v 1 "$RUNNER_TEMP/first.png"
ffmpeg -nostdin -v error -y -sseof -0.5 -i "$OUT" -frames:v 1 "$RUNNER_TEMP/last.png"
ffmpeg -nostdin -v error \
-i "$RUNNER_TEMP/first.png" -i "$RUNNER_TEMP/last.png" \
-lavfi "psnr=stats_file=$RUNNER_TEMP/psnr.log" -f null -
psnr=$(sed -n 's/.*psnr_avg:\([^ ]*\).*/\1/p' "$RUNNER_TEMP/psnr.log" | head -1)
# psnr_avg is the literal "inf" for two identical frames, so that case is decided here
# rather than left to awk's coercion.
if [ -z "$psnr" ] || [ "$psnr" = "inf" ]; then
echo "the first and last frames are identical; the take filmed nothing"
exit 1
fi
# Anything above ~50dB is indistinguishable by eye, which for a demo that navigates,
# clicks and types means the page never changed.
awk -v p="$psnr" 'BEGIN { exit (p + 0 < 50.0) ? 0 : 1 }' \
|| { echo "the first and last frames are the same picture; the take filmed nothing"; exit 1; }
the first and last frames are the same picture; the take filmed nothing
That line in a pull request is the whole idea. The demo is not a file someone remembers to refresh. It is a check that goes red.
Local and hosted
The recorder is free. The queue is the product.
Local
Free forever. Apache 2.0. No account.
- One Rust binary, Linux and macOS
- Brings its own browser over CDP, no screen recorder, no Wayland portals
- Runs on your laptop or in your own CI
- Your pages never leave your machine
Hosted
The same recorder, with the parts you would otherwise build.
- A queue, so a take does not occupy a CI minute
- Storage and a stable URL you can link from a README or a landing page
- Every take kept, so you can see what the demo looked like three releases ago
Pricing is coming. Join the waitlist and you will hear the number before anyone is asked to pay it.
Open source
Apache 2.0, unconditionally.
The recorder is the whole product. Everything that makes the video is in the repo under Apache 2.0, with no revenue threshold, no delayed-open clause and no field-of-use limit. Fork it, ship it inside your own tool, sell the result. The hosted service is a convenience, and the only thing the licence holds back is the name.