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