@motionscript/headless 0.0.0-stage → 0.1.0-alpha.0

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.
@@ -0,0 +1,293 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { chromium } from 'playwright';
5
+ import { EngineError } from './errors.js';
6
+ import { bundlePage } from './page-bundle.js';
7
+ /** How long to wait for the bundle to evaluate and publish the bridge. */
8
+ const BRIDGE_READY_TIMEOUT_MS = 60_000;
9
+ /**
10
+ * A synthetic origin the page is served from.
11
+ *
12
+ * A real origin rather than `about:blank` or a `data:` URL, because those cannot
13
+ * carry response headers and cannot host relative asset URLs — and rather than a
14
+ * localhost server, because a socket means a port, and a port means the
15
+ * free-port-then-bind race the previous driver wrote twenty lines of comment
16
+ * about. `page.route` fulfils from memory with neither.
17
+ */
18
+ const ORIGIN = 'https://motion-script.local';
19
+ /**
20
+ * Renders a project from Node by driving the **browser** engine in a headless
21
+ * page.
22
+ *
23
+ * The point is parity, not portability: `Canvas3D` draws nothing under the
24
+ * in-process CPU path and `loadAudio` throws there, so a server export and a
25
+ * user's preview are currently different pictures. Here they are the same code.
26
+ *
27
+ * ### It is an RPC client, not a `core.Engine`
28
+ *
29
+ * The five platform seams live *inside the page*; Node holds a proxy. So this
30
+ * deliberately does not extend `Engine` — it exposes the same operations with the
31
+ * same names, and every one of them is async because each is a round trip.
32
+ * Unifying the two behind one interface is worth doing and is not done here.
33
+ *
34
+ * ### Bytes cross base64-encoded
35
+ *
36
+ * Playwright cannot round-trip a `Uint8Array` through `evaluate`, so a frame
37
+ * comes back a third larger than it is. Fine per still; sized before trusting it
38
+ * for a long video.
39
+ */
40
+ export class MotionScriptServerEngine {
41
+ options;
42
+ browser = null;
43
+ page = null;
44
+ starting = null;
45
+ closed = false;
46
+ constructor(options = {}) {
47
+ this.options = options;
48
+ }
49
+ get started() {
50
+ return this.page !== null;
51
+ }
52
+ /** Bundle the project, launch the browser, and wait for the bridge. Idempotent. */
53
+ start() {
54
+ if (this.closed)
55
+ throw new EngineError('CLOSED', 'The engine was closed.');
56
+ if (this.page)
57
+ return Promise.resolve();
58
+ this.starting ??= this.boot().catch((err) => {
59
+ this.starting = null;
60
+ throw err;
61
+ });
62
+ return this.starting;
63
+ }
64
+ /** The default project's track ids, in paint order. */
65
+ async listTracks() {
66
+ const page = await this.ready();
67
+ return page.evaluate(() => globalThis.__motionScript.listTracks());
68
+ }
69
+ async projectName() {
70
+ const page = await this.ready();
71
+ return page.evaluate(() => globalThis.__motionScript.projectName);
72
+ }
73
+ async fps() {
74
+ const page = await this.ready();
75
+ return page.evaluate(() => globalThis.__motionScript.fps);
76
+ }
77
+ /** Render one frame and bring back its encoded bytes. */
78
+ async renderImage(options = {}) {
79
+ const page = await this.ready();
80
+ const request = {
81
+ document: options.document,
82
+ frame: options.frame ?? 'last',
83
+ scale: options.scale,
84
+ format: options.format,
85
+ };
86
+ const result = await page.evaluate((req) => globalThis.__motionScript.screenshot(req), request);
87
+ return {
88
+ bytes: Buffer.from(result.base64, 'base64'),
89
+ frame: result.frame,
90
+ totalFrames: result.totalFrames,
91
+ };
92
+ }
93
+ /** Render the timeline to an encoded video. */
94
+ async renderVideo(options = {}) {
95
+ const page = await this.ready();
96
+ const base64 = await page.evaluate((req) => globalThis.__motionScript.exportVideo(req), options);
97
+ return Buffer.from(base64, 'base64');
98
+ }
99
+ async close() {
100
+ this.closed = true;
101
+ await this.page?.close().catch(() => undefined);
102
+ await this.browser?.close().catch(() => undefined);
103
+ this.page = null;
104
+ this.browser = null;
105
+ }
106
+ async ready() {
107
+ await this.start();
108
+ if (!this.page)
109
+ throw new EngineError('START_FAILED', 'The page failed to start.');
110
+ return this.page;
111
+ }
112
+ async boot() {
113
+ const projectRoot = this.options.projectRoot ?? process.cwd();
114
+ const entry = this.options.entry ?? path.join(projectRoot, 'src', 'project.ts');
115
+ const bundle = await bundlePage({ entry, cacheDir: this.options.cacheDir });
116
+ this.browser = await chromium.launch(this.launchOptions());
117
+ const page = await this.browser.newPage();
118
+ page.on('pageerror', (err) => {
119
+ process.stderr.write(`[motion-script page] ${err.message}\n`);
120
+ });
121
+ // Forwarded too, not just uncaught errors: a render that silently draws
122
+ // nothing reports itself through `console.warn` (a missing font, an
123
+ // undeclared asset), and losing that leaves an all-black frame with no
124
+ // explanation anywhere on the Node side.
125
+ page.on('console', (message) => {
126
+ if (message.type() !== 'error' && message.type() !== 'warning')
127
+ return;
128
+ if (this.options.onPageMessage) {
129
+ this.options.onPageMessage({ type: message.type(), text: message.text() });
130
+ return;
131
+ }
132
+ process.stderr.write(`[motion-script page] ${message.type()}: ${message.text()}\n`);
133
+ });
134
+ const assetsDir = this.options.assetsDir ?? path.join(projectRoot, 'public');
135
+ await page.route(`${ORIGIN}/**`, async (route) => {
136
+ const url = new URL(route.request().url());
137
+ if (url.pathname === '/') {
138
+ return route.fulfill({
139
+ status: 200,
140
+ contentType: 'text/html; charset=utf-8',
141
+ body: '<!doctype html><meta charset="utf-8"><title>motion-script</title><body></body>',
142
+ });
143
+ }
144
+ if (url.pathname === '/canvaskit.wasm') {
145
+ return route.fulfill({
146
+ status: 200,
147
+ contentType: 'application/wasm',
148
+ body: fs.readFileSync(canvasKitWasmPath()),
149
+ });
150
+ }
151
+ if (url.pathname === '/esbuild.wasm') {
152
+ // Only ever fetched if a project actually registers a
153
+ // source-string node — browser's own lazy default compiler
154
+ // (dynamic-nodes.ts) doesn't import esbuild-wasm at all
155
+ // otherwise.
156
+ return route.fulfill({
157
+ status: 200,
158
+ contentType: 'application/wasm',
159
+ body: fs.readFileSync(esbuildWasmPath()),
160
+ });
161
+ }
162
+ if (url.pathname === '/bundle.js') {
163
+ return route.fulfill({
164
+ status: 200,
165
+ contentType: 'text/javascript; charset=utf-8',
166
+ body: bundle.code,
167
+ });
168
+ }
169
+ const extra = this.options.serve?.[url.pathname];
170
+ if (extra) {
171
+ return route.fulfill({
172
+ status: 200,
173
+ body: fs.readFileSync(extra),
174
+ contentType: contentTypeOf(extra),
175
+ });
176
+ }
177
+ return serveFile(route, assetsDir, url.pathname);
178
+ });
179
+ await page.goto(`${ORIGIN}/`, { waitUntil: 'load' });
180
+ await page.addScriptTag({ url: `${ORIGIN}/bundle.js` });
181
+ await page.waitForSelector('html[data-motion-script]', { timeout: BRIDGE_READY_TIMEOUT_MS, state: 'attached' }).catch(() => {
182
+ throw new EngineError('BRIDGE_TIMEOUT', `The page did not publish a bridge within ${BRIDGE_READY_TIMEOUT_MS}ms.`);
183
+ });
184
+ const state = await page.getAttribute('html', 'data-motion-script');
185
+ if (state === 'failed') {
186
+ const detail = await page.getAttribute('html', 'data-motion-script-error');
187
+ throw new EngineError('START_FAILED', `The project entry failed to load: ${detail}`);
188
+ }
189
+ // Checked by capability rather than a version number: the driver and the
190
+ // page bundle are built from the same source tree here, but a stale cache
191
+ // entry is exactly the case a number would fail to catch.
192
+ const missing = await page.evaluate(() => {
193
+ const bridge = globalThis.__motionScript;
194
+ if (!bridge)
195
+ return 'bridge not installed';
196
+ const required = [];
197
+ if (typeof bridge.screenshot !== 'function')
198
+ required.push('screenshot()');
199
+ if (typeof bridge.exportVideo !== 'function')
200
+ required.push('exportVideo()');
201
+ if (typeof bridge.listTracks !== 'function')
202
+ required.push('listTracks()');
203
+ if (typeof bridge.fps !== 'number')
204
+ required.push('fps');
205
+ return required.length > 0 ? required.join(', ') : null;
206
+ });
207
+ if (missing) {
208
+ throw new EngineError('BRIDGE_INCOMPATIBLE', `The page bridge is missing: ${missing}.`);
209
+ }
210
+ this.page = page;
211
+ if (this.options.manifest) {
212
+ await page.evaluate((m) => globalThis.__motionScript.setManifest(m), this.options.manifest);
213
+ }
214
+ }
215
+ /**
216
+ * Chromium flags, and why each is load-bearing.
217
+ *
218
+ * `--headless=new` with `headless: false`: Playwright's `headless: true`
219
+ * selects the *old* headless mode, which always falls back to SwiftShader —
220
+ * so an export there is far slower than the same render in a real browser.
221
+ *
222
+ * The GPU trio is not about speed. `packages/browser/vitest.config.ts` documents
223
+ * that without them headless Chromium has no GPU process to encode video
224
+ * with, and the failure is disguised: `VideoEncoder.isConfigSupported` still
225
+ * answers `supported: true`, then the first real `encode()` takes the whole
226
+ * page down with an error nowhere near the cause.
227
+ */
228
+ launchOptions() {
229
+ const software = this.options.softwareRender ?? process.env.MS_SOFTWARE_RENDER === '1';
230
+ const gpu = ['--enable-gpu', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader'];
231
+ if (software) {
232
+ return { args: [...gpu, '--use-gl=swiftshader', '--no-sandbox', '--headless=new'], headless: false };
233
+ }
234
+ return { args: [...gpu, '--headless=new'], headless: false };
235
+ }
236
+ }
237
+ /**
238
+ * The custom CanvasKit binary, resolved from the installed package.
239
+ *
240
+ * The only reason this package still depends on `@motionscript/canvaskit`: it
241
+ * serves the `.wasm` file to the page. Nothing here *runs* CanvasKit — the
242
+ * renderer is the page's — so the dependency is a file, not a library, and the
243
+ * binary must be the custom build the glue in `@motionscript/browser` expects
244
+ * (the stock one fails to link).
245
+ */
246
+ function canvasKitWasmPath() {
247
+ const url = import.meta.resolve('@motionscript/canvaskit/canvaskit.wasm');
248
+ return fileURLToPath(url);
249
+ }
250
+ /** Same resolution trick as `canvasKitWasmPath`, for `esbuild-wasm`'s own binary. */
251
+ function esbuildWasmPath() {
252
+ const url = import.meta.resolve('esbuild-wasm/esbuild.wasm');
253
+ return fileURLToPath(url);
254
+ }
255
+ /** Serve one file from the assets directory, or 404. */
256
+ async function serveFile(route, assetsDir, pathname) {
257
+ const rel = decodeURIComponent(pathname.replace(/^\//, ''));
258
+ const file = path.resolve(assetsDir, rel);
259
+ // A traversal out of the assets directory is a 404 rather than a read: the
260
+ // page is ours, but the paths in it come from a document.
261
+ if (!file.startsWith(path.resolve(assetsDir))) {
262
+ return route.fulfill({ status: 404, body: 'outside the assets directory' });
263
+ }
264
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
265
+ return route.fulfill({ status: 404, body: 'not found' });
266
+ }
267
+ return route.fulfill({ status: 200, body: fs.readFileSync(file), contentType: contentTypeOf(file) });
268
+ }
269
+ const CONTENT_TYPES = {
270
+ '.wasm': 'application/wasm',
271
+ '.js': 'text/javascript; charset=utf-8',
272
+ '.json': 'application/json; charset=utf-8',
273
+ '.png': 'image/png',
274
+ '.jpg': 'image/jpeg',
275
+ '.jpeg': 'image/jpeg',
276
+ '.webp': 'image/webp',
277
+ '.gif': 'image/gif',
278
+ '.mp4': 'video/mp4',
279
+ '.webm': 'video/webm',
280
+ '.mp3': 'audio/mpeg',
281
+ '.wav': 'audio/wav',
282
+ '.ttf': 'font/ttf',
283
+ '.otf': 'font/otf',
284
+ '.woff2': 'font/woff2',
285
+ };
286
+ function contentTypeOf(file) {
287
+ return CONTENT_TYPES[path.extname(file).toLowerCase()] ?? 'application/octet-stream';
288
+ }
289
+ /** Build a server engine. Call `start()` (or any render) to boot it. */
290
+ export function createServerEngine(options = {}) {
291
+ return new MotionScriptServerEngine(options);
292
+ }
293
+ //# sourceMappingURL=server-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-engine.js","sourceRoot":"","sources":["../src/server-engine.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { chromium, type Browser, type Page } from 'playwright';\nimport { EngineError } from './errors.js';\nimport { bundlePage } from './page-bundle.js';\nimport type { Bridge, ExportRequest, ScreenshotRequest } from './page-bridge.js';\n\n/**\n * How the page's bridge is reached from an `evaluate` callback.\n *\n * `globalThis`, not `window`: the callback's *source* is compiled here, where\n * this package deliberately has no DOM lib, and in the page the two are the same\n * object anyway.\n */\ntype BridgeHost = typeof globalThis & { __motionScript?: Bridge };\n\n/** How long to wait for the bundle to evaluate and publish the bridge. */\nconst BRIDGE_READY_TIMEOUT_MS = 60_000;\n\n/**\n * A synthetic origin the page is served from.\n *\n * A real origin rather than `about:blank` or a `data:` URL, because those cannot\n * carry response headers and cannot host relative asset URLs — and rather than a\n * localhost server, because a socket means a port, and a port means the\n * free-port-then-bind race the previous driver wrote twenty lines of comment\n * about. `page.route` fulfils from memory with neither.\n */\nconst ORIGIN = 'https://motion-script.local';\n\nexport interface ServerEngineOptions {\n /** The project entry to render. Defaults to `<projectRoot>/src/project.ts`. */\n entry?: string;\n /** Project root, used to find the entry and the assets directory. */\n projectRoot?: string;\n /** Directory served at the origin root, for the assets a scene names. */\n assetsDir?: string;\n /**\n * Force software rasterization. Set for a deterministic pixel-diff; leave off\n * for speed.\n *\n * A GPU frame and a SwiftShader frame **do not match**, so anything comparing\n * renders must fix this and record which mode produced a baseline. Also\n * honoured from `MS_SOFTWARE_RENDER=1`.\n */\n softwareRender?: boolean;\n /** Where the built page bundle is cached. */\n cacheDir?: string;\n /**\n * Asset metadata the page's catalog resolves `src` values against.\n *\n * Required for fonts: an undeclared family is never fetched, and the browser\n * shapes against a fallback rather than failing.\n */\n manifest?: unknown;\n /** Extra files to serve at the origin, by path. Overrides `assetsDir`. */\n serve?: Record<string, string>;\n /**\n * Called for every warning and error the page logs.\n *\n * The reason this exists rather than the driver just writing them to stderr:\n * a browser's failures are mostly **soft**. An undeclared font shapes against\n * a fallback, an undeclared image draws nothing — each a warning and a wrong\n * frame, where the Node path would have thrown. A harness that compares\n * pixels has to be able to turn those back into failures, and only it knows\n * which ones matter.\n */\n onPageMessage?: (message: { type: string; text: string }) => void;\n}\n\nexport interface ServerImageOptions {\n /** The document to render, as JSON. See {@link ScreenshotRequest.document}. */\n document?: unknown;\n frame?: number | 'first' | 'last';\n scale?: number;\n format?: 'png' | 'jpeg';\n}\n\nexport interface ServerImage {\n bytes: Uint8Array;\n frame: number;\n totalFrames: number;\n}\n\n/**\n * Renders a project from Node by driving the **browser** engine in a headless\n * page.\n *\n * The point is parity, not portability: `Canvas3D` draws nothing under the\n * in-process CPU path and `loadAudio` throws there, so a server export and a\n * user's preview are currently different pictures. Here they are the same code.\n *\n * ### It is an RPC client, not a `core.Engine`\n *\n * The five platform seams live *inside the page*; Node holds a proxy. So this\n * deliberately does not extend `Engine` — it exposes the same operations with the\n * same names, and every one of them is async because each is a round trip.\n * Unifying the two behind one interface is worth doing and is not done here.\n *\n * ### Bytes cross base64-encoded\n *\n * Playwright cannot round-trip a `Uint8Array` through `evaluate`, so a frame\n * comes back a third larger than it is. Fine per still; sized before trusting it\n * for a long video.\n */\nexport class MotionScriptServerEngine {\n private browser: Browser | null = null;\n private page: Page | null = null;\n private starting: Promise<void> | null = null;\n private closed = false;\n\n constructor(private readonly options: ServerEngineOptions = {}) { }\n\n get started(): boolean {\n return this.page !== null;\n }\n\n /** Bundle the project, launch the browser, and wait for the bridge. Idempotent. */\n start(): Promise<void> {\n if (this.closed) throw new EngineError('CLOSED', 'The engine was closed.');\n if (this.page) return Promise.resolve();\n this.starting ??= this.boot().catch((err: unknown) => {\n this.starting = null;\n throw err;\n });\n return this.starting;\n }\n\n /** The default project's track ids, in paint order. */\n async listTracks(): Promise<string[]> {\n const page = await this.ready();\n return page.evaluate(() => (globalThis as BridgeHost).__motionScript!.listTracks());\n }\n\n async projectName(): Promise<string> {\n const page = await this.ready();\n return page.evaluate(() => (globalThis as BridgeHost).__motionScript!.projectName);\n }\n\n async fps(): Promise<number> {\n const page = await this.ready();\n return page.evaluate(() => (globalThis as BridgeHost).__motionScript!.fps);\n }\n\n /** Render one frame and bring back its encoded bytes. */\n async renderImage(options: ServerImageOptions = {}): Promise<ServerImage> {\n const page = await this.ready();\n const request: ScreenshotRequest = {\n document: options.document,\n frame: options.frame ?? 'last',\n scale: options.scale,\n format: options.format,\n };\n const result = await page.evaluate(\n (req: ScreenshotRequest) => (globalThis as BridgeHost).__motionScript!.screenshot(req),\n request,\n );\n return {\n bytes: Buffer.from(result.base64, 'base64'),\n frame: result.frame,\n totalFrames: result.totalFrames,\n };\n }\n\n /** Render the timeline to an encoded video. */\n async renderVideo(options: ExportRequest & { scale?: number } = {}): Promise<Uint8Array> {\n const page = await this.ready();\n const base64 = await page.evaluate(\n (req: ExportRequest) => (globalThis as BridgeHost).__motionScript!.exportVideo(req),\n options,\n );\n return Buffer.from(base64, 'base64');\n }\n\n async close(): Promise<void> {\n this.closed = true;\n await this.page?.close().catch(() => undefined);\n await this.browser?.close().catch(() => undefined);\n this.page = null;\n this.browser = null;\n }\n\n private async ready(): Promise<Page> {\n await this.start();\n if (!this.page) throw new EngineError('START_FAILED', 'The page failed to start.');\n return this.page;\n }\n\n private async boot(): Promise<void> {\n const projectRoot = this.options.projectRoot ?? process.cwd();\n const entry = this.options.entry ?? path.join(projectRoot, 'src', 'project.ts');\n const bundle = await bundlePage({ entry, cacheDir: this.options.cacheDir });\n\n this.browser = await chromium.launch(this.launchOptions());\n const page = await this.browser.newPage();\n\n page.on('pageerror', (err) => {\n process.stderr.write(`[motion-script page] ${err.message}\\n`);\n });\n // Forwarded too, not just uncaught errors: a render that silently draws\n // nothing reports itself through `console.warn` (a missing font, an\n // undeclared asset), and losing that leaves an all-black frame with no\n // explanation anywhere on the Node side.\n page.on('console', (message) => {\n if (message.type() !== 'error' && message.type() !== 'warning') return;\n if (this.options.onPageMessage) {\n this.options.onPageMessage({ type: message.type(), text: message.text() });\n return;\n }\n process.stderr.write(`[motion-script page] ${message.type()}: ${message.text()}\\n`);\n });\n\n const assetsDir = this.options.assetsDir ?? path.join(projectRoot, 'public');\n await page.route(`${ORIGIN}/**`, async (route) => {\n const url = new URL(route.request().url());\n if (url.pathname === '/') {\n return route.fulfill({\n status: 200,\n contentType: 'text/html; charset=utf-8',\n body: '<!doctype html><meta charset=\"utf-8\"><title>motion-script</title><body></body>',\n });\n }\n if (url.pathname === '/canvaskit.wasm') {\n return route.fulfill({\n status: 200,\n contentType: 'application/wasm',\n body: fs.readFileSync(canvasKitWasmPath()),\n });\n }\n if (url.pathname === '/esbuild.wasm') {\n // Only ever fetched if a project actually registers a\n // source-string node — browser's own lazy default compiler\n // (dynamic-nodes.ts) doesn't import esbuild-wasm at all\n // otherwise.\n return route.fulfill({\n status: 200,\n contentType: 'application/wasm',\n body: fs.readFileSync(esbuildWasmPath()),\n });\n }\n if (url.pathname === '/bundle.js') {\n return route.fulfill({\n status: 200,\n contentType: 'text/javascript; charset=utf-8',\n body: bundle.code,\n });\n }\n const extra = this.options.serve?.[url.pathname];\n if (extra) {\n return route.fulfill({\n status: 200,\n body: fs.readFileSync(extra),\n contentType: contentTypeOf(extra),\n });\n }\n return serveFile(route, assetsDir, url.pathname);\n });\n\n await page.goto(`${ORIGIN}/`, { waitUntil: 'load' });\n await page.addScriptTag({ url: `${ORIGIN}/bundle.js` });\n\n await page.waitForSelector(\n 'html[data-motion-script]',\n { timeout: BRIDGE_READY_TIMEOUT_MS, state: 'attached' },\n ).catch(() => {\n throw new EngineError(\n 'BRIDGE_TIMEOUT',\n `The page did not publish a bridge within ${BRIDGE_READY_TIMEOUT_MS}ms.`,\n );\n });\n\n const state = await page.getAttribute('html', 'data-motion-script');\n if (state === 'failed') {\n const detail = await page.getAttribute('html', 'data-motion-script-error');\n throw new EngineError('START_FAILED', `The project entry failed to load: ${detail}`);\n }\n\n // Checked by capability rather than a version number: the driver and the\n // page bundle are built from the same source tree here, but a stale cache\n // entry is exactly the case a number would fail to catch.\n const missing = await page.evaluate(() => {\n const bridge = (globalThis as unknown as { __motionScript?: Partial<Bridge> }).__motionScript;\n if (!bridge) return 'bridge not installed';\n const required: string[] = [];\n if (typeof bridge.screenshot !== 'function') required.push('screenshot()');\n if (typeof bridge.exportVideo !== 'function') required.push('exportVideo()');\n if (typeof bridge.listTracks !== 'function') required.push('listTracks()');\n if (typeof bridge.fps !== 'number') required.push('fps');\n return required.length > 0 ? required.join(', ') : null;\n });\n if (missing) {\n throw new EngineError('BRIDGE_INCOMPATIBLE', `The page bridge is missing: ${missing}.`);\n }\n\n this.page = page;\n\n if (this.options.manifest) {\n await page.evaluate(\n (m: unknown) => (globalThis as BridgeHost).__motionScript!.setManifest(m),\n this.options.manifest,\n );\n }\n }\n\n /**\n * Chromium flags, and why each is load-bearing.\n *\n * `--headless=new` with `headless: false`: Playwright's `headless: true`\n * selects the *old* headless mode, which always falls back to SwiftShader —\n * so an export there is far slower than the same render in a real browser.\n *\n * The GPU trio is not about speed. `packages/browser/vitest.config.ts` documents\n * that without them headless Chromium has no GPU process to encode video\n * with, and the failure is disguised: `VideoEncoder.isConfigSupported` still\n * answers `supported: true`, then the first real `encode()` takes the whole\n * page down with an error nowhere near the cause.\n */\n private launchOptions(): Parameters<typeof chromium.launch>[0] {\n const software = this.options.softwareRender ?? process.env.MS_SOFTWARE_RENDER === '1';\n const gpu = ['--enable-gpu', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader'];\n if (software) {\n return { args: [...gpu, '--use-gl=swiftshader', '--no-sandbox', '--headless=new'], headless: false };\n }\n return { args: [...gpu, '--headless=new'], headless: false };\n }\n}\n\n/**\n * The custom CanvasKit binary, resolved from the installed package.\n *\n * The only reason this package still depends on `@motionscript/canvaskit`: it\n * serves the `.wasm` file to the page. Nothing here *runs* CanvasKit — the\n * renderer is the page's — so the dependency is a file, not a library, and the\n * binary must be the custom build the glue in `@motionscript/browser` expects\n * (the stock one fails to link).\n */\nfunction canvasKitWasmPath(): string {\n const url = import.meta.resolve('@motionscript/canvaskit/canvaskit.wasm');\n return fileURLToPath(url);\n}\n\n/** Same resolution trick as `canvasKitWasmPath`, for `esbuild-wasm`'s own binary. */\nfunction esbuildWasmPath(): string {\n const url = import.meta.resolve('esbuild-wasm/esbuild.wasm');\n return fileURLToPath(url);\n}\n\n/** Serve one file from the assets directory, or 404. */\nasync function serveFile(\n route: Parameters<Parameters<Page['route']>[1]>[0],\n assetsDir: string,\n pathname: string,\n): Promise<void> {\n const rel = decodeURIComponent(pathname.replace(/^\\//, ''));\n const file = path.resolve(assetsDir, rel);\n // A traversal out of the assets directory is a 404 rather than a read: the\n // page is ours, but the paths in it come from a document.\n if (!file.startsWith(path.resolve(assetsDir))) {\n return route.fulfill({ status: 404, body: 'outside the assets directory' });\n }\n if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {\n return route.fulfill({ status: 404, body: 'not found' });\n }\n return route.fulfill({ status: 200, body: fs.readFileSync(file), contentType: contentTypeOf(file) });\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.wasm': 'application/wasm',\n '.js': 'text/javascript; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.gif': 'image/gif',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.woff2': 'font/woff2',\n};\n\nfunction contentTypeOf(file: string): string {\n return CONTENT_TYPES[path.extname(file).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Build a server engine. Call `start()` (or any render) to boot it. */\nexport function createServerEngine(options: ServerEngineOptions = {}): MotionScriptServerEngine {\n return new MotionScriptServerEngine(options);\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /** Video codecs the page's encoder can be asked for. */
2
+ export type VideoCodec = 'avc' | 'hevc' | 'av1' | 'vp9';
3
+ /** Image formats a still can be encoded as. `'jpg'` is accepted as an alias of `'jpeg'`. */
4
+ export type ImageFormat = 'png' | 'jpeg' | 'jpg';
5
+ /**
6
+ * Validate a codec name.
7
+ *
8
+ * Accepting free text here is the point: a service takes this from a request
9
+ * body, and rejecting it at the edge with `INVALID_OPTION` beats letting the
10
+ * page throw halfway through a render.
11
+ */
12
+ export declare function parseCodec(raw: unknown): VideoCodec | undefined;
13
+ /**
14
+ * Parse a bitrate in bits per second, accepting the `k`/`M` suffixes anyone
15
+ * would actually reach for — `40M` is how this number is written everywhere
16
+ * else, and `40000000` is easy to mistype by an order of magnitude.
17
+ */
18
+ export declare function parseBitrate(raw: unknown): number | undefined;
19
+ /** What a still is encoded as, plus the two labels a caller has to produce anyway. */
20
+ export interface ResolvedImageFormat {
21
+ format: 'png' | 'jpeg';
22
+ /** File extension without the dot, preserving a caller's `jpg` spelling. */
23
+ extension: 'png' | 'jpg' | 'jpeg';
24
+ mimeType: 'image/png' | 'image/jpeg';
25
+ }
26
+ export declare function parseImageFormat(raw: unknown): ResolvedImageFormat;
27
+ /** A resolution multiplier: any finite number above zero. */
28
+ export declare function parseScale(raw: unknown, label?: string): number;
29
+ /**
30
+ * Supersampling factor. Bounded at 4 because the cost is quadratic: at 5x a
31
+ * 1080p frame is rendered at 5400p, which is a way to turn a render into a
32
+ * timeout rather than a way to improve it.
33
+ */
34
+ export declare function parseSupersample(raw: unknown): number | undefined;
35
+ export declare function parseConcurrency(raw: unknown): number;
36
+ /** A millisecond budget. `0` means "no limit", which is why it is not simply falsy-checked. */
37
+ export declare function parseTimeout(raw: unknown, fallback: number): number;
38
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAExD,4FAA4F;AAC5F,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAQjD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAO/D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAY7D;AAED,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,4EAA4E;IAC5E,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAClC,QAAQ,EAAE,WAAW,GAAG,YAAY,CAAC;CACxC;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,CAelE;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAU,GAAG,MAAM,CAOhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOjE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAOrD;AAED,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOnE"}
@@ -0,0 +1,103 @@
1
+ import { EngineError } from './errors.js';
2
+ const VIDEO_CODECS = ['avc', 'hevc', 'av1', 'vp9'];
3
+ function invalid(message) {
4
+ return new EngineError('INVALID_OPTION', message);
5
+ }
6
+ /**
7
+ * Validate a codec name.
8
+ *
9
+ * Accepting free text here is the point: a service takes this from a request
10
+ * body, and rejecting it at the edge with `INVALID_OPTION` beats letting the
11
+ * page throw halfway through a render.
12
+ */
13
+ export function parseCodec(raw) {
14
+ if (raw === undefined || raw === null || raw === '')
15
+ return undefined;
16
+ const value = String(raw).trim().toLowerCase();
17
+ if (!VIDEO_CODECS.includes(value)) {
18
+ throw invalid(`Invalid codec: ${String(raw)}. Expected one of: ${VIDEO_CODECS.join(', ')}.`);
19
+ }
20
+ return value;
21
+ }
22
+ /**
23
+ * Parse a bitrate in bits per second, accepting the `k`/`M` suffixes anyone
24
+ * would actually reach for — `40M` is how this number is written everywhere
25
+ * else, and `40000000` is easy to mistype by an order of magnitude.
26
+ */
27
+ export function parseBitrate(raw) {
28
+ if (raw === undefined || raw === null || raw === '')
29
+ return undefined;
30
+ if (typeof raw === 'number') {
31
+ if (!Number.isFinite(raw) || raw <= 0)
32
+ throw invalid(`Invalid bitrate: ${raw}.`);
33
+ return Math.round(raw);
34
+ }
35
+ const match = /^\s*([0-9]*\.?[0-9]+)\s*([kKmM])?(?:bps|bit\/s)?\s*$/.exec(String(raw));
36
+ if (!match)
37
+ throw invalid(`Invalid bitrate: ${String(raw)}. Expected e.g. 12000000, 12000k or 12M.`);
38
+ const scale = match[2] === undefined ? 1 : (match[2].toLowerCase() === 'k' ? 1e3 : 1e6);
39
+ const bits = Math.round(Number(match[1]) * scale);
40
+ if (!Number.isFinite(bits) || bits <= 0)
41
+ throw invalid(`Invalid bitrate: ${String(raw)}.`);
42
+ return bits;
43
+ }
44
+ export function parseImageFormat(raw) {
45
+ if (raw === undefined || raw === null || raw === '') {
46
+ return { format: 'png', extension: 'png', mimeType: 'image/png' };
47
+ }
48
+ const value = String(raw).trim().toLowerCase().replace(/^\./, '');
49
+ switch (value) {
50
+ case 'png':
51
+ return { format: 'png', extension: 'png', mimeType: 'image/png' };
52
+ case 'jpg':
53
+ return { format: 'jpeg', extension: 'jpg', mimeType: 'image/jpeg' };
54
+ case 'jpeg':
55
+ return { format: 'jpeg', extension: 'jpeg', mimeType: 'image/jpeg' };
56
+ default:
57
+ throw invalid(`Unsupported image format: ${String(raw)} (supported: png, jpg, jpeg).`);
58
+ }
59
+ }
60
+ /** A resolution multiplier: any finite number above zero. */
61
+ export function parseScale(raw, label = 'scale') {
62
+ if (raw === undefined || raw === null)
63
+ return 1;
64
+ const value = Number(raw);
65
+ if (!Number.isFinite(value) || value <= 0) {
66
+ throw invalid(`Invalid ${label}: ${String(raw)}. Expected a number greater than 0.`);
67
+ }
68
+ return value;
69
+ }
70
+ /**
71
+ * Supersampling factor. Bounded at 4 because the cost is quadratic: at 5x a
72
+ * 1080p frame is rendered at 5400p, which is a way to turn a render into a
73
+ * timeout rather than a way to improve it.
74
+ */
75
+ export function parseSupersample(raw) {
76
+ if (raw === undefined || raw === null || raw === '')
77
+ return undefined;
78
+ const value = Number(raw);
79
+ if (!Number.isInteger(value) || value < 1 || value > 4) {
80
+ throw invalid(`Invalid supersample: ${String(raw)}. Expected a whole number from 1 to 4.`);
81
+ }
82
+ return value;
83
+ }
84
+ export function parseConcurrency(raw) {
85
+ if (raw === undefined || raw === null)
86
+ return 1;
87
+ const value = Number(raw);
88
+ if (!Number.isInteger(value) || value < 1) {
89
+ throw invalid(`Invalid concurrency: ${String(raw)}. Expected a whole number of 1 or more.`);
90
+ }
91
+ return value;
92
+ }
93
+ /** A millisecond budget. `0` means "no limit", which is why it is not simply falsy-checked. */
94
+ export function parseTimeout(raw, fallback) {
95
+ if (raw === undefined || raw === null)
96
+ return fallback;
97
+ const value = Number(raw);
98
+ if (!Number.isFinite(value) || value < 0) {
99
+ throw invalid(`Invalid timeout: ${String(raw)}. Expected a non-negative number of milliseconds.`);
100
+ }
101
+ return value;
102
+ }
103
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import { EngineError } from './errors.js';\n/** Video codecs the page's encoder can be asked for. */\nexport type VideoCodec = 'avc' | 'hevc' | 'av1' | 'vp9';\n\n/** Image formats a still can be encoded as. `'jpg'` is accepted as an alias of `'jpeg'`. */\nexport type ImageFormat = 'png' | 'jpeg' | 'jpg';\n\nconst VIDEO_CODECS: readonly VideoCodec[] = ['avc', 'hevc', 'av1', 'vp9'];\n\nfunction invalid(message: string): EngineError {\n return new EngineError('INVALID_OPTION', message);\n}\n\n/**\n * Validate a codec name.\n *\n * Accepting free text here is the point: a service takes this from a request\n * body, and rejecting it at the edge with `INVALID_OPTION` beats letting the\n * page throw halfway through a render.\n */\nexport function parseCodec(raw: unknown): VideoCodec | undefined {\n if (raw === undefined || raw === null || raw === '') return undefined;\n const value = String(raw).trim().toLowerCase();\n if (!(VIDEO_CODECS as readonly string[]).includes(value)) {\n throw invalid(`Invalid codec: ${String(raw)}. Expected one of: ${VIDEO_CODECS.join(', ')}.`);\n }\n return value as VideoCodec;\n}\n\n/**\n * Parse a bitrate in bits per second, accepting the `k`/`M` suffixes anyone\n * would actually reach for — `40M` is how this number is written everywhere\n * else, and `40000000` is easy to mistype by an order of magnitude.\n */\nexport function parseBitrate(raw: unknown): number | undefined {\n if (raw === undefined || raw === null || raw === '') return undefined;\n if (typeof raw === 'number') {\n if (!Number.isFinite(raw) || raw <= 0) throw invalid(`Invalid bitrate: ${raw}.`);\n return Math.round(raw);\n }\n const match = /^\\s*([0-9]*\\.?[0-9]+)\\s*([kKmM])?(?:bps|bit\\/s)?\\s*$/.exec(String(raw));\n if (!match) throw invalid(`Invalid bitrate: ${String(raw)}. Expected e.g. 12000000, 12000k or 12M.`);\n const scale = match[2] === undefined ? 1 : (match[2].toLowerCase() === 'k' ? 1e3 : 1e6);\n const bits = Math.round(Number(match[1]) * scale);\n if (!Number.isFinite(bits) || bits <= 0) throw invalid(`Invalid bitrate: ${String(raw)}.`);\n return bits;\n}\n\n/** What a still is encoded as, plus the two labels a caller has to produce anyway. */\nexport interface ResolvedImageFormat {\n format: 'png' | 'jpeg';\n /** File extension without the dot, preserving a caller's `jpg` spelling. */\n extension: 'png' | 'jpg' | 'jpeg';\n mimeType: 'image/png' | 'image/jpeg';\n}\n\nexport function parseImageFormat(raw: unknown): ResolvedImageFormat {\n if (raw === undefined || raw === null || raw === '') {\n return { format: 'png', extension: 'png', mimeType: 'image/png' };\n }\n const value = String(raw).trim().toLowerCase().replace(/^\\./, '');\n switch (value) {\n case 'png':\n return { format: 'png', extension: 'png', mimeType: 'image/png' };\n case 'jpg':\n return { format: 'jpeg', extension: 'jpg', mimeType: 'image/jpeg' };\n case 'jpeg':\n return { format: 'jpeg', extension: 'jpeg', mimeType: 'image/jpeg' };\n default:\n throw invalid(`Unsupported image format: ${String(raw)} (supported: png, jpg, jpeg).`);\n }\n}\n\n/** A resolution multiplier: any finite number above zero. */\nexport function parseScale(raw: unknown, label = 'scale'): number {\n if (raw === undefined || raw === null) return 1;\n const value = Number(raw);\n if (!Number.isFinite(value) || value <= 0) {\n throw invalid(`Invalid ${label}: ${String(raw)}. Expected a number greater than 0.`);\n }\n return value;\n}\n\n/**\n * Supersampling factor. Bounded at 4 because the cost is quadratic: at 5x a\n * 1080p frame is rendered at 5400p, which is a way to turn a render into a\n * timeout rather than a way to improve it.\n */\nexport function parseSupersample(raw: unknown): number | undefined {\n if (raw === undefined || raw === null || raw === '') return undefined;\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 1 || value > 4) {\n throw invalid(`Invalid supersample: ${String(raw)}. Expected a whole number from 1 to 4.`);\n }\n return value;\n}\n\nexport function parseConcurrency(raw: unknown): number {\n if (raw === undefined || raw === null) return 1;\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 1) {\n throw invalid(`Invalid concurrency: ${String(raw)}. Expected a whole number of 1 or more.`);\n }\n return value;\n}\n\n/** A millisecond budget. `0` means \"no limit\", which is why it is not simply falsy-checked. */\nexport function parseTimeout(raw: unknown, fallback: number): number {\n if (raw === undefined || raw === null) return fallback;\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0) {\n throw invalid(`Invalid timeout: ${String(raw)}. Expected a non-negative number of milliseconds.`);\n }\n return value;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@motionscript/headless",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Render Motion Script projects to images and video from Node, through the browser engine in a headless Chromium page.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/motionscript-dev/motionscript.git",
8
+ "directory": "packages/headless"
9
+ },
10
+ "homepage": "https://motionscript.dev",
11
+ "bugs": {
12
+ "url": "https://github.com/motionscript-dev/motionscript/issues"
13
+ },
14
+ "license": "Apache-2.0",
15
+ "type": "module",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "CHANGELOG.md",
30
+ "!dist/**/*.tsbuildinfo"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "dependencies": {
36
+ "esbuild": "^0.28.1",
37
+ "esbuild-wasm": "^0.28.1",
38
+ "playwright": "^1.61.0",
39
+ "@motionscript/core": "0.1.0-alpha.0",
40
+ "@motionscript/canvaskit": "0.1.0-alpha.0",
41
+ "@motionscript/browser": "0.1.0-alpha.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.6.2",
45
+ "eslint": "^9.39.4",
46
+ "rimraf": "^6.1.3",
47
+ "typescript": "^6.0.3",
48
+ "typescript-eslint": "^8.48.0",
49
+ "vitest": "^4.1.5"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json",
53
+ "dev": "tsc -p tsconfig.build.json --watch",
54
+ "clean": "rimraf --glob dist .turbo *.tsbuildinfo",
55
+ "lint": "eslint .",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.page.json",
57
+ "test": "vitest"
58
+ }
6
59
  }