@bendyline/squisq-cli 2.4.2 → 2.4.4
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 +5 -6
- package/THIRD_PARTY_LICENSES.txt +1 -1
- package/dist/{api-KYHSJM5T.js → api-36UERSSU.js} +1 -1
- package/dist/api.js +230 -71
- package/dist/{chunk-VK6WPOSG.js → chunk-6OBPEBL4.js} +233 -73
- package/dist/index.js +18 -14
- package/dist/squisq-player-icons.global.js +1 -0
- package/dist/squisq-player.full.global.js +402 -401
- package/dist/squisq-player.global.js +56 -55
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -177,17 +177,16 @@ const result = await convert({ kind: 'markdown', markdown: '# Hello' }, 'docx');
|
|
|
177
177
|
|
|
178
178
|
```ts
|
|
179
179
|
import { renderDocToMp4, readInput } from '@bendyline/squisq-cli/api';
|
|
180
|
-
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
181
180
|
|
|
182
181
|
// Load a document from disk (.md, .zip/.dbk, folder, or Doc .json)
|
|
183
182
|
const input = await readInput('./my-article.md');
|
|
184
183
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
const doc
|
|
184
|
+
// `doc` is always normalized and ready to render. `markdownDoc` is present
|
|
185
|
+
// only when the source was markdown-shaped; `sourceFormat` identifies the input.
|
|
186
|
+
const { doc, container, markdownDoc, sourceFormat } = input;
|
|
188
187
|
|
|
189
188
|
// Render to MP4
|
|
190
|
-
const result = await renderDocToMp4(doc,
|
|
189
|
+
const result = await renderDocToMp4(doc, container, {
|
|
191
190
|
outputPath: './output.mp4',
|
|
192
191
|
fps: 30, // default 30
|
|
193
192
|
quality: 'high', // 'draft' | 'normal' | 'high' (default 'normal')
|
|
@@ -269,7 +268,7 @@ await extractThumbnails({
|
|
|
269
268
|
|
|
270
269
|
### Other exports
|
|
271
270
|
|
|
272
|
-
- `readInput(inputPath)` → `{ container:
|
|
271
|
+
- `readInput(inputPath)` → `{ doc: Doc, container: ContentContainer, markdownDoc?: MarkdownDocument, sourceFormat: FormatId }`
|
|
273
272
|
- `MemoryContentContainer` (re-export from `@bendyline/squisq/storage`)
|
|
274
273
|
- `VideoQuality`, `VideoOrientation` types (re-exports from `@bendyline/squisq-video`)
|
|
275
274
|
|
package/THIRD_PARTY_LICENSES.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
THIRD-PARTY LICENSES FOR @bendyline/squisq-cli
|
|
2
2
|
|
|
3
|
-
The inventory below covers the bundled light and full standalone player artifacts.
|
|
3
|
+
The inventory below covers the bundled light and full standalone player artifacts and their shared icon styles.
|
|
4
4
|
|
|
5
5
|
Generated from the actual esbuild input graph. Package-local license, copying,
|
|
6
6
|
and notice files are reproduced verbatim. When an npm tarball omits its
|
package/dist/api.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/api.ts
|
|
11
11
|
import { readFile as readFile3 } from "fs/promises";
|
|
12
|
-
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
12
|
+
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
13
13
|
import { flattenBlocks as flattenBlocks2 } from "@bendyline/squisq/doc";
|
|
14
14
|
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
15
15
|
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
@@ -22,17 +22,17 @@ import {
|
|
|
22
22
|
import { execFile } from "child_process";
|
|
23
23
|
function run(command, args, signal) {
|
|
24
24
|
signal?.throwIfAborted();
|
|
25
|
-
return new Promise((
|
|
25
|
+
return new Promise((resolve2, reject) => {
|
|
26
26
|
execFile(command, args, { timeout: 5e3, signal }, (err, stdout) => {
|
|
27
27
|
if (signal?.aborted) {
|
|
28
28
|
reject(signal.reason);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (err || !stdout.trim()) {
|
|
32
|
-
|
|
32
|
+
resolve2(null);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
resolve2(stdout.trim());
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
}
|
|
@@ -76,7 +76,7 @@ async function detectFfmpegDetailed(signal) {
|
|
|
76
76
|
import { computeAudioTimeline } from "@bendyline/squisq-video";
|
|
77
77
|
async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, signal) {
|
|
78
78
|
signal?.throwIfAborted();
|
|
79
|
-
const timeline = computeAudioTimeline(doc, coverPreRoll);
|
|
79
|
+
const timeline = computeAudioTimeline(doc, coverPreRoll, { includeVideoAudio: false });
|
|
80
80
|
if (timeline.length === 0) return null;
|
|
81
81
|
const bytesBySrc = /* @__PURE__ */ new Map();
|
|
82
82
|
const readSrc = async (src) => {
|
|
@@ -418,13 +418,45 @@ function createCliRegistry() {
|
|
|
418
418
|
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
419
419
|
|
|
420
420
|
// src/util/readInput.ts
|
|
421
|
-
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
422
|
-
import {
|
|
421
|
+
import { readFile as readFile2, readdir, realpath, stat } from "fs/promises";
|
|
422
|
+
import {
|
|
423
|
+
basename,
|
|
424
|
+
dirname,
|
|
425
|
+
extname,
|
|
426
|
+
isAbsolute,
|
|
427
|
+
join as join2,
|
|
428
|
+
posix,
|
|
429
|
+
relative,
|
|
430
|
+
resolve,
|
|
431
|
+
sep,
|
|
432
|
+
win32
|
|
433
|
+
} from "path";
|
|
423
434
|
import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
424
435
|
import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
|
|
436
|
+
import { resolveMediaSchedule, validateDocSchema } from "@bendyline/squisq/schemas";
|
|
425
437
|
import { MemoryContentContainer } from "@bendyline/squisq/storage";
|
|
426
438
|
import { zipToContainer } from "@bendyline/squisq-formats/container";
|
|
427
439
|
import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
|
|
440
|
+
var DocInputValidationError = class extends Error {
|
|
441
|
+
constructor(source, issues) {
|
|
442
|
+
const detail = issues.map(formatSchemaIssueForError).join("; ");
|
|
443
|
+
super(`${source} is not a valid squisq Doc: ${detail}`);
|
|
444
|
+
this.name = "DocInputValidationError";
|
|
445
|
+
this.issues = issues;
|
|
446
|
+
this.diagnostics = issues.map((issue) => ({
|
|
447
|
+
severity: "error",
|
|
448
|
+
code: "invalid-doc-schema",
|
|
449
|
+
message: `${issue.path} ${issue.message}`
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function formatSchemaIssueForError(issue) {
|
|
454
|
+
if (issue.path === "$") {
|
|
455
|
+
const got = /\(got ([^)]+)\)/.exec(issue.message)?.[1] ?? "an invalid value";
|
|
456
|
+
return `expected a JSON object, got ${got}`;
|
|
457
|
+
}
|
|
458
|
+
return `"${issue.path}" ${issue.message}`;
|
|
459
|
+
}
|
|
428
460
|
var MIME_TYPES = {
|
|
429
461
|
".md": "text/markdown",
|
|
430
462
|
".txt": "text/plain",
|
|
@@ -439,7 +471,11 @@ var MIME_TYPES = {
|
|
|
439
471
|
".wav": "audio/wav",
|
|
440
472
|
".ogg": "audio/ogg",
|
|
441
473
|
".mp4": "video/mp4",
|
|
442
|
-
".webm": "video/webm"
|
|
474
|
+
".webm": "video/webm",
|
|
475
|
+
".woff": "font/woff",
|
|
476
|
+
".woff2": "font/woff2",
|
|
477
|
+
".ttf": "font/ttf",
|
|
478
|
+
".otf": "font/otf"
|
|
443
479
|
};
|
|
444
480
|
var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"];
|
|
445
481
|
function mimeFromExt(filePath) {
|
|
@@ -465,8 +501,10 @@ async function readInput(inputPath, options) {
|
|
|
465
501
|
throwIfAborted(options?.signal);
|
|
466
502
|
const result = await readInputRaw(inputPath, options);
|
|
467
503
|
throwIfAborted(options?.signal);
|
|
504
|
+
assertValidDoc(result.doc, inputPath);
|
|
468
505
|
const doc = await resolveAudioMapping(result.doc, result.container);
|
|
469
506
|
throwIfAborted(options?.signal);
|
|
507
|
+
assertValidDoc(doc, inputPath);
|
|
470
508
|
return doc === result.doc ? result : { ...result, doc };
|
|
471
509
|
}
|
|
472
510
|
async function readInputRaw(inputPath, options) {
|
|
@@ -519,69 +557,160 @@ async function readUtf8File(filePath, signal) {
|
|
|
519
557
|
}
|
|
520
558
|
async function readMarkdownFile(filePath, signal) {
|
|
521
559
|
const content = await readUtf8File(filePath, signal);
|
|
522
|
-
const container = new MemoryContentContainer();
|
|
523
|
-
await container.writeDocument(content);
|
|
524
|
-
throwIfAborted(signal);
|
|
525
560
|
const markdownDoc = parseMarkdown(content);
|
|
526
|
-
|
|
561
|
+
const doc = markdownToDoc(markdownDoc);
|
|
562
|
+
const container = await buildBareMarkdownContainer(filePath, content, doc, signal);
|
|
563
|
+
return { doc, container, markdownDoc, sourceFormat: "md" };
|
|
527
564
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
fail(`expected a JSON object, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`);
|
|
541
|
-
}
|
|
542
|
-
const doc = parsed;
|
|
543
|
-
if (!Array.isArray(doc.blocks)) {
|
|
544
|
-
fail(`"blocks" must be an array${doc.blocks === void 0 ? " (field is missing)" : ""}`);
|
|
565
|
+
var NARRATION_EXTENSIONS = /* @__PURE__ */ new Set([".aac", ".flac", ".m4a", ".mp3", ".ogg", ".wav"]);
|
|
566
|
+
async function buildBareMarkdownContainer(filePath, content, doc, signal) {
|
|
567
|
+
const container = new MemoryContentContainer();
|
|
568
|
+
await container.writeDocument(content, basename(filePath));
|
|
569
|
+
throwIfAborted(signal);
|
|
570
|
+
const refs = collectAuthoredAssetRefs(content, doc);
|
|
571
|
+
const root = dirname(resolve(filePath));
|
|
572
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
573
|
+
throwIfAborted(signal);
|
|
574
|
+
if (entry.isFile() && NARRATION_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
575
|
+
refs.add(entry.name);
|
|
576
|
+
}
|
|
545
577
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
578
|
+
refs.add("timing.json");
|
|
579
|
+
for (const ref of [...refs]) {
|
|
580
|
+
if (NARRATION_EXTENSIONS.has(extname(stripUrlSuffix(ref)).toLowerCase())) {
|
|
581
|
+
refs.add(`${stripUrlSuffix(ref)}.timing.json`);
|
|
549
582
|
}
|
|
550
583
|
}
|
|
551
|
-
|
|
552
|
-
|
|
584
|
+
const rootReal = await realpath(root);
|
|
585
|
+
let fileCount = 0;
|
|
586
|
+
let totalBytes = 0;
|
|
587
|
+
for (const authoredRef of refs) {
|
|
588
|
+
throwIfAborted(signal);
|
|
589
|
+
const safe = normalizeAssetReference(authoredRef);
|
|
590
|
+
if (!safe) continue;
|
|
591
|
+
const absolute = resolve(root, ...safe.split("/"));
|
|
592
|
+
if (!isContainedPath(root, absolute)) continue;
|
|
593
|
+
let assetReal;
|
|
594
|
+
let info;
|
|
595
|
+
try {
|
|
596
|
+
assetReal = await realpath(absolute);
|
|
597
|
+
if (!isContainedPath(rootReal, assetReal)) continue;
|
|
598
|
+
info = await stat(assetReal);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (isMissingFileError(error)) continue;
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
if (!info.isFile()) continue;
|
|
604
|
+
if (info.size > MAX_RENDER_MEDIA_FILE_BYTES) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`Sibling asset "${safe}" exceeds the ${formatMiB(MAX_RENDER_MEDIA_FILE_BYTES)} per-file input limit.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (fileCount + 1 > MAX_RENDER_MEDIA_FILES) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Bare markdown input references more than ${MAX_RENDER_MEDIA_FILES} sibling assets.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (totalBytes + info.size > MAX_RENDER_MEDIA_TOTAL_BYTES) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Sibling assets exceed the ${formatMiB(MAX_RENDER_MEDIA_TOTAL_BYTES)} total input limit.`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
const data = await readBinaryFile(assetReal, signal);
|
|
620
|
+
if (data.byteLength > MAX_RENDER_MEDIA_FILE_BYTES) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Sibling asset "${safe}" exceeds the ${formatMiB(MAX_RENDER_MEDIA_FILE_BYTES)} per-file input limit.`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (totalBytes + data.byteLength > MAX_RENDER_MEDIA_TOTAL_BYTES) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Sibling assets exceed the ${formatMiB(MAX_RENDER_MEDIA_TOTAL_BYTES)} total input limit.`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
await container.writeFile(safe, data, mimeFromExt(safe));
|
|
631
|
+
fileCount += 1;
|
|
632
|
+
totalBytes += data.byteLength;
|
|
553
633
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
634
|
+
return container;
|
|
635
|
+
}
|
|
636
|
+
function collectAuthoredAssetRefs(content, doc) {
|
|
637
|
+
const refs = /* @__PURE__ */ new Set();
|
|
638
|
+
const add = (value) => {
|
|
639
|
+
if (typeof value === "string" && value.trim()) refs.add(value.trim());
|
|
640
|
+
};
|
|
641
|
+
const scanObject = (value, seen = /* @__PURE__ */ new Set()) => {
|
|
642
|
+
if (!value || typeof value !== "object") return;
|
|
643
|
+
if (seen.has(value)) return;
|
|
644
|
+
seen.add(value);
|
|
645
|
+
if (Array.isArray(value)) {
|
|
646
|
+
for (const item of value) scanObject(item, seen);
|
|
647
|
+
return;
|
|
557
648
|
}
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
fail(`"audio.segments[${index}]" must be an object`);
|
|
564
|
-
}
|
|
565
|
-
if (!isFiniteNumber(segment.duration)) {
|
|
566
|
-
fail(
|
|
567
|
-
`"audio.segments[${index}].duration" must be a finite number, got ${describe(segment.duration)}`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
649
|
+
for (const [key, item] of Object.entries(value)) {
|
|
650
|
+
if (typeof item === "string" && ["src", "url", "heroSrc", "posterSrc", "staticSrc", "videoSrc", "imageSrc"].includes(key)) {
|
|
651
|
+
add(item);
|
|
652
|
+
} else {
|
|
653
|
+
scanObject(item, seen);
|
|
570
654
|
}
|
|
571
655
|
}
|
|
572
|
-
}
|
|
573
|
-
return {
|
|
574
|
-
...doc,
|
|
575
|
-
audio: doc.audio ?? { segments: [] }
|
|
576
656
|
};
|
|
657
|
+
scanObject(doc);
|
|
658
|
+
for (const segment of doc.audio.segments) add(segment.src);
|
|
659
|
+
for (const clip of resolveMediaSchedule(doc)) add(clip.src);
|
|
660
|
+
const patterns = [
|
|
661
|
+
/\b(?:src|href)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi,
|
|
662
|
+
/\b(?:src|audio|video|image|font)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s}\]]+))/gi,
|
|
663
|
+
/\burl\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)]+))\s*\)/gi,
|
|
664
|
+
/!?\[[^\]]*\]\(\s*<?([^\s)>]+)>?/g
|
|
665
|
+
];
|
|
666
|
+
for (const pattern of patterns) {
|
|
667
|
+
let match;
|
|
668
|
+
while (match = pattern.exec(content)) add(match.slice(1).find(Boolean));
|
|
669
|
+
}
|
|
670
|
+
return refs;
|
|
577
671
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
672
|
+
function normalizeAssetReference(authoredRef) {
|
|
673
|
+
let value = stripUrlSuffix(authoredRef.trim().replace(/^<|>$/g, ""));
|
|
674
|
+
try {
|
|
675
|
+
value = decodeURIComponent(value);
|
|
676
|
+
} catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
if (!value || value.includes("\0") || /^[a-z][a-z\d+.-]*:/i.test(value) || posix.isAbsolute(value) || win32.isAbsolute(value) || isAbsolute(value)) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const parts = value.replace(/\\/g, "/").split("/");
|
|
683
|
+
if (parts.some((part) => part === "..")) return null;
|
|
684
|
+
const normalized = parts.filter((part) => part && part !== ".").join("/");
|
|
685
|
+
return normalized || null;
|
|
686
|
+
}
|
|
687
|
+
function stripUrlSuffix(value) {
|
|
688
|
+
return value.split(/[?#]/, 1)[0] ?? value;
|
|
689
|
+
}
|
|
690
|
+
function isContainedPath(root, candidate) {
|
|
691
|
+
const rel = relative(root, candidate);
|
|
692
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
580
693
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
694
|
+
function isMissingFileError(error) {
|
|
695
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
696
|
+
}
|
|
697
|
+
function formatMiB(bytes) {
|
|
698
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
699
|
+
}
|
|
700
|
+
function assertValidDoc(value, source) {
|
|
701
|
+
const issues = validateDocSchema(value);
|
|
702
|
+
if (issues.length > 0) throw new DocInputValidationError(source, issues);
|
|
703
|
+
}
|
|
704
|
+
function parseDocJson(content, source) {
|
|
705
|
+
let parsed;
|
|
706
|
+
try {
|
|
707
|
+
parsed = JSON.parse(content);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
710
|
+
throw new Error(`${source} is not valid JSON: ${detail}`);
|
|
711
|
+
}
|
|
712
|
+
assertValidDoc(parsed, source);
|
|
713
|
+
return parsed;
|
|
585
714
|
}
|
|
586
715
|
async function readDocJsonFile(filePath, signal) {
|
|
587
716
|
const content = await readUtf8File(filePath, signal);
|
|
@@ -689,18 +818,26 @@ function throwIfAborted(signal) {
|
|
|
689
818
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
690
819
|
var playerBundlePromise;
|
|
691
820
|
var fullPlayerBundlePromise;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
821
|
+
var playerIconBootstrapPromise;
|
|
822
|
+
function loadPlayerIconBootstrap() {
|
|
823
|
+
playerIconBootstrapPromise ??= readFile3(
|
|
824
|
+
new URL("../dist/squisq-player-icons.global.js", import.meta.url),
|
|
695
825
|
"utf8"
|
|
696
826
|
);
|
|
827
|
+
return playerIconBootstrapPromise;
|
|
828
|
+
}
|
|
829
|
+
function loadPlayerBundle() {
|
|
830
|
+
playerBundlePromise ??= Promise.all([
|
|
831
|
+
loadPlayerIconBootstrap(),
|
|
832
|
+
readFile3(new URL("../dist/squisq-player.global.js", import.meta.url), "utf8")
|
|
833
|
+
]).then(([icons, player]) => `${icons}${player}`);
|
|
697
834
|
return playerBundlePromise;
|
|
698
835
|
}
|
|
699
836
|
function loadFullPlayerBundle() {
|
|
700
|
-
fullPlayerBundlePromise ??=
|
|
701
|
-
|
|
702
|
-
"utf8"
|
|
703
|
-
);
|
|
837
|
+
fullPlayerBundlePromise ??= Promise.all([
|
|
838
|
+
loadPlayerIconBootstrap(),
|
|
839
|
+
readFile3(new URL("../dist/squisq-player.full.global.js", import.meta.url), "utf8")
|
|
840
|
+
]).then(([icons, player]) => `${icons}${player}`);
|
|
704
841
|
return fullPlayerBundlePromise;
|
|
705
842
|
}
|
|
706
843
|
async function convert(source, to, options = {}) {
|
|
@@ -754,7 +891,7 @@ async function captureDocFrames(doc, container, options) {
|
|
|
754
891
|
audio.set(seg.name, data);
|
|
755
892
|
}
|
|
756
893
|
}
|
|
757
|
-
const mediaSrcs = new Set(
|
|
894
|
+
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
758
895
|
for (const block of flattenBlocks2(doc.blocks)) {
|
|
759
896
|
for (const layer of block.layers ?? []) {
|
|
760
897
|
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
@@ -1005,7 +1142,7 @@ async function renderDocToGif(doc, container, options) {
|
|
|
1005
1142
|
options.signal?.throwIfAborted();
|
|
1006
1143
|
options.onProgress?.("done", 100);
|
|
1007
1144
|
options.signal?.throwIfAborted();
|
|
1008
|
-
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 ||
|
|
1145
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule2(doc).some((clip) => clip.kind === "audio");
|
|
1009
1146
|
return {
|
|
1010
1147
|
duration: capture.totalDuration,
|
|
1011
1148
|
frameCount,
|
|
@@ -1020,8 +1157,30 @@ async function extractThumbnails(options) {
|
|
|
1020
1157
|
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
1021
1158
|
const { existsSync } = await import("fs");
|
|
1022
1159
|
const { rm: rm2 } = await import("fs/promises");
|
|
1023
|
-
const {
|
|
1160
|
+
const { isAbsolute: isAbsolute2, relative: relative2, resolve: resolve2, sep: sep2 } = await import("path");
|
|
1024
1161
|
signal?.throwIfAborted();
|
|
1162
|
+
if (typeof slug !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/.test(slug)) {
|
|
1163
|
+
throw new TypeError(
|
|
1164
|
+
"Thumbnail slug must be 1\u2013128 filename-safe characters (letters, numbers, dot, dash, or underscore) and must start and end with a letter or number."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1168
|
+
if (!Number.isSafeInteger(thumb.width) || thumb.width <= 0) {
|
|
1169
|
+
throw new TypeError(`Thumbnail sizes[${index}].width must be a positive integer.`);
|
|
1170
|
+
}
|
|
1171
|
+
if (!Number.isSafeInteger(thumb.height) || thumb.height <= 0) {
|
|
1172
|
+
throw new TypeError(`Thumbnail sizes[${index}].height must be a positive integer.`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const resolvedOutputDir = resolve2(outputDir);
|
|
1176
|
+
const outputPaths = sizes.map((thumb) => {
|
|
1177
|
+
const outputPath = resolve2(resolvedOutputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
|
|
1178
|
+
const rel = relative2(resolvedOutputDir, outputPath);
|
|
1179
|
+
if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
1180
|
+
throw new TypeError("Thumbnail output path must remain inside outputDir.");
|
|
1181
|
+
}
|
|
1182
|
+
return outputPath;
|
|
1183
|
+
});
|
|
1025
1184
|
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1026
1185
|
if (!ffmpegPath) {
|
|
1027
1186
|
throw new Error(
|
|
@@ -1030,9 +1189,9 @@ async function extractThumbnails(options) {
|
|
|
1030
1189
|
}
|
|
1031
1190
|
const generatedPaths = [];
|
|
1032
1191
|
try {
|
|
1033
|
-
for (const thumb of sizes) {
|
|
1192
|
+
for (const [index, thumb] of sizes.entries()) {
|
|
1034
1193
|
signal?.throwIfAborted();
|
|
1035
|
-
const outputPath =
|
|
1194
|
+
const outputPath = outputPaths[index];
|
|
1036
1195
|
if (!force && existsSync(outputPath)) continue;
|
|
1037
1196
|
try {
|
|
1038
1197
|
await runFfmpeg(
|