route-graphics 1.44.1 → 1.44.2
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/README.md +1 -3
- package/bin/route-graphics-render.js +5 -615
- package/dist/RouteGraphics.js +178 -669
- package/dist/cli/routeGraphicsCli.js +64 -10
- package/package.json +17 -10
- package/src/cli/browserLaunch.js +0 -5
- package/src/cli/inspectTimeline.js +0 -72
- package/src/cli/inspectTimeline.test.js +0 -135
- package/src/cli/renderConfig.js +0 -519
- package/src/cli/renderVideo.js +0 -627
- package/src/cli/routeGraphicsCli.js +0 -967
- package/src/cli/stateSelection.js +0 -61
package/src/cli/renderVideo.js
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import fsPromises from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { performance } from "node:perf_hooks";
|
|
7
|
-
|
|
8
|
-
import { chromium } from "playwright";
|
|
9
|
-
|
|
10
|
-
import { getRendererBrowserLaunchOptions } from "./browserLaunch.js";
|
|
11
|
-
import { parseStateSelection } from "./stateSelection.js";
|
|
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
|
-
|
|
85
|
-
const createWriteStreamChunkBinding = async (page, outputPath) => {
|
|
86
|
-
const stream = fs.createWriteStream(outputPath);
|
|
87
|
-
|
|
88
|
-
await page.exposeBinding(
|
|
89
|
-
"__routeGraphicsWriteVideoChunk",
|
|
90
|
-
async (_source, base64) => {
|
|
91
|
-
const buffer = Buffer.from(base64, "base64");
|
|
92
|
-
await new Promise((resolve, reject) => {
|
|
93
|
-
stream.write(buffer, (error) => {
|
|
94
|
-
if (error) {
|
|
95
|
-
reject(error);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
resolve();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
return () =>
|
|
105
|
-
new Promise((resolve, reject) => {
|
|
106
|
-
stream.end((error) => {
|
|
107
|
-
if (error) {
|
|
108
|
-
reject(error);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
resolve();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const captureVideoWebm = async ({
|
|
117
|
-
origin,
|
|
118
|
-
width,
|
|
119
|
-
height,
|
|
120
|
-
backgroundColor,
|
|
121
|
-
states,
|
|
122
|
-
stateIndexes,
|
|
123
|
-
browserAssets,
|
|
124
|
-
fps,
|
|
125
|
-
holdMS,
|
|
126
|
-
initialHoldMS,
|
|
127
|
-
finalHoldMS,
|
|
128
|
-
maxStateDurationMS,
|
|
129
|
-
browserExecutablePath,
|
|
130
|
-
webmPath,
|
|
131
|
-
}) => {
|
|
132
|
-
debugVideoRender("launch browser");
|
|
133
|
-
const browser = await chromium.launch(
|
|
134
|
-
getRendererBrowserLaunchOptions(browserExecutablePath),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
debugVideoRender("new page", `${width}x${height}`);
|
|
139
|
-
const page = await browser.newPage({
|
|
140
|
-
viewport: {
|
|
141
|
-
width,
|
|
142
|
-
height,
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
const pageErrors = [];
|
|
146
|
-
|
|
147
|
-
page.on("pageerror", (error) => {
|
|
148
|
-
pageErrors.push(error.stack ?? error.message);
|
|
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
|
-
});
|
|
155
|
-
|
|
156
|
-
debugVideoRender("goto", origin);
|
|
157
|
-
await page.goto(origin, {
|
|
158
|
-
waitUntil: "domcontentloaded",
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
debugVideoRender("open chunk stream", webmPath);
|
|
162
|
-
const closeChunkStream = await createWriteStreamChunkBinding(
|
|
163
|
-
page,
|
|
164
|
-
webmPath,
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
let chunkStreamClosed = false;
|
|
168
|
-
const closeChunksOnce = async () => {
|
|
169
|
-
if (chunkStreamClosed) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
chunkStreamClosed = true;
|
|
173
|
-
await closeChunkStream();
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
debugVideoRender("evaluate capture script");
|
|
178
|
-
const result = await page.evaluate(
|
|
179
|
-
async ({ moduleUrl, renderPayload }) => {
|
|
180
|
-
const debug = (...args) => {
|
|
181
|
-
if (renderPayload.debug) {
|
|
182
|
-
console.debug("[capture]", ...args);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const sleep = (ms) =>
|
|
187
|
-
new Promise((resolve) => {
|
|
188
|
-
window.setTimeout(resolve, Math.max(0, ms));
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const nextFrame = async (count = 2) => {
|
|
192
|
-
await new Promise((resolve) => {
|
|
193
|
-
let remaining = count;
|
|
194
|
-
const tick = () => {
|
|
195
|
-
if (remaining <= 0) {
|
|
196
|
-
resolve();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
remaining -= 1;
|
|
200
|
-
requestAnimationFrame(tick);
|
|
201
|
-
};
|
|
202
|
-
requestAnimationFrame(tick);
|
|
203
|
-
});
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const bytesToBase64 = (bytes) => {
|
|
207
|
-
let binary = "";
|
|
208
|
-
const chunkSize = 0x8000;
|
|
209
|
-
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
210
|
-
binary += String.fromCharCode(
|
|
211
|
-
...bytes.subarray(index, index + chunkSize),
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
return btoa(binary);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const getRecorderMimeType = () => {
|
|
218
|
-
const candidates = [
|
|
219
|
-
"video/webm;codecs=vp9",
|
|
220
|
-
"video/webm;codecs=vp8",
|
|
221
|
-
"video/webm",
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
return candidates.find((candidate) =>
|
|
225
|
-
MediaRecorder.isTypeSupported(candidate),
|
|
226
|
-
);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
if (typeof MediaRecorder === "undefined") {
|
|
230
|
-
throw new Error("MediaRecorder is not available in this browser.");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const recorderMimeType = getRecorderMimeType();
|
|
234
|
-
if (!recorderMimeType) {
|
|
235
|
-
throw new Error(
|
|
236
|
-
"No supported WebM MediaRecorder codec is available.",
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const routeGraphicsModule = await import(moduleUrl);
|
|
241
|
-
debug("module imported");
|
|
242
|
-
const {
|
|
243
|
-
default: createRouteGraphics,
|
|
244
|
-
animatedSpritePlugin,
|
|
245
|
-
containerPlugin,
|
|
246
|
-
createAssetBufferManager,
|
|
247
|
-
inputPlugin,
|
|
248
|
-
particlesPlugin,
|
|
249
|
-
rectPlugin,
|
|
250
|
-
sliderPlugin,
|
|
251
|
-
soundPlugin,
|
|
252
|
-
spritePlugin,
|
|
253
|
-
textPlugin,
|
|
254
|
-
textRevealingPlugin,
|
|
255
|
-
tweenPlugin,
|
|
256
|
-
videoPlugin,
|
|
257
|
-
} = routeGraphicsModule;
|
|
258
|
-
|
|
259
|
-
const app = createRouteGraphics();
|
|
260
|
-
const assetBufferManager = createAssetBufferManager();
|
|
261
|
-
const pendingRenderWaiters = [];
|
|
262
|
-
|
|
263
|
-
const createRenderCompleteWaiter = (stateId) =>
|
|
264
|
-
new Promise((resolve, reject) => {
|
|
265
|
-
let waiter;
|
|
266
|
-
const timeoutId = window.setTimeout(() => {
|
|
267
|
-
const waiterIndex = pendingRenderWaiters.findIndex(
|
|
268
|
-
(candidate) => candidate === waiter,
|
|
269
|
-
);
|
|
270
|
-
if (waiterIndex >= 0) {
|
|
271
|
-
pendingRenderWaiters.splice(waiterIndex, 1);
|
|
272
|
-
}
|
|
273
|
-
reject(
|
|
274
|
-
new Error(
|
|
275
|
-
`Timed out waiting for renderComplete for state "${stateId}".`,
|
|
276
|
-
),
|
|
277
|
-
);
|
|
278
|
-
}, renderPayload.maxStateDurationMS);
|
|
279
|
-
|
|
280
|
-
waiter = {
|
|
281
|
-
stateId,
|
|
282
|
-
resolve: (payload) => {
|
|
283
|
-
window.clearTimeout(timeoutId);
|
|
284
|
-
resolve(payload);
|
|
285
|
-
},
|
|
286
|
-
reject: (error) => {
|
|
287
|
-
window.clearTimeout(timeoutId);
|
|
288
|
-
reject(error);
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
pendingRenderWaiters.push(waiter);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
debug("app init start");
|
|
296
|
-
await app.init({
|
|
297
|
-
width: renderPayload.width,
|
|
298
|
-
height: renderPayload.height,
|
|
299
|
-
backgroundColor: renderPayload.backgroundColor,
|
|
300
|
-
animationPlaybackMode: "auto",
|
|
301
|
-
plugins: {
|
|
302
|
-
elements: [
|
|
303
|
-
textPlugin,
|
|
304
|
-
rectPlugin,
|
|
305
|
-
spritePlugin,
|
|
306
|
-
videoPlugin,
|
|
307
|
-
sliderPlugin,
|
|
308
|
-
inputPlugin,
|
|
309
|
-
containerPlugin,
|
|
310
|
-
textRevealingPlugin,
|
|
311
|
-
animatedSpritePlugin,
|
|
312
|
-
particlesPlugin,
|
|
313
|
-
].filter(Boolean),
|
|
314
|
-
animations: [tweenPlugin].filter(Boolean),
|
|
315
|
-
audio: [soundPlugin].filter(Boolean),
|
|
316
|
-
},
|
|
317
|
-
eventHandler: (eventName, payload) => {
|
|
318
|
-
if (eventName !== "renderComplete") {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const waiterIndex = pendingRenderWaiters.findIndex(
|
|
323
|
-
(waiter) =>
|
|
324
|
-
payload?.aborted !== true &&
|
|
325
|
-
(waiter.stateId === payload?.id ||
|
|
326
|
-
waiter.stateId === undefined),
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
if (waiterIndex < 0) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const [waiter] = pendingRenderWaiters.splice(waiterIndex, 1);
|
|
334
|
-
waiter.resolve(payload);
|
|
335
|
-
},
|
|
336
|
-
debug: false,
|
|
337
|
-
});
|
|
338
|
-
debug("app init complete");
|
|
339
|
-
|
|
340
|
-
if (Object.keys(renderPayload.assets).length > 0) {
|
|
341
|
-
debug("asset load start", Object.keys(renderPayload.assets));
|
|
342
|
-
await assetBufferManager.load(renderPayload.assets);
|
|
343
|
-
debug("asset buffer load complete");
|
|
344
|
-
await app.loadAssets(assetBufferManager.getBufferMap());
|
|
345
|
-
debug("app asset load complete");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
document.body.replaceChildren(app.canvas);
|
|
349
|
-
await nextFrame(2);
|
|
350
|
-
debug("canvas mounted");
|
|
351
|
-
|
|
352
|
-
const stream = app.canvas.captureStream(renderPayload.fps);
|
|
353
|
-
debug("capture stream created", stream.getTracks().length);
|
|
354
|
-
const recorder = new MediaRecorder(stream, {
|
|
355
|
-
mimeType: recorderMimeType,
|
|
356
|
-
});
|
|
357
|
-
const chunkWrites = [];
|
|
358
|
-
const stopped = new Promise((resolve, reject) => {
|
|
359
|
-
recorder.addEventListener("stop", resolve, { once: true });
|
|
360
|
-
recorder.addEventListener("error", (event) => {
|
|
361
|
-
reject(event.error ?? new Error("MediaRecorder failed."));
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
recorder.addEventListener("dataavailable", (event) => {
|
|
366
|
-
if (!event.data || event.data.size === 0) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
debug("dataavailable", event.data.size);
|
|
370
|
-
|
|
371
|
-
chunkWrites.push(
|
|
372
|
-
(async () => {
|
|
373
|
-
const buffer = await event.data.arrayBuffer();
|
|
374
|
-
const base64 = bytesToBase64(new Uint8Array(buffer));
|
|
375
|
-
await window.__routeGraphicsWriteVideoChunk(base64);
|
|
376
|
-
})(),
|
|
377
|
-
);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
recorder.start(250);
|
|
381
|
-
debug("recorder started");
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
for (
|
|
385
|
-
let renderIndex = 0;
|
|
386
|
-
renderIndex < renderPayload.stateIndexes.length;
|
|
387
|
-
renderIndex += 1
|
|
388
|
-
) {
|
|
389
|
-
const stateIndex = renderPayload.stateIndexes[renderIndex];
|
|
390
|
-
const state = renderPayload.states[stateIndex];
|
|
391
|
-
const waitForComplete = createRenderCompleteWaiter(state.id);
|
|
392
|
-
|
|
393
|
-
debug("render state start", state.id);
|
|
394
|
-
app.render(state);
|
|
395
|
-
app.render(state);
|
|
396
|
-
await waitForComplete;
|
|
397
|
-
debug("render state complete", state.id);
|
|
398
|
-
|
|
399
|
-
const isFinal =
|
|
400
|
-
renderIndex === renderPayload.stateIndexes.length - 1;
|
|
401
|
-
const hold = isFinal
|
|
402
|
-
? renderPayload.finalHoldMS
|
|
403
|
-
: renderIndex === 0
|
|
404
|
-
? renderPayload.initialHoldMS
|
|
405
|
-
: renderPayload.holdMS;
|
|
406
|
-
|
|
407
|
-
if (hold > 0) {
|
|
408
|
-
debug("hold start", hold);
|
|
409
|
-
await sleep(hold);
|
|
410
|
-
debug("hold complete", hold);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
await nextFrame(2);
|
|
415
|
-
debug("post-render frames complete");
|
|
416
|
-
} finally {
|
|
417
|
-
if (recorder.state !== "inactive") {
|
|
418
|
-
debug("recorder stop requested");
|
|
419
|
-
recorder.stop();
|
|
420
|
-
}
|
|
421
|
-
await stopped;
|
|
422
|
-
debug("recorder stopped");
|
|
423
|
-
await Promise.all(chunkWrites);
|
|
424
|
-
debug("chunk writes complete");
|
|
425
|
-
stream.getTracks().forEach((track) => {
|
|
426
|
-
track.stop();
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
mimeType: recorderMimeType,
|
|
432
|
-
};
|
|
433
|
-
} finally {
|
|
434
|
-
pendingRenderWaiters.splice(0).forEach((waiter) => {
|
|
435
|
-
waiter.reject(
|
|
436
|
-
new Error("Render session ended before completion."),
|
|
437
|
-
);
|
|
438
|
-
});
|
|
439
|
-
app.destroy();
|
|
440
|
-
}
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
moduleUrl: `${origin}/dist/RouteGraphics.js`,
|
|
444
|
-
renderPayload: {
|
|
445
|
-
width,
|
|
446
|
-
height,
|
|
447
|
-
backgroundColor,
|
|
448
|
-
states,
|
|
449
|
-
stateIndexes,
|
|
450
|
-
assets: browserAssets,
|
|
451
|
-
fps,
|
|
452
|
-
holdMS,
|
|
453
|
-
initialHoldMS,
|
|
454
|
-
finalHoldMS,
|
|
455
|
-
maxStateDurationMS,
|
|
456
|
-
debug: process.env.ROUTE_GRAPHICS_RENDER_VIDEO_DEBUG === "1",
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
debugVideoRender("evaluate complete");
|
|
462
|
-
await closeChunksOnce();
|
|
463
|
-
debugVideoRender("chunk stream closed");
|
|
464
|
-
|
|
465
|
-
if (pageErrors.length > 0) {
|
|
466
|
-
throw new Error(pageErrors.join("\n"));
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return result;
|
|
470
|
-
} catch (error) {
|
|
471
|
-
await closeChunksOnce();
|
|
472
|
-
throw error;
|
|
473
|
-
}
|
|
474
|
-
} finally {
|
|
475
|
-
await browser.close();
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const runProcess = async (command, args) =>
|
|
480
|
-
new Promise((resolve, reject) => {
|
|
481
|
-
const child = spawn(command, args, {
|
|
482
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
483
|
-
});
|
|
484
|
-
const stdout = [];
|
|
485
|
-
const stderr = [];
|
|
486
|
-
|
|
487
|
-
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
488
|
-
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
489
|
-
child.on("error", reject);
|
|
490
|
-
child.on("close", (code) => {
|
|
491
|
-
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
492
|
-
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
493
|
-
|
|
494
|
-
if (code !== 0) {
|
|
495
|
-
reject(
|
|
496
|
-
new Error(
|
|
497
|
-
`${command} exited with code ${code}.\n${stderrText || stdoutText}`,
|
|
498
|
-
),
|
|
499
|
-
);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
resolve({ stdout: stdoutText, stderr: stderrText });
|
|
504
|
-
});
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
const transcodeWebmToMp4 = async ({ ffmpegPath, inputPath, outputPath }) => {
|
|
508
|
-
await fsPromises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
509
|
-
await runProcess(ffmpegPath, [
|
|
510
|
-
"-y",
|
|
511
|
-
"-i",
|
|
512
|
-
inputPath,
|
|
513
|
-
"-an",
|
|
514
|
-
"-c:v",
|
|
515
|
-
"libx264",
|
|
516
|
-
"-pix_fmt",
|
|
517
|
-
"yuv420p",
|
|
518
|
-
"-movflags",
|
|
519
|
-
"+faststart",
|
|
520
|
-
outputPath,
|
|
521
|
-
]);
|
|
522
|
-
};
|
|
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
|
-
|
|
559
|
-
export const renderMp4 = async ({
|
|
560
|
-
cliOptions,
|
|
561
|
-
definition,
|
|
562
|
-
inputPath,
|
|
563
|
-
outputPath,
|
|
564
|
-
origin,
|
|
565
|
-
browserAssets,
|
|
566
|
-
width,
|
|
567
|
-
height,
|
|
568
|
-
backgroundColor,
|
|
569
|
-
}) => {
|
|
570
|
-
const stateIndexes = parseStateSelection(
|
|
571
|
-
cliOptions.stateSelection,
|
|
572
|
-
definition.states.length,
|
|
573
|
-
);
|
|
574
|
-
const tempDir = await fsPromises.mkdtemp(
|
|
575
|
-
path.join(os.tmpdir(), "route-graphics-video-"),
|
|
576
|
-
);
|
|
577
|
-
const webmPath = path.join(tempDir, "capture.webm");
|
|
578
|
-
const ffmpegPath = cliOptions.ffmpegPath ?? "ffmpeg";
|
|
579
|
-
|
|
580
|
-
try {
|
|
581
|
-
const renderStartedAt = performance.now();
|
|
582
|
-
debugVideoRender("capture webm start", webmPath);
|
|
583
|
-
const captureInfo = await captureVideoWebmWithBrowserFallback({
|
|
584
|
-
origin,
|
|
585
|
-
width,
|
|
586
|
-
height,
|
|
587
|
-
backgroundColor,
|
|
588
|
-
states: definition.states,
|
|
589
|
-
stateIndexes,
|
|
590
|
-
browserAssets,
|
|
591
|
-
fps: cliOptions.fps ?? 30,
|
|
592
|
-
holdMS: cliOptions.holdMS ?? 0,
|
|
593
|
-
initialHoldMS: cliOptions.initialHoldMS ?? cliOptions.holdMS ?? 0,
|
|
594
|
-
finalHoldMS: cliOptions.finalHoldMS ?? 1000,
|
|
595
|
-
maxStateDurationMS:
|
|
596
|
-
cliOptions.maxStateDurationMS ?? cliOptions.timeoutMS ?? 15000,
|
|
597
|
-
browserExecutablePath: cliOptions.browserExecutablePath,
|
|
598
|
-
webmPath,
|
|
599
|
-
});
|
|
600
|
-
const renderDurationMS = performance.now() - renderStartedAt;
|
|
601
|
-
debugVideoRender(
|
|
602
|
-
"capture webm complete",
|
|
603
|
-
`${Math.round(renderDurationMS)}ms`,
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
const writeStartedAt = performance.now();
|
|
607
|
-
debugVideoRender("transcode start", outputPath);
|
|
608
|
-
await transcodeWebmToMp4({
|
|
609
|
-
ffmpegPath,
|
|
610
|
-
inputPath: webmPath,
|
|
611
|
-
outputPath,
|
|
612
|
-
});
|
|
613
|
-
const writeDurationMS = performance.now() - writeStartedAt;
|
|
614
|
-
debugVideoRender("transcode complete", `${Math.round(writeDurationMS)}ms`);
|
|
615
|
-
|
|
616
|
-
return {
|
|
617
|
-
inputPath,
|
|
618
|
-
outputPath,
|
|
619
|
-
renderDurationMS,
|
|
620
|
-
writeDurationMS,
|
|
621
|
-
captureMimeType: captureInfo.mimeType,
|
|
622
|
-
stateCount: stateIndexes.length,
|
|
623
|
-
};
|
|
624
|
-
} finally {
|
|
625
|
-
await fsPromises.rm(tempDir, { recursive: true, force: true });
|
|
626
|
-
}
|
|
627
|
-
};
|