route-graphics 1.15.0 → 1.16.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.
- package/dist/RouteGraphics.js +128 -126
- package/package.json +1 -1
- package/src/cli/renderVideo.js +152 -1
package/package.json
CHANGED
package/src/cli/renderVideo.js
CHANGED
|
@@ -10,6 +10,78 @@ import { chromium } from "playwright";
|
|
|
10
10
|
import { getRendererBrowserLaunchOptions } from "./browserLaunch.js";
|
|
11
11
|
import { parseStateSelection } from "./stateSelection.js";
|
|
12
12
|
|
|
13
|
+
const debugVideoRender = (...args) => {
|
|
14
|
+
if (process.env.ROUTE_GRAPHICS_RENDER_VIDEO_DEBUG === "1") {
|
|
15
|
+
console.error("[render-video]", ...args);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isUnsupportedMediaError = (error) =>
|
|
20
|
+
/DEMUXER_ERROR_NO_SUPPORTED_STREAMS|MEDIA_ERR_SRC_NOT_SUPPORTED|no supported streams/i.test(
|
|
21
|
+
error?.message ?? "",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const getPlaywrightCacheDir = () => {
|
|
25
|
+
const configuredPath = process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
26
|
+
|
|
27
|
+
if (configuredPath && configuredPath !== "0") {
|
|
28
|
+
return configuredPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return path.join(os.homedir(), ".cache", "ms-playwright");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const findCachedChromiumExecutables = async () => {
|
|
35
|
+
const cacheDir = getPlaywrightCacheDir();
|
|
36
|
+
let entries;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
entries = await fsPromises.readdir(cacheDir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const candidates = [];
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory()) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const match = /^chromium-(\d+)$/.exec(entry.name);
|
|
52
|
+
if (!match) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const revision = Number.parseInt(match[1], 10);
|
|
57
|
+
for (const executableRelativePath of [
|
|
58
|
+
"chrome-linux64/chrome",
|
|
59
|
+
"chrome-linux/chrome",
|
|
60
|
+
]) {
|
|
61
|
+
const executablePath = path.join(
|
|
62
|
+
cacheDir,
|
|
63
|
+
entry.name,
|
|
64
|
+
executableRelativePath,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await fsPromises.access(executablePath, fs.constants.X_OK);
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
candidates.push({
|
|
74
|
+
executablePath,
|
|
75
|
+
revision,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return candidates
|
|
81
|
+
.sort((left, right) => right.revision - left.revision)
|
|
82
|
+
.map((candidate) => candidate.executablePath);
|
|
83
|
+
};
|
|
84
|
+
|
|
13
85
|
const createWriteStreamChunkBinding = async (page, outputPath) => {
|
|
14
86
|
const stream = fs.createWriteStream(outputPath);
|
|
15
87
|
|
|
@@ -57,11 +129,13 @@ const captureVideoWebm = async ({
|
|
|
57
129
|
browserExecutablePath,
|
|
58
130
|
webmPath,
|
|
59
131
|
}) => {
|
|
132
|
+
debugVideoRender("launch browser");
|
|
60
133
|
const browser = await chromium.launch(
|
|
61
134
|
getRendererBrowserLaunchOptions(browserExecutablePath),
|
|
62
135
|
);
|
|
63
136
|
|
|
64
137
|
try {
|
|
138
|
+
debugVideoRender("new page", `${width}x${height}`);
|
|
65
139
|
const page = await browser.newPage({
|
|
66
140
|
viewport: {
|
|
67
141
|
width,
|
|
@@ -73,11 +147,18 @@ const captureVideoWebm = async ({
|
|
|
73
147
|
page.on("pageerror", (error) => {
|
|
74
148
|
pageErrors.push(error.stack ?? error.message);
|
|
75
149
|
});
|
|
150
|
+
page.on("console", (message) => {
|
|
151
|
+
if (process.env.ROUTE_GRAPHICS_RENDER_VIDEO_DEBUG === "1") {
|
|
152
|
+
console.error("[render-video:page]", message.type(), message.text());
|
|
153
|
+
}
|
|
154
|
+
});
|
|
76
155
|
|
|
156
|
+
debugVideoRender("goto", origin);
|
|
77
157
|
await page.goto(origin, {
|
|
78
158
|
waitUntil: "domcontentloaded",
|
|
79
159
|
});
|
|
80
160
|
|
|
161
|
+
debugVideoRender("open chunk stream", webmPath);
|
|
81
162
|
const closeChunkStream = await createWriteStreamChunkBinding(
|
|
82
163
|
page,
|
|
83
164
|
webmPath,
|
|
@@ -93,8 +174,15 @@ const captureVideoWebm = async ({
|
|
|
93
174
|
};
|
|
94
175
|
|
|
95
176
|
try {
|
|
177
|
+
debugVideoRender("evaluate capture script");
|
|
96
178
|
const result = await page.evaluate(
|
|
97
179
|
async ({ moduleUrl, renderPayload }) => {
|
|
180
|
+
const debug = (...args) => {
|
|
181
|
+
if (renderPayload.debug) {
|
|
182
|
+
console.debug("[capture]", ...args);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
98
186
|
const sleep = (ms) =>
|
|
99
187
|
new Promise((resolve) => {
|
|
100
188
|
window.setTimeout(resolve, Math.max(0, ms));
|
|
@@ -150,6 +238,7 @@ const captureVideoWebm = async ({
|
|
|
150
238
|
}
|
|
151
239
|
|
|
152
240
|
const routeGraphicsModule = await import(moduleUrl);
|
|
241
|
+
debug("module imported");
|
|
153
242
|
const {
|
|
154
243
|
default: createRouteGraphics,
|
|
155
244
|
animatedSpritePlugin,
|
|
@@ -203,6 +292,7 @@ const captureVideoWebm = async ({
|
|
|
203
292
|
});
|
|
204
293
|
|
|
205
294
|
try {
|
|
295
|
+
debug("app init start");
|
|
206
296
|
await app.init({
|
|
207
297
|
width: renderPayload.width,
|
|
208
298
|
height: renderPayload.height,
|
|
@@ -245,16 +335,22 @@ const captureVideoWebm = async ({
|
|
|
245
335
|
},
|
|
246
336
|
debug: false,
|
|
247
337
|
});
|
|
338
|
+
debug("app init complete");
|
|
248
339
|
|
|
249
340
|
if (Object.keys(renderPayload.assets).length > 0) {
|
|
341
|
+
debug("asset load start", Object.keys(renderPayload.assets));
|
|
250
342
|
await assetBufferManager.load(renderPayload.assets);
|
|
343
|
+
debug("asset buffer load complete");
|
|
251
344
|
await app.loadAssets(assetBufferManager.getBufferMap());
|
|
345
|
+
debug("app asset load complete");
|
|
252
346
|
}
|
|
253
347
|
|
|
254
348
|
document.body.replaceChildren(app.canvas);
|
|
255
349
|
await nextFrame(2);
|
|
350
|
+
debug("canvas mounted");
|
|
256
351
|
|
|
257
352
|
const stream = app.canvas.captureStream(renderPayload.fps);
|
|
353
|
+
debug("capture stream created", stream.getTracks().length);
|
|
258
354
|
const recorder = new MediaRecorder(stream, {
|
|
259
355
|
mimeType: recorderMimeType,
|
|
260
356
|
});
|
|
@@ -270,6 +366,7 @@ const captureVideoWebm = async ({
|
|
|
270
366
|
if (!event.data || event.data.size === 0) {
|
|
271
367
|
return;
|
|
272
368
|
}
|
|
369
|
+
debug("dataavailable", event.data.size);
|
|
273
370
|
|
|
274
371
|
chunkWrites.push(
|
|
275
372
|
(async () => {
|
|
@@ -281,6 +378,7 @@ const captureVideoWebm = async ({
|
|
|
281
378
|
});
|
|
282
379
|
|
|
283
380
|
recorder.start(250);
|
|
381
|
+
debug("recorder started");
|
|
284
382
|
|
|
285
383
|
try {
|
|
286
384
|
for (
|
|
@@ -292,9 +390,11 @@ const captureVideoWebm = async ({
|
|
|
292
390
|
const state = renderPayload.states[stateIndex];
|
|
293
391
|
const waitForComplete = createRenderCompleteWaiter(state.id);
|
|
294
392
|
|
|
393
|
+
debug("render state start", state.id);
|
|
295
394
|
app.render(state);
|
|
296
395
|
app.render(state);
|
|
297
396
|
await waitForComplete;
|
|
397
|
+
debug("render state complete", state.id);
|
|
298
398
|
|
|
299
399
|
const isFinal =
|
|
300
400
|
renderIndex === renderPayload.stateIndexes.length - 1;
|
|
@@ -305,17 +405,23 @@ const captureVideoWebm = async ({
|
|
|
305
405
|
: renderPayload.holdMS;
|
|
306
406
|
|
|
307
407
|
if (hold > 0) {
|
|
408
|
+
debug("hold start", hold);
|
|
308
409
|
await sleep(hold);
|
|
410
|
+
debug("hold complete", hold);
|
|
309
411
|
}
|
|
310
412
|
}
|
|
311
413
|
|
|
312
414
|
await nextFrame(2);
|
|
415
|
+
debug("post-render frames complete");
|
|
313
416
|
} finally {
|
|
314
417
|
if (recorder.state !== "inactive") {
|
|
418
|
+
debug("recorder stop requested");
|
|
315
419
|
recorder.stop();
|
|
316
420
|
}
|
|
317
421
|
await stopped;
|
|
422
|
+
debug("recorder stopped");
|
|
318
423
|
await Promise.all(chunkWrites);
|
|
424
|
+
debug("chunk writes complete");
|
|
319
425
|
stream.getTracks().forEach((track) => {
|
|
320
426
|
track.stop();
|
|
321
427
|
});
|
|
@@ -347,11 +453,14 @@ const captureVideoWebm = async ({
|
|
|
347
453
|
initialHoldMS,
|
|
348
454
|
finalHoldMS,
|
|
349
455
|
maxStateDurationMS,
|
|
456
|
+
debug: process.env.ROUTE_GRAPHICS_RENDER_VIDEO_DEBUG === "1",
|
|
350
457
|
},
|
|
351
458
|
},
|
|
352
459
|
);
|
|
353
460
|
|
|
461
|
+
debugVideoRender("evaluate complete");
|
|
354
462
|
await closeChunksOnce();
|
|
463
|
+
debugVideoRender("chunk stream closed");
|
|
355
464
|
|
|
356
465
|
if (pageErrors.length > 0) {
|
|
357
466
|
throw new Error(pageErrors.join("\n"));
|
|
@@ -412,6 +521,41 @@ const transcodeWebmToMp4 = async ({ ffmpegPath, inputPath, outputPath }) => {
|
|
|
412
521
|
]);
|
|
413
522
|
};
|
|
414
523
|
|
|
524
|
+
const captureVideoWebmWithBrowserFallback = async (options) => {
|
|
525
|
+
try {
|
|
526
|
+
return await captureVideoWebm(options);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (options.browserExecutablePath || !isUnsupportedMediaError(error)) {
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const fallbackExecutables = await findCachedChromiumExecutables();
|
|
533
|
+
const errors = [error];
|
|
534
|
+
|
|
535
|
+
for (const executablePath of fallbackExecutables) {
|
|
536
|
+
debugVideoRender("retry with cached chromium", executablePath);
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
return await captureVideoWebm({
|
|
540
|
+
...options,
|
|
541
|
+
browserExecutablePath: executablePath,
|
|
542
|
+
});
|
|
543
|
+
} catch (fallbackError) {
|
|
544
|
+
errors.push(fallbackError);
|
|
545
|
+
|
|
546
|
+
if (!isUnsupportedMediaError(fallbackError)) {
|
|
547
|
+
throw fallbackError;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const lastError = errors.at(-1) ?? error;
|
|
553
|
+
throw new Error(
|
|
554
|
+
`${lastError.message}\nThe selected Chromium build cannot decode at least one input video stream. Install or select a codec-capable Chrome/Chromium with --browser-executable.`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
415
559
|
export const renderMp4 = async ({
|
|
416
560
|
cliOptions,
|
|
417
561
|
definition,
|
|
@@ -435,7 +579,8 @@ export const renderMp4 = async ({
|
|
|
435
579
|
|
|
436
580
|
try {
|
|
437
581
|
const renderStartedAt = performance.now();
|
|
438
|
-
|
|
582
|
+
debugVideoRender("capture webm start", webmPath);
|
|
583
|
+
const captureInfo = await captureVideoWebmWithBrowserFallback({
|
|
439
584
|
origin,
|
|
440
585
|
width,
|
|
441
586
|
height,
|
|
@@ -453,14 +598,20 @@ export const renderMp4 = async ({
|
|
|
453
598
|
webmPath,
|
|
454
599
|
});
|
|
455
600
|
const renderDurationMS = performance.now() - renderStartedAt;
|
|
601
|
+
debugVideoRender(
|
|
602
|
+
"capture webm complete",
|
|
603
|
+
`${Math.round(renderDurationMS)}ms`,
|
|
604
|
+
);
|
|
456
605
|
|
|
457
606
|
const writeStartedAt = performance.now();
|
|
607
|
+
debugVideoRender("transcode start", outputPath);
|
|
458
608
|
await transcodeWebmToMp4({
|
|
459
609
|
ffmpegPath,
|
|
460
610
|
inputPath: webmPath,
|
|
461
611
|
outputPath,
|
|
462
612
|
});
|
|
463
613
|
const writeDurationMS = performance.now() - writeStartedAt;
|
|
614
|
+
debugVideoRender("transcode complete", `${Math.round(writeDurationMS)}ms`);
|
|
464
615
|
|
|
465
616
|
return {
|
|
466
617
|
inputPath,
|