You are reading the docs for v0.0.1-beta-3. View the latest docs.
Skip to content

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).

PieceWhat it isWhere
AdapterOne 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 forgeSources/kinhin-app/Demo/
App clipA 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 clipScripted Playwright session, in a container or locally; an adapter clip of kind browservideo/sources/web/, video/adapters/<name>/scenarios/, video/scripts/record-web.sh
Terminal clipA 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 scenesMotion Canvas (MIT), themed and sized by the project filevideo/src/
BuildVoice, retime, render, frame, mix, captionsvideo/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):

json
{
  "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) ​

bash
KINHIN_SUPPORT_DIR=$(mktemp -d) KINHIN_DEMO=launch scripts/dev-run.sh --env-tokens

KINHIN_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:

json
{
  "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 (keep start at or after arrive; a gap is time spent queued). run: seconds the job is busy. fails (optional): the instance goes failed, raises an alert, then is deleted.
  • account is a forge id (github, gitlab, gitea, forgejo). runtime is tart, appleContainer, docker or host; it sets how long clone, boot and registration take.
  • The app rejects a script that cannot play (job ends after the loop, start before arrive, unknown forge) and logs why. Keep concurrent instances at or below capacity.

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 ​

bash
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 cut

launch 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:

bash
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:local

Both 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:

bash
docker build -t kinhin-video:local video/ && video/scripts/image-smoke.sh kinhin-video:local

The 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.

  1. scripts/voice.mjs renders each cue of the narration.
  2. scripts/retime.mjs makes each scene as long as its speech needs and writes src/timing.ts, out/.work/timeline.json and out/.work/<slug>.captions.srt (out/.work/ is scratch; the container build removes it when it finishes).
  3. scripts/render-titles.mjs renders every drawn scene headlessly (Playwright's Chromium) to raw/scenes/<project>/.
  4. scripts/frames.mjs puts the device and terminal clips in their frames.
  5. scripts/fetch-music.mjs finds the project's music track, checks its SHA-256 and writes out/.work/music.json. No track, no music.
  6. scripts/assemble.mjs joins the scenes with short fades, the voice, music ducked under it, and captions into out/<project>/<project>.mp4, .webm, -poster.png, -hero.gif and .srt. Burned-in captions follow the project's captions block.

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 ​

bash
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 examples

The 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:

  1. 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).
  2. Change the FROM line in video/Dockerfile (tag and digest) and playwright-core in video/package.json (and npm install for the lockfile).
  3. docker build -t kinhin-video:local video/ && video/scripts/image-smoke.sh kinhin-video:local, then cd 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.