Making kinhin's demo video
How kinhin makes its own launch video and demos: a debug build replays a scripted fleet (jobs arrive, instances clone, boot, run and are deleted, one fails and its retry passes) while you screen-record the real app. This page is for people working on kinhin. To make videos of your own product, see Add videos to your project: the project file, scene types, adapters, captions, music and the .kinhin/videos/ folder are documented there.
← Home · Contributing · The app · Add videos to your project
The pieces
A video is a list of scenes (see The project file). Each scene is either a canvas scene drawn by Motion Canvas (title, statement, queue, parallel, map, outro) or a clip scene around a recording of your product: its app, a terminal, or a browser session. Canvas text wraps on whole words and shrinks to fit its box, and each slide has a character limit (title 40, subtitle 60, statement 90, sub 80, outro line 90, url 60, facts 100 joined, steps 60 each, compare titles 40 and items 60); a longer one fails the build and names the prop. Everything that is specific to one product lives in an adapter (video/adapters/<name>/ in this repository); the pipeline never mentions a product. Browser clips need no Mac, no desktop session and no permission grant, so they are the easy path for anything that runs in a browser (a device scene with "frame": "window" and a clip recorded by record-web.sh).
| Piece | What it is | Where |
|---|---|---|
| Adapter | One product's recorders, tape and committed clips; kinhin's is video/adapters/kinhin/, a starting point is adapters/template/ | video/adapters/<name>/ |
| Demo fleet (kinhin) | A debug-only FleetServing that replays a script; touches no config, Keychain, daemon or forge | Sources/kinhin-app/Demo/ |
| App clip | A screen recording of the product's window, from the adapter's record-app.sh (kinhin's: kinhin video record on a Mac, or in a throwaway Tart VM) | video/adapters/kinhin/record-app.sh, record-app-in-tart.sh |
| Browser clip | Scripted Playwright session, in a container or locally; an adapter clip of kind browser | video/sources/web/, video/adapters/<name>/scenarios/, video/scripts/record-web.sh |
| Terminal clip | A VHS tape, recorded in four variants by scripts/record-terminal.sh (kinhin's runs the real CLI from a debug build in a throwaway support dir and hides the empty-config lines) | video/adapters/kinhin/terminal/*.tape |
| Canvas scenes | Motion Canvas (MIT), themed and sized by the project file | video/src/ |
| Build | Voice, retime, render, frame, mix, captions | video/scripts/build.sh |
Motion Canvas rather than Remotion because Remotion's license requires a paid company license above three employees. Run video/scripts/doctor.sh to see which tools this machine is missing.
Screenshots as a scene
A device scene can show still screenshots instead of a recording. Put the PNG paths in props.screens (relative to video/), and the scene becomes a cross-fading slideshow of its own length. Each image is held for props.hold seconds (default 2.5):
{
"id": "screens",
"type": "device",
"props": {
"screens": ["screens/mobile/en-US/iPhone 17e-01-home.png", "screens/mobile/fr-FR/iPhone 17e-01-home.png"],
"hold": 3.5
},
"minLength": 8,
"narration": "Store screenshots from the same repo, in every language."
}{platform} in a path becomes the platform of the look (--platform ios|android, default ios), so one project can show either platform's screenshots. The screens sit in a phone frame drawn by ffmpeg (props.frame: iphone, android or none; it follows the platform by default). The frame has no device artwork, so it is safe to publish.
It is opt-in: a scene without props.screens is built as before. A project that uses screenshots does not need an adapter for that scene. The slideshow code is video/scripts/slideshow.mjs, called from video/scripts/frames.mjs.
Run the demo app (the kinhin adapter)
KINHIN_SUPPORT_DIR=$(mktemp -d) KINHIN_DEMO=launch scripts/dev-run.sh --env-tokensKINHIN_DEMO takes a built-in script name (launch) or the path to a JSON script. It only works in debug builds, so a release build can never be switched into demo mode. The temp support directory and --env-tokens keep the run away from your real config and Keychain.
Write a demo script
A script is a looping timeline. Copy video/adapters/kinhin/demos/example.json and edit it:
{
"period": 30,
"capacity": 2,
"jobs": [
{ "repo": "acme/api", "account": "github", "runtime": "docker", "workflow": "CI",
"arrive": 1, "start": 2, "run": 8, "fails": false }
]
}period: seconds before the script loops.capacity: concurrent instance slots shown.arrive: when the job enters the queue.start: when a slot claims it (keepstartat or afterarrive; a gap is time spent queued).run: seconds the job is busy.fails(optional): the instance goesfailed, raises an alert, then is deleted.accountis a forge id (github,gitlab,gitea,forgejo).runtimeistart,appleContainer,dockerorhost; it sets how long clone, boot and registration take.- The app rejects a script that cannot play (job ends after the loop,
startbeforearrive, unknown forge) and logs why. Keep concurrent instances at or belowcapacity.
Built-in scripts live in DemoTimeline.builtIn; add one there when a script should ship with the debug app.
Record
On a Mac with a desktop: video/adapters/kinhin/record-app.sh (needs Screen Recording permission for your terminal once, and Accessibility if you want it to resize the window). Run it directly in a terminal: piping its output (| tail) removes the TTY that scripts/dev-run.sh insists on. The debug app's process and window are named kinhin-app.
No spare desktop, or you want a clean repeatable one? video/adapters/kinhin/record-app-in-tart.sh records inside a throwaway Tart VM. The guest runs the same record-app.sh, which captures the window with kinhin video record, so the pointer is left out of the take whichever mode you use; the guest needs Screen Recording granted once.
--gui: a VM window on your Mac; the guest has a real desktop session.--vnc: the same desktop with no window; connect with Screen Sharing.--headless: no desktop, so no recording. It runs the demo unit tests and renders the offscreen UI captures (shots/capture) instead.
Both set KINHIN_DEMO the same way (KINHIN_DEMO=path/in/repo.json video/adapters/kinhin/record-app.sh). The pointer is left out of the take, so no cursor smoothing is needed; for auto-zoom, run the raw take through an editor such as OpenScreen or Screenize.
Build the launch video
video/scripts/build.sh --container # video/script/launch.json, in a container
video/scripts/build.sh --container --fast # a preview
video/scripts/build.sh --container --quality high # slower, larger encode
video/scripts/build.sh --project examples/kinhin-30s --container # the 30-second cutlaunch and examples/kinhin-30s use the kinhin adapter (video/adapters/kinhin/adapter.json), which sets two things for every kinhin video: burned-in captions are on in the 9:16 and 1:1 formats and off in 16:9 (a clean picture; the .srt is still written), and the music is First Light Particles. A project can override either ("captions": {"burn": true}, "music": false). How the build works, and every setting, is in Video config reference.
build.sh is the developer path: it builds from this working tree, in a container (--container) or on this machine. What users run is kinhin video build, which pulls a published image that has this video/ folder inside it (Dockerfile's last layer) and runs scripts/container-build.sh against their repository. To try that flow against your working tree:
docker build -t kinhin-video:local video/ # or: container build -t kinhin-video:local "$PWD/video"
cd /path/to/some/repo && kinhin video build launch --image kinhin-video:localBoth runtimes build it. With Apple's container use an absolute build path that is not under a symlink (not /tmp or /var/folders, which are symlinks on macOS): before container 1.5.0 (checked: 1.5.0 builds it correctly from a /var/folders path), container build silently copies no files from a directory when the context path has a symlinked parent (apple/container#2285), so COPY . /work yields an empty suite. scripts/build.sh --container resolves the path for you. The image is published to ghcr.io/rabinapps/kinhin-video by .github/workflows/video-image.yml: every pull request that touches video/ builds it and runs video/scripts/image-smoke.sh (a real video built and verified inside the image; publishes nothing), a push to develop and a weekly rebuild publish :edge, and a v* tag publishes :<version> (and :latest unless it is a prerelease). To run the same check yourself:
docker build -t kinhin-video:local video/ && video/scripts/image-smoke.sh kinhin-video:localThe kinhin launch video's music is First Light Particles by Yoiyami (CC0: no credit needed, anyone may reuse it), baked into the image from video/music/catalog.json. Use only CC0 tracks in kinhin's own videos, and do not commit tracks under licences that forbid redistributing the file on its own (Pixabay's does).
Recording the app
kinhin's record-app.sh starts the demo at a known moment (KINHIN_DEMO_ORIGIN) and writes app.json; the device scene starts the shot there. The recorder leaves the pointer out of the clip, so the mouse can move during a take. It also writes app-pointer.json (where the pointer was and where it clicked), which a video's autoZoom follows: click where the camera should go while the take runs, since a take with no clicks gives no zoom. After recording, scripts/make-reference.sh --adapter <name> refreshes the adapter's reference/ clips (--terminal for just the terminal ones).
How the build works
For the curious: these are the steps kinhin video build runs inside the image.
scripts/voice.mjsrenders each cue of the narration.scripts/retime.mjsmakes each scene as long as its speech needs and writessrc/timing.ts,out/.work/timeline.jsonandout/.work/<slug>.captions.srt(out/.work/is scratch; the container build removes it when it finishes).scripts/render-titles.mjsrenders every drawn scene headlessly (Playwright's Chromium) toraw/scenes/<project>/.scripts/frames.mjsputs the device and terminal clips in their frames.scripts/fetch-music.mjsfinds the project's music track, checks its SHA-256 and writesout/.work/music.json. No track, no music.scripts/assemble.mjsjoins the scenes with short fades, the voice, music ducked under it, and captions intoout/<project>/<project>.mp4,.webm,-poster.png,-hero.gifand.srt. Burned-in captions follow the project'scaptionsblock.
A scene is re-rendered only when something that shapes it changes, and so are its voice and its framed recording, so the cache at .kinhin/videos/.cache makes a second build after a one-line edit fast (see Faster rebuilds).
Building the image on Apple's container: before 1.5.0, container build copies no files from a directory when the build path passes through a symlink, such as /tmp or /var/folders (apple/container#2285). The suite is then empty (Cannot find module '/work/scripts/...'). Build from an absolute path that is not under a symlink, such as your home folder, or upgrade container. Docker is not affected, and neither is the published image.
Tests
video/scripts/test.sh # unit tests, seconds: themes, formats, projects, adapters, claims, CI planning, the verifier
video/scripts/test.sh --smoke # + real builds in a container: hello, the template adapter, a bad claim, a bad theme
video/scripts/test.sh --standard # + every theme, every format, every scene type, the cache, determinism
video/scripts/test.sh --full # + the kinhin launch video in every CI look, and all the examplesThe end-to-end tiers build real videos (in Apple's container or Docker; --host uses this machine) and judge them with video/scripts/verify.mjs, the same checker CI uses: frame size, length, loudness, black frames, captions, poster and GIF, and size budgets. Marketing video in CI runs the suite's builds in GitHub Actions, opt-in.
Updating Playwright
The image's base (mcr.microsoft.com/playwright:vX.Y.Z-noble, pinned by digest in video/Dockerfile) and playwright-core in video/package.json must be the same version: Playwright refuses to launch with the browsers of another version. Dependabot cannot change a Docker image and an npm package in one pull request, so it ignores both and you update them together:
- Pick the new version and read its digest:
kinhin image digest mcr.microsoft.com/playwright:vX.Y.Z-noble(the multi-arch index, so arm64 and amd64 both resolve). - Change the
FROMline invideo/Dockerfile(tag and digest) andplaywright-coreinvideo/package.json(andnpm installfor the lockfile). docker build -t kinhin-video:local video/ && video/scripts/image-smoke.sh kinhin-video:local, thencd video && node --test tests/unit.
The "Video image" workflow runs the same smoke test on a pull request, so a mismatch fails there.
The site
Copy the built <project>.mp4, <project>.webm and <project>-poster.png from video/out/<project>/ into website/public/ as launch.mp4, launch.webm and launch-poster.png. The home page shows the player only when launch.mp4 exists, so the site never renders a broken video. Keep the files small (under about 10 MB) or host them elsewhere. The player can use <project>.srt for captions.
Keep it honest
Only claim what the repo supports. The numbers in Benchmarks come from one machine and are a sample; say so. Forges and runtimes that are only planned (see Forges) do not belong on screen: the kinhin adapter's claims.json makes scripts/video-claims-lint.mjs fail a build that mentions them.
Not yet covered
The demo fleet fills the Fleet tab and menu data. The Logs and Diagnostics windows, and the Advanced tabs, still show real (empty) data in demo mode; seeding them is the next step if a video needs to open them.
