versioncam 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE.md +105 -0
- package/README.md +463 -0
- package/bin/versioncam.js +29 -0
- package/dist/.types-render/render-page/draw.d.ts +28 -0
- package/dist/.types-render/render-page/main.d.ts +24 -0
- package/dist/.types-render/render-page/theme.d.ts +37 -0
- package/dist/app-server.d.ts +59 -0
- package/dist/app-server.js +328 -0
- package/dist/app-server.js.map +1 -0
- package/dist/cli/app.d.ts +13 -0
- package/dist/cli/app.js +21 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/commands/check.d.ts +8 -0
- package/dist/cli/commands/check.js +51 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/doctor.js +130 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/dsl.d.ts +16 -0
- package/dist/cli/commands/dsl.js +22 -0
- package/dist/cli/commands/dsl.js.map +1 -0
- package/dist/cli/commands/frame.d.ts +8 -0
- package/dist/cli/commands/frame.js +72 -0
- package/dist/cli/commands/frame.js.map +1 -0
- package/dist/cli/commands/init.d.ts +23 -0
- package/dist/cli/commands/init.js +109 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +1 -0
- package/dist/cli/commands/inspect.js +32 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/install.d.ts +33 -0
- package/dist/cli/commands/install.js +66 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/login.d.ts +10 -0
- package/dist/cli/commands/login.js +49 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/measure.d.ts +1 -0
- package/dist/cli/commands/measure.js +36 -0
- package/dist/cli/commands/measure.js.map +1 -0
- package/dist/cli/commands/open-app.d.ts +14 -0
- package/dist/cli/commands/open-app.js +45 -0
- package/dist/cli/commands/open-app.js.map +1 -0
- package/dist/cli/commands/preview.d.ts +8 -0
- package/dist/cli/commands/preview.js +55 -0
- package/dist/cli/commands/preview.js.map +1 -0
- package/dist/cli/commands/record.d.ts +10 -0
- package/dist/cli/commands/record.js +86 -0
- package/dist/cli/commands/record.js.map +1 -0
- package/dist/cli/commands/render.d.ts +1 -0
- package/dist/cli/commands/render.js +93 -0
- package/dist/cli/commands/render.js.map +1 -0
- package/dist/cli/commands/review.d.ts +6 -0
- package/dist/cli/commands/review.js +89 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/sheet.d.ts +1 -0
- package/dist/cli/commands/sheet.js +48 -0
- package/dist/cli/commands/sheet.js.map +1 -0
- package/dist/cli/commands/stability.d.ts +14 -0
- package/dist/cli/commands/stability.js +110 -0
- package/dist/cli/commands/stability.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +69 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/usage.d.ts +10 -0
- package/dist/cli/usage.js +46 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/config.d.ts +250 -0
- package/dist/config.js +154 -0
- package/dist/config.js.map +1 -0
- package/dist/core/camera.d.ts +30 -0
- package/dist/core/camera.js +94 -0
- package/dist/core/camera.js.map +1 -0
- package/dist/core/compose.d.ts +38 -0
- package/dist/core/compose.js +81 -0
- package/dist/core/compose.js.map +1 -0
- package/dist/core/cursor.d.ts +38 -0
- package/dist/core/cursor.js +103 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/easing.d.ts +15 -0
- package/dist/core/easing.js +33 -0
- package/dist/core/easing.js.map +1 -0
- package/dist/core/loop.d.ts +30 -0
- package/dist/core/loop.js +96 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/motion-defaults.d.ts +61 -0
- package/dist/core/motion-defaults.js +62 -0
- package/dist/core/motion-defaults.js.map +1 -0
- package/dist/core/rng.d.ts +13 -0
- package/dist/core/rng.js +27 -0
- package/dist/core/rng.js.map +1 -0
- package/dist/core/sse.d.ts +15 -0
- package/dist/core/sse.js +16 -0
- package/dist/core/sse.js.map +1 -0
- package/dist/core/timeline.d.ts +146 -0
- package/dist/core/timeline.js +81 -0
- package/dist/core/timeline.js.map +1 -0
- package/dist/core/timing.d.ts +31 -0
- package/dist/core/timing.js +29 -0
- package/dist/core/timing.js.map +1 -0
- package/dist/core/typing.d.ts +12 -0
- package/dist/core/typing.js +35 -0
- package/dist/core/typing.js.map +1 -0
- package/dist/driver/clip.d.ts +71 -0
- package/dist/driver/clip.js +120 -0
- package/dist/driver/clip.js.map +1 -0
- package/dist/driver/compare.d.ts +34 -0
- package/dist/driver/compare.js +40 -0
- package/dist/driver/compare.js.map +1 -0
- package/dist/driver/gate.d.ts +36 -0
- package/dist/driver/gate.js +27 -0
- package/dist/driver/gate.js.map +1 -0
- package/dist/driver/launch.d.ts +43 -0
- package/dist/driver/launch.js +47 -0
- package/dist/driver/launch.js.map +1 -0
- package/dist/driver/page-hooks.d.ts +72 -0
- package/dist/driver/page-hooks.js +129 -0
- package/dist/driver/page-hooks.js.map +1 -0
- package/dist/driver/reports.d.ts +34 -0
- package/dist/driver/reports.js +42 -0
- package/dist/driver/reports.js.map +1 -0
- package/dist/driver/session.d.ts +285 -0
- package/dist/driver/session.js +773 -0
- package/dist/driver/session.js.map +1 -0
- package/dist/driver/settle.d.ts +41 -0
- package/dist/driver/settle.js +82 -0
- package/dist/driver/settle.js.map +1 -0
- package/dist/env.d.ts +11 -0
- package/dist/env.js +41 -0
- package/dist/env.js.map +1 -0
- package/dist/fixtures.d.ts +13 -0
- package/dist/fixtures.js +13 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/inspect.d.ts +70 -0
- package/dist/inspect/inspect.js +176 -0
- package/dist/inspect/inspect.js.map +1 -0
- package/dist/inspect/measure.d.ts +40 -0
- package/dist/inspect/measure.js +107 -0
- package/dist/inspect/measure.js.map +1 -0
- package/dist/loader.d.ts +28 -0
- package/dist/loader.js +143 -0
- package/dist/loader.js.map +1 -0
- package/dist/page/assets/index-DUom5amc.js +1 -0
- package/dist/page/index.html +18 -0
- package/dist/render/encode.d.ts +40 -0
- package/dist/render/encode.js +183 -0
- package/dist/render/encode.js.map +1 -0
- package/dist/render/ffmpeg.d.ts +13 -0
- package/dist/render/ffmpeg.js +72 -0
- package/dist/render/ffmpeg.js.map +1 -0
- package/dist/render/presentation.d.ts +19 -0
- package/dist/render/presentation.js +27 -0
- package/dist/render/presentation.js.map +1 -0
- package/dist/render/render.d.ts +59 -0
- package/dist/render/render.js +144 -0
- package/dist/render/render.js.map +1 -0
- package/dist/render/sampling.d.ts +47 -0
- package/dist/render/sampling.js +129 -0
- package/dist/render/sampling.js.map +1 -0
- package/dist/render/sequence.d.ts +24 -0
- package/dist/render/sequence.js +105 -0
- package/dist/render/sequence.js.map +1 -0
- package/dist/render/serve.d.ts +35 -0
- package/dist/render/serve.js +124 -0
- package/dist/render/serve.js.map +1 -0
- package/dist/review/review.d.ts +54 -0
- package/dist/review/review.js +229 -0
- package/dist/review/review.js.map +1 -0
- package/dist/scene.d.ts +23 -0
- package/dist/scene.js +2 -0
- package/dist/scene.js.map +1 -0
- package/dsl.md +119 -0
- package/package.json +76 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/README.md +105 -0
- package/plugin/agents/versioncam-reviewer.md +63 -0
- package/plugin/skills/versioncam/SKILL.md +235 -0
- package/plugin/skills/versioncam/authoring.md +226 -0
- package/plugin/skills/versioncam/onboarding.md +199 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the things matching a selector actually are.
|
|
3
|
+
*
|
|
4
|
+
* A drag or a lasso is authored as fractions of a target's box, and fractions
|
|
5
|
+
* chosen by guesswork enclose nothing — which is silent, because an empty
|
|
6
|
+
* selection is a legal state that simply fails to render whatever depended on
|
|
7
|
+
* it. Measuring first is what made every working stroke in the prototype work.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Above this many matches, a list of boxes is a wall of numbers and the grid
|
|
11
|
+
* is the answer. Below it, the boxes are the answer: a handful of markers is
|
|
12
|
+
* exactly the case where an author needs to know where each one is.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_LISTED = 12;
|
|
15
|
+
export async function measureSelector(page, selector) {
|
|
16
|
+
return page.evaluate(({ sel, maxListed }) => {
|
|
17
|
+
const cols = 12;
|
|
18
|
+
const rows = 8;
|
|
19
|
+
const grid = Array.from({ length: rows }, () => new Array(cols).fill(0));
|
|
20
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
21
|
+
const matches = Array.from(document.querySelectorAll(sel));
|
|
22
|
+
const boxes = matches.map((el) => {
|
|
23
|
+
const r = el.getBoundingClientRect();
|
|
24
|
+
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
|
25
|
+
});
|
|
26
|
+
// `innerText` where there is one: it is the text as rendered, so a hidden
|
|
27
|
+
// span does not speak and a line break is a space. SVG has only
|
|
28
|
+
// `textContent`. Long text is cut, because a match can be a whole list.
|
|
29
|
+
const texts = matches.map((el) => {
|
|
30
|
+
const raw = el.innerText ?? el.textContent ?? "";
|
|
31
|
+
const text = raw.replace(/\s+/g, " ").trim();
|
|
32
|
+
return text.length > 80 ? `${text.slice(0, 79)}…` : text;
|
|
33
|
+
});
|
|
34
|
+
if (boxes.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
selector: sel,
|
|
37
|
+
count: 0,
|
|
38
|
+
boxes: [],
|
|
39
|
+
texts: [],
|
|
40
|
+
bounds: null,
|
|
41
|
+
fractions: null,
|
|
42
|
+
viewport,
|
|
43
|
+
grid,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Over boxes, not over centres. Centres collapse a single match to a
|
|
47
|
+
// point — bounds of zero size, from which no framing can be planned —
|
|
48
|
+
// and understate a cluster by half an element on every side.
|
|
49
|
+
const bounds = {
|
|
50
|
+
x0: Math.min(...boxes.map((b) => b.x)),
|
|
51
|
+
y0: Math.min(...boxes.map((b) => b.y)),
|
|
52
|
+
x1: Math.max(...boxes.map((b) => b.x + b.w)),
|
|
53
|
+
y1: Math.max(...boxes.map((b) => b.y + b.h)),
|
|
54
|
+
};
|
|
55
|
+
// The grid stays a count of centres: it answers "where is the mass",
|
|
56
|
+
// and a box spanning cells would smear that.
|
|
57
|
+
for (const b of boxes) {
|
|
58
|
+
const c = Math.min(cols - 1, Math.max(0, Math.floor(((b.x + b.w / 2) / viewport.width) * cols)));
|
|
59
|
+
const r = Math.min(rows - 1, Math.max(0, Math.floor(((b.y + b.h / 2) / viewport.height) * rows)));
|
|
60
|
+
grid[r][c] += 1;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
selector: sel,
|
|
64
|
+
count: boxes.length,
|
|
65
|
+
boxes: boxes.length <= maxListed ? boxes : [],
|
|
66
|
+
texts: boxes.length <= maxListed ? texts : [],
|
|
67
|
+
bounds,
|
|
68
|
+
fractions: {
|
|
69
|
+
x0: bounds.x0 / viewport.width,
|
|
70
|
+
y0: bounds.y0 / viewport.height,
|
|
71
|
+
x1: bounds.x1 / viewport.width,
|
|
72
|
+
y1: bounds.y1 / viewport.height,
|
|
73
|
+
},
|
|
74
|
+
viewport,
|
|
75
|
+
grid,
|
|
76
|
+
};
|
|
77
|
+
}, { sel: selector, maxListed: MAX_LISTED });
|
|
78
|
+
}
|
|
79
|
+
export function formatMeasurement(m) {
|
|
80
|
+
if (m.count === 0)
|
|
81
|
+
return `${m.selector}: nothing matched.`;
|
|
82
|
+
const b = m.bounds;
|
|
83
|
+
const f = m.fractions;
|
|
84
|
+
const lines = [
|
|
85
|
+
`${m.selector}: ${m.count} match(es) in ${m.viewport.width}x${m.viewport.height}`,
|
|
86
|
+
` bounds px x ${b.x0.toFixed(0)}–${b.x1.toFixed(0)} y ${b.y0.toFixed(0)}–${b.y1.toFixed(0)}`,
|
|
87
|
+
` bounds frac x ${f.x0.toFixed(2)}–${f.x1.toFixed(2)} y ${f.y0.toFixed(2)}–${f.y1.toFixed(2)}`,
|
|
88
|
+
];
|
|
89
|
+
if (m.boxes.length > 0) {
|
|
90
|
+
lines.push(" boxes:");
|
|
91
|
+
for (const [index, box] of m.boxes.entries()) {
|
|
92
|
+
lines.push(` ${String(index).padStart(2)} @ ${box.x.toFixed(0)},${box.y.toFixed(0)} ` +
|
|
93
|
+
`${box.w.toFixed(0)}x${box.h.toFixed(0)} text=${JSON.stringify(m.texts[index] ?? "")}`);
|
|
94
|
+
}
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
lines.push(" density (rows = y, cols = x):");
|
|
98
|
+
for (const [r, row] of m.grid.entries()) {
|
|
99
|
+
const y = ((r + 0.5) / m.grid.length).toFixed(2);
|
|
100
|
+
lines.push(` y=${y} |` +
|
|
101
|
+
row.map((v) => (v === 0 ? " ." : String(v).padStart(5))).join(""));
|
|
102
|
+
}
|
|
103
|
+
lines.push(" x =" +
|
|
104
|
+
Array.from({ length: 12 }, (_, c) => ((c + 0.5) / 12).toFixed(2).slice(1).padStart(5)).join(""));
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=measure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure.js","sourceRoot":"","sources":["../../src/inspect/measure.ts"],"names":[],"mappings":"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"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.js";
|
|
2
|
+
import type { ClipDefinition } from "./driver/clip.js";
|
|
3
|
+
export declare function bundleModule(entry: string, platform: "node" | "browser", cacheDir: string): Promise<string>;
|
|
4
|
+
/** Import a bundle fresh, defeating the ESM module cache between runs. */
|
|
5
|
+
export declare function importFresh<T>(file: string): Promise<T>;
|
|
6
|
+
/**
|
|
7
|
+
* What to do about a missing config, wherever one is found missing.
|
|
8
|
+
*
|
|
9
|
+
* It is the first thing someone new meets — `doctor` in a fresh app, or any
|
|
10
|
+
* command run before the skill has — and the skill's first run writes a
|
|
11
|
+
* better config than a person starting from the type would, so that comes
|
|
12
|
+
* first; by hand is the way for anyone not using the skill.
|
|
13
|
+
*/
|
|
14
|
+
export declare const NO_CONFIG: string;
|
|
15
|
+
export declare function findConfigFile(cwd: string): string | null;
|
|
16
|
+
export declare function loadConfig(cwd: string): Promise<ResolvedConfig>;
|
|
17
|
+
/**
|
|
18
|
+
* Every clip the config points at, in file order.
|
|
19
|
+
*
|
|
20
|
+
* `only` filters by substring against the clip id, which is how
|
|
21
|
+
* `versioncam record 05` works: an id is long and nobody wants to type one in
|
|
22
|
+
* full.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadClips(config: ResolvedConfig, only?: string[]): Promise<ClipDefinition[]>;
|
|
25
|
+
/** The app's scenes, bundled for the browser. Null when it has none. */
|
|
26
|
+
export declare function bundleScenes(config: ResolvedConfig): Promise<string | null>;
|
|
27
|
+
/** Scene ids, without running a browser: needed to list and to filter. */
|
|
28
|
+
export declare function loadSceneIds(config: ResolvedConfig): Promise<string[]>;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { resolveConfig } from "./config.js";
|
|
8
|
+
/**
|
|
9
|
+
* Loads the TypeScript an app writes: its config, its clips, its scenes.
|
|
10
|
+
*
|
|
11
|
+
* One mechanism for all three, which is esbuild. Node's native type stripping
|
|
12
|
+
* was the tempting alternative — no dependency at all — but it requires
|
|
13
|
+
* explicit `.ts` extensions on every relative import, which is not how anyone
|
|
14
|
+
* writes an import, and it would pin apps to a Node minor. A bundler also
|
|
15
|
+
* gives the scenes their browser build for free, which they need anyway.
|
|
16
|
+
*/
|
|
17
|
+
const CACHE_VERSION = 1;
|
|
18
|
+
export async function bundleModule(entry, platform, cacheDir) {
|
|
19
|
+
await mkdir(cacheDir, { recursive: true });
|
|
20
|
+
const key = createHash("sha1")
|
|
21
|
+
.update(`${CACHE_VERSION}:${platform}:${entry}`)
|
|
22
|
+
.digest("hex")
|
|
23
|
+
.slice(0, 12);
|
|
24
|
+
const outfile = join(cacheDir, `${basename(entry, ".ts")}.${key}.mjs`);
|
|
25
|
+
await build({
|
|
26
|
+
entryPoints: [entry],
|
|
27
|
+
outfile,
|
|
28
|
+
bundle: true,
|
|
29
|
+
format: "esm",
|
|
30
|
+
platform,
|
|
31
|
+
target: "es2023",
|
|
32
|
+
sourcemap: "inline",
|
|
33
|
+
logLevel: "silent",
|
|
34
|
+
// Node builds keep their imports live so a clip gets the same package
|
|
35
|
+
// instance the CLI is running, and the same Playwright.
|
|
36
|
+
external: platform === "node"
|
|
37
|
+
? ["versioncam", "versioncam/*", "playwright", "node:*"]
|
|
38
|
+
: [],
|
|
39
|
+
});
|
|
40
|
+
return outfile;
|
|
41
|
+
}
|
|
42
|
+
/** Import a bundle fresh, defeating the ESM module cache between runs. */
|
|
43
|
+
export async function importFresh(file) {
|
|
44
|
+
const url = `${pathToFileURL(file).href}?t=${Date.now()}`;
|
|
45
|
+
return (await import(url));
|
|
46
|
+
}
|
|
47
|
+
const CONFIG_NAMES = [
|
|
48
|
+
"versioncam.config.ts",
|
|
49
|
+
"versioncam.config.js",
|
|
50
|
+
"versioncam.config.mjs",
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* What to do about a missing config, wherever one is found missing.
|
|
54
|
+
*
|
|
55
|
+
* It is the first thing someone new meets — `doctor` in a fresh app, or any
|
|
56
|
+
* command run before the skill has — and the skill's first run writes a
|
|
57
|
+
* better config than a person starting from the type would, so that comes
|
|
58
|
+
* first; by hand is the way for anyone not using the skill.
|
|
59
|
+
*/
|
|
60
|
+
export const NO_CONFIG = "`npx versioncam init`, then `/versioncam`: its first run writes one from the repository.\n" +
|
|
61
|
+
"Or by hand: a default export from defineRecorder(), as in the README's Configure section.";
|
|
62
|
+
export function findConfigFile(cwd) {
|
|
63
|
+
for (const name of CONFIG_NAMES) {
|
|
64
|
+
const candidate = resolve(cwd, name);
|
|
65
|
+
if (existsSync(candidate))
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
export async function loadConfig(cwd) {
|
|
71
|
+
const file = findConfigFile(cwd);
|
|
72
|
+
if (!file) {
|
|
73
|
+
throw new Error(`No versioncam.config.ts in ${cwd}.\n${NO_CONFIG}`);
|
|
74
|
+
}
|
|
75
|
+
// The cache directory comes from the config we have not read yet, so the
|
|
76
|
+
// bundle goes next to the config until we know better.
|
|
77
|
+
const bootstrapCache = resolve(dirname(file), ".versioncam", ".cache");
|
|
78
|
+
const bundle = await bundleModule(file, "node", bootstrapCache);
|
|
79
|
+
const module = await importFresh(bundle);
|
|
80
|
+
if (!module.default) {
|
|
81
|
+
throw new Error(`${file} must have a default export from defineRecorder().`);
|
|
82
|
+
}
|
|
83
|
+
return resolveConfig(module.default, dirname(file));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every clip the config points at, in file order.
|
|
87
|
+
*
|
|
88
|
+
* `only` filters by substring against the clip id, which is how
|
|
89
|
+
* `versioncam record 05` works: an id is long and nobody wants to type one in
|
|
90
|
+
* full.
|
|
91
|
+
*/
|
|
92
|
+
export async function loadClips(config, only = []) {
|
|
93
|
+
const { glob } = await import("node:fs/promises");
|
|
94
|
+
const files = [];
|
|
95
|
+
for (const pattern of config.clips) {
|
|
96
|
+
for await (const match of glob(pattern, { cwd: config.root })) {
|
|
97
|
+
files.push(resolve(config.root, match));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
files.sort();
|
|
101
|
+
const clips = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const bundle = await bundleModule(file, "node", config.cacheDir);
|
|
104
|
+
const module = await importFresh(bundle);
|
|
105
|
+
if (!module.default) {
|
|
106
|
+
throw new Error(`${file} must default-export a clip (or an array of them).`);
|
|
107
|
+
}
|
|
108
|
+
for (const clip of [module.default].flat()) {
|
|
109
|
+
if (typeof clip?.id !== "string" || typeof clip?.body !== "function") {
|
|
110
|
+
throw new Error(`${file} default-exports something that is not a clip.`);
|
|
111
|
+
}
|
|
112
|
+
clips.push(clip);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const clip of clips) {
|
|
117
|
+
if (seen.has(clip.id)) {
|
|
118
|
+
throw new Error(`Two clips share the id "${clip.id}".`);
|
|
119
|
+
}
|
|
120
|
+
seen.add(clip.id);
|
|
121
|
+
}
|
|
122
|
+
return only.length === 0
|
|
123
|
+
? clips
|
|
124
|
+
: clips.filter((clip) => only.some((needle) => clip.id.includes(needle)));
|
|
125
|
+
}
|
|
126
|
+
/** The app's scenes, bundled for the browser. Null when it has none. */
|
|
127
|
+
export async function bundleScenes(config) {
|
|
128
|
+
if (!config.scenes)
|
|
129
|
+
return null;
|
|
130
|
+
if (!existsSync(config.scenes)) {
|
|
131
|
+
throw new Error(`versioncam config: \`scenes\` points at a missing file: ${config.scenes}`);
|
|
132
|
+
}
|
|
133
|
+
return bundleModule(config.scenes, "browser", config.cacheDir);
|
|
134
|
+
}
|
|
135
|
+
/** Scene ids, without running a browser: needed to list and to filter. */
|
|
136
|
+
export async function loadSceneIds(config) {
|
|
137
|
+
if (!config.scenes)
|
|
138
|
+
return [];
|
|
139
|
+
const bundle = await bundleModule(config.scenes, "node", config.cacheDir);
|
|
140
|
+
const module = await importFresh(bundle);
|
|
141
|
+
return Object.keys(module.default ?? {});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let n=t(e);return n*n*n*(10+n*(-15+6*n))}function t(e){return e<=0?0:e>=1?1:e}function n(e,t,n){return e+(t-e)*n}function r(e,t,r){let i=e.x+e.w/2,a=e.y+e.h/2,o=t.x+t.w/2,s=t.y+t.h/2,c=e.w*(t.w/e.w)**+r,l=e.h*(t.h/e.h)**+r,u=n(i,o,r),d=n(a,s,r);return{x:u-c/2,y:d-l/2,w:c,h:l}}function i(t,n){if(t.length===0)throw Error(`evalCamera called with no keys`);if(n<=t[0].t)return t[0].rect;for(let i=1;i<t.length;i+=1){let a=t[i-1],o=t[i];if(n>=o.t)continue;if(o.ease===`hold`)return a.rect;let s=o.t-a.t,c=s<=0?1:(n-a.t)/s;return r(a.rect,o.rect,e(c))}return t[t.length-1].rect}function a(e,t,n,r){let i=1-r,a=i*i,o=2*i*r,s=r*r;return[a*e[0]+o*t[0]+s*n[0],a*e[1]+o*t[1]+s*n[1]]}function o(t,n){if(t.length===0)return[0,0];let r=t[0];if(n<=r.t0)return r.kind===`move`?r.from:r.at;for(let r of t){if(n>r.t1)continue;if(r.kind===`hold`)return r.at;let t=r.t1-r.t0,i=t<=0?1:(n-r.t0)/t;return a(r.from,r.ctrl,r.to,e(i))}let i=t[t.length-1];return i.kind===`move`?i.to:i.at}function s(e,t,n){let r=n.w/t.w;return[n.x+(e[0]-t.x)*r,n.y+(e[1]-t.y)*r]}function c(e,t,n){let r=n.w/t.w;return{x:n.x+(e.x-t.x)*r,y:n.y+(e.y-t.y)*r,w:e.w*r,h:e.h*r}}function l(e,t){return{x:e.x*t,y:e.y*t,w:e.w*t,h:e.h*t}}function u(e,t,n){let r=Math.min(e.width,e.height)*n,i=e.width-r*2,a=e.height-r*2,o=t.width/t.height,s=i,c=s/o;return c>a&&(c=a,s=c*o),{x:(e.width-s)/2,y:(e.height-c)/2,w:s,h:c}}function d(e,n,r){return r<=0?1:t((e-n+1)/r)}function f(e,n,r,i){return e<=n||e>=r?0:i<=0?1:Math.min(t((e-n)/i),t((r-e)/i))}function p(e,t){return e/t}function m(e,t,n){let r=n?.padding??0;return!n||r<=0?{x:0,y:0,w:e.width,h:e.height}:u(e,t,r)}function h(e){let t=e?.caption??`auto`;return t===`none`?`none`:t===`below`||t===`overlay`?t:(e?.padding??0)>0?`below`:`overlay`}var g={radius:16,hairline:`rgba(255, 255, 255, 0.16)`,shadow:`rgba(0, 0, 0, 0.45)`,shadowBlur:60,shadowOffsetY:18,captionFont:`600 30px "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif`,captionColor:`rgba(255, 255, 255, 0.94)`,captionShadow:`rgba(0, 0, 0, 0.5)`,captionScrim:`rgba(0, 0, 0, 0.45)`,captionScrimHeight:86,cursorFill:`#ffffff`,cursorStroke:`rgba(0, 0, 0, 0.55)`,cursorShadow:`rgba(0, 0, 0, 0.35)`,ringWidth:3,ringRadius:10,spotlightDim:.55,spotlightTint:`6, 2, 16`};function _(e,t){let n=/^#([0-9a-f]{6})$/i.exec(e.trim());if(!n)return e;let r=Number.parseInt(n[1],16);return`rgba(${r>>16&255}, ${r>>8&255}, ${r&255}, ${t})`}function v(e,t,n){let r=Math.min(n,t.w/2,t.h/2);e.beginPath(),e.moveTo(t.x+r,t.y),e.arcTo(t.x+t.w,t.y,t.x+t.w,t.y+t.h,r),e.arcTo(t.x+t.w,t.y+t.h,t.x,t.y+t.h,r),e.arcTo(t.x,t.y+t.h,t.x,t.y,r),e.arcTo(t.x,t.y,t.x+t.w,t.y,r),e.closePath()}function y(e,t,n){if(typeof n==`string`)e.fillStyle=n;else{let r=e.createLinearGradient(0,0,t.width,t.height);r.addColorStop(0,n[0]),r.addColorStop(1,n[1]),e.fillStyle=r}e.fillRect(0,0,t.width,t.height)}function b(e,t,n){for(let r of e.cuts)if(t>r&&t<=r+n)return{current:e.frames[t],previous:e.frames[r],alpha:d(t,r+1,n)};return{current:e.frames[t],previous:null,alpha:1}}function x(e,t,n,r,i,a,o){let s=l(i,t.viewport.dpr),{current:c,previous:u,alpha:d}=b(t,r,o.motion.crossfadeFrames),f=o.theme.presentation?.frame??!1;e.save(),f&&(v(e,a,g.radius),e.clip());let p=(t,r)=>{let i=n.images[t];i&&(e.globalAlpha=r,e.drawImage(i,s.x,s.y,s.w,s.h,a.x,a.y,a.w,a.h))};u!==null&&u!==c&&p(u,1),p(c,u===null?1:d),e.globalAlpha=1,e.restore()}function S(e,t){e.save(),e.shadowColor=g.shadow,e.shadowBlur=g.shadowBlur,e.shadowOffsetY=g.shadowOffsetY,e.fillStyle=`rgba(0,0,0,1)`,v(e,t,g.radius),e.fill(),e.restore()}function C(e,t){e.save(),e.strokeStyle=g.hairline,e.lineWidth=1,v(e,t,g.radius),e.stroke(),e.restore()}function w(e,t){return{x:e.x-t,y:e.y-t,w:e.w+t*2,h:e.h+t*2}}function T(e,t,n,r,i,a){let o=a.theme.presentation?.frame??!1;for(let s of t.highlights){let t=f(n,s.t0,s.t1,.25);if(t<=0)continue;let l=c(s.rect,r,i);e.save(),e.globalAlpha=t,s.style===`spotlight`?(e.save(),v(e,i,o?g.radius:0),e.clip(),e.fillStyle=`rgba(${g.spotlightTint}, ${g.spotlightDim})`,e.fillRect(i.x,i.y,i.w,i.h),e.globalCompositeOperation=`destination-out`,v(e,w(l,8),g.ringRadius),e.fill(),e.restore()):(e.strokeStyle=a.theme.accent,e.lineWidth=g.ringWidth,e.shadowColor=_(a.theme.accent,.35),e.shadowBlur=18,v(e,w(l,6),g.ringRadius),e.stroke()),e.restore()}}function E(e,t,n){let r=!1,i=[],a=0;for(let o of e.pointer){if(o.t>t)break;if(o.kind===`down`)r=!0,a=o.t;else{r=!1;let e=t-o.t;e>=0&&e<=n.motion.rippleMs/1e3&&i.push({at:o.at,age:e})}}let o=r?t-a:0,s=r?Math.min(1,o/(n.motion.pressScaleMs/1e3)):0;return{scale:1-(1-n.motion.pressScale)*s,ripples:i}}function D(e,t,n,r,i){for(let a of t){let t=a.age/(i.motion.rippleMs/1e3),[o,c]=s(a.at,n,r);e.save(),e.globalAlpha=(1-t)*.55,e.strokeStyle=i.theme.accent,e.lineWidth=2.5,e.beginPath(),e.arc(o,c,6+t*i.motion.rippleRadius,0,Math.PI*2),e.stroke(),e.restore()}}function O(e,t,n,r){let i=r*n;e.save(),e.translate(t[0],t[1]),e.scale(i,i),e.shadowColor=g.cursorShadow,e.shadowBlur=6/i,e.shadowOffsetY=2/i,e.beginPath(),e.moveTo(0,0),e.lineTo(0,16.6),e.lineTo(4.1,12.9),e.lineTo(6.9,18.9),e.lineTo(9.6,17.6),e.lineTo(6.9,11.8),e.lineTo(12.1,11.5),e.closePath(),e.fillStyle=g.cursorFill,e.fill(),e.shadowColor=`transparent`,e.strokeStyle=g.cursorStroke,e.lineWidth=1/i,e.stroke(),e.restore()}function k(e,t,n,r,i,a){let o=h(a.theme.presentation);if(o!==`none`)for(let s of t.captions){let t=f(n,s.t0,s.t1,a.motion.captionFadeS);if(t<=0)continue;if(e.save(),e.globalAlpha=t,o===`overlay`){let t=g.captionScrimHeight,n=e.createLinearGradient(0,i.height-t,0,i.height);n.addColorStop(0,`rgba(0, 0, 0, 0)`),n.addColorStop(1,g.captionScrim),e.fillStyle=n,e.fillRect(0,i.height-t,i.width,t)}e.font=g.captionFont,e.fillStyle=g.captionColor,e.textBaseline=`middle`,e.shadowColor=g.captionShadow,e.shadowBlur=8;let c=o===`overlay`?40:r.x,l=o===`overlay`?i.height-g.captionScrimHeight/2+4:(r.y+r.h+i.height)/2;e.fillText(s.text,c,l),e.restore()}}function A(e,t,n,r,a,c){let l=p(r,t.fps),u=i(t.camera,l),d=c.theme.presentation,f=m(a,t.viewport,d);d?.backdrop&&y(e,a,d.backdrop),d?.frame&&S(e,f),x(e,t,n,r,u,f,c),d?.frame&&C(e,f),T(e,t,l,u,f,c);let{scale:h,ripples:g}=E(t,l,c);D(e,g,u,f,c),O(e,s(o(t.cursor,l),u,f),h,c.theme.cursorScale),k(e,t,l,f,a,c)}var j=`modulepreload`,M=function(e,t){return new URL(e,t).href},N={},P=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=M(t,n),t=s(t),t in N)return;N[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:j,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},F=new URLSearchParams(location.search),I=F.has(`recording`),L=F.get(`scene`),R=F.get(`headless`)===`1`;R&&document.body.classList.add(`headless`);var z=document.getElementById(`stage`),B=document.getElementById(`scrub`),V=document.getElementById(`status`),H=document.getElementById(`play`),U=document.getElementById(`error`);function W(e){throw U.textContent=e,Error(e)}async function G(e,t){let n=await fetch(e);return n.ok||W(`Could not load ${t} (${e}): ${n.status}`),await n.json()}async function K(e){let t=await fetch(e);return t.ok||W(`Could not load ${e}: ${t.status}`),createImageBitmap(await t.blob())}function q(e){z.width=e.output.width,z.height=e.output.height,R?(z.style.width=`${e.output.width}px`,z.style.height=`${e.output.height}px`):(z.style.width=`min(100%, 1280px)`,z.style.height=`auto`);let t=z.getContext(`2d`,{alpha:!1});return t||W(`No 2D context`),t.imageSmoothingEnabled=!0,t.imageSmoothingQuality=`high`,t}function J(e,t,n,r){let i=!1;H.addEventListener(`click`,()=>{i=!i,H.textContent=i?`Pause`:`Play`;let a=performance.now(),o=s=>{i&&(s-a>=1e3/e&&(a=s,r((n()+1)%t)),requestAnimationFrame(o))};requestAnimationFrame(o)})}function Y(e,t,n,r){e.durationFrames=t,e.fps=n,e.setFrame=e=>new Promise(t=>{r(e),requestAnimationFrame(()=>requestAnimationFrame(()=>t()))})}async function X(e,t){let n=await G(`/recording/timeline.json`,`the recording`),r=q(t),i={images:await Promise.all(n.states.map(e=>K(`/recording/states/${e}`)))};await document.fonts.ready;let a=-1,o=e=>{let o=Math.max(0,Math.min(n.durationFrames-1,e));A(r,n,i,o,z,t),a=o,R||(B.value=String(o),V.textContent=`${o+1} / ${n.durationFrames} · ${(o/n.fps).toFixed(2)}s · state ${n.frames[o]}`)};B.max=String(n.durationFrames-1),B.addEventListener(`input`,()=>o(Number(B.value))),J(n.fps,n.durationFrames,()=>a,o),o(Number(F.get(`frame`)??0)),Y(e,n.durationFrames,n.fps,o)}async function Z(e,t,n){let r=(await P(()=>import(`/scenes.js`),[],import.meta.url)).default,i=r?.[n];i||W(`No scene "${n}". The scenes module exports: ${Object.keys(r??{}).join(`, `)||`(nothing)`}.`);let a=q(t);await document.fonts.ready;let o=-1,s=e=>{let t=Math.max(0,Math.min(i.durationFrames-1,e));i.draw(a,t,z),o=t,R||(B.value=String(t),V.textContent=`${t+1} / ${i.durationFrames} · ${(t/i.fps).toFixed(2)}s`)};B.max=String(i.durationFrames-1),B.addEventListener(`input`,()=>s(Number(B.value))),J(i.fps,i.durationFrames,()=>o,s),s(Number(F.get(`frame`)??0)),Y(e,i.durationFrames,i.fps,s)}async function Q(e){let t=await G(`/render-config.json`,`the render config`);if(L)return Z(e,t,L);if(I)return X(e,t);W(`Pass ?recording or ?scene=<id>.`)}var $={ready:Promise.resolve(),setFrame:async()=>{},durationFrames:0,fps:60};window.__render=$,$.ready=Q($);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>versioncam — render</title>
|
|
6
|
+
<style>*{box-sizing:border-box;margin:0;padding:0;transition:none!important;animation:none!important}body{color:#e8e2f5;background:#0b0614;flex-direction:column;align-items:center;min-height:100vh;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;display:flex}body.headless{background:0 0;display:block}#stage{display:block}body.headless #stage{position:fixed;inset:0}#controls{align-items:center;gap:14px;width:min(100%,1280px);padding:14px 18px 28px;font-size:13px;display:flex}body.headless #controls{display:none}#scrub{flex:1}#status{font-variant-numeric:tabular-nums;white-space:nowrap;opacity:.75}button{font:inherit;color:#fff;cursor:pointer;background:#2f6fed;border:0;border-radius:6px;padding:6px 14px}#error{color:#ff9a9a;white-space:pre-wrap;padding:24px;font-family:ui-monospace,Menlo,monospace}</style>
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-DUom5amc.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<canvas id="stage"></canvas>
|
|
11
|
+
<div id="controls">
|
|
12
|
+
<button id="play" type="button">Play</button>
|
|
13
|
+
<input id="scrub" type="range" min="0" max="0" step="1" value="0" />
|
|
14
|
+
<span id="status">–</span>
|
|
15
|
+
</div>
|
|
16
|
+
<pre id="error"></pre>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
export type EncodeResult = {
|
|
3
|
+
frames: number;
|
|
4
|
+
mp4: string;
|
|
5
|
+
webm: string;
|
|
6
|
+
poster: string;
|
|
7
|
+
};
|
|
8
|
+
export type EncodeOptions = {
|
|
9
|
+
id: string;
|
|
10
|
+
fps: number;
|
|
11
|
+
durationFrames: number;
|
|
12
|
+
/** Frame used for the poster image; defaults to a third of the way in. */
|
|
13
|
+
posterFrame?: number;
|
|
14
|
+
outDir: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Walk the render page frame by frame, screenshotting each one into both
|
|
18
|
+
* encoders. The page is a pure function of the frame number, so this loop can
|
|
19
|
+
* be re-run, interrupted or parallelised without changing its output.
|
|
20
|
+
*/
|
|
21
|
+
export declare function encodeFromPage(page: Page, options: EncodeOptions): Promise<EncodeResult>;
|
|
22
|
+
/** A strip of frames, for reviewing a clip in a pull request. */
|
|
23
|
+
export declare function contactSheet(page: Page, options: {
|
|
24
|
+
id: string;
|
|
25
|
+
durationFrames: number;
|
|
26
|
+
count?: number;
|
|
27
|
+
/** Exactly which frames to show. Evenly spaced when absent. */
|
|
28
|
+
frames?: number[];
|
|
29
|
+
outDir: string;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
declare global {
|
|
32
|
+
interface Window {
|
|
33
|
+
__render?: {
|
|
34
|
+
ready: Promise<void>;
|
|
35
|
+
setFrame: (frame: number) => Promise<void>;
|
|
36
|
+
durationFrames: number;
|
|
37
|
+
fps: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ffmpegPath } from "./ffmpeg.js";
|
|
5
|
+
import { uniformFrames } from "./sampling.js";
|
|
6
|
+
/**
|
|
7
|
+
* Pipe PNG frames into ffmpeg twice — once for H.264, once for VP9.
|
|
8
|
+
*
|
|
9
|
+
* Two encoders rather than a transcode because both come from the same
|
|
10
|
+
* lossless frames; transcoding the mp4 into a webm would compound artefacts
|
|
11
|
+
* for no saving worth having on a ten-second clip.
|
|
12
|
+
*/
|
|
13
|
+
function startEncoder(args) {
|
|
14
|
+
const child = spawn(ffmpegPath(), args, {
|
|
15
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
16
|
+
});
|
|
17
|
+
let stderr = "";
|
|
18
|
+
child.stderr?.on("data", (chunk) => {
|
|
19
|
+
stderr += chunk.toString();
|
|
20
|
+
});
|
|
21
|
+
child.on("error", (error) => {
|
|
22
|
+
throw new Error(`ffmpeg could not be started (${error.message}). Install it, or add ` +
|
|
23
|
+
"the optional ffmpeg-static dependency.");
|
|
24
|
+
});
|
|
25
|
+
child.on("close", (code) => {
|
|
26
|
+
if (code !== 0) {
|
|
27
|
+
console.error(stderr.split("\n").slice(-25).join("\n"));
|
|
28
|
+
throw new Error(`ffmpeg exited with ${code}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return child;
|
|
32
|
+
}
|
|
33
|
+
function mp4Args(fps, out) {
|
|
34
|
+
return [
|
|
35
|
+
"-y",
|
|
36
|
+
"-f",
|
|
37
|
+
"image2pipe",
|
|
38
|
+
"-framerate",
|
|
39
|
+
String(fps),
|
|
40
|
+
"-c:v",
|
|
41
|
+
"png",
|
|
42
|
+
"-i",
|
|
43
|
+
"-",
|
|
44
|
+
"-an",
|
|
45
|
+
"-c:v",
|
|
46
|
+
"libx264",
|
|
47
|
+
"-preset",
|
|
48
|
+
"slow",
|
|
49
|
+
"-crf",
|
|
50
|
+
"18",
|
|
51
|
+
"-pix_fmt",
|
|
52
|
+
"yuv420p",
|
|
53
|
+
"-profile:v",
|
|
54
|
+
"high",
|
|
55
|
+
// Lets a browser start playing before the whole file has downloaded.
|
|
56
|
+
"-movflags",
|
|
57
|
+
"+faststart",
|
|
58
|
+
out,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
function webmArgs(fps, out) {
|
|
62
|
+
return [
|
|
63
|
+
"-y",
|
|
64
|
+
"-f",
|
|
65
|
+
"image2pipe",
|
|
66
|
+
"-framerate",
|
|
67
|
+
String(fps),
|
|
68
|
+
"-c:v",
|
|
69
|
+
"png",
|
|
70
|
+
"-i",
|
|
71
|
+
"-",
|
|
72
|
+
"-an",
|
|
73
|
+
"-c:v",
|
|
74
|
+
"libvpx-vp9",
|
|
75
|
+
"-crf",
|
|
76
|
+
"30",
|
|
77
|
+
"-b:v",
|
|
78
|
+
"0",
|
|
79
|
+
"-row-mt",
|
|
80
|
+
"1",
|
|
81
|
+
out,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
function writeAll(stream, buffer) {
|
|
85
|
+
return new Promise((resolveWrite, rejectWrite) => {
|
|
86
|
+
// Respect backpressure: ffmpeg is slower than the screenshot loop for the
|
|
87
|
+
// first few seconds and an unbounded queue would hold every frame of the
|
|
88
|
+
// clip in memory at once.
|
|
89
|
+
const ok = stream.write(buffer, (error) => error ? rejectWrite(error) : undefined);
|
|
90
|
+
if (ok)
|
|
91
|
+
resolveWrite();
|
|
92
|
+
else
|
|
93
|
+
stream.once("drain", () => resolveWrite());
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function closeEncoder(child) {
|
|
97
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
98
|
+
child.on("close", (code) => code === 0
|
|
99
|
+
? resolveClose()
|
|
100
|
+
: rejectClose(new Error(`ffmpeg exited ${code}`)));
|
|
101
|
+
child.stdin?.end();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Walk the render page frame by frame, screenshotting each one into both
|
|
106
|
+
* encoders. The page is a pure function of the frame number, so this loop can
|
|
107
|
+
* be re-run, interrupted or parallelised without changing its output.
|
|
108
|
+
*/
|
|
109
|
+
export async function encodeFromPage(page, options) {
|
|
110
|
+
const outDir = options.outDir;
|
|
111
|
+
mkdirSync(outDir, { recursive: true });
|
|
112
|
+
const mp4 = join(outDir, `${options.id}.mp4`);
|
|
113
|
+
const webm = join(outDir, `${options.id}.webm`);
|
|
114
|
+
const poster = join(outDir, `${options.id}-poster.png`);
|
|
115
|
+
const posterFrame = options.posterFrame ?? Math.floor(options.durationFrames / 3);
|
|
116
|
+
const h264 = startEncoder(mp4Args(options.fps, mp4));
|
|
117
|
+
const vp9 = startEncoder(webmArgs(options.fps, webm));
|
|
118
|
+
const stage = page.locator("#stage");
|
|
119
|
+
for (let frame = 0; frame < options.durationFrames; frame += 1) {
|
|
120
|
+
await page.evaluate((n) => window.__render?.setFrame(n), frame);
|
|
121
|
+
const buffer = await stage.screenshot({
|
|
122
|
+
type: "png",
|
|
123
|
+
animations: "disabled",
|
|
124
|
+
});
|
|
125
|
+
if (frame === posterFrame)
|
|
126
|
+
writeFileSync(poster, buffer);
|
|
127
|
+
await writeAll(h264.stdin, buffer);
|
|
128
|
+
await writeAll(vp9.stdin, buffer);
|
|
129
|
+
}
|
|
130
|
+
await Promise.all([closeEncoder(h264), closeEncoder(vp9)]);
|
|
131
|
+
return { frames: options.durationFrames, mp4, webm, poster };
|
|
132
|
+
}
|
|
133
|
+
/** A strip of frames, for reviewing a clip in a pull request. */
|
|
134
|
+
export async function contactSheet(page, options) {
|
|
135
|
+
const outDir = options.outDir;
|
|
136
|
+
mkdirSync(outDir, { recursive: true });
|
|
137
|
+
const frames = options.frames ?? uniformFrames(options.durationFrames, options.count);
|
|
138
|
+
const tileDir = join(outDir, `${options.id}-sheet`);
|
|
139
|
+
mkdirSync(tileDir, { recursive: true });
|
|
140
|
+
const stage = page.locator("#stage");
|
|
141
|
+
const tiles = [];
|
|
142
|
+
for (const [index, frame] of frames.entries()) {
|
|
143
|
+
await page.evaluate((n) => window.__render?.setFrame(n), frame);
|
|
144
|
+
const file = join(tileDir, `${String(index).padStart(2, "0")}.png`);
|
|
145
|
+
writeFileSync(file, await stage.screenshot({ type: "png" }));
|
|
146
|
+
tiles.push(file);
|
|
147
|
+
}
|
|
148
|
+
const sheet = join(outDir, `${options.id}-sheet.png`);
|
|
149
|
+
await new Promise((resolveSheet, rejectSheet) => {
|
|
150
|
+
// `tile` composites frames of ONE stream, so the tiles are fed in as a
|
|
151
|
+
// numbered image sequence rather than as N separate inputs.
|
|
152
|
+
// Two rows once there are enough tiles to fill them, one row before that.
|
|
153
|
+
// `tile` pads a grid it cannot fill, so three frames in a 2x2 gave a fourth
|
|
154
|
+
// cell holding a repeat of the third — a tile a reviewer has to work out is
|
|
155
|
+
// not evidence, which is exactly the work a contact sheet exists to save.
|
|
156
|
+
const rows = frames.length > 4 ? 2 : 1;
|
|
157
|
+
const columns = Math.ceil(frames.length / rows);
|
|
158
|
+
const child = spawn(ffmpegPath(), [
|
|
159
|
+
"-y",
|
|
160
|
+
"-framerate",
|
|
161
|
+
"1",
|
|
162
|
+
"-i",
|
|
163
|
+
join(tileDir, "%02d.png"),
|
|
164
|
+
"-vf",
|
|
165
|
+
`scale=960:-1,tile=${columns}x${rows}`,
|
|
166
|
+
"-frames:v",
|
|
167
|
+
"1",
|
|
168
|
+
sheet,
|
|
169
|
+
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
170
|
+
let stderr = "";
|
|
171
|
+
child.stderr.on("data", (c) => {
|
|
172
|
+
stderr += c.toString();
|
|
173
|
+
});
|
|
174
|
+
child.on("close", (code) => {
|
|
175
|
+
if (code === 0)
|
|
176
|
+
resolveSheet();
|
|
177
|
+
else
|
|
178
|
+
rejectSheet(new Error(`contact sheet failed:\n${stderr.slice(-800)}`));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
return sheet;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","sourceRoot":"","sources":["../../src/render/encode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function ffmpegPath(): string;
|
|
2
|
+
/**
|
|
3
|
+
* ffprobe, which `ffmpeg-static` does not ship — it is a separate package.
|
|
4
|
+
* Only the system one is looked for; without it, duration checks are
|
|
5
|
+
* unavailable rather than wrong.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ffprobePath(): string;
|
|
8
|
+
export declare function ffprobeSync(args: string[]): string;
|
|
9
|
+
/** For `versioncam doctor`: report rather than throw. */
|
|
10
|
+
export declare function ffmpegStatus(): {
|
|
11
|
+
ffmpeg: string | null;
|
|
12
|
+
ffprobe: string | null;
|
|
13
|
+
};
|