@bendyline/squisq-cli 2.8.9 → 2.8.11
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 +12 -0
- package/dist/{api-QLYK5AQE.js → api-P7LOLIZT.js} +4 -2
- package/dist/api.d.ts +120 -2
- package/dist/api.js +480 -84
- package/dist/{chunk-XD2ICI5G.js → chunk-6MZC4YQV.js} +483 -87
- package/dist/{chunk-Z3BHD4AF.js → chunk-RQSP3OPR.js} +2 -1
- package/dist/{chunk-GGVASRPG.js → chunk-Y5XKUWJ5.js} +2 -1
- package/dist/index.js +58 -7
- package/dist/{nativeEncoder-PMVSFLMN.js → nativeEncoder-RILHXTXI.js} +1 -1
- package/dist/{nativeEncoder-RZ3T4Z6S.js → nativeEncoder-T43J5UMP.js} +1 -1
- package/dist/squisq-player.full.global.js +428 -428
- package/dist/squisq-player.global.js +71 -71
- package/package.json +5 -5
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
GIF_EXPORT_DEFAULTS,
|
|
4
|
+
lastMeaningfulFfmpegLine,
|
|
4
5
|
runFfmpeg
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-Y5XKUWJ5.js";
|
|
6
7
|
|
|
7
8
|
// src/api.ts
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
10
|
+
import { mkdir as mkdir2, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
12
|
+
import { dirname as dirname2, join as join4, resolve as resolvePath } from "path";
|
|
10
13
|
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
11
14
|
import { flattenBlocks as flattenBlocks2 } from "@bendyline/squisq/doc";
|
|
12
|
-
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
15
|
+
import { ffmpegGifOutputArgs, frameTimeSeconds, generateRenderHtml } from "@bendyline/squisq-video";
|
|
13
16
|
import { resolveDashboardDimensions, resolveDimensions } from "@bendyline/squisq-video";
|
|
14
17
|
import {
|
|
15
18
|
convert as formatsConvert,
|
|
@@ -132,13 +135,13 @@ function round3(n) {
|
|
|
132
135
|
return Math.round(n * 1e3) / 1e3;
|
|
133
136
|
}
|
|
134
137
|
async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
135
|
-
const { writeFile:
|
|
136
|
-
const { join:
|
|
137
|
-
const { tmpdir:
|
|
138
|
-
const { randomBytes:
|
|
138
|
+
const { writeFile: writeFile3, readFile: readFile5, mkdir: mkdir3, rm: rm3 } = await import("fs/promises");
|
|
139
|
+
const { join: join5 } = await import("path");
|
|
140
|
+
const { tmpdir: tmpdir3 } = await import("os");
|
|
141
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
139
142
|
signal?.throwIfAborted();
|
|
140
|
-
const workDir =
|
|
141
|
-
await
|
|
143
|
+
const workDir = join5(tmpdir3(), `squisq-audio-mix-${randomBytes3(8).toString("hex")}`);
|
|
144
|
+
await mkdir3(workDir, { recursive: true });
|
|
142
145
|
try {
|
|
143
146
|
signal?.throwIfAborted();
|
|
144
147
|
const inputs = [];
|
|
@@ -146,15 +149,15 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
|
146
149
|
const labels = [];
|
|
147
150
|
for (const { clip, buffer } of usable) {
|
|
148
151
|
signal?.throwIfAborted();
|
|
149
|
-
const p =
|
|
150
|
-
await
|
|
152
|
+
const p = join5(workDir, `clip-${inputs.length}.mp3`);
|
|
153
|
+
await writeFile3(p, new Uint8Array(buffer));
|
|
151
154
|
signal?.throwIfAborted();
|
|
152
155
|
const i = inputs.push(p) - 1;
|
|
153
156
|
filters.push(`[${i}:a]${clipFilterChain(clip)}[a${i}]`);
|
|
154
157
|
labels.push(`[a${i}]`);
|
|
155
158
|
}
|
|
156
159
|
const graph = `${filters.join(";")};${labels.join("")}amix=inputs=${labels.length}:normalize=0:dropout_transition=0[aout]`;
|
|
157
|
-
const outputPath =
|
|
160
|
+
const outputPath = join5(workDir, "mixed.mp3");
|
|
158
161
|
const args = ["-y"];
|
|
159
162
|
for (const p of inputs) args.push("-i", p);
|
|
160
163
|
args.push(
|
|
@@ -174,11 +177,11 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
|
174
177
|
signal
|
|
175
178
|
});
|
|
176
179
|
signal?.throwIfAborted();
|
|
177
|
-
const data = await
|
|
180
|
+
const data = await readFile5(outputPath);
|
|
178
181
|
signal?.throwIfAborted();
|
|
179
182
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
180
183
|
} finally {
|
|
181
|
-
await
|
|
184
|
+
await rm3(workDir, { recursive: true, force: true });
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -321,11 +324,188 @@ function isRecord(value) {
|
|
|
321
324
|
return typeof value === "object" && value !== null;
|
|
322
325
|
}
|
|
323
326
|
|
|
327
|
+
// src/util/frameSink.ts
|
|
328
|
+
import { spawn } from "child_process";
|
|
329
|
+
import { createHash } from "crypto";
|
|
330
|
+
import { once } from "events";
|
|
331
|
+
import { access, mkdir, readFile, writeFile } from "fs/promises";
|
|
332
|
+
import { join } from "path";
|
|
333
|
+
import { isDeepStrictEqual } from "util";
|
|
334
|
+
import {
|
|
335
|
+
audioBitrateArg,
|
|
336
|
+
ffmpegAudioMuxArgs,
|
|
337
|
+
ffmpegVideoQualityArgs
|
|
338
|
+
} from "@bendyline/squisq-video";
|
|
339
|
+
function ffmpegPipeArgs(options) {
|
|
340
|
+
const { outputPath, fps, width, height, quality, captureFormat, audioPath } = options;
|
|
341
|
+
const args = [
|
|
342
|
+
"-y",
|
|
343
|
+
"-f",
|
|
344
|
+
"image2pipe",
|
|
345
|
+
"-framerate",
|
|
346
|
+
String(fps),
|
|
347
|
+
"-c:v",
|
|
348
|
+
captureFormat === "jpeg" ? "mjpeg" : "png",
|
|
349
|
+
"-i",
|
|
350
|
+
"pipe:0"
|
|
351
|
+
];
|
|
352
|
+
if (audioPath) args.push("-i", audioPath);
|
|
353
|
+
const range = captureFormat === "jpeg" ? ":in_range=pc:out_range=tv" : "";
|
|
354
|
+
args.push(
|
|
355
|
+
"-c:v",
|
|
356
|
+
"libx264",
|
|
357
|
+
...ffmpegVideoQualityArgs(quality),
|
|
358
|
+
"-pix_fmt",
|
|
359
|
+
"yuv420p",
|
|
360
|
+
"-color_range",
|
|
361
|
+
"tv",
|
|
362
|
+
"-vf",
|
|
363
|
+
`scale=${width}:${height}:force_original_aspect_ratio=decrease${range},pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
|
|
364
|
+
);
|
|
365
|
+
if (audioPath) args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));
|
|
366
|
+
args.push("-movflags", "+faststart", outputPath);
|
|
367
|
+
return args;
|
|
368
|
+
}
|
|
369
|
+
var STDERR_TAIL_LINES = 200;
|
|
370
|
+
var IDLE_POLL_MS = 5e3;
|
|
371
|
+
function createFfmpegPipeSink(options) {
|
|
372
|
+
const { ffmpegPath, signal, idleTimeoutMs = 12e4 } = options;
|
|
373
|
+
const child = spawn(ffmpegPath, ffmpegPipeArgs(options), {
|
|
374
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
375
|
+
});
|
|
376
|
+
const stderrTail = [];
|
|
377
|
+
let lastActivity = Date.now();
|
|
378
|
+
let failure = null;
|
|
379
|
+
let frames = 0;
|
|
380
|
+
let exitCode;
|
|
381
|
+
child.stderr.setEncoding("utf8");
|
|
382
|
+
child.stderr.on("data", (chunk) => {
|
|
383
|
+
lastActivity = Date.now();
|
|
384
|
+
for (const line of chunk.split(/\r?\n|\r/)) {
|
|
385
|
+
if (!line.trim()) continue;
|
|
386
|
+
stderrTail.push(line);
|
|
387
|
+
if (stderrTail.length > STDERR_TAIL_LINES) stderrTail.shift();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
child.on("error", (err) => {
|
|
391
|
+
failure ??= err;
|
|
392
|
+
});
|
|
393
|
+
child.stdin.on("error", (err) => {
|
|
394
|
+
failure ??= err;
|
|
395
|
+
});
|
|
396
|
+
const exited = new Promise((resolve2) => {
|
|
397
|
+
child.on("exit", (code) => {
|
|
398
|
+
exitCode = code;
|
|
399
|
+
resolve2();
|
|
400
|
+
});
|
|
401
|
+
child.on("error", () => resolve2());
|
|
402
|
+
});
|
|
403
|
+
const idleTimer = setInterval(() => {
|
|
404
|
+
if (Date.now() - lastActivity > idleTimeoutMs) {
|
|
405
|
+
failure ??= new Error(`ffmpeg produced no output for ${Math.round(idleTimeoutMs / 1e3)}s`);
|
|
406
|
+
child.kill("SIGKILL");
|
|
407
|
+
}
|
|
408
|
+
}, IDLE_POLL_MS);
|
|
409
|
+
idleTimer.unref();
|
|
410
|
+
const onAbort = () => {
|
|
411
|
+
child.kill("SIGKILL");
|
|
412
|
+
};
|
|
413
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
414
|
+
const cleanup = () => {
|
|
415
|
+
clearInterval(idleTimer);
|
|
416
|
+
signal?.removeEventListener("abort", onAbort);
|
|
417
|
+
};
|
|
418
|
+
const describeFailure = () => {
|
|
419
|
+
const detail = lastMeaningfulFfmpegLine(stderrTail.join("\n"));
|
|
420
|
+
if (failure && failure.code === "ENOENT") {
|
|
421
|
+
return "the ffmpeg binary could not be started (ENOENT). Run `squisq doctor` to check your FFmpeg installation.";
|
|
422
|
+
}
|
|
423
|
+
const head = failure ? failure.message : exitCode === void 0 ? "ffmpeg exited unexpectedly" : `ffmpeg exited unsuccessfully (exit code ${exitCode})`;
|
|
424
|
+
return detail ? `${head} (last output: ${detail})` : head;
|
|
425
|
+
};
|
|
426
|
+
const throwIfDead = () => {
|
|
427
|
+
signal?.throwIfAborted();
|
|
428
|
+
if (failure || exitCode !== void 0) {
|
|
429
|
+
throw new Error(`ffmpeg failed: ${describeFailure()}`);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
get frameCount() {
|
|
434
|
+
return frames;
|
|
435
|
+
},
|
|
436
|
+
async write(frame, repeat = 1) {
|
|
437
|
+
if (!Number.isSafeInteger(repeat) || repeat < 1) {
|
|
438
|
+
throw new Error("Frame repetition count must be a positive integer");
|
|
439
|
+
}
|
|
440
|
+
throwIfDead();
|
|
441
|
+
for (let i = 0; i < repeat; i += 1) {
|
|
442
|
+
lastActivity = Date.now();
|
|
443
|
+
if (!child.stdin.write(frame)) {
|
|
444
|
+
await Promise.race([once(child.stdin, "drain").catch(() => void 0), exited]);
|
|
445
|
+
}
|
|
446
|
+
throwIfDead();
|
|
447
|
+
frames += 1;
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
async finish() {
|
|
451
|
+
throwIfDead();
|
|
452
|
+
child.stdin.end();
|
|
453
|
+
await exited;
|
|
454
|
+
cleanup();
|
|
455
|
+
signal?.throwIfAborted();
|
|
456
|
+
if (failure || exitCode !== 0) throw new Error(`ffmpeg failed: ${describeFailure()}`);
|
|
457
|
+
},
|
|
458
|
+
async abort() {
|
|
459
|
+
cleanup();
|
|
460
|
+
if (exitCode === void 0) child.kill("SIGKILL");
|
|
461
|
+
await exited;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function docRenderHash(doc) {
|
|
466
|
+
return createHash("sha256").update(JSON.stringify(doc)).digest("hex");
|
|
467
|
+
}
|
|
468
|
+
var MANIFEST_NAME = "manifest.json";
|
|
469
|
+
async function openDirectoryFrameStore(dir, manifest, resume) {
|
|
470
|
+
await mkdir(dir, { recursive: true });
|
|
471
|
+
const manifestPath = join(dir, MANIFEST_NAME);
|
|
472
|
+
let existing = null;
|
|
473
|
+
try {
|
|
474
|
+
existing = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
475
|
+
} catch {
|
|
476
|
+
existing = null;
|
|
477
|
+
}
|
|
478
|
+
const reusable = resume && existing !== null && isDeepStrictEqual(existing, manifest);
|
|
479
|
+
if (!reusable) await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
480
|
+
const extension = manifest.captureFormat === "jpeg" ? "jpg" : "png";
|
|
481
|
+
const path = (index) => join(dir, `frame-${String(index).padStart(6, "0")}.${extension}`);
|
|
482
|
+
return {
|
|
483
|
+
dir,
|
|
484
|
+
reusable,
|
|
485
|
+
path,
|
|
486
|
+
async has(index) {
|
|
487
|
+
try {
|
|
488
|
+
await access(path(index));
|
|
489
|
+
return true;
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
async read(index) {
|
|
495
|
+
const bytes = await readFile(path(index));
|
|
496
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
497
|
+
},
|
|
498
|
+
async write(index, frame) {
|
|
499
|
+
await writeFile(path(index), frame);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
324
504
|
// src/registry.ts
|
|
325
505
|
import { randomBytes } from "crypto";
|
|
326
|
-
import { readFile, rm } from "fs/promises";
|
|
506
|
+
import { readFile as readFile2, rm } from "fs/promises";
|
|
327
507
|
import { tmpdir } from "os";
|
|
328
|
-
import { join } from "path";
|
|
508
|
+
import { join as join2 } from "path";
|
|
329
509
|
import { defaultRegistry } from "@bendyline/squisq-formats";
|
|
330
510
|
var MP4_DEFAULTS = {
|
|
331
511
|
fps: 30,
|
|
@@ -360,9 +540,9 @@ function mp4Format() {
|
|
|
360
540
|
const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
|
|
361
541
|
const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
|
|
362
542
|
const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
|
|
363
|
-
const outputPath =
|
|
543
|
+
const outputPath = join2(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
364
544
|
try {
|
|
365
|
-
const { renderDocToMp4: renderDocToMp42 } = await import("./api-
|
|
545
|
+
const { renderDocToMp4: renderDocToMp42 } = await import("./api-P7LOLIZT.js");
|
|
366
546
|
await renderDocToMp42(input.doc, input.container, {
|
|
367
547
|
outputPath,
|
|
368
548
|
fps,
|
|
@@ -377,7 +557,7 @@ function mp4Format() {
|
|
|
377
557
|
onProgress: mp4Opts.onProgress
|
|
378
558
|
});
|
|
379
559
|
options.signal?.throwIfAborted();
|
|
380
|
-
const data = await
|
|
560
|
+
const data = await readFile2(outputPath, { signal: options.signal });
|
|
381
561
|
options.signal?.throwIfAborted();
|
|
382
562
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
383
563
|
return { bytes, mimeType: "video/mp4", suggestedFilename: "", warnings: [] };
|
|
@@ -401,9 +581,9 @@ function gifFormat() {
|
|
|
401
581
|
const portrait = orientation === "portrait";
|
|
402
582
|
const defaultWidth = portrait ? GIF_DEFAULTS.height : GIF_DEFAULTS.width;
|
|
403
583
|
const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
|
|
404
|
-
const outputPath =
|
|
584
|
+
const outputPath = join2(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
|
|
405
585
|
try {
|
|
406
|
-
const { renderDocToGif: renderDocToGif2 } = await import("./api-
|
|
586
|
+
const { renderDocToGif: renderDocToGif2 } = await import("./api-P7LOLIZT.js");
|
|
407
587
|
const result = await renderDocToGif2(input.doc, input.container, {
|
|
408
588
|
outputPath,
|
|
409
589
|
fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
|
|
@@ -421,7 +601,7 @@ function gifFormat() {
|
|
|
421
601
|
onProgress: gifOpts.onProgress
|
|
422
602
|
});
|
|
423
603
|
options.signal?.throwIfAborted();
|
|
424
|
-
const data = await
|
|
604
|
+
const data = await readFile2(outputPath, { signal: options.signal });
|
|
425
605
|
options.signal?.throwIfAborted();
|
|
426
606
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
427
607
|
return {
|
|
@@ -446,7 +626,7 @@ function pngFormat() {
|
|
|
446
626
|
async exportDoc(input, options) {
|
|
447
627
|
options.signal?.throwIfAborted();
|
|
448
628
|
const pngOpts = options.formatOptions?.png ?? {};
|
|
449
|
-
const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api-
|
|
629
|
+
const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api-P7LOLIZT.js");
|
|
450
630
|
const result = await renderDocToDashboardPng2(input.doc, input.container, {
|
|
451
631
|
resolution: pngOpts.resolution,
|
|
452
632
|
width: pngOpts.width,
|
|
@@ -475,13 +655,13 @@ function createCliRegistry() {
|
|
|
475
655
|
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
476
656
|
|
|
477
657
|
// src/util/readInput.ts
|
|
478
|
-
import { readFile as
|
|
658
|
+
import { readFile as readFile3, readdir, realpath, stat } from "fs/promises";
|
|
479
659
|
import {
|
|
480
660
|
basename,
|
|
481
661
|
dirname,
|
|
482
662
|
extname,
|
|
483
663
|
isAbsolute,
|
|
484
|
-
join as
|
|
664
|
+
join as join3,
|
|
485
665
|
posix,
|
|
486
666
|
relative,
|
|
487
667
|
resolve,
|
|
@@ -547,7 +727,7 @@ async function walkDir(root, prefix = "", signal) {
|
|
|
547
727
|
throwIfAborted(signal);
|
|
548
728
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
549
729
|
if (entry.isDirectory()) {
|
|
550
|
-
paths.push(...await walkDir(
|
|
730
|
+
paths.push(...await walkDir(join3(root, entry.name), relPath, signal));
|
|
551
731
|
} else if (entry.isFile()) {
|
|
552
732
|
paths.push(relPath);
|
|
553
733
|
}
|
|
@@ -598,7 +778,7 @@ async function readArrayBuffer(filePath, signal) {
|
|
|
598
778
|
async function readBinaryFile(filePath, signal) {
|
|
599
779
|
throwIfAborted(signal);
|
|
600
780
|
try {
|
|
601
|
-
const data = await
|
|
781
|
+
const data = await readFile3(filePath, { signal });
|
|
602
782
|
throwIfAborted(signal);
|
|
603
783
|
return data;
|
|
604
784
|
} catch (error) {
|
|
@@ -609,7 +789,7 @@ async function readBinaryFile(filePath, signal) {
|
|
|
609
789
|
async function readUtf8File(filePath, signal) {
|
|
610
790
|
throwIfAborted(signal);
|
|
611
791
|
try {
|
|
612
|
-
const content = await
|
|
792
|
+
const content = await readFile3(filePath, { encoding: "utf-8", signal });
|
|
613
793
|
throwIfAborted(signal);
|
|
614
794
|
return content;
|
|
615
795
|
} catch (error) {
|
|
@@ -864,7 +1044,7 @@ async function readFolder(dirPath, signal) {
|
|
|
864
1044
|
throwIfAborted(signal);
|
|
865
1045
|
for (const relPath of files) {
|
|
866
1046
|
throwIfAborted(signal);
|
|
867
|
-
const absPath =
|
|
1047
|
+
const absPath = join3(dirPath, relPath);
|
|
868
1048
|
const data = await readBinaryFile(absPath, signal);
|
|
869
1049
|
throwIfAborted(signal);
|
|
870
1050
|
await container.writeFile(
|
|
@@ -898,11 +1078,13 @@ import {
|
|
|
898
1078
|
validateDashboardImageDimensions
|
|
899
1079
|
} from "@bendyline/squisq-video";
|
|
900
1080
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
1081
|
+
var RENDER_BOOT_TIMEOUT_MS = 6e4;
|
|
1082
|
+
var JPEG_CAPTURE_QUALITY = 92;
|
|
901
1083
|
var playerBundlePromise;
|
|
902
1084
|
var fullPlayerBundlePromise;
|
|
903
1085
|
var playerIconBootstrapPromise;
|
|
904
1086
|
function loadPlayerIconBootstrap() {
|
|
905
|
-
playerIconBootstrapPromise ??=
|
|
1087
|
+
playerIconBootstrapPromise ??= readFile4(
|
|
906
1088
|
new URL("../dist/squisq-player-icons.global.js", import.meta.url),
|
|
907
1089
|
"utf8"
|
|
908
1090
|
);
|
|
@@ -911,14 +1093,14 @@ function loadPlayerIconBootstrap() {
|
|
|
911
1093
|
function loadPlayerBundle() {
|
|
912
1094
|
playerBundlePromise ??= Promise.all([
|
|
913
1095
|
loadPlayerIconBootstrap(),
|
|
914
|
-
|
|
1096
|
+
readFile4(new URL("../dist/squisq-player.global.js", import.meta.url), "utf8")
|
|
915
1097
|
]).then(([icons, player]) => `${icons}${player}`);
|
|
916
1098
|
return playerBundlePromise;
|
|
917
1099
|
}
|
|
918
1100
|
function loadFullPlayerBundle() {
|
|
919
1101
|
fullPlayerBundlePromise ??= Promise.all([
|
|
920
1102
|
loadPlayerIconBootstrap(),
|
|
921
|
-
|
|
1103
|
+
readFile4(new URL("../dist/squisq-player.full.global.js", import.meta.url), "utf8")
|
|
922
1104
|
]).then(([icons, player]) => `${icons}${player}`);
|
|
923
1105
|
return fullPlayerBundlePromise;
|
|
924
1106
|
}
|
|
@@ -943,6 +1125,7 @@ async function withRenderPage(doc, container, options, fn) {
|
|
|
943
1125
|
height,
|
|
944
1126
|
includeAudio,
|
|
945
1127
|
captionStyle,
|
|
1128
|
+
motion,
|
|
946
1129
|
animationsEnabled,
|
|
947
1130
|
displayMode,
|
|
948
1131
|
dashboard,
|
|
@@ -1003,6 +1186,7 @@ async function withRenderPage(doc, container, options, fn) {
|
|
|
1003
1186
|
width,
|
|
1004
1187
|
height,
|
|
1005
1188
|
captionStyle,
|
|
1189
|
+
motion,
|
|
1006
1190
|
animationsEnabled,
|
|
1007
1191
|
displayMode,
|
|
1008
1192
|
dashboard
|
|
@@ -1013,7 +1197,14 @@ async function withRenderPage(doc, container, options, fn) {
|
|
|
1013
1197
|
signal?.throwIfAborted();
|
|
1014
1198
|
let browser;
|
|
1015
1199
|
try {
|
|
1016
|
-
browser = await chromium.launch({
|
|
1200
|
+
browser = await chromium.launch({
|
|
1201
|
+
headless: true,
|
|
1202
|
+
// Keyframe animations must be sampled by the main thread: the
|
|
1203
|
+
// compositor rasterises a transforming layer differently depending on
|
|
1204
|
+
// whether it caught the animation running, which made mid-motion stills
|
|
1205
|
+
// differ between otherwise identical renders.
|
|
1206
|
+
args: ["--disable-threaded-animation"]
|
|
1207
|
+
});
|
|
1017
1208
|
} catch (err) {
|
|
1018
1209
|
signal?.throwIfAborted();
|
|
1019
1210
|
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
@@ -1052,7 +1243,7 @@ async function withRenderPage(doc, container, options, fn) {
|
|
|
1052
1243
|
const player = window.SquisqPlayer;
|
|
1053
1244
|
return root ? player?.getHandle(root)?.getRenderAPI() != null : false;
|
|
1054
1245
|
},
|
|
1055
|
-
{ timeout:
|
|
1246
|
+
{ timeout: RENDER_BOOT_TIMEOUT_MS }
|
|
1056
1247
|
);
|
|
1057
1248
|
} catch {
|
|
1058
1249
|
signal?.throwIfAborted();
|
|
@@ -1060,9 +1251,16 @@ async function withRenderPage(doc, container, options, fn) {
|
|
|
1060
1251
|
Page errors:
|
|
1061
1252
|
${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
|
|
1062
1253
|
throw new Error(
|
|
1063
|
-
`The standalone player failed to boot in headless Chromium. Render API did not initialize within
|
|
1254
|
+
`The standalone player failed to boot in headless Chromium. Render API did not initialize within ${RENDER_BOOT_TIMEOUT_MS / 1e3} seconds.${errorDetail}`
|
|
1064
1255
|
);
|
|
1065
1256
|
}
|
|
1257
|
+
signal?.throwIfAborted();
|
|
1258
|
+
await page.evaluate(
|
|
1259
|
+
() => Promise.race([
|
|
1260
|
+
document.fonts?.ready ?? Promise.resolve(void 0),
|
|
1261
|
+
new Promise((resolve2) => setTimeout(resolve2, 1e4))
|
|
1262
|
+
]).then(() => void 0)
|
|
1263
|
+
);
|
|
1066
1264
|
renderAPI = await page.evaluateHandle(() => {
|
|
1067
1265
|
const root = document.getElementById("squisq-root");
|
|
1068
1266
|
const player = window.SquisqPlayer;
|
|
@@ -1071,6 +1269,13 @@ Page errors:
|
|
|
1071
1269
|
return api;
|
|
1072
1270
|
});
|
|
1073
1271
|
signal?.throwIfAborted();
|
|
1272
|
+
const mounted = await renderAPI.evaluate((api) => api.getViewport ? api.getViewport() : null);
|
|
1273
|
+
signal?.throwIfAborted();
|
|
1274
|
+
if (mounted && (mounted.width !== width || mounted.height !== height)) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
`The player composed a ${mounted.width}x${mounted.height} viewport for a ${width}x${height} render; the output would be letterboxed. Rebuild the CLI player bundle (npm run build:cli) so the render page can pin its viewport.`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1074
1279
|
return await fn({ page, renderAPI });
|
|
1075
1280
|
} finally {
|
|
1076
1281
|
signal?.removeEventListener("abort", handleAbort);
|
|
@@ -1078,10 +1283,7 @@ Page errors:
|
|
|
1078
1283
|
await browser.close().catch(() => void 0);
|
|
1079
1284
|
}
|
|
1080
1285
|
}
|
|
1081
|
-
async function
|
|
1082
|
-
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
|
|
1083
|
-
signal?.throwIfAborted();
|
|
1084
|
-
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
1286
|
+
async function requireFfmpeg(signal) {
|
|
1085
1287
|
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1086
1288
|
signal?.throwIfAborted();
|
|
1087
1289
|
if (!ffmpegPath) {
|
|
@@ -1089,53 +1291,106 @@ async function captureDocFrames(doc, container, options) {
|
|
|
1089
1291
|
"ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg\nOr: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary."
|
|
1090
1292
|
);
|
|
1091
1293
|
}
|
|
1294
|
+
return ffmpegPath;
|
|
1295
|
+
}
|
|
1296
|
+
async function iterateDocFrames(session, options, visit) {
|
|
1297
|
+
const { page, renderAPI } = session;
|
|
1298
|
+
const { fps, coverPreRoll, captureFormat, signal, onProgress } = options;
|
|
1299
|
+
const screenshot = () => captureFormat === "jpeg" ? page.screenshot({ type: "jpeg", quality: JPEG_CAPTURE_QUALITY }) : page.screenshot({ type: "png" });
|
|
1300
|
+
const docDuration = await renderAPI.evaluate((api) => api.getDuration());
|
|
1301
|
+
signal?.throwIfAborted();
|
|
1302
|
+
if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
|
|
1303
|
+
const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
1304
|
+
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
1305
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
1306
|
+
const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
|
|
1307
|
+
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
1308
|
+
onProgress?.("capturing frames", 20);
|
|
1309
|
+
if (preRollFrameCount > 0) {
|
|
1310
|
+
signal?.throwIfAborted();
|
|
1311
|
+
await visit(
|
|
1312
|
+
{ index: 0, time: 0, isCover: true, repeat: preRollFrameCount, totalFrames },
|
|
1313
|
+
async () => {
|
|
1314
|
+
await renderAPI.evaluate((api) => api.showCover());
|
|
1315
|
+
await page.waitForTimeout(100);
|
|
1316
|
+
signal?.throwIfAborted();
|
|
1317
|
+
const frame = await screenshot();
|
|
1318
|
+
await renderAPI.evaluate((api) => api.hideCover());
|
|
1319
|
+
return frame;
|
|
1320
|
+
}
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
const progressEvery = Math.max(1, Math.floor(fps / 2));
|
|
1324
|
+
for (let i = 0; i < storyFrameCount; i++) {
|
|
1325
|
+
signal?.throwIfAborted();
|
|
1326
|
+
const time = frameTimeSeconds(i, fps);
|
|
1327
|
+
await visit(
|
|
1328
|
+
{ index: preRollFrameCount + i, time, isCover: false, repeat: 1, totalFrames },
|
|
1329
|
+
async () => {
|
|
1330
|
+
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
1331
|
+
signal?.throwIfAborted();
|
|
1332
|
+
return screenshot();
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
if (i % progressEvery === 0 || i === storyFrameCount - 1) {
|
|
1336
|
+
onProgress?.(
|
|
1337
|
+
"capturing frames",
|
|
1338
|
+
20 + Math.round((preRollFrameCount + i + 1) / totalFrames * 60)
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
totalDuration: docDuration + appliedCoverPreRoll,
|
|
1344
|
+
appliedCoverPreRoll,
|
|
1345
|
+
totalFrames,
|
|
1346
|
+
preRollFrameCount
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function captureDocFrames(doc, container, options) {
|
|
1350
|
+
const {
|
|
1351
|
+
fps,
|
|
1352
|
+
width,
|
|
1353
|
+
height,
|
|
1354
|
+
captionStyle,
|
|
1355
|
+
motion,
|
|
1356
|
+
coverPreRoll,
|
|
1357
|
+
animationsEnabled,
|
|
1358
|
+
onProgress,
|
|
1359
|
+
signal
|
|
1360
|
+
} = options;
|
|
1361
|
+
signal?.throwIfAborted();
|
|
1362
|
+
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
1363
|
+
const ffmpegPath = await requireFfmpeg(signal);
|
|
1092
1364
|
const capturedFrames = new CapturedFrameCollector();
|
|
1093
1365
|
try {
|
|
1094
1366
|
return await withRenderPage(
|
|
1095
1367
|
doc,
|
|
1096
1368
|
container,
|
|
1097
|
-
{
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
await page.waitForTimeout(100);
|
|
1113
|
-
capturedFrames.throwIfAborted(signal);
|
|
1114
|
-
const coverFrame = await page.screenshot({ type: "png" });
|
|
1115
|
-
capturedFrames.throwIfAborted(signal);
|
|
1116
|
-
capturedFrames.append(coverFrame, preRollFrameCount);
|
|
1117
|
-
await renderAPI.evaluate((api) => api.hideCover());
|
|
1118
|
-
}
|
|
1119
|
-
const frameInterval = 1 / fps;
|
|
1120
|
-
for (let i = 0; i < storyFrameCount; i++) {
|
|
1121
|
-
capturedFrames.throwIfAborted(signal);
|
|
1122
|
-
const time = i * frameInterval;
|
|
1123
|
-
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
1124
|
-
const frame = await page.screenshot({ type: "png" });
|
|
1125
|
-
capturedFrames.throwIfAborted(signal);
|
|
1126
|
-
capturedFrames.append(frame);
|
|
1127
|
-
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
1128
|
-
onProgress?.(
|
|
1129
|
-
"capturing frames",
|
|
1130
|
-
20 + Math.round(capturedFrames.frameCount / totalFrames * 60)
|
|
1131
|
-
);
|
|
1369
|
+
{
|
|
1370
|
+
signal,
|
|
1371
|
+
width,
|
|
1372
|
+
height,
|
|
1373
|
+
includeAudio: true,
|
|
1374
|
+
captionStyle,
|
|
1375
|
+
motion,
|
|
1376
|
+
animationsEnabled,
|
|
1377
|
+
onProgress
|
|
1378
|
+
},
|
|
1379
|
+
async (session) => {
|
|
1380
|
+
const loop = await iterateDocFrames(
|
|
1381
|
+
session,
|
|
1382
|
+
{ fps, coverPreRoll, captureFormat: "png", signal, onProgress },
|
|
1383
|
+
async (frame, capture) => {
|
|
1132
1384
|
capturedFrames.throwIfAborted(signal);
|
|
1385
|
+
const bytes = await capture();
|
|
1386
|
+
capturedFrames.throwIfAborted(signal);
|
|
1387
|
+
capturedFrames.append(bytes, frame.repeat);
|
|
1133
1388
|
}
|
|
1134
|
-
|
|
1389
|
+
);
|
|
1135
1390
|
return {
|
|
1136
1391
|
frames: capturedFrames.release(),
|
|
1137
|
-
totalDuration:
|
|
1138
|
-
appliedCoverPreRoll,
|
|
1392
|
+
totalDuration: loop.totalDuration,
|
|
1393
|
+
appliedCoverPreRoll: loop.appliedCoverPreRoll,
|
|
1139
1394
|
ffmpegPath
|
|
1140
1395
|
};
|
|
1141
1396
|
}
|
|
@@ -1146,6 +1401,130 @@ async function captureDocFrames(doc, container, options) {
|
|
|
1146
1401
|
throw err;
|
|
1147
1402
|
}
|
|
1148
1403
|
}
|
|
1404
|
+
async function renderDocToMp4Piped(doc, container, options, render) {
|
|
1405
|
+
const { signal, onProgress } = options;
|
|
1406
|
+
const { fps, quality, width, height, captureFormat } = render;
|
|
1407
|
+
const coverPreRoll = options.coverPreRoll ?? 0;
|
|
1408
|
+
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
1409
|
+
if (options.resume && !options.framesDir) {
|
|
1410
|
+
throw new Error("resume requires framesDir so there is a spool to resume from");
|
|
1411
|
+
}
|
|
1412
|
+
const captionStyle = options.captionStyle === "off" ? void 0 : options.captionStyle;
|
|
1413
|
+
const motion = options.motion ?? null;
|
|
1414
|
+
const animationsEnabled = options.animationsEnabled ?? true;
|
|
1415
|
+
const ffmpegPath = await requireFfmpeg(signal);
|
|
1416
|
+
const outputPath = resolvePath(options.outputPath);
|
|
1417
|
+
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
1418
|
+
const manifest = {
|
|
1419
|
+
generatedBy: "squisq-cli",
|
|
1420
|
+
docHash: docRenderHash(doc),
|
|
1421
|
+
fps,
|
|
1422
|
+
width,
|
|
1423
|
+
height,
|
|
1424
|
+
captionStyle: captionStyle ?? null,
|
|
1425
|
+
motion: motion === null ? null : JSON.stringify(motion),
|
|
1426
|
+
animationsEnabled,
|
|
1427
|
+
coverPreRoll,
|
|
1428
|
+
captureFormat
|
|
1429
|
+
};
|
|
1430
|
+
const store = options.framesDir ? await openDirectoryFrameStore(
|
|
1431
|
+
resolvePath(options.framesDir),
|
|
1432
|
+
manifest,
|
|
1433
|
+
options.resume === true
|
|
1434
|
+
) : null;
|
|
1435
|
+
const audioPath = join4(tmpdir2(), `squisq-audio-${randomBytes2(8).toString("hex")}.mp3`);
|
|
1436
|
+
let audioWritten = false;
|
|
1437
|
+
let sink = null;
|
|
1438
|
+
let reusedFrameCount = 0;
|
|
1439
|
+
try {
|
|
1440
|
+
const { loop, activeSink } = await withRenderPage(
|
|
1441
|
+
doc,
|
|
1442
|
+
container,
|
|
1443
|
+
{
|
|
1444
|
+
signal,
|
|
1445
|
+
width,
|
|
1446
|
+
height,
|
|
1447
|
+
includeAudio: true,
|
|
1448
|
+
captionStyle,
|
|
1449
|
+
motion,
|
|
1450
|
+
animationsEnabled,
|
|
1451
|
+
onProgress
|
|
1452
|
+
},
|
|
1453
|
+
async (session) => {
|
|
1454
|
+
const hasCover = coverPreRoll > 0 ? await session.renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
1455
|
+
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
1456
|
+
onProgress?.("mixing audio", 18);
|
|
1457
|
+
const encodingAudio = await buildMixedAudioTrack(
|
|
1458
|
+
doc,
|
|
1459
|
+
container,
|
|
1460
|
+
ffmpegPath,
|
|
1461
|
+
appliedCoverPreRoll,
|
|
1462
|
+
signal
|
|
1463
|
+
);
|
|
1464
|
+
signal?.throwIfAborted();
|
|
1465
|
+
if (encodingAudio) {
|
|
1466
|
+
await writeFile2(audioPath, encodingAudio);
|
|
1467
|
+
audioWritten = true;
|
|
1468
|
+
}
|
|
1469
|
+
const pipe = createFfmpegPipeSink({
|
|
1470
|
+
ffmpegPath,
|
|
1471
|
+
outputPath,
|
|
1472
|
+
fps,
|
|
1473
|
+
width,
|
|
1474
|
+
height,
|
|
1475
|
+
quality,
|
|
1476
|
+
captureFormat,
|
|
1477
|
+
audioPath: audioWritten ? audioPath : null,
|
|
1478
|
+
signal
|
|
1479
|
+
});
|
|
1480
|
+
sink = pipe;
|
|
1481
|
+
const result = await iterateDocFrames(
|
|
1482
|
+
session,
|
|
1483
|
+
{ fps, coverPreRoll, captureFormat, signal, onProgress },
|
|
1484
|
+
async (frame, capture) => {
|
|
1485
|
+
const started = Date.now();
|
|
1486
|
+
let bytes;
|
|
1487
|
+
let reused = false;
|
|
1488
|
+
if (store?.reusable && await store.has(frame.index)) {
|
|
1489
|
+
bytes = await store.read(frame.index);
|
|
1490
|
+
reused = true;
|
|
1491
|
+
reusedFrameCount += frame.repeat;
|
|
1492
|
+
} else {
|
|
1493
|
+
bytes = await capture();
|
|
1494
|
+
if (store) await store.write(frame.index, bytes);
|
|
1495
|
+
}
|
|
1496
|
+
await pipe.write(bytes, frame.repeat);
|
|
1497
|
+
options.onFrame?.({
|
|
1498
|
+
index: frame.index,
|
|
1499
|
+
time: frame.time,
|
|
1500
|
+
isCover: frame.isCover,
|
|
1501
|
+
totalFrames: frame.totalFrames,
|
|
1502
|
+
reused,
|
|
1503
|
+
captureMs: reused ? 0 : Date.now() - started
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
);
|
|
1507
|
+
return { loop: result, activeSink: pipe };
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
onProgress?.("encoding video", 85);
|
|
1511
|
+
await activeSink.finish();
|
|
1512
|
+
signal?.throwIfAborted();
|
|
1513
|
+
onProgress?.("done", 100);
|
|
1514
|
+
return {
|
|
1515
|
+
duration: loop.totalDuration,
|
|
1516
|
+
frameCount: activeSink.frameCount,
|
|
1517
|
+
outputPath,
|
|
1518
|
+
reusedFrameCount,
|
|
1519
|
+
...store ? { framesDir: store.dir } : {}
|
|
1520
|
+
};
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
await sink?.abort();
|
|
1523
|
+
throw err;
|
|
1524
|
+
} finally {
|
|
1525
|
+
if (audioWritten) await rm2(audioPath, { force: true }).catch(() => void 0);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1149
1528
|
async function renderDocCoverToPng(doc, container, options = {}) {
|
|
1150
1529
|
const width = options.width ?? 1920;
|
|
1151
1530
|
const height = options.height ?? 1080;
|
|
@@ -1178,8 +1557,8 @@ async function renderDocCoverToPng(doc, container, options = {}) {
|
|
|
1178
1557
|
let writtenPath;
|
|
1179
1558
|
if (options.outputPath) {
|
|
1180
1559
|
const absolute = resolvePath(options.outputPath);
|
|
1181
|
-
await
|
|
1182
|
-
await
|
|
1560
|
+
await mkdir2(dirname2(absolute), { recursive: true });
|
|
1561
|
+
await writeFile2(absolute, bytes);
|
|
1183
1562
|
writtenPath = absolute;
|
|
1184
1563
|
}
|
|
1185
1564
|
options.onProgress?.("done", 100);
|
|
@@ -1227,8 +1606,8 @@ async function renderDocToDashboardPng(doc, container, options = {}) {
|
|
|
1227
1606
|
let writtenPath;
|
|
1228
1607
|
if (outputPath) {
|
|
1229
1608
|
const absolute = resolvePath(outputPath);
|
|
1230
|
-
await
|
|
1231
|
-
await
|
|
1609
|
+
await mkdir2(dirname2(absolute), { recursive: true });
|
|
1610
|
+
await writeFile2(absolute, bytes);
|
|
1232
1611
|
writtenPath = absolute;
|
|
1233
1612
|
}
|
|
1234
1613
|
onProgress?.("done", 100);
|
|
@@ -1251,11 +1630,26 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
1251
1630
|
fps,
|
|
1252
1631
|
quality
|
|
1253
1632
|
});
|
|
1633
|
+
const frameTransport = options.frameTransport ?? "pipe";
|
|
1634
|
+
const captureFormat = options.captureFormat ?? (quality === "high" ? "png" : "jpeg");
|
|
1635
|
+
if (frameTransport === "pipe") {
|
|
1636
|
+
return renderDocToMp4Piped(doc, container, options, {
|
|
1637
|
+
fps,
|
|
1638
|
+
quality,
|
|
1639
|
+
width: dimensions.width,
|
|
1640
|
+
height: dimensions.height,
|
|
1641
|
+
captureFormat
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (options.framesDir || options.resume || options.onFrame) {
|
|
1645
|
+
throw new Error("framesDir, resume, and onFrame require the pipe frame transport");
|
|
1646
|
+
}
|
|
1254
1647
|
const capture = await captureDocFrames(doc, container, {
|
|
1255
1648
|
fps,
|
|
1256
1649
|
width: dimensions.width,
|
|
1257
1650
|
height: dimensions.height,
|
|
1258
1651
|
captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle,
|
|
1652
|
+
motion: options.motion ?? null,
|
|
1259
1653
|
coverPreRoll: options.coverPreRoll ?? 0,
|
|
1260
1654
|
animationsEnabled: options.animationsEnabled ?? true,
|
|
1261
1655
|
onProgress: options.onProgress,
|
|
@@ -1273,7 +1667,7 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
1273
1667
|
options.signal
|
|
1274
1668
|
);
|
|
1275
1669
|
options.signal?.throwIfAborted();
|
|
1276
|
-
const { framesToMp4Native: framesToMp4Native2 } = await import("./nativeEncoder-
|
|
1670
|
+
const { framesToMp4Native: framesToMp4Native2 } = await import("./nativeEncoder-RILHXTXI.js");
|
|
1277
1671
|
await framesToMp4Native2(capture.ffmpegPath, capture.frames, encodingAudio, options.outputPath, {
|
|
1278
1672
|
fps,
|
|
1279
1673
|
quality,
|
|
@@ -1319,6 +1713,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1319
1713
|
width,
|
|
1320
1714
|
height,
|
|
1321
1715
|
captionStyle: options.captionStyle === "off" ? void 0 : options.captionStyle ?? "standard",
|
|
1716
|
+
motion: options.motion ?? null,
|
|
1322
1717
|
coverPreRoll: options.coverPreRoll ?? 0,
|
|
1323
1718
|
animationsEnabled: options.animationsEnabled ?? false,
|
|
1324
1719
|
onProgress: options.onProgress,
|
|
@@ -1328,7 +1723,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1328
1723
|
try {
|
|
1329
1724
|
options.onProgress?.("encoding GIF", 80);
|
|
1330
1725
|
options.signal?.throwIfAborted();
|
|
1331
|
-
const { framesToGifNative: framesToGifNative2 } = await import("./nativeEncoder-
|
|
1726
|
+
const { framesToGifNative: framesToGifNative2 } = await import("./nativeEncoder-RILHXTXI.js");
|
|
1332
1727
|
await framesToGifNative2(capture.ffmpegPath, capture.frames, options.outputPath, {
|
|
1333
1728
|
fps,
|
|
1334
1729
|
orientation,
|
|
@@ -1358,7 +1753,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1358
1753
|
async function extractThumbnails(options) {
|
|
1359
1754
|
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
1360
1755
|
const { existsSync } = await import("fs");
|
|
1361
|
-
const { rm:
|
|
1756
|
+
const { rm: rm3 } = await import("fs/promises");
|
|
1362
1757
|
const { isAbsolute: isAbsolute2, relative: relative2, resolve: resolve2, sep: sep2 } = await import("path");
|
|
1363
1758
|
signal?.throwIfAborted();
|
|
1364
1759
|
if (typeof slug !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/.test(slug)) {
|
|
@@ -1408,13 +1803,13 @@ async function extractThumbnails(options) {
|
|
|
1408
1803
|
signal?.throwIfAborted();
|
|
1409
1804
|
generatedPaths.push(outputPath);
|
|
1410
1805
|
} catch (error) {
|
|
1411
|
-
await
|
|
1806
|
+
await rm3(outputPath, { force: true });
|
|
1412
1807
|
throw error;
|
|
1413
1808
|
}
|
|
1414
1809
|
}
|
|
1415
1810
|
signal?.throwIfAborted();
|
|
1416
1811
|
} catch (error) {
|
|
1417
|
-
await Promise.allSettled(generatedPaths.map((path) =>
|
|
1812
|
+
await Promise.allSettled(generatedPaths.map((path) => rm3(path, { force: true })));
|
|
1418
1813
|
throw error;
|
|
1419
1814
|
}
|
|
1420
1815
|
}
|
|
@@ -1426,6 +1821,7 @@ export {
|
|
|
1426
1821
|
detectFfmpegDetailed,
|
|
1427
1822
|
MAX_CAPTURED_FRAME_BYTES,
|
|
1428
1823
|
CapturedFrameBudgetError,
|
|
1824
|
+
ffmpegPipeArgs,
|
|
1429
1825
|
convert,
|
|
1430
1826
|
prepareConversion,
|
|
1431
1827
|
renderDocCoverToPng,
|