@bendyline/squisq-cli 1.2.2 → 2.0.0

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,7 +1,15 @@
1
+ import {
2
+ GIF_EXPORT_DEFAULTS,
3
+ framesToGifNative,
4
+ framesToGifNativeBytes,
5
+ framesToMp4Native,
6
+ framesToMp4NativeBytes
7
+ } from "./chunk-ESFEFKY4.js";
8
+
1
9
  // src/api.ts
2
10
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
3
11
  import { flattenBlocks } from "@bendyline/squisq/doc";
4
- import { generateRenderHtml } from "@bendyline/squisq-video";
12
+ import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
5
13
  import { resolveDimensions } from "@bendyline/squisq-video";
6
14
  import { convert as formatsConvert } from "@bendyline/squisq-formats";
7
15
 
@@ -49,10 +57,6 @@ async function detectFfmpegDetailed() {
49
57
  }
50
58
  return null;
51
59
  }
52
- async function detectFfmpeg() {
53
- const detection = await detectFfmpegDetailed();
54
- return detection?.path ?? null;
55
- }
56
60
 
57
61
  // src/util/audioMix.ts
58
62
  import { computeAudioTimeline } from "@bendyline/squisq-video";
@@ -127,6 +131,14 @@ async function mixTimelineClips(ffmpegPath, usable) {
127
131
  }
128
132
  }
129
133
 
134
+ // src/util/coverPreRoll.ts
135
+ function resolveAppliedCoverPreRoll(requestedSeconds, hasCover) {
136
+ if (!Number.isFinite(requestedSeconds) || requestedSeconds < 0) {
137
+ throw new Error("Cover pre-roll must be a finite number of seconds greater than or equal to 0");
138
+ }
139
+ return hasCover ? requestedSeconds : 0;
140
+ }
141
+
130
142
  // src/registry.ts
131
143
  import { randomBytes } from "crypto";
132
144
  import { readFile, rm } from "fs/promises";
@@ -137,7 +149,19 @@ var MP4_DEFAULTS = {
137
149
  fps: 30,
138
150
  quality: "normal",
139
151
  orientation: "landscape",
140
- coverPreRoll: 0
152
+ coverPreRoll: 0,
153
+ animationsEnabled: true
154
+ };
155
+ var GIF_DEFAULTS = {
156
+ fps: 10,
157
+ orientation: "landscape",
158
+ width: 960,
159
+ height: 540,
160
+ coverPreRoll: 0,
161
+ animationsEnabled: false,
162
+ loop: 0,
163
+ maxColors: 256,
164
+ dither: "sierra2_4a"
141
165
  };
142
166
  function mp4Format() {
143
167
  return {
@@ -151,14 +175,20 @@ function mp4Format() {
151
175
  const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
152
176
  const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
153
177
  const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
178
+ const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
154
179
  const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
155
180
  try {
156
- await renderDocToMp4(input.doc, input.container, {
181
+ const { renderDocToMp4: renderDocToMp42 } = await import("./api.js");
182
+ await renderDocToMp42(input.doc, input.container, {
157
183
  outputPath,
158
184
  fps,
159
185
  quality,
160
186
  orientation,
161
- coverPreRoll
187
+ width: mp4Opts.width,
188
+ height: mp4Opts.height,
189
+ captionStyle: mp4Opts.captionStyle,
190
+ coverPreRoll,
191
+ animationsEnabled
162
192
  });
163
193
  const data = await readFile(outputPath);
164
194
  const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
@@ -169,27 +199,64 @@ function mp4Format() {
169
199
  }
170
200
  };
171
201
  }
202
+ function gifFormat() {
203
+ return {
204
+ id: "gif",
205
+ label: "Animated GIF",
206
+ mimeType: "image/gif",
207
+ extensions: [".gif"],
208
+ async exportDoc(input, options) {
209
+ const gifOpts = options.formatOptions?.gif ?? {};
210
+ const orientation = typeof gifOpts.orientation === "string" ? gifOpts.orientation : GIF_DEFAULTS.orientation;
211
+ const portrait = orientation === "portrait";
212
+ const defaultWidth = portrait ? GIF_DEFAULTS.height : GIF_DEFAULTS.width;
213
+ const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
214
+ const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
215
+ try {
216
+ const { renderDocToGif: renderDocToGif2 } = await import("./api.js");
217
+ const result = await renderDocToGif2(input.doc, input.container, {
218
+ outputPath,
219
+ fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
220
+ orientation,
221
+ width: typeof gifOpts.width === "number" ? gifOpts.width : defaultWidth,
222
+ height: typeof gifOpts.height === "number" ? gifOpts.height : defaultHeight,
223
+ captionStyle: gifOpts.captionStyle,
224
+ coverPreRoll: typeof gifOpts.coverPreRoll === "number" ? gifOpts.coverPreRoll : GIF_DEFAULTS.coverPreRoll,
225
+ animationsEnabled: typeof gifOpts.animationsEnabled === "boolean" ? gifOpts.animationsEnabled : GIF_DEFAULTS.animationsEnabled,
226
+ loop: typeof gifOpts.loop === "number" ? gifOpts.loop : GIF_DEFAULTS.loop,
227
+ maxColors: typeof gifOpts.maxColors === "number" ? gifOpts.maxColors : GIF_DEFAULTS.maxColors,
228
+ dither: gifOpts.dither ?? GIF_DEFAULTS.dither,
229
+ bayerScale: gifOpts.bayerScale
230
+ });
231
+ const data = await readFile(outputPath);
232
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
233
+ return {
234
+ bytes,
235
+ mimeType: "image/gif",
236
+ suggestedFilename: "",
237
+ warnings: result.warnings
238
+ };
239
+ } finally {
240
+ await rm(outputPath, { force: true });
241
+ }
242
+ }
243
+ };
244
+ }
172
245
  function createCliRegistry() {
173
246
  const registry = defaultRegistry();
174
247
  registry.register(mp4Format());
248
+ registry.register(gifFormat());
175
249
  return registry;
176
250
  }
177
251
 
178
252
  // src/api.ts
179
253
  import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
180
254
 
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
255
  // src/util/readInput.ts
189
256
  import { readFile as readFile2, readdir, stat } from "fs/promises";
190
257
  import { join as join2, extname } from "path";
191
258
  import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
192
- import { markdownToDoc } from "@bendyline/squisq/doc";
259
+ import { markdownToDoc, resolveAudioMapping } from "@bendyline/squisq/doc";
193
260
  import { MemoryContentContainer } from "@bendyline/squisq/storage";
194
261
  import { zipToContainer } from "@bendyline/squisq-formats/container";
195
262
  import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
@@ -226,7 +293,12 @@ async function walkDir(root, prefix = "") {
226
293
  }
227
294
  return paths;
228
295
  }
229
- async function readInput(inputPath) {
296
+ async function readInput(inputPath, options) {
297
+ const result = await readInputRaw(inputPath, options);
298
+ const doc = await resolveAudioMapping(result.doc, result.container);
299
+ return doc === result.doc ? result : { ...result, doc };
300
+ }
301
+ async function readInputRaw(inputPath, options) {
230
302
  const info = await stat(inputPath);
231
303
  if (info.isDirectory()) {
232
304
  return readFolder(inputPath);
@@ -241,7 +313,7 @@ async function readInput(inputPath) {
241
313
  if (IMPORTER_EXTS.includes(ext)) {
242
314
  const def = defaultRegistry2().byExtension(ext);
243
315
  if (def && (def.importContainer || def.importDoc)) {
244
- return readViaImporter(inputPath, def);
316
+ return readViaImporter(inputPath, def, options);
245
317
  }
246
318
  }
247
319
  return readMarkdownFile(inputPath);
@@ -263,16 +335,24 @@ async function readDocJsonFile(filePath) {
263
335
  const container = new MemoryContentContainer();
264
336
  return { doc, container, sourceFormat: "json" };
265
337
  }
266
- async function readViaImporter(filePath, def) {
338
+ async function readViaImporter(filePath, def, options) {
267
339
  const buffer = await readArrayBuffer(filePath);
340
+ const convertOptions = {
341
+ formatOptions: {
342
+ [def.id]: {
343
+ inferTheme: options?.inferTheme !== false,
344
+ inferLayouts: options?.inferLayouts !== false
345
+ }
346
+ }
347
+ };
268
348
  let container;
269
349
  let markdownDoc;
270
350
  if (def.importContainer) {
271
- container = await def.importContainer(buffer, {});
351
+ container = await def.importContainer(buffer, convertOptions);
272
352
  const text = await container.readDocument();
273
353
  markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
274
354
  } else {
275
- markdownDoc = await def.importDoc(buffer, {});
355
+ markdownDoc = await def.importDoc(buffer, convertOptions);
276
356
  const mem = new MemoryContentContainer();
277
357
  await mem.writeDocument(stringifyMarkdown(markdownDoc));
278
358
  container = mem;
@@ -331,22 +411,10 @@ async function convert(source, to, options = {}) {
331
411
  ...options
332
412
  });
333
413
  }
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
348
- });
349
- const ffmpegPath = await detectFfmpeg();
414
+ async function captureDocFrames(doc, container, options) {
415
+ const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress } = options;
416
+ resolveAppliedCoverPreRoll(coverPreRoll, true);
417
+ const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
350
418
  if (!ffmpegPath) {
351
419
  throw new Error(
352
420
  "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."
@@ -354,25 +422,20 @@ async function renderDocToMp4(doc, container, options) {
354
422
  }
355
423
  onProgress?.("collecting media", 0);
356
424
  const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
357
- const imagePaths = collectImagePaths(doc);
358
425
  const images = /* @__PURE__ */ new Map();
359
- for (const imgPath of imagePaths) {
426
+ for (const imgPath of collectImagePaths(doc)) {
360
427
  const data = await container.readFile(imgPath);
361
- if (data) {
362
- images.set(imgPath, data);
363
- }
428
+ if (data) images.set(imgPath, data);
364
429
  }
365
430
  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
- }
431
+ for (const seg of doc.audio?.segments ?? []) {
432
+ const data = await container.readFile(seg.src);
433
+ if (data) {
434
+ audio.set(seg.src, data);
435
+ audio.set(seg.name, data);
373
436
  }
374
437
  }
375
- const mediaSrcs = new Set(resolveMediaSchedule(doc).map((c) => c.src));
438
+ const mediaSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
376
439
  for (const block of flattenBlocks(doc.blocks)) {
377
440
  for (const layer of block.layers ?? []) {
378
441
  if (layer.type === "video") mediaSrcs.add(layer.content.src);
@@ -389,9 +452,10 @@ async function renderDocToMp4(doc, container, options) {
389
452
  playerScript: PLAYER_BUNDLE,
390
453
  images,
391
454
  audio: audio.size > 0 ? audio : void 0,
392
- width: dimensions.width,
393
- height: dimensions.height,
394
- captionStyle
455
+ width,
456
+ height,
457
+ captionStyle,
458
+ animationsEnabled
395
459
  });
396
460
  onProgress?.("launching browser", 15);
397
461
  const { chromium } = await import("playwright-core");
@@ -405,91 +469,163 @@ async function renderDocToMp4(doc, container, options) {
405
469
  (launch failed: ${detail})`
406
470
  );
407
471
  }
408
- const page = await browser.newPage({
409
- viewport: { width: dimensions.width, height: dimensions.height }
410
- });
472
+ const page = await browser.newPage({ viewport: { width, height } });
411
473
  const pageErrors = [];
412
474
  page.on("pageerror", (err) => pageErrors.push(err.message));
413
- await page.setContent(renderHtml, { waitUntil: "load" });
414
- await page.waitForTimeout(500);
475
+ let renderAPI = null;
415
476
  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 ? `
477
+ await page.setContent(renderHtml, { waitUntil: "load" });
478
+ await page.waitForTimeout(500);
479
+ try {
480
+ await page.waitForFunction(
481
+ () => {
482
+ const root = document.getElementById("squisq-root");
483
+ const player = window.SquisqPlayer;
484
+ return root ? player?.getHandle(root)?.getRenderAPI() != null : false;
485
+ },
486
+ { timeout: 15e3 }
487
+ );
488
+ } catch {
489
+ const errorDetail = pageErrors.length ? `
423
490
  Page errors:
424
491
  ${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;
492
+ throw new Error(
493
+ `The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
494
+ );
495
+ }
496
+ renderAPI = await page.evaluateHandle(() => {
497
+ const root = document.getElementById("squisq-root");
498
+ const player = window.SquisqPlayer;
499
+ const api = root ? player?.getHandle(root)?.getRenderAPI() : null;
500
+ if (!api) throw new Error("Squisq render API disappeared after initialization.");
501
+ return api;
445
502
  });
446
- if (hasCover) {
447
- await page.evaluate(() => {
448
- window.showCover();
449
- });
503
+ const docDuration = await renderAPI.evaluate((api) => api.getDuration());
504
+ if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
505
+ const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
506
+ const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
507
+ const storyFrameCount = Math.ceil(docDuration * fps);
508
+ const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
509
+ const totalFrames = preRollFrameCount + storyFrameCount;
510
+ const frames = [];
511
+ onProgress?.("capturing frames", 20);
512
+ if (preRollFrameCount > 0) {
513
+ await renderAPI.evaluate((api) => api.showCover());
450
514
  await page.waitForTimeout(100);
451
515
  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
- });
516
+ for (let i = 0; i < preRollFrameCount; i++) frames.push(coverFrame);
517
+ await renderAPI.evaluate((api) => api.hideCover());
458
518
  }
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);
519
+ const frameInterval = 1 / fps;
520
+ for (let i = 0; i < storyFrameCount; i++) {
521
+ const time = i * frameInterval;
522
+ await renderAPI.evaluate((api, t) => api.seekTo(t), time);
523
+ frames.push(new Uint8Array(await page.screenshot({ type: "png" })));
524
+ if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
525
+ onProgress?.("capturing frames", 20 + Math.round(frames.length / totalFrames * 60));
526
+ }
471
527
  }
528
+ return {
529
+ frames,
530
+ totalDuration: docDuration + appliedCoverPreRoll,
531
+ appliedCoverPreRoll,
532
+ ffmpegPath
533
+ };
534
+ } finally {
535
+ await renderAPI?.dispose();
536
+ await browser.close();
472
537
  }
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, {
538
+ }
539
+ async function renderDocToMp4(doc, container, options) {
540
+ const fps = options.fps ?? 30;
541
+ const quality = options.quality ?? "normal";
542
+ const orientation = options.orientation ?? "landscape";
543
+ const dimensions = resolveDimensions({
544
+ orientation,
545
+ width: options.width,
546
+ height: options.height,
547
+ fps,
548
+ quality
549
+ });
550
+ const capture = await captureDocFrames(doc, container, {
551
+ fps,
552
+ width: dimensions.width,
553
+ height: dimensions.height,
554
+ captionStyle: options.captionStyle,
555
+ coverPreRoll: options.coverPreRoll ?? 0,
556
+ animationsEnabled: options.animationsEnabled ?? true,
557
+ onProgress: options.onProgress
558
+ });
559
+ options.onProgress?.("encoding video", 80);
560
+ const encodingAudio = await buildMixedAudioTrack(
561
+ doc,
562
+ container,
563
+ capture.ffmpegPath,
564
+ capture.appliedCoverPreRoll
565
+ );
566
+ const { framesToMp4Native: framesToMp4Native2 } = await import("./nativeEncoder-IQPN2RMC.js");
567
+ await framesToMp4Native2(capture.ffmpegPath, capture.frames, encodingAudio, options.outputPath, {
478
568
  fps,
479
569
  quality,
480
570
  orientation,
481
571
  width: dimensions.width,
482
572
  height: dimensions.height,
483
- onProgress: (percent, phase) => {
484
- onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2));
485
- }
573
+ onProgress: (percent, phase) => options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2))
574
+ });
575
+ options.onProgress?.("done", 100);
576
+ return {
577
+ duration: capture.totalDuration,
578
+ frameCount: capture.frames.length,
579
+ outputPath: options.outputPath
580
+ };
581
+ }
582
+ async function renderDocToGif(doc, container, options) {
583
+ const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;
584
+ if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {
585
+ throw new RangeError("GIF FPS must be a finite number between 1 and 100.");
586
+ }
587
+ const orientation = options.orientation ?? "landscape";
588
+ const portrait = orientation === "portrait";
589
+ const width = options.width ?? (portrait ? GIF_EXPORT_DEFAULTS.height : GIF_EXPORT_DEFAULTS.width);
590
+ const height = options.height ?? (portrait ? GIF_EXPORT_DEFAULTS.width : GIF_EXPORT_DEFAULTS.height);
591
+ resolveDimensions({ orientation, width, height, fps });
592
+ ffmpegGifOutputArgs({
593
+ width,
594
+ height,
595
+ loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,
596
+ maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,
597
+ dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,
598
+ bayerScale: options.bayerScale
599
+ });
600
+ const capture = await captureDocFrames(doc, container, {
601
+ fps,
602
+ width,
603
+ height,
604
+ captionStyle: options.captionStyle,
605
+ coverPreRoll: options.coverPreRoll ?? 0,
606
+ 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))
486
621
  });
487
- onProgress?.("done", 100);
488
- const totalDuration = docDuration + coverPreRoll;
622
+ options.onProgress?.("done", 100);
623
+ const hasAudio = (doc.audio?.segments?.length ?? 0) > 0 || resolveMediaSchedule(doc).some((clip) => clip.kind === "audio");
489
624
  return {
490
- duration: totalDuration,
491
- frameCount: frames.length,
492
- outputPath
625
+ duration: capture.totalDuration,
626
+ frameCount: capture.frames.length,
627
+ outputPath: options.outputPath,
628
+ warnings: hasAudio ? ["Animated GIF does not support audio; audio tracks were omitted."] : []
493
629
  };
494
630
  }
495
631
  async function extractThumbnails(options) {
@@ -497,7 +633,7 @@ async function extractThumbnails(options) {
497
633
  const { existsSync } = await import("fs");
498
634
  const { join: join3 } = await import("path");
499
635
  const { execFile: execFile2 } = await import("child_process");
500
- const ffmpegPath = await detectFfmpeg();
636
+ const ffmpegPath = (await detectFfmpegDetailed())?.path ?? null;
501
637
  if (!ffmpegPath) {
502
638
  throw new Error(
503
639
  "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"
@@ -521,11 +657,17 @@ async function extractThumbnails(options) {
521
657
  }
522
658
  export {
523
659
  ConversionError,
660
+ GIF_EXPORT_DEFAULTS,
524
661
  MemoryContentContainer2 as MemoryContentContainer,
525
662
  convert,
526
663
  createCliRegistry,
527
664
  extractThumbnails,
665
+ framesToGifNative,
666
+ framesToGifNativeBytes,
667
+ framesToMp4Native,
668
+ framesToMp4NativeBytes,
528
669
  readInput,
670
+ renderDocToGif,
529
671
  renderDocToMp4
530
672
  };
531
673
  //# sourceMappingURL=api.js.map