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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "route-graphics",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "A 2D graphics rendering interface that takes JSON input and renders pixels using PixiJS",
5
5
  "main": "dist/RouteGraphics.js",
6
6
  "type": "module",
@@ -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
- const captureInfo = await captureVideoWebm({
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,