@bendyline/squisq-cli 2.0.0 → 2.0.1
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/dist/{api-6KN3F6IQ.js → api-LE75OSRQ.js} +9 -3
- package/dist/api.d.ts +43 -7
- package/dist/api.js +375 -120
- package/dist/api.js.map +1 -1
- package/dist/{chunk-JEE26SZS.js → chunk-FVFK4ZJU.js} +378 -123
- package/dist/chunk-FVFK4ZJU.js.map +1 -0
- package/dist/{chunk-VM3ZFP3J.js → chunk-JDHONKFZ.js} +48 -15
- package/dist/chunk-JDHONKFZ.js.map +1 -0
- package/dist/{chunk-ESFEFKY4.js → chunk-OEN2RADG.js} +48 -15
- package/dist/chunk-OEN2RADG.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{nativeEncoder-IQPN2RMC.js → nativeEncoder-ET3HUSOE.js} +2 -2
- package/dist/{nativeEncoder-WHUIQZLL.js → nativeEncoder-LRPK2YV5.js} +2 -2
- package/package.json +5 -5
- package/dist/chunk-ESFEFKY4.js.map +0 -1
- package/dist/chunk-JEE26SZS.js.map +0 -1
- package/dist/chunk-VM3ZFP3J.js.map +0 -1
- /package/dist/{api-6KN3F6IQ.js.map → api-LE75OSRQ.js.map} +0 -0
- /package/dist/{nativeEncoder-IQPN2RMC.js.map → nativeEncoder-ET3HUSOE.js.map} +0 -0
- /package/dist/{nativeEncoder-WHUIQZLL.js.map → nativeEncoder-LRPK2YV5.js.map} +0 -0
package/dist/api.js
CHANGED
|
@@ -3,21 +3,30 @@ import {
|
|
|
3
3
|
framesToGifNative,
|
|
4
4
|
framesToGifNativeBytes,
|
|
5
5
|
framesToMp4Native,
|
|
6
|
-
framesToMp4NativeBytes
|
|
7
|
-
|
|
6
|
+
framesToMp4NativeBytes,
|
|
7
|
+
runFfmpeg
|
|
8
|
+
} from "./chunk-OEN2RADG.js";
|
|
8
9
|
|
|
9
10
|
// src/api.ts
|
|
10
11
|
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
11
12
|
import { flattenBlocks } from "@bendyline/squisq/doc";
|
|
12
13
|
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
13
14
|
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
convert as formatsConvert,
|
|
17
|
+
prepareConversion as formatsPrepareConversion
|
|
18
|
+
} from "@bendyline/squisq-formats";
|
|
15
19
|
|
|
16
20
|
// src/util/detectFfmpeg.ts
|
|
17
21
|
import { execFile } from "child_process";
|
|
18
|
-
function run(command, args) {
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
function run(command, args, signal) {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
execFile(command, args, { timeout: 5e3, signal }, (err, stdout) => {
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
reject(signal.reason);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
21
30
|
if (err || !stdout.trim()) {
|
|
22
31
|
resolve(null);
|
|
23
32
|
return;
|
|
@@ -26,14 +35,15 @@ function run(command, args) {
|
|
|
26
35
|
});
|
|
27
36
|
});
|
|
28
37
|
}
|
|
29
|
-
async function getFfmpegVersion(path) {
|
|
30
|
-
const out = await run(path, ["-version"]);
|
|
38
|
+
async function getFfmpegVersion(path, signal) {
|
|
39
|
+
const out = await run(path, ["-version"], signal);
|
|
31
40
|
return out ? out.split("\n")[0].trim() : null;
|
|
32
41
|
}
|
|
33
|
-
async function detectFfmpegDetailed() {
|
|
42
|
+
async function detectFfmpegDetailed(signal) {
|
|
43
|
+
signal?.throwIfAborted();
|
|
34
44
|
const envPath = process.env.SQUISQ_FFMPEG;
|
|
35
45
|
if (envPath) {
|
|
36
|
-
const version = await getFfmpegVersion(envPath);
|
|
46
|
+
const version = await getFfmpegVersion(envPath, signal);
|
|
37
47
|
if (!version) {
|
|
38
48
|
throw new Error(
|
|
39
49
|
`SQUISQ_FFMPEG is set to "${envPath}" but running "${envPath} -version" failed. Fix the path or unset SQUISQ_FFMPEG.`
|
|
@@ -42,11 +52,12 @@ async function detectFfmpegDetailed() {
|
|
|
42
52
|
return { path: envPath, source: "env" };
|
|
43
53
|
}
|
|
44
54
|
const command = process.platform === "win32" ? "where" : "which";
|
|
45
|
-
const found = await run(command, ["ffmpeg"]);
|
|
55
|
+
const found = await run(command, ["ffmpeg"], signal);
|
|
46
56
|
if (found) {
|
|
47
57
|
return { path: found.split("\n")[0].trim(), source: "path" };
|
|
48
58
|
}
|
|
49
59
|
try {
|
|
60
|
+
signal?.throwIfAborted();
|
|
50
61
|
const specifier = "ffmpeg-static";
|
|
51
62
|
const mod = await import(specifier);
|
|
52
63
|
const candidate = typeof mod === "string" ? mod : typeof mod.default === "string" ? mod.default : null;
|
|
@@ -54,46 +65,55 @@ async function detectFfmpegDetailed() {
|
|
|
54
65
|
return { path: candidate, source: "ffmpeg-static" };
|
|
55
66
|
}
|
|
56
67
|
} catch {
|
|
68
|
+
signal?.throwIfAborted();
|
|
57
69
|
}
|
|
70
|
+
signal?.throwIfAborted();
|
|
58
71
|
return null;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
// src/util/audioMix.ts
|
|
62
75
|
import { computeAudioTimeline } from "@bendyline/squisq-video";
|
|
63
|
-
async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll) {
|
|
76
|
+
async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, signal) {
|
|
77
|
+
signal?.throwIfAborted();
|
|
64
78
|
const timeline = computeAudioTimeline(doc, coverPreRoll);
|
|
65
79
|
if (timeline.length === 0) return null;
|
|
66
80
|
const bytesBySrc = /* @__PURE__ */ new Map();
|
|
67
81
|
const readSrc = async (src) => {
|
|
82
|
+
signal?.throwIfAborted();
|
|
68
83
|
if (bytesBySrc.has(src)) return bytesBySrc.get(src);
|
|
69
84
|
const data = await container.readFile(src);
|
|
85
|
+
signal?.throwIfAborted();
|
|
70
86
|
bytesBySrc.set(src, data ?? null);
|
|
71
87
|
return data ?? null;
|
|
72
88
|
};
|
|
73
89
|
const usable = [];
|
|
74
90
|
for (const clip of timeline) {
|
|
91
|
+
signal?.throwIfAborted();
|
|
75
92
|
const buffer = await readSrc(clip.src);
|
|
76
93
|
if (buffer) usable.push({ clip, buffer });
|
|
77
94
|
}
|
|
78
95
|
if (usable.length === 0) return null;
|
|
79
|
-
return mixTimelineClips(ffmpegPath, usable);
|
|
96
|
+
return mixTimelineClips(ffmpegPath, usable, signal);
|
|
80
97
|
}
|
|
81
|
-
async function mixTimelineClips(ffmpegPath, usable) {
|
|
98
|
+
async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
82
99
|
const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
|
|
83
100
|
const { join: join3 } = await import("path");
|
|
84
101
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
85
102
|
const { randomBytes: randomBytes2 } = await import("crypto");
|
|
86
|
-
|
|
103
|
+
signal?.throwIfAborted();
|
|
87
104
|
const workDir = join3(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
|
|
88
105
|
await mkdir(workDir, { recursive: true });
|
|
89
106
|
const ms = (s) => Math.max(0, Math.round(s * 1e3));
|
|
90
107
|
try {
|
|
108
|
+
signal?.throwIfAborted();
|
|
91
109
|
const inputs = [];
|
|
92
110
|
const filters = [];
|
|
93
111
|
const labels = [];
|
|
94
112
|
for (const { clip, buffer } of usable) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
95
114
|
const p = join3(workDir, `clip-${inputs.length}.mp3`);
|
|
96
115
|
await writeFile(p, new Uint8Array(buffer));
|
|
116
|
+
signal?.throwIfAborted();
|
|
97
117
|
const i = inputs.push(p) - 1;
|
|
98
118
|
const delayMs = ms(clip.startSec);
|
|
99
119
|
const start = Math.max(0, clip.sourceInSec);
|
|
@@ -118,19 +138,91 @@ async function mixTimelineClips(ffmpegPath, usable) {
|
|
|
118
138
|
"192k",
|
|
119
139
|
outputPath
|
|
120
140
|
);
|
|
121
|
-
await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
141
|
+
await runFfmpeg(ffmpegPath, args, {
|
|
142
|
+
timeoutMs: 18e4,
|
|
143
|
+
failureMessage: "ffmpeg audio mix failed",
|
|
144
|
+
signal
|
|
126
145
|
});
|
|
146
|
+
signal?.throwIfAborted();
|
|
127
147
|
const data = await readFile3(outputPath);
|
|
148
|
+
signal?.throwIfAborted();
|
|
128
149
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
129
150
|
} finally {
|
|
130
151
|
await rm2(workDir, { recursive: true, force: true });
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
// src/util/capturedFrameBudget.ts
|
|
156
|
+
var MAX_CAPTURED_FRAME_BYTES = 256 * 1024 * 1024;
|
|
157
|
+
var CapturedFrameBudgetError = class extends Error {
|
|
158
|
+
constructor(capturedBytes, attemptedFrameBytes, maximumBytes) {
|
|
159
|
+
super(
|
|
160
|
+
`Captured PNG frames would retain ${capturedBytes + attemptedFrameBytes} bytes, exceeding the ${maximumBytes}-byte rendered-media memory limit.`
|
|
161
|
+
);
|
|
162
|
+
this.capturedBytes = capturedBytes;
|
|
163
|
+
this.attemptedFrameBytes = attemptedFrameBytes;
|
|
164
|
+
this.maximumBytes = maximumBytes;
|
|
165
|
+
this.code = "captured-frame-budget-exceeded";
|
|
166
|
+
this.name = "CapturedFrameBudgetError";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var CapturedFrameCollector = class {
|
|
170
|
+
constructor(maximumBytes = MAX_CAPTURED_FRAME_BYTES) {
|
|
171
|
+
this.maximumBytes = maximumBytes;
|
|
172
|
+
this.values = [];
|
|
173
|
+
this.seen = /* @__PURE__ */ new WeakSet();
|
|
174
|
+
this.bytes = 0;
|
|
175
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > MAX_CAPTURED_FRAME_BYTES) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Captured-frame byte limit must be between 1 and ${MAX_CAPTURED_FRAME_BYTES}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
get frameCount() {
|
|
182
|
+
return this.values.length;
|
|
183
|
+
}
|
|
184
|
+
get retainedBytes() {
|
|
185
|
+
return this.bytes;
|
|
186
|
+
}
|
|
187
|
+
append(frame, repetitions = 1) {
|
|
188
|
+
if (!Number.isSafeInteger(repetitions) || repetitions < 1) {
|
|
189
|
+
throw new Error("Captured-frame repetition count must be a positive integer");
|
|
190
|
+
}
|
|
191
|
+
const additionalBytes = this.seen.has(frame) ? 0 : frame.byteLength;
|
|
192
|
+
if (this.bytes + additionalBytes > this.maximumBytes) {
|
|
193
|
+
const capturedBytes = this.bytes;
|
|
194
|
+
this.clear();
|
|
195
|
+
throw new CapturedFrameBudgetError(capturedBytes, additionalBytes, this.maximumBytes);
|
|
196
|
+
}
|
|
197
|
+
if (additionalBytes > 0) {
|
|
198
|
+
this.seen.add(frame);
|
|
199
|
+
this.bytes += additionalBytes;
|
|
200
|
+
}
|
|
201
|
+
for (let index = 0; index < repetitions; index += 1) this.values.push(frame);
|
|
202
|
+
}
|
|
203
|
+
/** Clear retained frame references before preserving the caller's abort reason. */
|
|
204
|
+
throwIfAborted(signal) {
|
|
205
|
+
if (!signal?.aborted) return;
|
|
206
|
+
this.clear();
|
|
207
|
+
signal.throwIfAborted();
|
|
208
|
+
}
|
|
209
|
+
/** Transfer ownership of the captured frames to the encoder. */
|
|
210
|
+
release() {
|
|
211
|
+
const released = this.values;
|
|
212
|
+
this.values = [];
|
|
213
|
+
this.seen = /* @__PURE__ */ new WeakSet();
|
|
214
|
+
this.bytes = 0;
|
|
215
|
+
return released;
|
|
216
|
+
}
|
|
217
|
+
/** Drop every retained reference so failed/cancelled captures can be collected promptly. */
|
|
218
|
+
clear() {
|
|
219
|
+
this.values.length = 0;
|
|
220
|
+
this.values = [];
|
|
221
|
+
this.seen = /* @__PURE__ */ new WeakSet();
|
|
222
|
+
this.bytes = 0;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
134
226
|
// src/util/coverPreRoll.ts
|
|
135
227
|
function resolveAppliedCoverPreRoll(requestedSeconds, hasCover) {
|
|
136
228
|
if (!Number.isFinite(requestedSeconds) || requestedSeconds < 0) {
|
|
@@ -170,6 +262,7 @@ function mp4Format() {
|
|
|
170
262
|
mimeType: "video/mp4",
|
|
171
263
|
extensions: [".mp4"],
|
|
172
264
|
async exportDoc(input, options) {
|
|
265
|
+
options.signal?.throwIfAborted();
|
|
173
266
|
const mp4Opts = options.formatOptions?.mp4 ?? {};
|
|
174
267
|
const fps = typeof mp4Opts.fps === "number" ? mp4Opts.fps : MP4_DEFAULTS.fps;
|
|
175
268
|
const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
|
|
@@ -188,9 +281,13 @@ function mp4Format() {
|
|
|
188
281
|
height: mp4Opts.height,
|
|
189
282
|
captionStyle: mp4Opts.captionStyle,
|
|
190
283
|
coverPreRoll,
|
|
191
|
-
animationsEnabled
|
|
284
|
+
animationsEnabled,
|
|
285
|
+
signal: options.signal,
|
|
286
|
+
onProgress: mp4Opts.onProgress
|
|
192
287
|
});
|
|
193
|
-
|
|
288
|
+
options.signal?.throwIfAborted();
|
|
289
|
+
const data = await readFile(outputPath, { signal: options.signal });
|
|
290
|
+
options.signal?.throwIfAborted();
|
|
194
291
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
195
292
|
return { bytes, mimeType: "video/mp4", suggestedFilename: "", warnings: [] };
|
|
196
293
|
} finally {
|
|
@@ -206,6 +303,7 @@ function gifFormat() {
|
|
|
206
303
|
mimeType: "image/gif",
|
|
207
304
|
extensions: [".gif"],
|
|
208
305
|
async exportDoc(input, options) {
|
|
306
|
+
options.signal?.throwIfAborted();
|
|
209
307
|
const gifOpts = options.formatOptions?.gif ?? {};
|
|
210
308
|
const orientation = typeof gifOpts.orientation === "string" ? gifOpts.orientation : GIF_DEFAULTS.orientation;
|
|
211
309
|
const portrait = orientation === "portrait";
|
|
@@ -226,9 +324,13 @@ function gifFormat() {
|
|
|
226
324
|
loop: typeof gifOpts.loop === "number" ? gifOpts.loop : GIF_DEFAULTS.loop,
|
|
227
325
|
maxColors: typeof gifOpts.maxColors === "number" ? gifOpts.maxColors : GIF_DEFAULTS.maxColors,
|
|
228
326
|
dither: gifOpts.dither ?? GIF_DEFAULTS.dither,
|
|
229
|
-
bayerScale: gifOpts.bayerScale
|
|
327
|
+
bayerScale: gifOpts.bayerScale,
|
|
328
|
+
signal: options.signal,
|
|
329
|
+
onProgress: gifOpts.onProgress
|
|
230
330
|
});
|
|
231
|
-
|
|
331
|
+
options.signal?.throwIfAborted();
|
|
332
|
+
const data = await readFile(outputPath, { signal: options.signal });
|
|
333
|
+
options.signal?.throwIfAborted();
|
|
232
334
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
233
335
|
return {
|
|
234
336
|
bytes,
|
|
@@ -280,13 +382,16 @@ var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"]
|
|
|
280
382
|
function mimeFromExt(filePath) {
|
|
281
383
|
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
282
384
|
}
|
|
283
|
-
async function walkDir(root, prefix = "") {
|
|
385
|
+
async function walkDir(root, prefix = "", signal) {
|
|
386
|
+
throwIfAborted(signal);
|
|
284
387
|
const entries = await readdir(root, { withFileTypes: true });
|
|
388
|
+
throwIfAborted(signal);
|
|
285
389
|
const paths = [];
|
|
286
390
|
for (const entry of entries) {
|
|
391
|
+
throwIfAborted(signal);
|
|
287
392
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
288
393
|
if (entry.isDirectory()) {
|
|
289
|
-
paths.push(...await walkDir(join2(root, entry.name), relPath));
|
|
394
|
+
paths.push(...await walkDir(join2(root, entry.name), relPath, signal));
|
|
290
395
|
} else if (entry.isFile()) {
|
|
291
396
|
paths.push(relPath);
|
|
292
397
|
}
|
|
@@ -294,21 +399,26 @@ async function walkDir(root, prefix = "") {
|
|
|
294
399
|
return paths;
|
|
295
400
|
}
|
|
296
401
|
async function readInput(inputPath, options) {
|
|
402
|
+
throwIfAborted(options?.signal);
|
|
297
403
|
const result = await readInputRaw(inputPath, options);
|
|
404
|
+
throwIfAborted(options?.signal);
|
|
298
405
|
const doc = await resolveAudioMapping(result.doc, result.container);
|
|
406
|
+
throwIfAborted(options?.signal);
|
|
299
407
|
return doc === result.doc ? result : { ...result, doc };
|
|
300
408
|
}
|
|
301
409
|
async function readInputRaw(inputPath, options) {
|
|
410
|
+
throwIfAborted(options?.signal);
|
|
302
411
|
const info = await stat(inputPath);
|
|
412
|
+
throwIfAborted(options?.signal);
|
|
303
413
|
if (info.isDirectory()) {
|
|
304
|
-
return readFolder(inputPath);
|
|
414
|
+
return readFolder(inputPath, options?.signal);
|
|
305
415
|
}
|
|
306
416
|
const ext = extname(inputPath).toLowerCase();
|
|
307
417
|
if (ext === ".zip" || ext === ".dbk") {
|
|
308
|
-
return readContainer(inputPath);
|
|
418
|
+
return readContainer(inputPath, options?.signal);
|
|
309
419
|
}
|
|
310
420
|
if (ext === ".json") {
|
|
311
|
-
return readDocJsonFile(inputPath);
|
|
421
|
+
return readDocJsonFile(inputPath, options?.signal);
|
|
312
422
|
}
|
|
313
423
|
if (IMPORTER_EXTS.includes(ext)) {
|
|
314
424
|
const def = defaultRegistry2().byExtension(ext);
|
|
@@ -316,28 +426,54 @@ async function readInputRaw(inputPath, options) {
|
|
|
316
426
|
return readViaImporter(inputPath, def, options);
|
|
317
427
|
}
|
|
318
428
|
}
|
|
319
|
-
return readMarkdownFile(inputPath);
|
|
429
|
+
return readMarkdownFile(inputPath, options?.signal);
|
|
320
430
|
}
|
|
321
|
-
async function readArrayBuffer(filePath) {
|
|
322
|
-
const data = await
|
|
431
|
+
async function readArrayBuffer(filePath, signal) {
|
|
432
|
+
const data = await readBinaryFile(filePath, signal);
|
|
323
433
|
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
324
434
|
}
|
|
325
|
-
async function
|
|
326
|
-
|
|
435
|
+
async function readBinaryFile(filePath, signal) {
|
|
436
|
+
throwIfAborted(signal);
|
|
437
|
+
try {
|
|
438
|
+
const data = await readFile2(filePath, { signal });
|
|
439
|
+
throwIfAborted(signal);
|
|
440
|
+
return data;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
throwIfAborted(signal);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function readUtf8File(filePath, signal) {
|
|
447
|
+
throwIfAborted(signal);
|
|
448
|
+
try {
|
|
449
|
+
const content = await readFile2(filePath, { encoding: "utf-8", signal });
|
|
450
|
+
throwIfAborted(signal);
|
|
451
|
+
return content;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throwIfAborted(signal);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function readMarkdownFile(filePath, signal) {
|
|
458
|
+
const content = await readUtf8File(filePath, signal);
|
|
327
459
|
const container = new MemoryContentContainer();
|
|
328
460
|
await container.writeDocument(content);
|
|
461
|
+
throwIfAborted(signal);
|
|
329
462
|
const markdownDoc = parseMarkdown(content);
|
|
330
463
|
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: "md" };
|
|
331
464
|
}
|
|
332
|
-
async function readDocJsonFile(filePath) {
|
|
333
|
-
const content = await
|
|
465
|
+
async function readDocJsonFile(filePath, signal) {
|
|
466
|
+
const content = await readUtf8File(filePath, signal);
|
|
334
467
|
const doc = JSON.parse(content);
|
|
335
468
|
const container = new MemoryContentContainer();
|
|
336
469
|
return { doc, container, sourceFormat: "json" };
|
|
337
470
|
}
|
|
338
471
|
async function readViaImporter(filePath, def, options) {
|
|
339
|
-
const
|
|
472
|
+
const signal = options?.signal;
|
|
473
|
+
throwIfAborted(signal);
|
|
474
|
+
const buffer = await readArrayBuffer(filePath, signal);
|
|
340
475
|
const convertOptions = {
|
|
476
|
+
signal,
|
|
341
477
|
formatOptions: {
|
|
342
478
|
[def.id]: {
|
|
343
479
|
inferTheme: options?.inferTheme !== false,
|
|
@@ -348,59 +484,85 @@ async function readViaImporter(filePath, def, options) {
|
|
|
348
484
|
let container;
|
|
349
485
|
let markdownDoc;
|
|
350
486
|
if (def.importContainer) {
|
|
487
|
+
throwIfAborted(signal);
|
|
351
488
|
container = await def.importContainer(buffer, convertOptions);
|
|
489
|
+
throwIfAborted(signal);
|
|
352
490
|
const text = await container.readDocument();
|
|
491
|
+
throwIfAborted(signal);
|
|
353
492
|
markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
|
|
354
493
|
} else {
|
|
494
|
+
throwIfAborted(signal);
|
|
355
495
|
markdownDoc = await def.importDoc(buffer, convertOptions);
|
|
496
|
+
throwIfAborted(signal);
|
|
356
497
|
const mem = new MemoryContentContainer();
|
|
357
498
|
await mem.writeDocument(stringifyMarkdown(markdownDoc));
|
|
499
|
+
throwIfAborted(signal);
|
|
358
500
|
container = mem;
|
|
359
501
|
}
|
|
360
502
|
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };
|
|
361
503
|
}
|
|
362
504
|
var DOC_JSON_NAMES = ["doc.json", "story.json"];
|
|
363
|
-
async function resolveContainer(container, sourceFormat, missingMessage) {
|
|
505
|
+
async function resolveContainer(container, sourceFormat, missingMessage, signal) {
|
|
364
506
|
for (const name of DOC_JSON_NAMES) {
|
|
507
|
+
throwIfAborted(signal);
|
|
365
508
|
const jsonData = await container.readFile(name);
|
|
509
|
+
throwIfAborted(signal);
|
|
366
510
|
if (jsonData) {
|
|
367
511
|
const doc = JSON.parse(new TextDecoder().decode(jsonData));
|
|
368
512
|
return { doc, container, sourceFormat };
|
|
369
513
|
}
|
|
370
514
|
}
|
|
515
|
+
throwIfAborted(signal);
|
|
371
516
|
const markdown = await container.readDocument();
|
|
517
|
+
throwIfAborted(signal);
|
|
372
518
|
if (!markdown) {
|
|
373
519
|
throw new Error(missingMessage);
|
|
374
520
|
}
|
|
375
521
|
const markdownDoc = parseMarkdown(markdown);
|
|
376
522
|
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };
|
|
377
523
|
}
|
|
378
|
-
async function readContainer(filePath) {
|
|
379
|
-
|
|
524
|
+
async function readContainer(filePath, signal) {
|
|
525
|
+
throwIfAborted(signal);
|
|
526
|
+
const container = await zipToContainer(await readArrayBuffer(filePath, signal));
|
|
527
|
+
throwIfAborted(signal);
|
|
380
528
|
return resolveContainer(
|
|
381
529
|
container,
|
|
382
530
|
"dbk",
|
|
383
|
-
`No markdown document or doc.json found in container: ${filePath}
|
|
531
|
+
`No markdown document or doc.json found in container: ${filePath}`,
|
|
532
|
+
signal
|
|
384
533
|
);
|
|
385
534
|
}
|
|
386
|
-
async function readFolder(dirPath) {
|
|
535
|
+
async function readFolder(dirPath, signal) {
|
|
536
|
+
throwIfAborted(signal);
|
|
387
537
|
const container = new MemoryContentContainer();
|
|
388
|
-
const files = await walkDir(dirPath);
|
|
538
|
+
const files = await walkDir(dirPath, "", signal);
|
|
539
|
+
throwIfAborted(signal);
|
|
389
540
|
for (const relPath of files) {
|
|
541
|
+
throwIfAborted(signal);
|
|
390
542
|
const absPath = join2(dirPath, relPath);
|
|
391
|
-
const data = await
|
|
543
|
+
const data = await readBinaryFile(absPath, signal);
|
|
544
|
+
throwIfAborted(signal);
|
|
392
545
|
await container.writeFile(
|
|
393
546
|
relPath,
|
|
394
547
|
new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
395
548
|
mimeFromExt(relPath)
|
|
396
549
|
);
|
|
550
|
+
throwIfAborted(signal);
|
|
397
551
|
}
|
|
398
552
|
return resolveContainer(
|
|
399
553
|
container,
|
|
400
554
|
"folder",
|
|
401
|
-
`No markdown document or doc.json found in folder: ${dirPath}
|
|
555
|
+
`No markdown document or doc.json found in folder: ${dirPath}`,
|
|
556
|
+
signal
|
|
402
557
|
);
|
|
403
558
|
}
|
|
559
|
+
function throwIfAborted(signal) {
|
|
560
|
+
if (!signal?.aborted) return;
|
|
561
|
+
if (signal.reason !== void 0) throw signal.reason;
|
|
562
|
+
const error = new Error("Input reading was cancelled");
|
|
563
|
+
error.name = "AbortError";
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
404
566
|
|
|
405
567
|
// src/api.ts
|
|
406
568
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
@@ -411,25 +573,40 @@ async function convert(source, to, options = {}) {
|
|
|
411
573
|
...options
|
|
412
574
|
});
|
|
413
575
|
}
|
|
576
|
+
async function prepareConversion(source, options = {}) {
|
|
577
|
+
return formatsPrepareConversion(source, {
|
|
578
|
+
registry: createCliRegistry(),
|
|
579
|
+
resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
|
|
580
|
+
...options
|
|
581
|
+
});
|
|
582
|
+
}
|
|
414
583
|
async function captureDocFrames(doc, container, options) {
|
|
415
|
-
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress } = options;
|
|
584
|
+
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
|
|
585
|
+
signal?.throwIfAborted();
|
|
416
586
|
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
417
|
-
const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
|
|
587
|
+
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
588
|
+
signal?.throwIfAborted();
|
|
418
589
|
if (!ffmpegPath) {
|
|
419
590
|
throw new Error(
|
|
420
591
|
"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."
|
|
421
592
|
);
|
|
422
593
|
}
|
|
423
594
|
onProgress?.("collecting media", 0);
|
|
595
|
+
signal?.throwIfAborted();
|
|
424
596
|
const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
597
|
+
signal?.throwIfAborted();
|
|
425
598
|
const images = /* @__PURE__ */ new Map();
|
|
426
599
|
for (const imgPath of collectImagePaths(doc)) {
|
|
600
|
+
signal?.throwIfAborted();
|
|
427
601
|
const data = await container.readFile(imgPath);
|
|
602
|
+
signal?.throwIfAborted();
|
|
428
603
|
if (data) images.set(imgPath, data);
|
|
429
604
|
}
|
|
430
605
|
const audio = /* @__PURE__ */ new Map();
|
|
431
606
|
for (const seg of doc.audio?.segments ?? []) {
|
|
607
|
+
signal?.throwIfAborted();
|
|
432
608
|
const data = await container.readFile(seg.src);
|
|
609
|
+
signal?.throwIfAborted();
|
|
433
610
|
if (data) {
|
|
434
611
|
audio.set(seg.src, data);
|
|
435
612
|
audio.set(seg.name, data);
|
|
@@ -442,12 +619,16 @@ async function captureDocFrames(doc, container, options) {
|
|
|
442
619
|
}
|
|
443
620
|
}
|
|
444
621
|
for (const src of mediaSrcs) {
|
|
622
|
+
signal?.throwIfAborted();
|
|
445
623
|
if (images.has(src)) continue;
|
|
446
624
|
const data = await container.readFile(src);
|
|
625
|
+
signal?.throwIfAborted();
|
|
447
626
|
if (data) images.set(src, data);
|
|
448
627
|
}
|
|
449
628
|
onProgress?.("generating render HTML", 10);
|
|
629
|
+
signal?.throwIfAborted();
|
|
450
630
|
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
631
|
+
signal?.throwIfAborted();
|
|
451
632
|
const renderHtml = generateRenderHtml(doc, {
|
|
452
633
|
playerScript: PLAYER_BUNDLE,
|
|
453
634
|
images,
|
|
@@ -458,24 +639,39 @@ async function captureDocFrames(doc, container, options) {
|
|
|
458
639
|
animationsEnabled
|
|
459
640
|
});
|
|
460
641
|
onProgress?.("launching browser", 15);
|
|
642
|
+
signal?.throwIfAborted();
|
|
461
643
|
const { chromium } = await import("playwright-core");
|
|
644
|
+
signal?.throwIfAborted();
|
|
462
645
|
let browser;
|
|
463
646
|
try {
|
|
464
647
|
browser = await chromium.launch({ headless: true });
|
|
465
648
|
} catch (err) {
|
|
649
|
+
signal?.throwIfAborted();
|
|
466
650
|
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
467
651
|
throw new Error(
|
|
468
652
|
`Playwright Chromium is not installed. Run: npx playwright install chromium
|
|
469
653
|
(launch failed: ${detail})`
|
|
470
654
|
);
|
|
471
655
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
656
|
+
const handleAbort = () => {
|
|
657
|
+
void browser.close().catch(() => void 0);
|
|
658
|
+
};
|
|
659
|
+
if (signal?.aborted) {
|
|
660
|
+
await browser.close().catch(() => void 0);
|
|
661
|
+
signal.throwIfAborted();
|
|
662
|
+
}
|
|
663
|
+
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
475
664
|
let renderAPI = null;
|
|
665
|
+
const capturedFrames = new CapturedFrameCollector();
|
|
476
666
|
try {
|
|
667
|
+
const page = await browser.newPage({ viewport: { width, height } });
|
|
668
|
+
const pageErrors = [];
|
|
669
|
+
page.on("pageerror", (err) => pageErrors.push(err.message));
|
|
670
|
+
signal?.throwIfAborted();
|
|
477
671
|
await page.setContent(renderHtml, { waitUntil: "load" });
|
|
672
|
+
signal?.throwIfAborted();
|
|
478
673
|
await page.waitForTimeout(500);
|
|
674
|
+
signal?.throwIfAborted();
|
|
479
675
|
try {
|
|
480
676
|
await page.waitForFunction(
|
|
481
677
|
() => {
|
|
@@ -486,6 +682,7 @@ async function captureDocFrames(doc, container, options) {
|
|
|
486
682
|
{ timeout: 15e3 }
|
|
487
683
|
);
|
|
488
684
|
} catch {
|
|
685
|
+
signal?.throwIfAborted();
|
|
489
686
|
const errorDetail = pageErrors.length ? `
|
|
490
687
|
Page errors:
|
|
491
688
|
${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
|
|
@@ -500,43 +697,61 @@ Page errors:
|
|
|
500
697
|
if (!api) throw new Error("Squisq render API disappeared after initialization.");
|
|
501
698
|
return api;
|
|
502
699
|
});
|
|
700
|
+
signal?.throwIfAborted();
|
|
503
701
|
const docDuration = await renderAPI.evaluate((api) => api.getDuration());
|
|
702
|
+
signal?.throwIfAborted();
|
|
504
703
|
if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
|
|
505
704
|
const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
506
705
|
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
507
706
|
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
508
707
|
const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
|
|
509
708
|
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
510
|
-
const frames = [];
|
|
511
709
|
onProgress?.("capturing frames", 20);
|
|
710
|
+
capturedFrames.throwIfAborted(signal);
|
|
512
711
|
if (preRollFrameCount > 0) {
|
|
712
|
+
capturedFrames.throwIfAborted(signal);
|
|
513
713
|
await renderAPI.evaluate((api) => api.showCover());
|
|
514
714
|
await page.waitForTimeout(100);
|
|
515
|
-
|
|
516
|
-
|
|
715
|
+
capturedFrames.throwIfAborted(signal);
|
|
716
|
+
const coverFrame = await page.screenshot({ type: "png" });
|
|
717
|
+
capturedFrames.throwIfAborted(signal);
|
|
718
|
+
capturedFrames.append(coverFrame, preRollFrameCount);
|
|
517
719
|
await renderAPI.evaluate((api) => api.hideCover());
|
|
518
720
|
}
|
|
519
721
|
const frameInterval = 1 / fps;
|
|
520
722
|
for (let i = 0; i < storyFrameCount; i++) {
|
|
723
|
+
capturedFrames.throwIfAborted(signal);
|
|
521
724
|
const time = i * frameInterval;
|
|
522
725
|
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
523
|
-
|
|
726
|
+
const frame = await page.screenshot({ type: "png" });
|
|
727
|
+
capturedFrames.throwIfAborted(signal);
|
|
728
|
+
capturedFrames.append(frame);
|
|
524
729
|
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
525
|
-
onProgress?.(
|
|
730
|
+
onProgress?.(
|
|
731
|
+
"capturing frames",
|
|
732
|
+
20 + Math.round(capturedFrames.frameCount / totalFrames * 60)
|
|
733
|
+
);
|
|
734
|
+
capturedFrames.throwIfAborted(signal);
|
|
526
735
|
}
|
|
527
736
|
}
|
|
528
737
|
return {
|
|
529
|
-
frames,
|
|
738
|
+
frames: capturedFrames.release(),
|
|
530
739
|
totalDuration: docDuration + appliedCoverPreRoll,
|
|
531
740
|
appliedCoverPreRoll,
|
|
532
741
|
ffmpegPath
|
|
533
742
|
};
|
|
743
|
+
} catch (err) {
|
|
744
|
+
capturedFrames.clear();
|
|
745
|
+
signal?.throwIfAborted();
|
|
746
|
+
throw err;
|
|
534
747
|
} finally {
|
|
535
|
-
|
|
536
|
-
await
|
|
748
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
749
|
+
await renderAPI?.dispose().catch(() => void 0);
|
|
750
|
+
await browser.close().catch(() => void 0);
|
|
537
751
|
}
|
|
538
752
|
}
|
|
539
753
|
async function renderDocToMp4(doc, container, options) {
|
|
754
|
+
options.signal?.throwIfAborted();
|
|
540
755
|
const fps = options.fps ?? 30;
|
|
541
756
|
const quality = options.quality ?? "normal";
|
|
542
757
|
const orientation = options.orientation ?? "landscape";
|
|
@@ -554,32 +769,45 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
554
769
|
captionStyle: options.captionStyle,
|
|
555
770
|
coverPreRoll: options.coverPreRoll ?? 0,
|
|
556
771
|
animationsEnabled: options.animationsEnabled ?? true,
|
|
557
|
-
onProgress: options.onProgress
|
|
772
|
+
onProgress: options.onProgress,
|
|
773
|
+
signal: options.signal
|
|
558
774
|
});
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
775
|
+
const frameCount = capture.frames.length;
|
|
776
|
+
try {
|
|
777
|
+
options.onProgress?.("encoding video", 80);
|
|
778
|
+
options.signal?.throwIfAborted();
|
|
779
|
+
const encodingAudio = await buildMixedAudioTrack(
|
|
780
|
+
doc,
|
|
781
|
+
container,
|
|
782
|
+
capture.ffmpegPath,
|
|
783
|
+
capture.appliedCoverPreRoll,
|
|
784
|
+
options.signal
|
|
785
|
+
);
|
|
786
|
+
options.signal?.throwIfAborted();
|
|
787
|
+
const { framesToMp4Native: framesToMp4Native2 } = await import("./nativeEncoder-ET3HUSOE.js");
|
|
788
|
+
await framesToMp4Native2(capture.ffmpegPath, capture.frames, encodingAudio, options.outputPath, {
|
|
789
|
+
fps,
|
|
790
|
+
quality,
|
|
791
|
+
orientation,
|
|
792
|
+
width: dimensions.width,
|
|
793
|
+
height: dimensions.height,
|
|
794
|
+
signal: options.signal,
|
|
795
|
+
onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
|
|
796
|
+
});
|
|
797
|
+
options.signal?.throwIfAborted();
|
|
798
|
+
options.onProgress?.("done", 100);
|
|
799
|
+
options.signal?.throwIfAborted();
|
|
800
|
+
return {
|
|
801
|
+
duration: capture.totalDuration,
|
|
802
|
+
frameCount,
|
|
803
|
+
outputPath: options.outputPath
|
|
804
|
+
};
|
|
805
|
+
} finally {
|
|
806
|
+
capture.frames.length = 0;
|
|
807
|
+
}
|
|
581
808
|
}
|
|
582
809
|
async function renderDocToGif(doc, container, options) {
|
|
810
|
+
options.signal?.throwIfAborted();
|
|
583
811
|
const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;
|
|
584
812
|
if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {
|
|
585
813
|
throw new RangeError("GIF FPS must be a finite number between 1 and 100.");
|
|
@@ -604,60 +832,86 @@ async function renderDocToGif(doc, container, options) {
|
|
|
604
832
|
captionStyle: options.captionStyle,
|
|
605
833
|
coverPreRoll: options.coverPreRoll ?? 0,
|
|
606
834
|
animationsEnabled: options.animationsEnabled ?? false,
|
|
607
|
-
onProgress: options.onProgress
|
|
608
|
-
|
|
609
|
-
options.onProgress?.("encoding GIF", 80);
|
|
610
|
-
const { framesToGifNative: framesToGifNative2 } = await import("./nativeEncoder-IQPN2RMC.js");
|
|
611
|
-
await framesToGifNative2(capture.ffmpegPath, capture.frames, options.outputPath, {
|
|
612
|
-
fps,
|
|
613
|
-
orientation,
|
|
614
|
-
width,
|
|
615
|
-
height,
|
|
616
|
-
loop: options.loop,
|
|
617
|
-
maxColors: options.maxColors,
|
|
618
|
-
dither: options.dither,
|
|
619
|
-
bayerScale: options.bayerScale,
|
|
620
|
-
onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
|
|
835
|
+
onProgress: options.onProgress,
|
|
836
|
+
signal: options.signal
|
|
621
837
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
838
|
+
const frameCount = capture.frames.length;
|
|
839
|
+
try {
|
|
840
|
+
options.onProgress?.("encoding GIF", 80);
|
|
841
|
+
options.signal?.throwIfAborted();
|
|
842
|
+
const { framesToGifNative: framesToGifNative2 } = await import("./nativeEncoder-ET3HUSOE.js");
|
|
843
|
+
await framesToGifNative2(capture.ffmpegPath, capture.frames, options.outputPath, {
|
|
844
|
+
fps,
|
|
845
|
+
orientation,
|
|
846
|
+
width,
|
|
847
|
+
height,
|
|
848
|
+
loop: options.loop,
|
|
849
|
+
maxColors: options.maxColors,
|
|
850
|
+
dither: options.dither,
|
|
851
|
+
bayerScale: options.bayerScale,
|
|
852
|
+
signal: options.signal,
|
|
853
|
+
onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
|
|
854
|
+
});
|
|
855
|
+
options.signal?.throwIfAborted();
|
|
856
|
+
options.onProgress?.("done", 100);
|
|
857
|
+
options.signal?.throwIfAborted();
|
|
858
|
+
const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule(doc).some((clip) => clip.kind === "audio");
|
|
859
|
+
return {
|
|
860
|
+
duration: capture.totalDuration,
|
|
861
|
+
frameCount,
|
|
862
|
+
outputPath: options.outputPath,
|
|
863
|
+
warnings: hasAudio ? ["Animated GIF does not support audio; audio tracks were omitted."] : []
|
|
864
|
+
};
|
|
865
|
+
} finally {
|
|
866
|
+
capture.frames.length = 0;
|
|
867
|
+
}
|
|
630
868
|
}
|
|
631
869
|
async function extractThumbnails(options) {
|
|
632
|
-
const { videoPath, outputDir, slug, sizes, force } = options;
|
|
870
|
+
const { videoPath, outputDir, slug, sizes, force, signal } = options;
|
|
633
871
|
const { existsSync } = await import("fs");
|
|
872
|
+
const { rm: rm2 } = await import("fs/promises");
|
|
634
873
|
const { join: join3 } = await import("path");
|
|
635
|
-
|
|
636
|
-
const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
|
|
874
|
+
signal?.throwIfAborted();
|
|
875
|
+
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
637
876
|
if (!ffmpegPath) {
|
|
638
877
|
throw new Error(
|
|
639
878
|
"ffmpeg is required for thumbnail extraction but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg"
|
|
640
879
|
);
|
|
641
880
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
881
|
+
const generatedPaths = [];
|
|
882
|
+
try {
|
|
883
|
+
for (const thumb of sizes) {
|
|
884
|
+
signal?.throwIfAborted();
|
|
885
|
+
const outputPath = join3(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
|
|
886
|
+
if (!force && existsSync(outputPath)) continue;
|
|
887
|
+
try {
|
|
888
|
+
await runFfmpeg(
|
|
889
|
+
ffmpegPath,
|
|
890
|
+
["-y", "-i", videoPath, "-vf", thumb.filter, "-frames:v", "1", "-q:v", "2", outputPath],
|
|
891
|
+
{
|
|
892
|
+
timeoutMs: 3e4,
|
|
893
|
+
failureMessage: `Thumbnail extraction failed (${thumb.name})`,
|
|
894
|
+
signal
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
signal?.throwIfAborted();
|
|
898
|
+
generatedPaths.push(outputPath);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await rm2(outputPath, { force: true });
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
signal?.throwIfAborted();
|
|
905
|
+
} catch (error) {
|
|
906
|
+
await Promise.allSettled(generatedPaths.map((path) => rm2(path, { force: true })));
|
|
907
|
+
throw error;
|
|
656
908
|
}
|
|
657
909
|
}
|
|
658
910
|
export {
|
|
911
|
+
CapturedFrameBudgetError,
|
|
659
912
|
ConversionError,
|
|
660
913
|
GIF_EXPORT_DEFAULTS,
|
|
914
|
+
MAX_CAPTURED_FRAME_BYTES,
|
|
661
915
|
MemoryContentContainer2 as MemoryContentContainer,
|
|
662
916
|
convert,
|
|
663
917
|
createCliRegistry,
|
|
@@ -666,6 +920,7 @@ export {
|
|
|
666
920
|
framesToGifNativeBytes,
|
|
667
921
|
framesToMp4Native,
|
|
668
922
|
framesToMp4NativeBytes,
|
|
923
|
+
prepareConversion,
|
|
669
924
|
readInput,
|
|
670
925
|
renderDocToGif,
|
|
671
926
|
renderDocToMp4
|