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. |
Starting the app
Section titled “Starting the app”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:
- It asks
urlwhether anything is serving. Any answer counts, a 401 included. - If something answers, it uses that server as it is and leaves it running.
With
CIset it refuses instead: on a runner, a server that was up before the job started is not the build under test.reuseExistingServer: trueallows it. - If nothing answers, it runs
commandwith the shell’s environment plusenv. It does not pass on what the recorder loaded from.env.recordingfor itself. It waits forurlto answer and writes the server’s output to.versioncam/webserver.log. - 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.
Presentation
Section titled “Presentation”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.