Skip to content

Configure

Source README.md · versioncam 0.5.3 from npm

versioncam.config.ts sits beside your clips. It is the one place your app describes itself to the recorder.

import { defineRecorder } from "versioncam";
export default defineRecorder({
baseUrl: process.env.DEMOS_BASE_URL ?? "http://localhost:5173",
viewport: { width: 1440, height: 900 },
clips: "demos/clips/**/*.clip.ts",
// How to start the app when nothing is serving it.
webServer: { command: "npm run dev" },
// Either a saved browser session…
auth: { storageState: ".versioncam/auth.json" },
// …or a hook, if signing in needs steps.
// auth: { login: async (page) => { … } },
// What "the page is done" means beyond a quiet DOM and an idle network.
settle: {
ready: async (page) => page.evaluate(() => window.__map?.loaded() ?? true),
},
});

If the recorder needs to know something about your app that this object cannot say, that is a missing field. It is not a reason to fork.

Field Default What it sets
baseUrl required Where the app is served.
webServer none How to start the app when nothing is serving it. See Starting the app.
clips "demos/**/*.clip.ts" Where the clips are: a glob, or a list of them, relative to the config.
viewport { width: 1440, height: 900 } The page’s size. The video has the same shape.
dpr 2 Device pixel ratio, from 1 to 3. At 2, a zoom shows real pixels.
fps 60 Frames per second, from 10 to 120.
output { width: 1920 } The video’s width. Its height follows the viewport. threads fixes how many threads each encoder uses: an encoder’s bytes depend on it, so a fixed number renders the same recording to the same file on any machine. VERSIONCAM_FFMPEG_THREADS sets it for one run.
auth none How the app signs in: a saved session, { storageState }, or a hook, { login }.
settle quietMs: 150, timeoutMs: 15000, yieldMs: 20 When a page counts as done. ready adds a condition of your own.
capture gate: true, forceCaptureEvery: 30, transitions: "play" Which frames get a screenshot (see Speed), and whether the app’s CSS transitions play on its clock or take no time, "zero" (see Determinism).
review minSeconds: 9, maxSeconds: 11 How long a clip should run. versioncam review holds clips to it.
cursorStart { x: 0.72, y: 0.28 } Where the cursor rests in the first frame, as fractions of the viewport.
clock time: "2026-09-01T09:00:00Z", timezoneId: "UTC", locale: "en-GB" The instant, time zone and locale the app sees.
theme accent: "#2f6fed", cursorScale: 2.1 The colour of ripples and rings, the cursor’s size, and an optional presentation. See Presentation.
motion MOTION Dwells, strokes and camera timing. MOTION is exported, with the reason for each number beside it.
outDir ".versioncam" Where everything the recorder writes goes. Gitignore it.
scenes none A module whose default export is Record<string, Scene>, keyed by scene id: export default { [intro.id]: intro }. A named export is not read, and a module with no default export is refused.
sequence none Clip ids, in order, for versioncam render --sequence.

webServer is Playwright’s, field for field:

Field Default What it sets
command required Run through a shell to start the app.
url baseUrl What to ask whether the app is up.
cwd the config’s directory Where command runs.
timeoutMs 120000 How long to wait for url to answer.
reuseExistingServer true, and false when CI is set Use a server that already answers instead of starting one.
env {} Added to the shell’s environment, for the server only.

Every command that drives the app (record, check, stability, inspect, measure, login and doctor) goes through the same steps:

  1. It asks url whether anything is serving. Any answer counts, a 401 included.
  2. If something answers, it uses that server as it is and leaves it running. With CI set it refuses instead: on a runner, a server that was up before the job started is not the build under test. reuseExistingServer: true allows it.
  3. If nothing answers, it runs command with the shell’s environment plus env. It does not pass on what the recorder loaded from .env.recording for itself. It waits for url to answer and writes the server’s output to .versioncam/webserver.log.
  4. When it finishes, normally, on an error or on Ctrl-C, it stops what it started. It signals the command’s whole process group: SIGTERM, then SIGKILL after five seconds. The log says which one it took.

A process the start command hands off to outside its group, such as a container runtime, is out of reach. The command warns when the port still answers after the stop. Start a stack like that yourself, and any stack with a slow cold start: every command reuses what is already running.

For a stack of several services, point url at the one that is ready last, not at the page. A dev server answers seconds before the API behind it, and a clip that opens too early meets a sign-in form with nothing to sign in to.

theme.presentation is what surrounds the app in the video. A backdrop, a device frame and padding are off by default, because the product should record the product. They are there for a clip that should look like a product shot, on a landing page say. A caption is drawn over the app on a soft scrim unless caption says otherwise.

theme: {
presentation: { captionInset: { right: 40 } },
},
Field Default What it sets
backdrop none Behind the app: two colours for a diagonal gradient, or one flat colour.
frame false Rounded corners, a hairline and a drop shadow around the app.
padding 0 How far in the app is drawn, as a fraction of the video’s shorter side, up to 0.3.
caption "auto" Where captions go: "below" the app, "overlay" on it, or "none". "auto" is below when padding makes room, and over the app when it does not.
captionInset { left: 40 } Where a caption over the app sits: { left: n } or { right: n }, pixels of the video in from that side. right puts it in the bottom-right corner, for an app whose own controls are at the bottom left.

A caption inset is in the video’s pixels. The video is output.width wide, 1920 unless the config says otherwise, so with a page 1440 wide, four of the video’s pixels are three of the page’s.