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