@bendyline/squisq-cli 1.2.2 → 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.js CHANGED
@@ -1,15 +1,32 @@
1
+ import {
2
+ GIF_EXPORT_DEFAULTS,
3
+ framesToGifNative,
4
+ framesToGifNativeBytes,
5
+ framesToMp4Native,
6
+ framesToMp4NativeBytes,
7
+ runFfmpeg
8
+ } from "./chunk-OEN2RADG.js";
9
+
1
10
  // src/api.ts
2
11
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
3
12
  import { flattenBlocks } from "@bendyline/squisq/doc";
4
- import { generateRenderHtml } from "@bendyline/squisq-video";
13
+ import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
5
14
  import { resolveDimensions } from "@bendyline/squisq-video";
6
- import { convert as formatsConvert } from "@bendyline/squisq-formats";
15
+ import {
16
+ convert as formatsConvert,
17
+ prepareConversion as formatsPrepareConversion
18
+ } from "@bendyline/squisq-formats";
7
19
 
8
20
  // src/util/detectFfmpeg.ts
9
21
  import { execFile } from "child_process";
10
- function run(command, args) {
11
- return new Promise((resolve) => {
12
- execFile(command, args, { timeout: 5e3 }, (err, stdout) => {
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
+ }
13
30
  if (err || !stdout.trim()) {
14
31
  resolve(null);
15
32
  return;
@@ -18,14 +35,15 @@ function run(command, args) {
18
35
  });
19
36
  });
20
37
  }
21
- async function getFfmpegVersion(path) {
22
- const out = await run(path, ["-version"]);
38
+ async function getFfmpegVersion(path, signal) {
39
+ const out = await run(path, ["-version"], signal);
23
40
  return out ? out.split("\n")[0].trim() : null;
24
41
  }
25
- async function detectFfmpegDetailed() {
42
+ async function detectFfmpegDetailed(signal) {
43
+ signal?.throwIfAborted();
26
44
  const envPath = process.env.SQUISQ_FFMPEG;
27
45
  if (envPath) {
28
- const version = await getFfmpegVersion(envPath);
46
+ const version = await getFfmpegVersion(envPath, signal);
29
47
  if (!version) {
30
48
  throw new Error(
31
49
  `SQUISQ_FFMPEG is set to "${envPath}" but running "${envPath} -version" failed. Fix the path or unset SQUISQ_FFMPEG.`
@@ -34,11 +52,12 @@ async function detectFfmpegDetailed() {
34
52
  return { path: envPath, source: "env" };
35
53
  }
36
54
  const command = process.platform === "win32" ? "where" : "which";
37
- const found = await run(command, ["ffmpeg"]);
55
+ const found = await run(command, ["ffmpeg"], signal);
38
56
  if (found) {
39
57
  return { path: found.split("\n")[0].trim(), source: "path" };
40
58
  }
41
59
  try {
60
+ signal?.throwIfAborted();
42
61
  const specifier = "ffmpeg-static";
43
62
  const mod = await import(specifier);
44
63
  const candidate = typeof mod === "string" ? mod : typeof mod.default === "string" ? mod.default : null;
@@ -46,50 +65,55 @@ async function detectFfmpegDetailed() {
46
65
  return { path: candidate, source: "ffmpeg-static" };
47
66
  }
48
67
  } catch {
68
+ signal?.throwIfAborted();
49
69
  }
70
+ signal?.throwIfAborted();
50
71
  return null;
51
72
  }
52
- async function detectFfmpeg() {
53
- const detection = await detectFfmpegDetailed();
54
- return detection?.path ?? null;
55
- }
56
73
 
57
74
  // src/util/audioMix.ts
58
75
  import { computeAudioTimeline } from "@bendyline/squisq-video";
59
- async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll) {
76
+ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, signal) {
77
+ signal?.throwIfAborted();
60
78
  const timeline = computeAudioTimeline(doc, coverPreRoll);
61
79
  if (timeline.length === 0) return null;
62
80
  const bytesBySrc = /* @__PURE__ */ new Map();
63
81
  const readSrc = async (src) => {
82
+ signal?.throwIfAborted();
64
83
  if (bytesBySrc.has(src)) return bytesBySrc.get(src);
65
84
  const data = await container.readFile(src);
85
+ signal?.throwIfAborted();
66
86
  bytesBySrc.set(src, data ?? null);
67
87
  return data ?? null;
68
88
  };
69
89
  const usable = [];
70
90
  for (const clip of timeline) {
91
+ signal?.throwIfAborted();
71
92
  const buffer = await readSrc(clip.src);
72
93
  if (buffer) usable.push({ clip, buffer });
73
94
  }
74
95
  if (usable.length === 0) return null;
75
- return mixTimelineClips(ffmpegPath, usable);
96
+ return mixTimelineClips(ffmpegPath, usable, signal);
76
97
  }
77
- async function mixTimelineClips(ffmpegPath, usable) {
98
+ async function mixTimelineClips(ffmpegPath, usable, signal) {
78
99
  const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
79
100
  const { join: join3 } = await import("path");
80
101
  const { tmpdir: tmpdir2 } = await import("os");
81
102
  const { randomBytes: randomBytes2 } = await import("crypto");
82
- const { execFile: execFile2 } = await import("child_process");
103
+ signal?.throwIfAborted();
83
104
  const workDir = join3(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
84
105
  await mkdir(workDir, { recursive: true });
85
106
  const ms = (s) => Math.max(0, Math.round(s * 1e3));
86
107
  try {
108
+ signal?.throwIfAborted();
87
109
  const inputs = [];
88
110
  const filters = [];
89
111
  const labels = [];
90
112
  for (const { clip, buffer } of usable) {
113
+ signal?.throwIfAborted();
91
114
  const p = join3(workDir, `clip-${inputs.length}.mp3`);
92
115
  await writeFile(p, new Uint8Array(buffer));
116
+ signal?.throwIfAborted();
93
117
  const i = inputs.push(p) - 1;
94
118
  const delayMs = ms(clip.startSec);
95
119
  const start = Math.max(0, clip.sourceInSec);
@@ -114,19 +138,99 @@ async function mixTimelineClips(ffmpegPath, usable) {
114
138
  "192k",
115
139
  outputPath
116
140
  );
117
- await new Promise((resolve, reject) => {
118
- execFile2(ffmpegPath, args, { timeout: 18e4 }, (err) => {
119
- if (err) reject(new Error(`ffmpeg audio mix failed: ${err.message}`));
120
- else resolve();
121
- });
141
+ await runFfmpeg(ffmpegPath, args, {
142
+ timeoutMs: 18e4,
143
+ failureMessage: "ffmpeg audio mix failed",
144
+ signal
122
145
  });
146
+ signal?.throwIfAborted();
123
147
  const data = await readFile3(outputPath);
148
+ signal?.throwIfAborted();
124
149
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
125
150
  } finally {
126
151
  await rm2(workDir, { recursive: true, force: true });
127
152
  }
128
153
  }
129
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
+
226
+ // src/util/coverPreRoll.ts
227
+ function resolveAppliedCoverPreRoll(requestedSeconds, hasCover) {
228
+ if (!Number.isFinite(requestedSeconds) || requestedSeconds < 0) {
229
+ throw new Error("Cover pre-roll must be a finite number of seconds greater than or equal to 0");
230
+ }
231
+ return hasCover ? requestedSeconds : 0;
232
+ }
233
+
130
234
  // src/registry.ts
131
235
  import { randomBytes } from "crypto";
132
236
  import { readFile, rm } from "fs/promises";
@@ -137,7 +241,19 @@ var MP4_DEFAULTS = {
137
241
  fps: 30,
138
242
  quality: "normal",
139
243
  orientation: "landscape",
140
- coverPreRoll: 0
244
+ coverPreRoll: 0,
245
+ animationsEnabled: true
246
+ };
247
+ var GIF_DEFAULTS = {
248
+ fps: 10,
249
+ orientation: "landscape",
250
+ width: 960,
251
+ height: 540,
252
+ coverPreRoll: 0,
253
+ animationsEnabled: false,
254
+ loop: 0,
255
+ maxColors: 256,
256
+ dither: "sierra2_4a"
141
257
  };
142
258
  function mp4Format() {
143
259
  return {
@@ -146,21 +262,32 @@ function mp4Format() {
146
262
  mimeType: "video/mp4",
147
263
  extensions: [".mp4"],
148
264
  async exportDoc(input, options) {
265
+ options.signal?.throwIfAborted();
149
266
  const mp4Opts = options.formatOptions?.mp4 ?? {};
150
267
  const fps = typeof mp4Opts.fps === "number" ? mp4Opts.fps : MP4_DEFAULTS.fps;
151
268
  const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
152
269
  const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
153
270
  const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
271
+ const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
154
272
  const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
155
273
  try {
156
- await renderDocToMp4(input.doc, input.container, {
274
+ const { renderDocToMp4: renderDocToMp42 } = await import("./api.js");
275
+ await renderDocToMp42(input.doc, input.container, {
157
276
  outputPath,
158
277
  fps,
159
278
  quality,
160
279
  orientation,
161
- coverPreRoll
280
+ width: mp4Opts.width,
281
+ height: mp4Opts.height,
282
+ captionStyle: mp4Opts.captionStyle,
283
+ coverPreRoll,
284
+ animationsEnabled,
285
+ signal: options.signal,
286
+ onProgress: mp4Opts.onProgress
162
287
  });
163
- const data = await readFile(outputPath);
288
+ options.signal?.throwIfAborted();
289
+ const data = await readFile(outputPath, { signal: options.signal });
290
+ options.signal?.throwIfAborted();
164
291
  const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
165
292
  return { bytes, mimeType: "video/mp4", suggestedFilename: "", warnings: [] };
166
293
  } finally {
@@ -169,27 +296,69 @@ function mp4Format() {
169
296
  }
170
297
  };
171
298
  }
299
+ function gifFormat() {
300
+ return {
301
+ id: "gif",
302
+ label: "Animated GIF",
303
+ mimeType: "image/gif",
304
+ extensions: [".gif"],
305
+ async exportDoc(input, options) {
306
+ options.signal?.throwIfAborted();
307
+ const gifOpts = options.formatOptions?.gif ?? {};
308
+ const orientation = typeof gifOpts.orientation === "string" ? gifOpts.orientation : GIF_DEFAULTS.orientation;
309
+ const portrait = orientation === "portrait";
310
+ const defaultWidth = portrait ? GIF_DEFAULTS.height : GIF_DEFAULTS.width;
311
+ const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
312
+ const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
313
+ try {
314
+ const { renderDocToGif: renderDocToGif2 } = await import("./api.js");
315
+ const result = await renderDocToGif2(input.doc, input.container, {
316
+ outputPath,
317
+ fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
318
+ orientation,
319
+ width: typeof gifOpts.width === "number" ? gifOpts.width : defaultWidth,
320
+ height: typeof gifOpts.height === "number" ? gifOpts.height : defaultHeight,
321
+ captionStyle: gifOpts.captionStyle,
322
+ coverPreRoll: typeof gifOpts.coverPreRoll === "number" ? gifOpts.coverPreRoll : GIF_DEFAULTS.coverPreRoll,
323
+ animationsEnabled: typeof gifOpts.animationsEnabled === "boolean" ? gifOpts.animationsEnabled : GIF_DEFAULTS.animationsEnabled,
324
+ loop: typeof gifOpts.loop === "number" ? gifOpts.loop : GIF_DEFAULTS.loop,
325
+ maxColors: typeof gifOpts.maxColors === "number" ? gifOpts.maxColors : GIF_DEFAULTS.maxColors,
326
+ dither: gifOpts.dither ?? GIF_DEFAULTS.dither,
327
+ bayerScale: gifOpts.bayerScale,
328
+ signal: options.signal,
329
+ onProgress: gifOpts.onProgress
330
+ });
331
+ options.signal?.throwIfAborted();
332
+ const data = await readFile(outputPath, { signal: options.signal });
333
+ options.signal?.throwIfAborted();
334
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
335
+ return {
336
+ bytes,
337
+ mimeType: "image/gif",
338
+ suggestedFilename: "",
339
+ warnings: result.warnings
340
+ };
341
+ } finally {
342
+ await rm(outputPath, { force: true });
343
+ }
344
+ }
345
+ };
346
+ }
172
347
  function createCliRegistry() {
173
348
  const registry = defaultRegistry();
174
349
  registry.register(mp4Format());
350
+ registry.register(gifFormat());
175
351
  return registry;
176
352
  }
177
353
 
178
354
  // src/api.ts
179
355
  import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
180
356
 
181
- // src/util/domPolyfill.ts
182
- import { DOMParser as XmldomDOMParser } from "@xmldom/xmldom";
183
- var globalScope = globalThis;
184
- if (typeof globalScope.DOMParser === "undefined") {
185
- globalScope.DOMParser = XmldomDOMParser;
186
- }
187
-
188
357
  // src/util/readInput.ts
189
358
  import { readFile as readFile2, readdir, stat } from "fs/promises";
190
359
  import { join as join2, extname } from "path";
191
360
  import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
192
- import { markdownToDoc } from "@bendyline/squisq/doc";
361
+ import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
193
362
  import { MemoryContentContainer } from "@bendyline/squisq/storage";
194
363
  import { zipToContainer } from "@bendyline/squisq-formats/container";
195
364
  import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
@@ -213,114 +382,187 @@ var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"]
213
382
  function mimeFromExt(filePath) {
214
383
  return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
215
384
  }
216
- async function walkDir(root, prefix = "") {
385
+ async function walkDir(root, prefix = "", signal) {
386
+ throwIfAborted(signal);
217
387
  const entries = await readdir(root, { withFileTypes: true });
388
+ throwIfAborted(signal);
218
389
  const paths = [];
219
390
  for (const entry of entries) {
391
+ throwIfAborted(signal);
220
392
  const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
221
393
  if (entry.isDirectory()) {
222
- paths.push(...await walkDir(join2(root, entry.name), relPath));
394
+ paths.push(...await walkDir(join2(root, entry.name), relPath, signal));
223
395
  } else if (entry.isFile()) {
224
396
  paths.push(relPath);
225
397
  }
226
398
  }
227
399
  return paths;
228
400
  }
229
- async function readInput(inputPath) {
401
+ async function readInput(inputPath, options) {
402
+ throwIfAborted(options?.signal);
403
+ const result = await readInputRaw(inputPath, options);
404
+ throwIfAborted(options?.signal);
405
+ const doc = await resolveAudioMapping(result.doc, result.container);
406
+ throwIfAborted(options?.signal);
407
+ return doc === result.doc ? result : { ...result, doc };
408
+ }
409
+ async function readInputRaw(inputPath, options) {
410
+ throwIfAborted(options?.signal);
230
411
  const info = await stat(inputPath);
412
+ throwIfAborted(options?.signal);
231
413
  if (info.isDirectory()) {
232
- return readFolder(inputPath);
414
+ return readFolder(inputPath, options?.signal);
233
415
  }
234
416
  const ext = extname(inputPath).toLowerCase();
235
417
  if (ext === ".zip" || ext === ".dbk") {
236
- return readContainer(inputPath);
418
+ return readContainer(inputPath, options?.signal);
237
419
  }
238
420
  if (ext === ".json") {
239
- return readDocJsonFile(inputPath);
421
+ return readDocJsonFile(inputPath, options?.signal);
240
422
  }
241
423
  if (IMPORTER_EXTS.includes(ext)) {
242
424
  const def = defaultRegistry2().byExtension(ext);
243
425
  if (def && (def.importContainer || def.importDoc)) {
244
- return readViaImporter(inputPath, def);
426
+ return readViaImporter(inputPath, def, options);
245
427
  }
246
428
  }
247
- return readMarkdownFile(inputPath);
429
+ return readMarkdownFile(inputPath, options?.signal);
248
430
  }
249
- async function readArrayBuffer(filePath) {
250
- const data = await readFile2(filePath);
431
+ async function readArrayBuffer(filePath, signal) {
432
+ const data = await readBinaryFile(filePath, signal);
251
433
  return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
252
434
  }
253
- async function readMarkdownFile(filePath) {
254
- const content = await readFile2(filePath, "utf-8");
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);
255
459
  const container = new MemoryContentContainer();
256
460
  await container.writeDocument(content);
461
+ throwIfAborted(signal);
257
462
  const markdownDoc = parseMarkdown(content);
258
463
  return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: "md" };
259
464
  }
260
- async function readDocJsonFile(filePath) {
261
- const content = await readFile2(filePath, "utf-8");
465
+ async function readDocJsonFile(filePath, signal) {
466
+ const content = await readUtf8File(filePath, signal);
262
467
  const doc = JSON.parse(content);
263
468
  const container = new MemoryContentContainer();
264
469
  return { doc, container, sourceFormat: "json" };
265
470
  }
266
- async function readViaImporter(filePath, def) {
267
- const buffer = await readArrayBuffer(filePath);
471
+ async function readViaImporter(filePath, def, options) {
472
+ const signal = options?.signal;
473
+ throwIfAborted(signal);
474
+ const buffer = await readArrayBuffer(filePath, signal);
475
+ const convertOptions = {
476
+ signal,
477
+ formatOptions: {
478
+ [def.id]: {
479
+ inferTheme: options?.inferTheme !== false,
480
+ inferLayouts: options?.inferLayouts !== false
481
+ }
482
+ }
483
+ };
268
484
  let container;
269
485
  let markdownDoc;
270
486
  if (def.importContainer) {
271
- container = await def.importContainer(buffer, {});
487
+ throwIfAborted(signal);
488
+ container = await def.importContainer(buffer, convertOptions);
489
+ throwIfAborted(signal);
272
490
  const text = await container.readDocument();
491
+ throwIfAborted(signal);
273
492
  markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
274
493
  } else {
275
- markdownDoc = await def.importDoc(buffer, {});
494
+ throwIfAborted(signal);
495
+ markdownDoc = await def.importDoc(buffer, convertOptions);
496
+ throwIfAborted(signal);
276
497
  const mem = new MemoryContentContainer();
277
498
  await mem.writeDocument(stringifyMarkdown(markdownDoc));
499
+ throwIfAborted(signal);
278
500
  container = mem;
279
501
  }
280
502
  return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };
281
503
  }
282
504
  var DOC_JSON_NAMES = ["doc.json", "story.json"];
283
- async function resolveContainer(container, sourceFormat, missingMessage) {
505
+ async function resolveContainer(container, sourceFormat, missingMessage, signal) {
284
506
  for (const name of DOC_JSON_NAMES) {
507
+ throwIfAborted(signal);
285
508
  const jsonData = await container.readFile(name);
509
+ throwIfAborted(signal);
286
510
  if (jsonData) {
287
511
  const doc = JSON.parse(new TextDecoder().decode(jsonData));
288
512
  return { doc, container, sourceFormat };
289
513
  }
290
514
  }
515
+ throwIfAborted(signal);
291
516
  const markdown = await container.readDocument();
517
+ throwIfAborted(signal);
292
518
  if (!markdown) {
293
519
  throw new Error(missingMessage);
294
520
  }
295
521
  const markdownDoc = parseMarkdown(markdown);
296
522
  return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };
297
523
  }
298
- async function readContainer(filePath) {
299
- const container = await zipToContainer(await readArrayBuffer(filePath));
524
+ async function readContainer(filePath, signal) {
525
+ throwIfAborted(signal);
526
+ const container = await zipToContainer(await readArrayBuffer(filePath, signal));
527
+ throwIfAborted(signal);
300
528
  return resolveContainer(
301
529
  container,
302
530
  "dbk",
303
- `No markdown document or doc.json found in container: ${filePath}`
531
+ `No markdown document or doc.json found in container: ${filePath}`,
532
+ signal
304
533
  );
305
534
  }
306
- async function readFolder(dirPath) {
535
+ async function readFolder(dirPath, signal) {
536
+ throwIfAborted(signal);
307
537
  const container = new MemoryContentContainer();
308
- const files = await walkDir(dirPath);
538
+ const files = await walkDir(dirPath, "", signal);
539
+ throwIfAborted(signal);
309
540
  for (const relPath of files) {
541
+ throwIfAborted(signal);
310
542
  const absPath = join2(dirPath, relPath);
311
- const data = await readFile2(absPath);
543
+ const data = await readBinaryFile(absPath, signal);
544
+ throwIfAborted(signal);
312
545
  await container.writeFile(
313
546
  relPath,
314
547
  new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
315
548
  mimeFromExt(relPath)
316
549
  );
550
+ throwIfAborted(signal);
317
551
  }
318
552
  return resolveContainer(
319
553
  container,
320
554
  "folder",
321
- `No markdown document or doc.json found in folder: ${dirPath}`
555
+ `No markdown document or doc.json found in folder: ${dirPath}`,
556
+ signal
322
557
  );
323
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
+ }
324
566
 
325
567
  // src/api.ts
326
568
  import { ConversionError } from "@bendyline/squisq-formats";
@@ -331,201 +573,356 @@ async function convert(source, to, options = {}) {
331
573
  ...options
332
574
  });
333
575
  }
334
- async function renderDocToMp4(doc, container, options) {
335
- const {
336
- outputPath,
337
- fps = 30,
338
- quality = "normal",
339
- orientation = "landscape",
340
- captionStyle,
341
- coverPreRoll = 0,
342
- onProgress
343
- } = options;
344
- const dimensions = resolveDimensions({
345
- orientation,
346
- width: options.width,
347
- height: options.height
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
348
581
  });
349
- const ffmpegPath = await detectFfmpeg();
582
+ }
583
+ async function captureDocFrames(doc, container, options) {
584
+ const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
585
+ signal?.throwIfAborted();
586
+ resolveAppliedCoverPreRoll(coverPreRoll, true);
587
+ const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
588
+ signal?.throwIfAborted();
350
589
  if (!ffmpegPath) {
351
590
  throw new Error(
352
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."
353
592
  );
354
593
  }
355
594
  onProgress?.("collecting media", 0);
595
+ signal?.throwIfAborted();
356
596
  const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
357
- const imagePaths = collectImagePaths(doc);
597
+ signal?.throwIfAborted();
358
598
  const images = /* @__PURE__ */ new Map();
359
- for (const imgPath of imagePaths) {
599
+ for (const imgPath of collectImagePaths(doc)) {
600
+ signal?.throwIfAborted();
360
601
  const data = await container.readFile(imgPath);
361
- if (data) {
362
- images.set(imgPath, data);
363
- }
602
+ signal?.throwIfAborted();
603
+ if (data) images.set(imgPath, data);
364
604
  }
365
605
  const audio = /* @__PURE__ */ new Map();
366
- if (doc.audio?.segments?.length) {
367
- for (const seg of doc.audio.segments) {
368
- const data = await container.readFile(seg.src);
369
- if (data) {
370
- audio.set(seg.src, data);
371
- audio.set(seg.name, data);
372
- }
606
+ for (const seg of doc.audio?.segments ?? []) {
607
+ signal?.throwIfAborted();
608
+ const data = await container.readFile(seg.src);
609
+ signal?.throwIfAborted();
610
+ if (data) {
611
+ audio.set(seg.src, data);
612
+ audio.set(seg.name, data);
373
613
  }
374
614
  }
375
- const mediaSrcs = new Set(resolveMediaSchedule(doc).map((c) => c.src));
615
+ const mediaSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
376
616
  for (const block of flattenBlocks(doc.blocks)) {
377
617
  for (const layer of block.layers ?? []) {
378
618
  if (layer.type === "video") mediaSrcs.add(layer.content.src);
379
619
  }
380
620
  }
381
621
  for (const src of mediaSrcs) {
622
+ signal?.throwIfAborted();
382
623
  if (images.has(src)) continue;
383
624
  const data = await container.readFile(src);
625
+ signal?.throwIfAborted();
384
626
  if (data) images.set(src, data);
385
627
  }
386
628
  onProgress?.("generating render HTML", 10);
629
+ signal?.throwIfAborted();
387
630
  const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
631
+ signal?.throwIfAborted();
388
632
  const renderHtml = generateRenderHtml(doc, {
389
633
  playerScript: PLAYER_BUNDLE,
390
634
  images,
391
635
  audio: audio.size > 0 ? audio : void 0,
392
- width: dimensions.width,
393
- height: dimensions.height,
394
- captionStyle
636
+ width,
637
+ height,
638
+ captionStyle,
639
+ animationsEnabled
395
640
  });
396
641
  onProgress?.("launching browser", 15);
642
+ signal?.throwIfAborted();
397
643
  const { chromium } = await import("playwright-core");
644
+ signal?.throwIfAborted();
398
645
  let browser;
399
646
  try {
400
647
  browser = await chromium.launch({ headless: true });
401
648
  } catch (err) {
649
+ signal?.throwIfAborted();
402
650
  const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
403
651
  throw new Error(
404
652
  `Playwright Chromium is not installed. Run: npx playwright install chromium
405
653
  (launch failed: ${detail})`
406
654
  );
407
655
  }
408
- const page = await browser.newPage({
409
- viewport: { width: dimensions.width, height: dimensions.height }
410
- });
411
- const pageErrors = [];
412
- page.on("pageerror", (err) => pageErrors.push(err.message));
413
- await page.setContent(renderHtml, { waitUntil: "load" });
414
- await page.waitForTimeout(500);
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 });
664
+ let renderAPI = null;
665
+ const capturedFrames = new CapturedFrameCollector();
415
666
  try {
416
- await page.waitForFunction(
417
- () => typeof window.getDuration === "function",
418
- { timeout: 15e3 }
419
- );
420
- } catch {
421
- await browser.close();
422
- const errorDetail = pageErrors.length ? `
667
+ const page = await browser.newPage({ viewport: { width, height } });
668
+ const pageErrors = [];
669
+ page.on("pageerror", (err) => pageErrors.push(err.message));
670
+ signal?.throwIfAborted();
671
+ await page.setContent(renderHtml, { waitUntil: "load" });
672
+ signal?.throwIfAborted();
673
+ await page.waitForTimeout(500);
674
+ signal?.throwIfAborted();
675
+ try {
676
+ await page.waitForFunction(
677
+ () => {
678
+ const root = document.getElementById("squisq-root");
679
+ const player = window.SquisqPlayer;
680
+ return root ? player?.getHandle(root)?.getRenderAPI() != null : false;
681
+ },
682
+ { timeout: 15e3 }
683
+ );
684
+ } catch {
685
+ signal?.throwIfAborted();
686
+ const errorDetail = pageErrors.length ? `
423
687
  Page errors:
424
688
  ${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
425
- throw new Error(
426
- `The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
427
- );
428
- }
429
- const docDuration = await page.evaluate(() => {
430
- return window.getDuration();
431
- });
432
- if (docDuration <= 0) {
433
- await browser.close();
434
- throw new Error("Document has zero duration \u2014 nothing to render");
435
- }
436
- const storyFrameCount = Math.ceil(docDuration * fps);
437
- const preRollFrameCount = Math.ceil(coverPreRoll * fps);
438
- const totalFrames = preRollFrameCount + storyFrameCount;
439
- const frames = [];
440
- onProgress?.("capturing frames", 20);
441
- if (preRollFrameCount > 0) {
442
- const hasCover = await page.evaluate(() => {
443
- const w = window;
444
- return typeof w.hasCoverBlock === "function" ? w.hasCoverBlock() : false;
689
+ throw new Error(
690
+ `The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
691
+ );
692
+ }
693
+ renderAPI = await page.evaluateHandle(() => {
694
+ const root = document.getElementById("squisq-root");
695
+ const player = window.SquisqPlayer;
696
+ const api = root ? player?.getHandle(root)?.getRenderAPI() : null;
697
+ if (!api) throw new Error("Squisq render API disappeared after initialization.");
698
+ return api;
445
699
  });
446
- if (hasCover) {
447
- await page.evaluate(() => {
448
- window.showCover();
449
- });
700
+ signal?.throwIfAborted();
701
+ const docDuration = await renderAPI.evaluate((api) => api.getDuration());
702
+ signal?.throwIfAborted();
703
+ if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
704
+ const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
705
+ const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
706
+ const storyFrameCount = Math.ceil(docDuration * fps);
707
+ const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
708
+ const totalFrames = preRollFrameCount + storyFrameCount;
709
+ onProgress?.("capturing frames", 20);
710
+ capturedFrames.throwIfAborted(signal);
711
+ if (preRollFrameCount > 0) {
712
+ capturedFrames.throwIfAborted(signal);
713
+ await renderAPI.evaluate((api) => api.showCover());
450
714
  await page.waitForTimeout(100);
451
- const coverFrame = new Uint8Array(await page.screenshot({ type: "png" }));
452
- for (let i = 0; i < preRollFrameCount; i++) {
453
- frames.push(coverFrame);
454
- }
455
- await page.evaluate(() => {
456
- window.hideCover();
457
- });
715
+ capturedFrames.throwIfAborted(signal);
716
+ const coverFrame = await page.screenshot({ type: "png" });
717
+ capturedFrames.throwIfAborted(signal);
718
+ capturedFrames.append(coverFrame, preRollFrameCount);
719
+ await renderAPI.evaluate((api) => api.hideCover());
458
720
  }
459
- }
460
- const frameInterval = 1 / fps;
461
- for (let i = 0; i < storyFrameCount; i++) {
462
- const time = i * frameInterval;
463
- await page.evaluate((t) => {
464
- return window.seekTo(t);
465
- }, time);
466
- const screenshot = await page.screenshot({ type: "png" });
467
- frames.push(new Uint8Array(screenshot));
468
- if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
469
- const pct = 20 + Math.round(frames.length / totalFrames * 60);
470
- onProgress?.("capturing frames", pct);
721
+ const frameInterval = 1 / fps;
722
+ for (let i = 0; i < storyFrameCount; i++) {
723
+ capturedFrames.throwIfAborted(signal);
724
+ const time = i * frameInterval;
725
+ await renderAPI.evaluate((api, t) => api.seekTo(t), time);
726
+ const frame = await page.screenshot({ type: "png" });
727
+ capturedFrames.throwIfAborted(signal);
728
+ capturedFrames.append(frame);
729
+ if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
730
+ onProgress?.(
731
+ "capturing frames",
732
+ 20 + Math.round(capturedFrames.frameCount / totalFrames * 60)
733
+ );
734
+ capturedFrames.throwIfAborted(signal);
735
+ }
471
736
  }
737
+ return {
738
+ frames: capturedFrames.release(),
739
+ totalDuration: docDuration + appliedCoverPreRoll,
740
+ appliedCoverPreRoll,
741
+ ffmpegPath
742
+ };
743
+ } catch (err) {
744
+ capturedFrames.clear();
745
+ signal?.throwIfAborted();
746
+ throw err;
747
+ } finally {
748
+ signal?.removeEventListener("abort", handleAbort);
749
+ await renderAPI?.dispose().catch(() => void 0);
750
+ await browser.close().catch(() => void 0);
472
751
  }
473
- await browser.close();
474
- onProgress?.("encoding video", 80);
475
- const encodingAudio = await buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll);
476
- const { framesToMp4Native } = await import("./nativeEncoder-2HGWDEZZ.js");
477
- await framesToMp4Native(ffmpegPath, frames, encodingAudio, outputPath, {
478
- fps,
479
- quality,
752
+ }
753
+ async function renderDocToMp4(doc, container, options) {
754
+ options.signal?.throwIfAborted();
755
+ const fps = options.fps ?? 30;
756
+ const quality = options.quality ?? "normal";
757
+ const orientation = options.orientation ?? "landscape";
758
+ const dimensions = resolveDimensions({
480
759
  orientation,
760
+ width: options.width,
761
+ height: options.height,
762
+ fps,
763
+ quality
764
+ });
765
+ const capture = await captureDocFrames(doc, container, {
766
+ fps,
481
767
  width: dimensions.width,
482
768
  height: dimensions.height,
483
- onProgress: (percent, phase) => {
484
- onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2));
485
- }
769
+ captionStyle: options.captionStyle,
770
+ coverPreRoll: options.coverPreRoll ?? 0,
771
+ animationsEnabled: options.animationsEnabled ?? true,
772
+ onProgress: options.onProgress,
773
+ signal: options.signal
486
774
  });
487
- onProgress?.("done", 100);
488
- const totalDuration = docDuration + coverPreRoll;
489
- return {
490
- duration: totalDuration,
491
- frameCount: frames.length,
492
- outputPath
493
- };
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
+ }
808
+ }
809
+ async function renderDocToGif(doc, container, options) {
810
+ options.signal?.throwIfAborted();
811
+ const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;
812
+ if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {
813
+ throw new RangeError("GIF FPS must be a finite number between 1 and 100.");
814
+ }
815
+ const orientation = options.orientation ?? "landscape";
816
+ const portrait = orientation === "portrait";
817
+ const width = options.width ?? (portrait ? GIF_EXPORT_DEFAULTS.height : GIF_EXPORT_DEFAULTS.width);
818
+ const height = options.height ?? (portrait ? GIF_EXPORT_DEFAULTS.width : GIF_EXPORT_DEFAULTS.height);
819
+ resolveDimensions({ orientation, width, height, fps });
820
+ ffmpegGifOutputArgs({
821
+ width,
822
+ height,
823
+ loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,
824
+ maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,
825
+ dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,
826
+ bayerScale: options.bayerScale
827
+ });
828
+ const capture = await captureDocFrames(doc, container, {
829
+ fps,
830
+ width,
831
+ height,
832
+ captionStyle: options.captionStyle,
833
+ coverPreRoll: options.coverPreRoll ?? 0,
834
+ animationsEnabled: options.animationsEnabled ?? false,
835
+ onProgress: options.onProgress,
836
+ signal: options.signal
837
+ });
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
+ }
494
868
  }
495
869
  async function extractThumbnails(options) {
496
- const { videoPath, outputDir, slug, sizes, force } = options;
870
+ const { videoPath, outputDir, slug, sizes, force, signal } = options;
497
871
  const { existsSync } = await import("fs");
872
+ const { rm: rm2 } = await import("fs/promises");
498
873
  const { join: join3 } = await import("path");
499
- const { execFile: execFile2 } = await import("child_process");
500
- const ffmpegPath = await detectFfmpeg();
874
+ signal?.throwIfAborted();
875
+ const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
501
876
  if (!ffmpegPath) {
502
877
  throw new Error(
503
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"
504
879
  );
505
880
  }
506
- for (const thumb of sizes) {
507
- const outputPath = join3(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
508
- if (!force && existsSync(outputPath)) continue;
509
- await new Promise((resolve, reject) => {
510
- execFile2(
511
- ffmpegPath,
512
- ["-y", "-i", videoPath, "-vf", thumb.filter, "-frames:v", "1", "-q:v", "2", outputPath],
513
- { timeout: 3e4 },
514
- (err) => {
515
- if (err) reject(new Error(`Thumbnail extraction failed (${thumb.name}): ${err.message}`));
516
- else resolve();
517
- }
518
- );
519
- });
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;
520
908
  }
521
909
  }
522
910
  export {
911
+ CapturedFrameBudgetError,
523
912
  ConversionError,
913
+ GIF_EXPORT_DEFAULTS,
914
+ MAX_CAPTURED_FRAME_BYTES,
524
915
  MemoryContentContainer2 as MemoryContentContainer,
525
916
  convert,
526
917
  createCliRegistry,
527
918
  extractThumbnails,
919
+ framesToGifNative,
920
+ framesToGifNativeBytes,
921
+ framesToMp4Native,
922
+ framesToMp4NativeBytes,
923
+ prepareConversion,
528
924
  readInput,
925
+ renderDocToGif,
529
926
  renderDocToMp4
530
927
  };
531
928
  //# sourceMappingURL=api.js.map