@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api.ts","../src/util/detectFfmpeg.ts","../src/util/audioMix.ts","../src/util/capturedFrameBudget.ts","../src/util/coverPreRoll.ts","../src/registry.ts","../src/util/readInput.ts"],"sourcesContent":["/**\n * Programmatic Rendered-Media API\n *\n * Provides library-style entry points for rendering Squisq documents to MP4\n * or animated GIF from Node.js callers (e.g., Qualla's pipeline). This avoids\n * the need to shell\n * out to the `squisq video` CLI and gives callers full control over the Doc,\n * MemoryContentContainer, and encoding options.\n *\n * Orchestrates the full pipeline: Doc → render HTML → Playwright frame capture → FFmpeg encode.\n *\n * Usage:\n * import { renderDocToMp4 } from '@bendyline/squisq-cli/api';\n *\n * await renderDocToMp4(doc, container, {\n * outputPath: '/tmp/output.mp4',\n * fps: 30,\n * quality: 'normal',\n * orientation: 'landscape',\n * });\n *\n * await renderDocToGif(doc, container, {\n * outputPath: '/tmp/output.gif',\n * animationsEnabled: false,\n * });\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport { resolveMediaSchedule } from '@bendyline/squisq/schemas';\nimport { flattenBlocks } from '@bendyline/squisq/doc';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport type { GifDither, VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\nimport { ffmpegGifOutputArgs, generateRenderHtml } from '@bendyline/squisq-video';\nimport { resolveDimensions } from '@bendyline/squisq-video';\nimport {\n convert as formatsConvert,\n prepareConversion as formatsPrepareConversion,\n} from '@bendyline/squisq-formats';\nimport type {\n ConvertSource,\n ConvertOptions,\n ConversionResult,\n FormatId,\n PreparedConversion,\n} from '@bendyline/squisq-formats';\nimport { detectFfmpegDetailed } from './util/detectFfmpeg.js';\nimport { buildMixedAudioTrack } from './util/audioMix.js';\nimport { CapturedFrameCollector } from './util/capturedFrameBudget.js';\nimport { resolveAppliedCoverPreRoll } from './util/coverPreRoll.js';\nimport { GIF_EXPORT_DEFAULTS } from './util/nativeEncoder.js';\nimport { runFfmpeg } from './util/runFfmpeg.js';\nimport { createCliRegistry } from './registry.js';\nimport type { GifFormatOptions, Mp4FormatOptions } from './registry.js';\n\n// Re-export utility types and functions callers may need\nexport type { GifDither, VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\nexport { MemoryContentContainer } from '@bendyline/squisq/storage';\nexport { readInput } from './util/readInput.js';\nexport type { ReadInputResult } from './util/readInput.js';\nexport { CapturedFrameBudgetError, MAX_CAPTURED_FRAME_BYTES } from './util/capturedFrameBudget.js';\nexport {\n GIF_EXPORT_DEFAULTS,\n framesToGifNative,\n framesToGifNativeBytes,\n framesToMp4Native,\n framesToMp4NativeBytes,\n} from './util/nativeEncoder.js';\nexport type { GifExportOptions, NativeVideoExportOptions } from './util/nativeEncoder.js';\nexport type { GifFormatOptions, Mp4FormatOptions } from './registry.js';\n\n/** Convert options with the CLI-only rendered-media adapters strongly typed. */\nexport type CliConvertOptions = Omit<ConvertOptions, 'formatOptions'> & {\n formatOptions?: ConvertOptions['formatOptions'] & {\n mp4?: Mp4FormatOptions;\n gif?: GifFormatOptions;\n };\n};\n\n// ── Format registry / convert() surface ───────────────────────────\n// Re-export the CLI's format registry factory plus the registry types and the\n// structured error, so `@bendyline/squisq-cli/api` is a one-stop programmatic\n// front door for both video rendering and document conversion.\nexport { createCliRegistry } from './registry.js';\nexport { ConversionError } from '@bendyline/squisq-formats';\nexport type {\n ConvertSource,\n ConvertOptions,\n ConversionResult,\n FormatId,\n FormatRegistry,\n FormatDefinition,\n NormalizedInput,\n ConversionErrorCode,\n ConversionErrorOptions,\n PreparedConversion,\n PreparedExportOptions,\n} from '@bendyline/squisq-formats';\n\n/**\n * Convert a document to a target format using the CLI's format registry.\n *\n * This is a thin, pre-bound wrapper over `convert()` from\n * `@bendyline/squisq-formats`: it injects the CLI registry (which adds the\n * `mp4` and `gif` formats on top of every built-in exporter) and a default\n * `resolvePlayerScript` that lazily loads the standalone player IIFE bundle\n * (required for HTML/EPUB-style exports). Callers may override either via\n * `options`.\n *\n * @param source - A bytes / markdown / doc {@link ConvertSource}.\n * @param to - Target format id (`docx`, `pdf`, `pptx`, `html`, `mp4`, `gif`, …).\n * @param options - Conversion options; `registry` and `resolvePlayerScript`\n * default to the CLI's values but can be overridden.\n * @returns The encoded bytes plus mime type, suggested filename, and warnings.\n * @throws {@link ConversionError} on any failure, with a stable `code`.\n */\nexport async function convert(\n source: ConvertSource,\n to: FormatId,\n options: CliConvertOptions = {},\n): Promise<ConversionResult> {\n return formatsConvert(source, to, {\n registry: createCliRegistry(),\n resolvePlayerScript: () =>\n import('@bendyline/squisq-react/standalone-source').then((m) => m.PLAYER_BUNDLE),\n ...options,\n });\n}\n\n/** Normalize and transform once using the CLI registry, then export one or more targets. */\nexport async function prepareConversion(\n source: ConvertSource,\n options: CliConvertOptions = {},\n): Promise<PreparedConversion> {\n return formatsPrepareConversion(source, {\n registry: createCliRegistry(),\n resolvePlayerScript: () =>\n import('@bendyline/squisq-react/standalone-source').then((m) => m.PLAYER_BUNDLE),\n ...options,\n });\n}\n\n/** Options for renderDocToMp4. */\nexport interface RenderDocToMp4Options {\n /** Cancel capture/encoding and terminate browser and FFmpeg work. */\n signal?: AbortSignal;\n /** Output file path for the MP4. */\n outputPath: string;\n\n /** Frames per second (default: 30). */\n fps?: number;\n\n /** Encoding quality preset (default: 'normal'). */\n quality?: VideoQuality;\n\n /** Video orientation (default: 'landscape'). */\n orientation?: VideoOrientation;\n\n /** Override video width in pixels. */\n width?: number;\n\n /** Override video height in pixels. */\n height?: number;\n\n /** Caption style to bake into the video (default: none). */\n captionStyle?: 'standard' | 'social';\n\n /** Render layer animations and block transitions (default: true). */\n animationsEnabled?: boolean;\n\n /**\n * Seconds of cover-slide pre-roll before the story starts (default: 0).\n *\n * Note: the `squisq video` CLI defaults its `--cover-preroll` flag to 2\n * seconds; this programmatic API deliberately defaults to 0 so library\n * callers get exactly the duration they ask for.\n */\n coverPreRoll?: number;\n\n /**\n * Progress callback. Called with a phase name and 0-100 percentage.\n */\n onProgress?: (phase: string, percent: number) => void;\n}\n\n/** Options for rendering a document to an animated GIF. */\nexport interface RenderDocToGifOptions {\n /** Cancel capture/encoding and terminate browser and FFmpeg work. */\n signal?: AbortSignal;\n /** Output file path for the GIF. */\n outputPath: string;\n /** Frames per second (default: 10). */\n fps?: number;\n /** Viewport orientation (default: landscape). */\n orientation?: VideoOrientation;\n /** Override output width (default: 960 landscape / 540 portrait). */\n width?: number;\n /** Override output height (default: 540 landscape / 960 portrait). */\n height?: number;\n /** Caption style to bake into the GIF (default: none). */\n captionStyle?: 'standard' | 'social';\n /** Seconds of cover-slide pre-roll (default: 0). */\n coverPreRoll?: number;\n /** Render layer animations and block transitions (default: false). */\n animationsEnabled?: boolean;\n /** Number of repeats; 0 loops forever and -1 disables looping. */\n loop?: number;\n /** Palette size, from 2 through 256 (default: 256). */\n maxColors?: number;\n /** Palette dithering algorithm (default: sierra2_4a). */\n dither?: GifDither;\n /** Bayer strength when dither is bayer (0-5, default: 3). */\n bayerScale?: number;\n /** Progress callback. */\n onProgress?: (phase: string, percent: number) => void;\n}\n\n/** Result returned by renderDocToMp4. */\nexport interface RenderDocToMp4Result {\n /** Duration of the rendered video in seconds (including pre-roll). */\n duration: number;\n\n /** Number of frames captured. */\n frameCount: number;\n\n /** Output file path. */\n outputPath: string;\n}\n\n/** Result returned by renderDocToGif. */\nexport interface RenderDocToGifResult extends RenderDocToMp4Result {\n /** Non-fatal fidelity warnings, including GIF's lack of audio. */\n warnings: string[];\n}\n\n/** Minimal standalone-player contract used inside Playwright's page realm. */\ninterface BrowserRenderAPI {\n seekTo(time: number): Promise<void>;\n getDuration(): number;\n hasCoverBlock(): boolean;\n showCover(): Promise<void>;\n hideCover(): Promise<void>;\n}\n\ninterface BrowserPlayerHandle {\n getRenderAPI(): BrowserRenderAPI | null;\n}\n\ninterface BrowserStandalonePlayer {\n getHandle(element: Element): BrowserPlayerHandle | undefined;\n}\n\ninterface SquisqBrowserWindow {\n SquisqPlayer?: BrowserStandalonePlayer;\n}\n\ninterface CaptureDocFramesOptions {\n signal?: AbortSignal;\n fps: number;\n width: number;\n height: number;\n captionStyle?: 'standard' | 'social';\n coverPreRoll: number;\n animationsEnabled: boolean;\n onProgress?: (phase: string, percent: number) => void;\n}\n\ninterface CapturedDocFrames {\n frames: Uint8Array[];\n totalDuration: number;\n appliedCoverPreRoll: number;\n ffmpegPath: string;\n}\n\n/** Capture deterministic PNG frames once, independent of the output encoder. */\nasync function captureDocFrames(\n doc: Doc,\n container: ContentContainer,\n options: CaptureDocFramesOptions,\n): Promise<CapturedDocFrames> {\n const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } =\n options;\n\n signal?.throwIfAborted();\n resolveAppliedCoverPreRoll(coverPreRoll, true);\n const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;\n signal?.throwIfAborted();\n if (!ffmpegPath) {\n throw new Error(\n 'ffmpeg is required but not found in PATH.\\n' +\n 'Install it with:\\n' +\n ' macOS: brew install ffmpeg\\n' +\n ' Ubuntu: sudo apt install ffmpeg\\n' +\n ' Windows: winget install ffmpeg\\n' +\n 'Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary.',\n );\n }\n\n onProgress?.('collecting media', 0);\n signal?.throwIfAborted();\n const { collectImagePaths } = await import('@bendyline/squisq-formats/html');\n signal?.throwIfAborted();\n const images = new Map<string, ArrayBuffer>();\n for (const imgPath of collectImagePaths(doc)) {\n signal?.throwIfAborted();\n const data = await container.readFile(imgPath);\n signal?.throwIfAborted();\n if (data) images.set(imgPath, data);\n }\n\n // Audio remains available to the headless player even for silent GIF output:\n // narration metadata and media duration still define the visual timeline.\n const audio = new Map<string, ArrayBuffer>();\n for (const seg of doc.audio?.segments ?? []) {\n signal?.throwIfAborted();\n const data = await container.readFile(seg.src);\n signal?.throwIfAborted();\n if (data) {\n audio.set(seg.src, data);\n audio.set(seg.name, data);\n }\n }\n\n const mediaSrcs = new Set<string>(resolveMediaSchedule(doc).map((clip) => clip.src));\n for (const block of flattenBlocks(doc.blocks)) {\n for (const layer of block.layers ?? []) {\n if (layer.type === 'video') mediaSrcs.add(layer.content.src);\n }\n }\n for (const src of mediaSrcs) {\n signal?.throwIfAborted();\n if (images.has(src)) continue;\n const data = await container.readFile(src);\n signal?.throwIfAborted();\n if (data) images.set(src, data);\n }\n\n onProgress?.('generating render HTML', 10);\n signal?.throwIfAborted();\n const { PLAYER_BUNDLE } = await import('@bendyline/squisq-react/standalone-source');\n signal?.throwIfAborted();\n const renderHtml = generateRenderHtml(doc, {\n playerScript: PLAYER_BUNDLE,\n images,\n audio: audio.size > 0 ? audio : undefined,\n width,\n height,\n captionStyle,\n animationsEnabled,\n });\n\n onProgress?.('launching browser', 15);\n signal?.throwIfAborted();\n const { chromium } = await import('playwright-core');\n signal?.throwIfAborted();\n let browser: import('playwright-core').Browser;\n try {\n browser = await chromium.launch({ headless: true });\n } catch (err: unknown) {\n signal?.throwIfAborted();\n const detail = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n throw new Error(\n 'Playwright Chromium is not installed. Run: npx playwright install chromium\\n' +\n `(launch failed: ${detail})`,\n );\n }\n\n const handleAbort = (): void => {\n void browser.close().catch(() => undefined);\n };\n if (signal?.aborted) {\n await browser.close().catch(() => undefined);\n signal.throwIfAborted();\n }\n signal?.addEventListener('abort', handleAbort, { once: true });\n let renderAPI: import('playwright-core').JSHandle<BrowserRenderAPI> | null = null;\n const capturedFrames = new CapturedFrameCollector();\n\n try {\n const page = await browser.newPage({ viewport: { width, height } });\n const pageErrors: string[] = [];\n page.on('pageerror', (err) => pageErrors.push(err.message));\n signal?.throwIfAborted();\n await page.setContent(renderHtml, { waitUntil: 'load' });\n signal?.throwIfAborted();\n await page.waitForTimeout(500);\n signal?.throwIfAborted();\n try {\n await page.waitForFunction(\n () => {\n const root = document.getElementById('squisq-root');\n const player = (window as unknown as SquisqBrowserWindow).SquisqPlayer;\n return root ? player?.getHandle(root)?.getRenderAPI() != null : false;\n },\n { timeout: 15000 },\n );\n } catch {\n signal?.throwIfAborted();\n const errorDetail = pageErrors.length\n ? `\\nPage errors:\\n ${pageErrors.join('\\n ')}`\n : '\\nNo page errors captured — the player may have failed to mount.';\n throw new Error(\n `The standalone player failed to boot in headless Chromium. ` +\n `Render API did not initialize within 15 seconds.${errorDetail}`,\n );\n }\n\n renderAPI = await page.evaluateHandle(() => {\n const root = document.getElementById('squisq-root');\n const player = (window as unknown as SquisqBrowserWindow).SquisqPlayer;\n const api = root ? player?.getHandle(root)?.getRenderAPI() : null;\n if (!api) throw new Error('Squisq render API disappeared after initialization.');\n return api;\n });\n signal?.throwIfAborted();\n const docDuration = await renderAPI.evaluate((api) => api.getDuration());\n signal?.throwIfAborted();\n if (docDuration <= 0) throw new Error('Document has zero duration — nothing to render');\n\n const hasCover =\n coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;\n const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);\n const storyFrameCount = Math.ceil(docDuration * fps);\n const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);\n const totalFrames = preRollFrameCount + storyFrameCount;\n onProgress?.('capturing frames', 20);\n capturedFrames.throwIfAborted(signal);\n\n if (preRollFrameCount > 0) {\n capturedFrames.throwIfAborted(signal);\n await renderAPI.evaluate((api) => api.showCover());\n await page.waitForTimeout(100);\n capturedFrames.throwIfAborted(signal);\n const coverFrame = await page.screenshot({ type: 'png' });\n capturedFrames.throwIfAborted(signal);\n capturedFrames.append(coverFrame, preRollFrameCount);\n await renderAPI.evaluate((api) => api.hideCover());\n }\n\n const frameInterval = 1 / fps;\n for (let i = 0; i < storyFrameCount; i++) {\n capturedFrames.throwIfAborted(signal);\n const time = i * frameInterval;\n await renderAPI.evaluate((api, t: number) => api.seekTo(t), time);\n const frame = await page.screenshot({ type: 'png' });\n capturedFrames.throwIfAborted(signal);\n capturedFrames.append(frame);\n if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {\n onProgress?.(\n 'capturing frames',\n 20 + Math.round((capturedFrames.frameCount / totalFrames) * 60),\n );\n capturedFrames.throwIfAborted(signal);\n }\n }\n\n return {\n frames: capturedFrames.release(),\n totalDuration: docDuration + appliedCoverPreRoll,\n appliedCoverPreRoll,\n ffmpegPath,\n };\n } catch (err: unknown) {\n capturedFrames.clear();\n signal?.throwIfAborted();\n throw err;\n } finally {\n signal?.removeEventListener('abort', handleAbort);\n await renderAPI?.dispose().catch(() => undefined);\n await browser.close().catch(() => undefined);\n }\n}\n\n/** Render a Doc + media container to an MP4 video file. */\nexport async function renderDocToMp4(\n doc: Doc,\n container: ContentContainer,\n options: RenderDocToMp4Options,\n): Promise<RenderDocToMp4Result> {\n options.signal?.throwIfAborted();\n const fps = options.fps ?? 30;\n const quality = options.quality ?? 'normal';\n const orientation = options.orientation ?? 'landscape';\n const dimensions = resolveDimensions({\n orientation,\n width: options.width,\n height: options.height,\n fps,\n quality,\n });\n const capture = await captureDocFrames(doc, container, {\n fps,\n width: dimensions.width,\n height: dimensions.height,\n captionStyle: options.captionStyle,\n coverPreRoll: options.coverPreRoll ?? 0,\n animationsEnabled: options.animationsEnabled ?? true,\n onProgress: options.onProgress,\n signal: options.signal,\n });\n const frameCount = capture.frames.length;\n try {\n options.onProgress?.('encoding video', 80);\n options.signal?.throwIfAborted();\n const encodingAudio = await buildMixedAudioTrack(\n doc,\n container,\n capture.ffmpegPath,\n capture.appliedCoverPreRoll,\n options.signal,\n );\n options.signal?.throwIfAborted();\n const { framesToMp4Native } = await import('./util/nativeEncoder.js');\n await framesToMp4Native(capture.ffmpegPath, capture.frames, encodingAudio, options.outputPath, {\n fps,\n quality,\n orientation,\n width: dimensions.width,\n height: dimensions.height,\n signal: options.signal,\n onProgress: (percent, phase) =>\n options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2)),\n });\n options.signal?.throwIfAborted();\n options.onProgress?.('done', 100);\n options.signal?.throwIfAborted();\n return {\n duration: capture.totalDuration,\n frameCount,\n outputPath: options.outputPath,\n };\n } finally {\n capture.frames.length = 0;\n }\n}\n\n/** Render a Doc + media container to a silent animated GIF. */\nexport async function renderDocToGif(\n doc: Doc,\n container: ContentContainer,\n options: RenderDocToGifOptions,\n): Promise<RenderDocToGifResult> {\n options.signal?.throwIfAborted();\n const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;\n if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {\n throw new RangeError('GIF FPS must be a finite number between 1 and 100.');\n }\n const orientation = options.orientation ?? 'landscape';\n const portrait = orientation === 'portrait';\n const width =\n options.width ?? (portrait ? GIF_EXPORT_DEFAULTS.height : GIF_EXPORT_DEFAULTS.width);\n const height =\n options.height ?? (portrait ? GIF_EXPORT_DEFAULTS.width : GIF_EXPORT_DEFAULTS.height);\n // Reuse shared dimension validation without inheriting MP4's 1080p defaults.\n resolveDimensions({ orientation, width, height, fps });\n // Validate palette/muxer options before the expensive browser capture.\n ffmpegGifOutputArgs({\n width,\n height,\n loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,\n maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,\n dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,\n bayerScale: options.bayerScale,\n });\n\n const capture = await captureDocFrames(doc, container, {\n fps,\n width,\n height,\n captionStyle: options.captionStyle,\n coverPreRoll: options.coverPreRoll ?? 0,\n animationsEnabled: options.animationsEnabled ?? false,\n onProgress: options.onProgress,\n signal: options.signal,\n });\n const frameCount = capture.frames.length;\n try {\n options.onProgress?.('encoding GIF', 80);\n options.signal?.throwIfAborted();\n const { framesToGifNative } = await import('./util/nativeEncoder.js');\n await framesToGifNative(capture.ffmpegPath, capture.frames, options.outputPath, {\n fps,\n orientation,\n width,\n height,\n loop: options.loop,\n maxColors: options.maxColors,\n dither: options.dither,\n bayerScale: options.bayerScale,\n signal: options.signal,\n onProgress: (percent, phase) =>\n options.onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2)),\n });\n options.signal?.throwIfAborted();\n options.onProgress?.('done', 100);\n options.signal?.throwIfAborted();\n\n const hasAudio =\n (doc.audio?.segments?.length ?? 0) > 0 ||\n resolveMediaSchedule(doc).some((clip) => clip.kind === 'audio');\n return {\n duration: capture.totalDuration,\n frameCount,\n outputPath: options.outputPath,\n warnings: hasAudio ? ['Animated GIF does not support audio; audio tracks were omitted.'] : [],\n };\n } finally {\n capture.frames.length = 0;\n }\n}\n\n// ── Thumbnail extraction ──────────────────────────────────────────\n\n/** A thumbnail size specification. */\nexport interface ThumbnailSpec {\n /** Label for the thumbnail (used in filename: `{slug}-{width}x{height}.jpg`). */\n name: string;\n /** Output width in pixels. */\n width: number;\n /** Output height in pixels. */\n height: number;\n /** FFmpeg video filter string (e.g., 'scale=1280:720'). */\n filter: string;\n}\n\n/** Options for extractThumbnails. */\nexport interface ExtractThumbnailsOptions {\n /** Path to the source MP4 or animated GIF. */\n videoPath: string;\n /** Directory to write thumbnails into. */\n outputDir: string;\n /** Base slug for filenames (produces `{slug}-{width}x{height}.jpg`). */\n slug: string;\n /** Thumbnail sizes to generate. */\n sizes: ThumbnailSpec[];\n /** Overwrite existing thumbnails (default: false). */\n force?: boolean;\n /** Cancels FFmpeg and rejects with the caller's exact abort reason. */\n signal?: AbortSignal;\n}\n\n/**\n * Extract thumbnail images from the first frame of an MP4 or animated GIF.\n * Produces JPEG files at each specified size using FFmpeg video filters.\n */\nexport async function extractThumbnails(options: ExtractThumbnailsOptions): Promise<void> {\n const { videoPath, outputDir, slug, sizes, force, signal } = options;\n const { existsSync } = await import('node:fs');\n const { rm } = await import('node:fs/promises');\n const { join } = await import('node:path');\n\n signal?.throwIfAborted();\n const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;\n if (!ffmpegPath) {\n throw new Error(\n 'ffmpeg is required for thumbnail extraction but not found in PATH.\\n' +\n 'Install it with:\\n' +\n ' macOS: brew install ffmpeg\\n' +\n ' Ubuntu: sudo apt install ffmpeg\\n' +\n ' Windows: winget install ffmpeg',\n );\n }\n\n const generatedPaths: string[] = [];\n try {\n for (const thumb of sizes) {\n signal?.throwIfAborted();\n const outputPath = join(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);\n if (!force && existsSync(outputPath)) continue;\n\n try {\n await runFfmpeg(\n ffmpegPath,\n ['-y', '-i', videoPath, '-vf', thumb.filter, '-frames:v', '1', '-q:v', '2', outputPath],\n {\n timeoutMs: 30_000,\n failureMessage: `Thumbnail extraction failed (${thumb.name})`,\n signal,\n },\n );\n signal?.throwIfAborted();\n generatedPaths.push(outputPath);\n } catch (error: unknown) {\n await rm(outputPath, { force: true });\n throw error;\n }\n }\n signal?.throwIfAborted();\n } catch (error: unknown) {\n await Promise.allSettled(generatedPaths.map((path) => rm(path, { force: true })));\n throw error;\n }\n}\n","/**\n * detectFfmpeg\n *\n * Locates a native ffmpeg binary. Resolution order:\n * 1. `SQUISQ_FFMPEG` environment variable — verified by running\n * `<path> -version`. A value that is set but broken is a hard error\n * (never silently ignored), so misconfiguration surfaces immediately.\n * 2. System PATH lookup (`which` on macOS/Linux, `where` on Windows).\n * 3. The optional `ffmpeg-static` npm package, when the user has installed\n * it themselves. It is deliberately NOT a dependency of the CLI — the\n * import is opportunistic and falls through cleanly when absent.\n */\n\nimport { execFile } from 'node:child_process';\n\n/** Where a detected ffmpeg binary came from. */\nexport type FfmpegSource = 'env' | 'path' | 'ffmpeg-static';\n\n/** A successfully detected ffmpeg binary. */\nexport interface FfmpegDetection {\n /** Absolute path to the ffmpeg binary. */\n path: string;\n /** Which resolution step found it. */\n source: FfmpegSource;\n}\n\n/** Run a command and resolve with trimmed stdout, or null on any failure. */\nfunction run(command: string, args: string[], signal?: AbortSignal): Promise<string | null> {\n signal?.throwIfAborted();\n return new Promise((resolve, reject) => {\n execFile(command, args, { timeout: 5000, signal }, (err, stdout) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n if (err || !stdout.trim()) {\n resolve(null);\n return;\n }\n resolve(stdout.trim());\n });\n });\n}\n\n/**\n * Run `<path> -version` and return the first line of output\n * (e.g. \"ffmpeg version 7.1 ...\"), or null when the binary fails to run.\n */\nexport async function getFfmpegVersion(path: string, signal?: AbortSignal): Promise<string | null> {\n const out = await run(path, ['-version'], signal);\n return out ? out.split('\\n')[0].trim() : null;\n}\n\n/**\n * Detect ffmpeg and report which source found it (see module doc for the\n * resolution order). Returns null when no working binary is found.\n *\n * @throws Error when `SQUISQ_FFMPEG` is set but the binary does not run —\n * an explicit override must never be silently ignored.\n */\nexport async function detectFfmpegDetailed(signal?: AbortSignal): Promise<FfmpegDetection | null> {\n signal?.throwIfAborted();\n // 1. Explicit override\n const envPath = process.env.SQUISQ_FFMPEG;\n if (envPath) {\n const version = await getFfmpegVersion(envPath, signal);\n if (!version) {\n throw new Error(\n `SQUISQ_FFMPEG is set to \"${envPath}\" but running \"${envPath} -version\" failed. ` +\n 'Fix the path or unset SQUISQ_FFMPEG.',\n );\n }\n return { path: envPath, source: 'env' };\n }\n\n // 2. System PATH\n const command = process.platform === 'win32' ? 'where' : 'which';\n const found = await run(command, ['ffmpeg'], signal);\n if (found) {\n // `which` and `where` can return multiple lines; take the first\n return { path: found.split('\\n')[0].trim(), source: 'path' };\n }\n\n // 3. Optional ffmpeg-static package. The specifier is a variable so\n // bundlers/TypeScript don't try to resolve a package we don't depend on.\n try {\n signal?.throwIfAborted();\n const specifier = 'ffmpeg-static';\n const mod = (await import(specifier)) as { default?: unknown } | string;\n // CJS default export shape (`module.exports = path`) surfaces as\n // `mod.default`; some build setups expose the string directly.\n const candidate =\n typeof mod === 'string' ? mod : typeof mod.default === 'string' ? mod.default : null;\n if (candidate) {\n return { path: candidate, source: 'ffmpeg-static' };\n }\n } catch {\n signal?.throwIfAborted();\n // Not installed — fall through.\n }\n\n signal?.throwIfAborted();\n return null;\n}\n","/**\n * audioMix — Build the MP4's final audio track from a doc's audio timeline.\n *\n * The schedule is produced by ONE source of truth: `computeAudioTimeline`\n * (from `@bendyline/squisq-video`), which flattens a doc's narration segments\n * (laid sequentially) and its timed media clips (placed at absolute positions\n * via `resolveMediaSchedule`) into a flat list of absolute-timed\n * `AudioTimelineClip`s. This CLI-side helper is a pure *consumer* of that\n * schedule: it loads each clip's bytes and assembles an ffmpeg `amix`\n * filtergraph where every clip is trimmed to its source window and delayed to\n * its absolute start.\n *\n * Because the browser MP4 export and this CLI mix now read the same timeline,\n * their audio placement can no longer drift. In particular, zero-duration\n * narration segments are skipped here exactly as they are in the browser\n * (`computeAudioTimeline` never emits a clip with `durationSec <= 0`).\n *\n * Frame capture stays silent — this reconstructs the audio offline from the\n * same schedule the renderer uses, keeping audio and video aligned.\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport { computeAudioTimeline } from '@bendyline/squisq-video';\nimport type { AudioTimelineClip } from '@bendyline/squisq-video';\nimport { runFfmpeg } from './runFfmpeg.js';\n\n/**\n * Build the final mixed MP3 audio track for a doc's MP4 export.\n *\n * Drives the ffmpeg filtergraph entirely from `computeAudioTimeline(doc,\n * coverPreRoll)`: each clip's `startSec` → `adelay`, `sourceInSec` → `atrim`\n * start, `durationSec` → `atrim` duration, keyed by `src`. All clips are summed\n * with `amix` (`normalize=0` so individual levels are preserved).\n *\n * @param doc - The document whose audio to schedule.\n * @param container - Media container to read each clip's `src` bytes from.\n * @param ffmpegPath - Path to the ffmpeg binary.\n * @param coverPreRoll - Seconds of cover-slide pre-roll; shifts every clip's\n * start (matching the silent leading frames).\n * @returns The mixed MP3 bytes, or `null` when the doc has no audio to place.\n */\nexport async function buildMixedAudioTrack(\n doc: Doc,\n container: ContentContainer,\n ffmpegPath: string,\n coverPreRoll: number,\n signal?: AbortSignal,\n): Promise<Uint8Array | null> {\n signal?.throwIfAborted();\n const timeline = computeAudioTimeline(doc, coverPreRoll);\n if (timeline.length === 0) return null;\n\n // Load each clip's bytes, caching reads by `src` (a src may back several clips).\n const bytesBySrc = new Map<string, ArrayBuffer | null>();\n const readSrc = async (src: string): Promise<ArrayBuffer | null> => {\n signal?.throwIfAborted();\n if (bytesBySrc.has(src)) return bytesBySrc.get(src)!;\n const data = await container.readFile(src);\n signal?.throwIfAborted();\n bytesBySrc.set(src, data ?? null);\n return data ?? null;\n };\n\n const usable: Array<{ clip: AudioTimelineClip; buffer: ArrayBuffer }> = [];\n for (const clip of timeline) {\n signal?.throwIfAborted();\n const buffer = await readSrc(clip.src);\n if (buffer) usable.push({ clip, buffer });\n }\n if (usable.length === 0) return null;\n\n return mixTimelineClips(ffmpegPath, usable, signal);\n}\n\n/** Assemble the ffmpeg `amix` filtergraph and run it, returning the mixed MP3. */\nasync function mixTimelineClips(\n ffmpegPath: string,\n usable: Array<{ clip: AudioTimelineClip; buffer: ArrayBuffer }>,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n const { writeFile, readFile, mkdir, rm } = await import('node:fs/promises');\n const { join } = await import('node:path');\n const { tmpdir } = await import('node:os');\n const { randomBytes } = await import('node:crypto');\n signal?.throwIfAborted();\n\n const workDir = join(tmpdir(), `squisq-audio-mix-${randomBytes(8).toString('hex')}`);\n await mkdir(workDir, { recursive: true });\n\n const ms = (s: number) => Math.max(0, Math.round(s * 1000));\n\n try {\n signal?.throwIfAborted();\n const inputs: string[] = [];\n const filters: string[] = [];\n const labels: string[] = [];\n\n for (const { clip, buffer } of usable) {\n signal?.throwIfAborted();\n const p = join(workDir, `clip-${inputs.length}.mp3`);\n await writeFile(p, new Uint8Array(buffer));\n signal?.throwIfAborted();\n const i = inputs.push(p) - 1;\n const delayMs = ms(clip.startSec);\n const start = Math.max(0, clip.sourceInSec);\n const end = start + Math.max(0, clip.durationSec);\n filters.push(\n `[${i}:a]atrim=start=${start}:end=${end},asetpts=PTS-STARTPTS,adelay=${delayMs}|${delayMs}[a${i}]`,\n );\n labels.push(`[a${i}]`);\n }\n\n const graph =\n `${filters.join(';')};${labels.join('')}` +\n `amix=inputs=${labels.length}:normalize=0:dropout_transition=0[aout]`;\n\n const outputPath = join(workDir, 'mixed.mp3');\n const args = ['-y'];\n for (const p of inputs) args.push('-i', p);\n args.push(\n '-filter_complex',\n graph,\n '-map',\n '[aout]',\n '-c:a',\n 'libmp3lame',\n '-b:a',\n '192k',\n outputPath,\n );\n\n await runFfmpeg(ffmpegPath, args, {\n timeoutMs: 180_000,\n failureMessage: 'ffmpeg audio mix failed',\n signal,\n });\n\n signal?.throwIfAborted();\n const data = await readFile(outputPath);\n signal?.throwIfAborted();\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n","/**\n * Hard retained-memory ceiling for PNG frames captured before native encoding.\n *\n * The Node renderer currently keeps captured PNGs in memory while FFmpeg is\n * prepared. Bounding the actual encoded PNG bytes prevents visually complex\n * documents from exhausting the process even when their dimensions and frame\n * counts satisfy higher-level render budgets.\n */\nexport const MAX_CAPTURED_FRAME_BYTES = 256 * 1024 * 1024;\n\n/** Stable failure raised before another captured frame is retained. */\nexport class CapturedFrameBudgetError extends Error {\n public readonly code = 'captured-frame-budget-exceeded';\n\n public constructor(\n public readonly capturedBytes: number,\n public readonly attemptedFrameBytes: number,\n public readonly maximumBytes: number,\n ) {\n super(\n `Captured PNG frames would retain ${capturedBytes + attemptedFrameBytes} bytes, ` +\n `exceeding the ${maximumBytes}-byte rendered-media memory limit.`,\n );\n this.name = 'CapturedFrameBudgetError';\n }\n}\n\n/**\n * Owns captured-frame references until they are handed to an encoder.\n * Repeated references (cover pre-roll) count once toward retained bytes.\n */\nexport class CapturedFrameCollector {\n private values: Uint8Array[] = [];\n private seen = new WeakSet<Uint8Array>();\n private bytes = 0;\n\n public constructor(private readonly maximumBytes = MAX_CAPTURED_FRAME_BYTES) {\n if (\n !Number.isSafeInteger(maximumBytes) ||\n maximumBytes < 1 ||\n maximumBytes > MAX_CAPTURED_FRAME_BYTES\n ) {\n throw new Error(\n `Captured-frame byte limit must be between 1 and ${MAX_CAPTURED_FRAME_BYTES}`,\n );\n }\n }\n\n public get frameCount(): number {\n return this.values.length;\n }\n\n public get retainedBytes(): number {\n return this.bytes;\n }\n\n public append(frame: Uint8Array, repetitions = 1): void {\n if (!Number.isSafeInteger(repetitions) || repetitions < 1) {\n throw new Error('Captured-frame repetition count must be a positive integer');\n }\n const additionalBytes = this.seen.has(frame) ? 0 : frame.byteLength;\n if (this.bytes + additionalBytes > this.maximumBytes) {\n const capturedBytes = this.bytes;\n this.clear();\n throw new CapturedFrameBudgetError(capturedBytes, additionalBytes, this.maximumBytes);\n }\n if (additionalBytes > 0) {\n this.seen.add(frame);\n this.bytes += additionalBytes;\n }\n for (let index = 0; index < repetitions; index += 1) this.values.push(frame);\n }\n\n /** Clear retained frame references before preserving the caller's abort reason. */\n public throwIfAborted(signal?: AbortSignal): void {\n if (!signal?.aborted) return;\n this.clear();\n signal.throwIfAborted();\n }\n\n /** Transfer ownership of the captured frames to the encoder. */\n public release(): Uint8Array[] {\n const released = this.values;\n this.values = [];\n this.seen = new WeakSet<Uint8Array>();\n this.bytes = 0;\n return released;\n }\n\n /** Drop every retained reference so failed/cancelled captures can be collected promptly. */\n public clear(): void {\n this.values.length = 0;\n this.values = [];\n this.seen = new WeakSet<Uint8Array>();\n this.bytes = 0;\n }\n}\n","/**\n * Resolve the pre-roll that will actually be rendered.\n *\n * A requested pre-roll only has meaning when the document exposes a cover.\n * Keeping this decision explicit prevents frame counts, audio offsets, and\n * reported duration from drifting apart when a document has no cover block.\n */\nexport function resolveAppliedCoverPreRoll(requestedSeconds: number, hasCover: boolean): number {\n if (!Number.isFinite(requestedSeconds) || requestedSeconds < 0) {\n throw new Error('Cover pre-roll must be a finite number of seconds greater than or equal to 0');\n }\n return hasCover ? requestedSeconds : 0;\n}\n","/**\n * CLI format registry.\n *\n * The CLI extends the formats package's default registry with `mp4` and `gif`\n * formats so `convert()` can produce rendered media like DOCX/PDF/HTML. These\n * exports are Node-only (Playwright + FFmpeg), which is why they live here\n * rather than in the browser-pure formats package.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { readFile, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { defaultRegistry } from '@bendyline/squisq-formats';\nimport type {\n ConversionResult,\n ConvertOptions,\n FormatDefinition,\n FormatRegistry,\n NormalizedInput,\n} from '@bendyline/squisq-formats';\nimport type { GifDither, VideoOrientation, VideoQuality } from '@bendyline/squisq-video';\n\nexport interface Mp4FormatOptions {\n onProgress?: (phase: string, percent: number) => void;\n fps?: number;\n quality?: VideoQuality;\n orientation?: VideoOrientation;\n width?: number;\n height?: number;\n captionStyle?: 'standard' | 'social';\n coverPreRoll?: number;\n animationsEnabled?: boolean;\n}\n\n/** Per-export options for `convert(..., 'gif')`. */\nexport interface GifFormatOptions {\n onProgress?: (phase: string, percent: number) => void;\n fps?: number;\n orientation?: VideoOrientation;\n width?: number;\n height?: number;\n captionStyle?: 'standard' | 'social';\n coverPreRoll?: number;\n animationsEnabled?: boolean;\n loop?: number;\n maxColors?: number;\n dither?: GifDither;\n bayerScale?: number;\n}\n\n/** Sensible defaults for `convert(..., 'mp4')` — a full-quality landscape clip. */\nconst MP4_DEFAULTS = {\n fps: 30,\n quality: 'normal' as VideoQuality,\n orientation: 'landscape' as VideoOrientation,\n coverPreRoll: 0,\n animationsEnabled: true,\n} as const;\n\n/** Compression-friendly defaults for `convert(..., 'gif')`. */\nconst GIF_DEFAULTS = {\n fps: 10,\n orientation: 'landscape' as VideoOrientation,\n width: 960,\n height: 540,\n coverPreRoll: 0,\n animationsEnabled: false,\n loop: 0,\n maxColors: 256,\n dither: 'sierra2_4a' as GifDither,\n} as const;\n\n/**\n * The `mp4` {@link FormatDefinition}.\n *\n * Export-only: it renders the normalized Doc to a temporary MP4 on disk via\n * {@link renderDocToMp4}, reads the produced file back into a `Uint8Array`, and\n * always deletes the temp file (even on failure). Per-render knobs (fps,\n * quality, orientation, coverPreRoll) can be supplied through\n * `options.formatOptions.mp4`; otherwise {@link MP4_DEFAULTS} apply.\n */\nfunction mp4Format(): FormatDefinition {\n return {\n id: 'mp4',\n label: 'MP4 Video',\n mimeType: 'video/mp4',\n extensions: ['.mp4'],\n async exportDoc(input: NormalizedInput, options: ConvertOptions): Promise<ConversionResult> {\n options.signal?.throwIfAborted();\n // The renderer is lazy-loaded below so api.ts can create this registry\n // without an eager ES-module initialization cycle.\n const mp4Opts = (options.formatOptions?.mp4 ?? {}) as Mp4FormatOptions;\n const fps = typeof mp4Opts.fps === 'number' ? mp4Opts.fps : MP4_DEFAULTS.fps;\n const quality =\n typeof mp4Opts.quality === 'string'\n ? (mp4Opts.quality as VideoQuality)\n : MP4_DEFAULTS.quality;\n const orientation =\n typeof mp4Opts.orientation === 'string'\n ? (mp4Opts.orientation as VideoOrientation)\n : MP4_DEFAULTS.orientation;\n const coverPreRoll =\n typeof mp4Opts.coverPreRoll === 'number' ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;\n const animationsEnabled =\n typeof mp4Opts.animationsEnabled === 'boolean'\n ? mp4Opts.animationsEnabled\n : MP4_DEFAULTS.animationsEnabled;\n\n const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString('hex')}.mp4`);\n try {\n const { renderDocToMp4 } = await import('./api.js');\n await renderDocToMp4(input.doc, input.container, {\n outputPath,\n fps,\n quality,\n orientation,\n width: mp4Opts.width,\n height: mp4Opts.height,\n captionStyle: mp4Opts.captionStyle,\n coverPreRoll,\n animationsEnabled,\n signal: options.signal,\n onProgress: mp4Opts.onProgress,\n });\n options.signal?.throwIfAborted();\n const data = await readFile(outputPath, { signal: options.signal });\n options.signal?.throwIfAborted();\n const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n return { bytes, mimeType: 'video/mp4', suggestedFilename: '', warnings: [] };\n } finally {\n await rm(outputPath, { force: true });\n }\n },\n };\n}\n\n/** The CLI-only animated GIF format definition. */\nfunction gifFormat(): FormatDefinition {\n return {\n id: 'gif',\n label: 'Animated GIF',\n mimeType: 'image/gif',\n extensions: ['.gif'],\n async exportDoc(input: NormalizedInput, options: ConvertOptions): Promise<ConversionResult> {\n options.signal?.throwIfAborted();\n const gifOpts = (options.formatOptions?.gif ?? {}) as GifFormatOptions;\n const orientation =\n typeof gifOpts.orientation === 'string'\n ? (gifOpts.orientation as VideoOrientation)\n : GIF_DEFAULTS.orientation;\n const portrait = orientation === 'portrait';\n const defaultWidth = portrait ? GIF_DEFAULTS.height : GIF_DEFAULTS.width;\n const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;\n const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString('hex')}.gif`);\n try {\n const { renderDocToGif } = await import('./api.js');\n const result = await renderDocToGif(input.doc, input.container, {\n outputPath,\n fps: typeof gifOpts.fps === 'number' ? gifOpts.fps : GIF_DEFAULTS.fps,\n orientation,\n width: typeof gifOpts.width === 'number' ? gifOpts.width : defaultWidth,\n height: typeof gifOpts.height === 'number' ? gifOpts.height : defaultHeight,\n captionStyle: gifOpts.captionStyle,\n coverPreRoll:\n typeof gifOpts.coverPreRoll === 'number'\n ? gifOpts.coverPreRoll\n : GIF_DEFAULTS.coverPreRoll,\n animationsEnabled:\n typeof gifOpts.animationsEnabled === 'boolean'\n ? gifOpts.animationsEnabled\n : GIF_DEFAULTS.animationsEnabled,\n loop: typeof gifOpts.loop === 'number' ? gifOpts.loop : GIF_DEFAULTS.loop,\n maxColors:\n typeof gifOpts.maxColors === 'number' ? gifOpts.maxColors : GIF_DEFAULTS.maxColors,\n dither: gifOpts.dither ?? GIF_DEFAULTS.dither,\n bayerScale: gifOpts.bayerScale,\n signal: options.signal,\n onProgress: gifOpts.onProgress,\n });\n options.signal?.throwIfAborted();\n const data = await readFile(outputPath, { signal: options.signal });\n options.signal?.throwIfAborted();\n const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n return {\n bytes,\n mimeType: 'image/gif',\n suggestedFilename: '',\n warnings: result.warnings,\n };\n } finally {\n await rm(outputPath, { force: true });\n }\n },\n };\n}\n\n/**\n * Build the CLI's format registry: every built-in format from\n * `@bendyline/squisq-formats` plus the CLI-only rendered-media exporters.\n */\nexport function createCliRegistry(): FormatRegistry {\n const registry = defaultRegistry();\n registry.register(mp4Format());\n registry.register(gifFormat());\n return registry;\n}\n","/**\n * readInput\n *\n * Unified input reader for the CLI. Accepts a path to a markdown file, a JSON\n * Doc file, a `.zip`/`.dbk` container, a folder, or any importable binary\n * format (`.docx`/`.pptx`/`.pdf`/`.xlsx`/`.csv`/`.html`/`.htm`) and always\n * resolves it to a fully-populated {@link ReadInputResult}: a `Doc`, a\n * `ContentContainer` holding the document + any bundled media, the parsed\n * `MarkdownDocument` when the source was markdown-shaped, and the detected\n * `sourceFormat`.\n *\n * Binary formats are dispatched through the shared format registry's importer\n * (preferring `importContainer`, falling back to `importDoc`); every path\n * derives the `Doc` via `markdownToDoc()` when the source is markdown-shaped.\n */\n\nimport { readFile, readdir, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { parseMarkdown, stringifyMarkdown } from '@bendyline/squisq/markdown';\nimport type { MarkdownDocument } from '@bendyline/squisq/markdown';\nimport { markdownToDoc, resolveAudioMapping } from '@bendyline/squisq/doc';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport { MemoryContentContainer } from '@bendyline/squisq/storage';\nimport { zipToContainer } from '@bendyline/squisq-formats/container';\nimport { defaultRegistry } from '@bendyline/squisq-formats';\nimport type { FormatId } from '@bendyline/squisq-formats';\n\nexport interface ReadInputResult {\n /** The resolved document. Always present. */\n doc: Doc;\n /** Virtual file system holding the document plus any bundled media. */\n container: ContentContainer;\n /**\n * The parsed markdown document. Present when the source was markdown-shaped\n * (`.md`, container/folder markdown, or a binary format imported as\n * markdown); absent for JSON-Doc input.\n */\n markdownDoc?: MarkdownDocument;\n /** Detected source format id (registry id, or `md`/`json`/`dbk`/`folder`). */\n sourceFormat: FormatId;\n}\n\nexport interface ReadInputOptions {\n /** Cancel filesystem traversal, import, or audio resolution at a bounded boundary. */\n signal?: AbortSignal;\n /**\n * Infer a Squisq theme from an OOXML source's theme part (PPTX today).\n * Default true — the importer carries it as frontmatter.\n */\n inferTheme?: boolean;\n /** Derive custom layout templates from PPTX slide layouts. Default true. */\n inferLayouts?: boolean;\n}\n\n/** MIME type lookup by extension (common content types) */\nconst MIME_TYPES: Record<string, string> = {\n '.md': 'text/markdown',\n '.txt': 'text/plain',\n '.json': 'application/json',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.svg': 'image/svg+xml',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.ogg': 'audio/ogg',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n};\n\n/**\n * Binary extensions dispatched through the registry importer. `.md`, `.zip`,\n * `.dbk`, and `.json` are intentionally excluded — they have dedicated paths\n * that also detect bundled media / doc.json.\n */\nconst IMPORTER_EXTS = ['.docx', '.pptx', '.pdf', '.xlsx', '.csv', '.html', '.htm'];\n\nfunction mimeFromExt(filePath: string): string {\n return MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/**\n * Recursively walk a directory and return all file paths (relative to root).\n */\nasync function walkDir(root: string, prefix = '', signal?: AbortSignal): Promise<string[]> {\n throwIfAborted(signal);\n const entries = await readdir(root, { withFileTypes: true });\n throwIfAborted(signal);\n const paths: string[] = [];\n\n for (const entry of entries) {\n throwIfAborted(signal);\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n paths.push(...(await walkDir(join(root, entry.name), relPath, signal)));\n } else if (entry.isFile()) {\n paths.push(relPath);\n }\n }\n\n return paths;\n}\n\n/**\n * Read input from a file path (markdown, JSON Doc, ZIP/DBK container, folder,\n * or an importable binary format) and resolve it to a populated\n * {@link ReadInputResult}.\n */\nexport async function readInput(\n inputPath: string,\n options?: ReadInputOptions,\n): Promise<ReadInputResult> {\n throwIfAborted(options?.signal);\n const result = await readInputRaw(inputPath, options);\n throwIfAborted(options?.signal);\n // Audio rides in the container: a document-anchored narration take\n // (`{[audio src=… anchor=document]}` + timing sidecar) re-times the\n // block timeline, and per-block audio files map into segments — so\n // `squisq convert`/`video` exports pace exactly like the editor preview.\n const doc = await resolveAudioMapping(result.doc, result.container);\n throwIfAborted(options?.signal);\n return doc === result.doc ? result : { ...result, doc };\n}\n\nasync function readInputRaw(\n inputPath: string,\n options?: ReadInputOptions,\n): Promise<ReadInputResult> {\n throwIfAborted(options?.signal);\n const info = await stat(inputPath);\n throwIfAborted(options?.signal);\n\n if (info.isDirectory()) {\n return readFolder(inputPath, options?.signal);\n }\n\n const ext = extname(inputPath).toLowerCase();\n if (ext === '.zip' || ext === '.dbk') {\n return readContainer(inputPath, options?.signal);\n }\n\n if (ext === '.json') {\n return readDocJsonFile(inputPath, options?.signal);\n }\n\n if (IMPORTER_EXTS.includes(ext)) {\n const def = defaultRegistry().byExtension(ext);\n if (def && (def.importContainer || def.importDoc)) {\n return readViaImporter(inputPath, def, options);\n }\n }\n\n // Default: treat as a markdown file\n return readMarkdownFile(inputPath, options?.signal);\n}\n\n/** Read a file into an ArrayBuffer (a fresh copy, not a Buffer view). */\nasync function readArrayBuffer(filePath: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n const data = await readBinaryFile(filePath, signal);\n return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n}\n\nasync function readBinaryFile(filePath: string, signal?: AbortSignal): Promise<Uint8Array> {\n throwIfAborted(signal);\n try {\n const data = await readFile(filePath, { signal });\n throwIfAborted(signal);\n return data;\n } catch (error: unknown) {\n throwIfAborted(signal);\n throw error;\n }\n}\n\nasync function readUtf8File(filePath: string, signal?: AbortSignal): Promise<string> {\n throwIfAborted(signal);\n try {\n const content = await readFile(filePath, { encoding: 'utf-8', signal });\n throwIfAborted(signal);\n return content;\n } catch (error: unknown) {\n throwIfAborted(signal);\n throw error;\n }\n}\n\nasync function readMarkdownFile(filePath: string, signal?: AbortSignal): Promise<ReadInputResult> {\n const content = await readUtf8File(filePath, signal);\n const container = new MemoryContentContainer();\n await container.writeDocument(content);\n throwIfAborted(signal);\n const markdownDoc = parseMarkdown(content);\n return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: 'md' };\n}\n\n/**\n * Read a standalone Doc JSON file. The container is empty (no media bundled);\n * callers should populate it or set basePath for media resolution.\n */\nasync function readDocJsonFile(filePath: string, signal?: AbortSignal): Promise<ReadInputResult> {\n const content = await readUtf8File(filePath, signal);\n const doc = JSON.parse(content) as Doc;\n const container = new MemoryContentContainer();\n return { doc, container, sourceFormat: 'json' };\n}\n\n/**\n * Dispatch an importable binary format (docx/pptx/pdf/…) through the registry\n * importer. Prefers `importContainer` (extracts media alongside the markdown),\n * falling back to `importDoc` wrapped in a fresh container.\n */\nasync function readViaImporter(\n filePath: string,\n def: import('@bendyline/squisq-formats').FormatDefinition,\n options?: ReadInputOptions,\n): Promise<ReadInputResult> {\n const signal = options?.signal;\n throwIfAborted(signal);\n const buffer = await readArrayBuffer(filePath, signal);\n const convertOptions = {\n signal,\n formatOptions: {\n [def.id]: {\n inferTheme: options?.inferTheme !== false,\n inferLayouts: options?.inferLayouts !== false,\n },\n },\n };\n\n let container: ContentContainer;\n let markdownDoc: MarkdownDocument;\n if (def.importContainer) {\n throwIfAborted(signal);\n container = await def.importContainer(buffer, convertOptions);\n throwIfAborted(signal);\n const text = await container.readDocument();\n throwIfAborted(signal);\n markdownDoc = text ? parseMarkdown(text) : { type: 'document', children: [] };\n } else {\n // importDoc is guaranteed present by the caller's guard.\n throwIfAborted(signal);\n markdownDoc = await def.importDoc!(buffer, convertOptions);\n throwIfAborted(signal);\n const mem = new MemoryContentContainer();\n await mem.writeDocument(stringifyMarkdown(markdownDoc));\n throwIfAborted(signal);\n container = mem;\n }\n\n return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };\n}\n\n/**\n * Known filenames for Doc JSON inside containers and folders.\n * Checked in priority order before falling back to markdown discovery.\n */\nconst DOC_JSON_NAMES = ['doc.json', 'story.json'];\n\n/** Resolve a container that may hold either a doc.json or a markdown document. */\nasync function resolveContainer(\n container: ContentContainer,\n sourceFormat: FormatId,\n missingMessage: string,\n signal?: AbortSignal,\n): Promise<ReadInputResult> {\n // Check for Doc JSON first.\n for (const name of DOC_JSON_NAMES) {\n throwIfAborted(signal);\n const jsonData = await container.readFile(name);\n throwIfAborted(signal);\n if (jsonData) {\n const doc = JSON.parse(new TextDecoder().decode(jsonData)) as Doc;\n return { doc, container, sourceFormat };\n }\n }\n\n throwIfAborted(signal);\n const markdown = await container.readDocument();\n throwIfAborted(signal);\n if (!markdown) {\n throw new Error(missingMessage);\n }\n\n const markdownDoc = parseMarkdown(markdown);\n return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };\n}\n\nasync function readContainer(filePath: string, signal?: AbortSignal): Promise<ReadInputResult> {\n throwIfAborted(signal);\n const container = await zipToContainer(await readArrayBuffer(filePath, signal));\n throwIfAborted(signal);\n return resolveContainer(\n container,\n 'dbk',\n `No markdown document or doc.json found in container: ${filePath}`,\n signal,\n );\n}\n\nasync function readFolder(dirPath: string, signal?: AbortSignal): Promise<ReadInputResult> {\n throwIfAborted(signal);\n const container = new MemoryContentContainer();\n const files = await walkDir(dirPath, '', signal);\n throwIfAborted(signal);\n\n for (const relPath of files) {\n throwIfAborted(signal);\n const absPath = join(dirPath, relPath);\n const data = await readBinaryFile(absPath, signal);\n throwIfAborted(signal);\n await container.writeFile(\n relPath,\n new Uint8Array(data.buffer, data.byteOffset, data.byteLength),\n mimeFromExt(relPath),\n );\n throwIfAborted(signal);\n }\n\n return resolveContainer(\n container,\n 'folder',\n `No markdown document or doc.json found in folder: ${dirPath}`,\n signal,\n );\n}\n\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (!signal?.aborted) return;\n if (signal.reason !== undefined) throw signal.reason;\n const error = new Error('Input reading was cancelled');\n error.name = 'AbortError';\n throw error;\n}\n"],"mappings":"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","names":["readFile","rm","join","tmpdir","randomBytes","renderDocToMp4","renderDocToGif","MemoryContentContainer","readFile","join","defaultRegistry","framesToMp4Native","framesToGifNative","rm","join"]}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/util/nativeEncoder.ts
|
|
4
|
-
import { execFile } from "child_process";
|
|
5
4
|
import { writeFile, readFile, mkdir, rm } from "fs/promises";
|
|
6
5
|
import { join } from "path";
|
|
7
6
|
import { tmpdir } from "os";
|
|
@@ -13,6 +12,26 @@ import {
|
|
|
13
12
|
ffmpegAudioMuxArgs,
|
|
14
13
|
ffmpegGifOutputArgs
|
|
15
14
|
} from "@bendyline/squisq-video";
|
|
15
|
+
|
|
16
|
+
// src/util/runFfmpeg.ts
|
|
17
|
+
import { execFile } from "child_process";
|
|
18
|
+
async function runFfmpeg(command, args, options) {
|
|
19
|
+
options.signal?.throwIfAborted();
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
execFile(command, [...args], { timeout: options.timeoutMs, signal: options.signal }, (err) => {
|
|
22
|
+
if (options.signal?.aborted) {
|
|
23
|
+
reject(options.signal.reason);
|
|
24
|
+
} else if (err) {
|
|
25
|
+
reject(new Error(`${options.failureMessage}: ${err.message}`));
|
|
26
|
+
} else {
|
|
27
|
+
resolve();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
options.signal?.throwIfAborted();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/util/nativeEncoder.ts
|
|
16
35
|
var GIF_EXPORT_DEFAULTS = {
|
|
17
36
|
fps: 10,
|
|
18
37
|
width: 960,
|
|
@@ -44,6 +63,7 @@ function resolveGifFps(options) {
|
|
|
44
63
|
return fps;
|
|
45
64
|
}
|
|
46
65
|
async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
|
|
66
|
+
options.signal?.throwIfAborted();
|
|
47
67
|
const fps = options.fps ?? 30;
|
|
48
68
|
const quality = options.quality ?? "normal";
|
|
49
69
|
const { width, height } = resolveDimensions(options);
|
|
@@ -55,19 +75,24 @@ async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options
|
|
|
55
75
|
const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
|
|
56
76
|
await mkdir(workDir, { recursive: true });
|
|
57
77
|
try {
|
|
78
|
+
options.signal?.throwIfAborted();
|
|
58
79
|
onProgress?.(0, "writing frames");
|
|
59
80
|
const padLen = String(frames.length).length;
|
|
60
81
|
for (let i = 0; i < frames.length; i++) {
|
|
82
|
+
options.signal?.throwIfAborted();
|
|
61
83
|
const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
|
|
62
84
|
await writeFile(join(workDir, name), frames[i]);
|
|
85
|
+
options.signal?.throwIfAborted();
|
|
63
86
|
if (onProgress && i % 10 === 0) {
|
|
64
87
|
onProgress(Math.round(i / frames.length * 30), "writing frames");
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
let audioPath = null;
|
|
68
91
|
if (audio) {
|
|
92
|
+
options.signal?.throwIfAborted();
|
|
69
93
|
audioPath = join(workDir, "audio-input");
|
|
70
94
|
await writeFile(audioPath, audio);
|
|
95
|
+
options.signal?.throwIfAborted();
|
|
71
96
|
}
|
|
72
97
|
onProgress?.(30, "encoding");
|
|
73
98
|
const padPattern = join(workDir, `frame-%0${padLen}d.png`);
|
|
@@ -88,32 +113,33 @@ async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options
|
|
|
88
113
|
args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));
|
|
89
114
|
}
|
|
90
115
|
args.push(outputPath);
|
|
91
|
-
await
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} else {
|
|
96
|
-
resolve();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
116
|
+
await runFfmpeg(ffmpegPath, args, {
|
|
117
|
+
timeoutMs: 6e5,
|
|
118
|
+
failureMessage: "ffmpeg failed",
|
|
119
|
+
signal: options.signal
|
|
99
120
|
});
|
|
100
121
|
onProgress?.(100, "done");
|
|
122
|
+
options.signal?.throwIfAborted();
|
|
101
123
|
} finally {
|
|
102
124
|
await rm(workDir, { recursive: true, force: true });
|
|
103
125
|
}
|
|
104
126
|
}
|
|
105
127
|
async function framesToMp4NativeBytes(ffmpegPath, frames, audio, options = {}) {
|
|
128
|
+
options.signal?.throwIfAborted();
|
|
106
129
|
const tmpId = randomBytes(8).toString("hex");
|
|
107
130
|
const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);
|
|
108
131
|
try {
|
|
109
132
|
await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);
|
|
133
|
+
options.signal?.throwIfAborted();
|
|
110
134
|
const data = await readFile(tmpOutput);
|
|
135
|
+
options.signal?.throwIfAborted();
|
|
111
136
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
112
137
|
} finally {
|
|
113
138
|
await rm(tmpOutput, { force: true });
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
142
|
+
options.signal?.throwIfAborted();
|
|
117
143
|
const fps = resolveGifFps(options);
|
|
118
144
|
const { width, height } = resolveGifDimensions(options);
|
|
119
145
|
const onProgress = options.onProgress;
|
|
@@ -132,11 +158,14 @@ async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
|
132
158
|
const workDir = join(tmpdir(), `squisq-gif-${tmpId}`);
|
|
133
159
|
await mkdir(workDir, { recursive: true });
|
|
134
160
|
try {
|
|
161
|
+
options.signal?.throwIfAborted();
|
|
135
162
|
onProgress?.(0, "writing frames");
|
|
136
163
|
const padLen = String(frames.length).length;
|
|
137
164
|
for (let i = 0; i < frames.length; i++) {
|
|
165
|
+
options.signal?.throwIfAborted();
|
|
138
166
|
const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
|
|
139
167
|
await writeFile(join(workDir, name), frames[i]);
|
|
168
|
+
options.signal?.throwIfAborted();
|
|
140
169
|
if (onProgress && i % 10 === 0) {
|
|
141
170
|
onProgress(Math.round(i / frames.length * 30), "writing frames");
|
|
142
171
|
}
|
|
@@ -144,23 +173,26 @@ async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
|
144
173
|
onProgress?.(30, "encoding");
|
|
145
174
|
const padPattern = join(workDir, `frame-%0${padLen}d.png`);
|
|
146
175
|
const args = ["-y", "-framerate", String(fps), "-i", padPattern, ...gifOutputArgs, outputPath];
|
|
147
|
-
await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
176
|
+
await runFfmpeg(ffmpegPath, args, {
|
|
177
|
+
timeoutMs: 6e5,
|
|
178
|
+
failureMessage: "ffmpeg GIF encoding failed",
|
|
179
|
+
signal: options.signal
|
|
152
180
|
});
|
|
153
181
|
onProgress?.(100, "done");
|
|
182
|
+
options.signal?.throwIfAborted();
|
|
154
183
|
} finally {
|
|
155
184
|
await rm(workDir, { recursive: true, force: true });
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
187
|
async function framesToGifNativeBytes(ffmpegPath, frames, options = {}) {
|
|
188
|
+
options.signal?.throwIfAborted();
|
|
159
189
|
const tmpId = randomBytes(8).toString("hex");
|
|
160
190
|
const tmpOutput = join(tmpdir(), `squisq-gif-out-${tmpId}.gif`);
|
|
161
191
|
try {
|
|
162
192
|
await framesToGifNative(ffmpegPath, frames, tmpOutput, options);
|
|
193
|
+
options.signal?.throwIfAborted();
|
|
163
194
|
const data = await readFile(tmpOutput);
|
|
195
|
+
options.signal?.throwIfAborted();
|
|
164
196
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
165
197
|
} finally {
|
|
166
198
|
await rm(tmpOutput, { force: true });
|
|
@@ -168,10 +200,11 @@ async function framesToGifNativeBytes(ffmpegPath, frames, options = {}) {
|
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
export {
|
|
203
|
+
runFfmpeg,
|
|
171
204
|
GIF_EXPORT_DEFAULTS,
|
|
172
205
|
framesToMp4Native,
|
|
173
206
|
framesToMp4NativeBytes,
|
|
174
207
|
framesToGifNative,
|
|
175
208
|
framesToGifNativeBytes
|
|
176
209
|
};
|
|
177
|
-
//# sourceMappingURL=chunk-
|
|
210
|
+
//# sourceMappingURL=chunk-JDHONKFZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/nativeEncoder.ts","../src/util/runFfmpeg.ts"],"sourcesContent":["/**\n * Native FFmpeg Encoder\n *\n * Encodes PNG frames to MP4 or animated GIF using a locally installed ffmpeg\n * binary. Writes frames to a temporary directory, invokes ffmpeg as a child\n * process, and reads the resulting media file.\n *\n * This is the Node fast path used after the CLI detects native FFmpeg. Browser\n * callers use the WebCodecs/ffmpeg.wasm paths in the video packages instead.\n */\n\nimport { writeFile, readFile, mkdir, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomBytes } from 'node:crypto';\n\nimport type { GifDither, VideoExportOptions, VideoOrientation } from '@bendyline/squisq-video';\nimport {\n resolveDimensions,\n ffmpegVideoQualityArgs,\n audioBitrateArg,\n ffmpegAudioMuxArgs,\n ffmpegGifOutputArgs,\n} from '@bendyline/squisq-video';\nimport { runFfmpeg } from './runFfmpeg.js';\n\n/** Node-native video options with cooperative and child-process cancellation. */\nexport interface NativeVideoExportOptions extends VideoExportOptions {\n signal?: AbortSignal;\n}\n\n/** Options for native animated-GIF encoding. */\nexport interface GifExportOptions {\n /** Cancel staging or encoding and terminate the native FFmpeg process. */\n signal?: AbortSignal;\n /** Frames per second (default: 10; GIF timing is centisecond-based). */\n fps?: number;\n /** Output width in pixels (default: 960 landscape / 540 portrait). */\n width?: number;\n /** Output height in pixels (default: 540 landscape / 960 portrait). */\n height?: number;\n /** Viewport orientation used for default dimensions. */\n orientation?: VideoOrientation;\n /** Number of repeats; 0 loops forever and -1 disables looping. */\n loop?: number;\n /** Palette size, between 2 and 256 (default: 256). */\n maxColors?: number;\n /** Palette dithering algorithm (default: sierra2_4a). */\n dither?: GifDither;\n /** Bayer strength when dither is bayer (0-5, default: 3). */\n bayerScale?: number;\n /** Encoding progress callback. */\n onProgress?: (percent: number, phase: string) => void;\n}\n\n/** Compression-friendly defaults for animated GIF output. */\nexport const GIF_EXPORT_DEFAULTS = {\n fps: 10,\n width: 960,\n height: 540,\n loop: 0,\n maxColors: 256,\n dither: 'sierra2_4a' as GifDither,\n} as const;\n\nfunction resolveGifDimensions(options: GifExportOptions): { width: number; height: number } {\n const portrait = options.orientation === 'portrait';\n const defaults = portrait\n ? { width: GIF_EXPORT_DEFAULTS.height, height: GIF_EXPORT_DEFAULTS.width }\n : { width: GIF_EXPORT_DEFAULTS.width, height: GIF_EXPORT_DEFAULTS.height };\n const width = options.width ?? defaults.width;\n const height = options.height ?? defaults.height;\n for (const [label, value] of [\n ['width', width],\n ['height', height],\n ] as const) {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new RangeError(`GIF ${label} must be a positive integer.`);\n }\n }\n return { width, height };\n}\n\nfunction resolveGifFps(options: GifExportOptions): number {\n const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;\n // GIF delays are stored in centiseconds, so rates above 100 cannot be\n // represented without timestamp rounding.\n if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {\n throw new RangeError('GIF FPS must be a finite number between 1 and 100.');\n }\n return fps;\n}\n\n/**\n * Encode frame PNGs to MP4 using native ffmpeg.\n *\n * @param ffmpegPath - Absolute path to the ffmpeg binary\n * @param frames - Array of PNG image bytes (one per frame)\n * @param audio - Optional audio file bytes to mux\n * @param outputPath - Where to write the final MP4\n * @param options - Encoding options (fps, quality, dimensions, progress)\n */\nexport async function framesToMp4Native(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n outputPath: string,\n options: NativeVideoExportOptions = {},\n): Promise<void> {\n options.signal?.throwIfAborted();\n const fps = options.fps ?? 30;\n const quality = options.quality ?? 'normal';\n const { width, height } = resolveDimensions(options);\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n // Create a temp working directory\n const tmpId = randomBytes(8).toString('hex');\n const workDir = join(tmpdir(), `squisq-video-${tmpId}`);\n await mkdir(workDir, { recursive: true });\n\n try {\n options.signal?.throwIfAborted();\n onProgress?.(0, 'writing frames');\n\n // Write frame PNGs to temp directory\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n options.signal?.throwIfAborted();\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await writeFile(join(workDir, name), frames[i]);\n options.signal?.throwIfAborted();\n\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 30), 'writing frames');\n }\n }\n\n // Write audio if provided\n let audioPath: string | null = null;\n if (audio) {\n options.signal?.throwIfAborted();\n audioPath = join(workDir, 'audio-input');\n await writeFile(audioPath, audio);\n options.signal?.throwIfAborted();\n }\n\n onProgress?.(30, 'encoding');\n\n // Build ffmpeg arguments\n const padPattern = join(workDir, `frame-%0${padLen}d.png`);\n const args = ['-y', '-framerate', String(fps), '-i', padPattern];\n\n if (audioPath) {\n args.push('-i', audioPath);\n }\n\n args.push(\n '-c:v',\n 'libx264',\n ...ffmpegVideoQualityArgs(quality),\n '-pix_fmt',\n 'yuv420p',\n '-vf',\n `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`,\n );\n\n if (audioPath) {\n args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));\n }\n\n args.push(outputPath);\n\n // Run ffmpeg. execFile buffers stdout/stderr internally for the callback,\n // so the child's pipes are already drained — no manual stderr listener needed.\n await runFfmpeg(ffmpegPath, args, {\n timeoutMs: 600_000,\n failureMessage: 'ffmpeg failed',\n signal: options.signal,\n });\n\n onProgress?.(100, 'done');\n options.signal?.throwIfAborted();\n } finally {\n // Clean up temp directory\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Encode frames using native ffmpeg and return the MP4 bytes (instead of writing to disk).\n * Useful when the caller needs the bytes in memory.\n */\nexport async function framesToMp4NativeBytes(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n options: NativeVideoExportOptions = {},\n): Promise<Uint8Array> {\n options.signal?.throwIfAborted();\n const tmpId = randomBytes(8).toString('hex');\n const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);\n\n try {\n await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);\n options.signal?.throwIfAborted();\n const data = await readFile(tmpOutput);\n options.signal?.throwIfAborted();\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(tmpOutput, { force: true });\n }\n}\n\n/**\n * Encode PNG frames to an animated GIF using native FFmpeg.\n *\n * The shared palette graph uses changed-frame statistics and rectangle-scoped\n * palette application so static slide regions remain stable and compress well.\n * GIF has no audio track; callers should surface that limitation before calling.\n */\nexport async function framesToGifNative(\n ffmpegPath: string,\n frames: Uint8Array[],\n outputPath: string,\n options: GifExportOptions = {},\n): Promise<void> {\n options.signal?.throwIfAborted();\n const fps = resolveGifFps(options);\n const { width, height } = resolveGifDimensions(options);\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n const gifOutputArgs = ffmpegGifOutputArgs({\n width,\n height,\n loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,\n maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,\n dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,\n bayerScale: options.bayerScale,\n });\n\n const tmpId = randomBytes(8).toString('hex');\n const workDir = join(tmpdir(), `squisq-gif-${tmpId}`);\n await mkdir(workDir, { recursive: true });\n\n try {\n options.signal?.throwIfAborted();\n onProgress?.(0, 'writing frames');\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n options.signal?.throwIfAborted();\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await writeFile(join(workDir, name), frames[i]);\n options.signal?.throwIfAborted();\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 30), 'writing frames');\n }\n }\n\n onProgress?.(30, 'encoding');\n const padPattern = join(workDir, `frame-%0${padLen}d.png`);\n const args = ['-y', '-framerate', String(fps), '-i', padPattern, ...gifOutputArgs, outputPath];\n\n await runFfmpeg(ffmpegPath, args, {\n timeoutMs: 600_000,\n failureMessage: 'ffmpeg GIF encoding failed',\n signal: options.signal,\n });\n onProgress?.(100, 'done');\n options.signal?.throwIfAborted();\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\n/** Encode frames with native FFmpeg and return animated-GIF bytes. */\nexport async function framesToGifNativeBytes(\n ffmpegPath: string,\n frames: Uint8Array[],\n options: GifExportOptions = {},\n): Promise<Uint8Array> {\n options.signal?.throwIfAborted();\n const tmpId = randomBytes(8).toString('hex');\n const tmpOutput = join(tmpdir(), `squisq-gif-out-${tmpId}.gif`);\n try {\n await framesToGifNative(ffmpegPath, frames, tmpOutput, options);\n options.signal?.throwIfAborted();\n const data = await readFile(tmpOutput);\n options.signal?.throwIfAborted();\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(tmpOutput, { force: true });\n }\n}\n","import { execFile } from 'node:child_process';\n\nexport interface RunFfmpegOptions {\n timeoutMs: number;\n failureMessage: string;\n signal?: AbortSignal;\n}\n\n/** Run one bounded FFmpeg-style child process while preserving caller cancellation. */\nexport async function runFfmpeg(\n command: string,\n args: readonly string[],\n options: RunFfmpegOptions,\n): Promise<void> {\n options.signal?.throwIfAborted();\n await new Promise<void>((resolve, reject) => {\n execFile(command, [...args], { timeout: options.timeoutMs, signal: options.signal }, (err) => {\n if (options.signal?.aborted) {\n reject(options.signal.reason);\n } else if (err) {\n reject(new Error(`${options.failureMessage}: ${err.message}`));\n } else {\n resolve();\n }\n });\n });\n options.signal?.throwIfAborted();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/util/nativeEncoder.ts
|
|
2
|
-
import { execFile } from "child_process";
|
|
3
2
|
import { writeFile, readFile, mkdir, rm } from "fs/promises";
|
|
4
3
|
import { join } from "path";
|
|
5
4
|
import { tmpdir } from "os";
|
|
@@ -11,6 +10,26 @@ import {
|
|
|
11
10
|
ffmpegAudioMuxArgs,
|
|
12
11
|
ffmpegGifOutputArgs
|
|
13
12
|
} from "@bendyline/squisq-video";
|
|
13
|
+
|
|
14
|
+
// src/util/runFfmpeg.ts
|
|
15
|
+
import { execFile } from "child_process";
|
|
16
|
+
async function runFfmpeg(command, args, options) {
|
|
17
|
+
options.signal?.throwIfAborted();
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
execFile(command, [...args], { timeout: options.timeoutMs, signal: options.signal }, (err) => {
|
|
20
|
+
if (options.signal?.aborted) {
|
|
21
|
+
reject(options.signal.reason);
|
|
22
|
+
} else if (err) {
|
|
23
|
+
reject(new Error(`${options.failureMessage}: ${err.message}`));
|
|
24
|
+
} else {
|
|
25
|
+
resolve();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
options.signal?.throwIfAborted();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/util/nativeEncoder.ts
|
|
14
33
|
var GIF_EXPORT_DEFAULTS = {
|
|
15
34
|
fps: 10,
|
|
16
35
|
width: 960,
|
|
@@ -42,6 +61,7 @@ function resolveGifFps(options) {
|
|
|
42
61
|
return fps;
|
|
43
62
|
}
|
|
44
63
|
async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
|
|
64
|
+
options.signal?.throwIfAborted();
|
|
45
65
|
const fps = options.fps ?? 30;
|
|
46
66
|
const quality = options.quality ?? "normal";
|
|
47
67
|
const { width, height } = resolveDimensions(options);
|
|
@@ -53,19 +73,24 @@ async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options
|
|
|
53
73
|
const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
|
|
54
74
|
await mkdir(workDir, { recursive: true });
|
|
55
75
|
try {
|
|
76
|
+
options.signal?.throwIfAborted();
|
|
56
77
|
onProgress?.(0, "writing frames");
|
|
57
78
|
const padLen = String(frames.length).length;
|
|
58
79
|
for (let i = 0; i < frames.length; i++) {
|
|
80
|
+
options.signal?.throwIfAborted();
|
|
59
81
|
const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
|
|
60
82
|
await writeFile(join(workDir, name), frames[i]);
|
|
83
|
+
options.signal?.throwIfAborted();
|
|
61
84
|
if (onProgress && i % 10 === 0) {
|
|
62
85
|
onProgress(Math.round(i / frames.length * 30), "writing frames");
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
88
|
let audioPath = null;
|
|
66
89
|
if (audio) {
|
|
90
|
+
options.signal?.throwIfAborted();
|
|
67
91
|
audioPath = join(workDir, "audio-input");
|
|
68
92
|
await writeFile(audioPath, audio);
|
|
93
|
+
options.signal?.throwIfAborted();
|
|
69
94
|
}
|
|
70
95
|
onProgress?.(30, "encoding");
|
|
71
96
|
const padPattern = join(workDir, `frame-%0${padLen}d.png`);
|
|
@@ -86,32 +111,33 @@ async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options
|
|
|
86
111
|
args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));
|
|
87
112
|
}
|
|
88
113
|
args.push(outputPath);
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} else {
|
|
94
|
-
resolve();
|
|
95
|
-
}
|
|
96
|
-
});
|
|
114
|
+
await runFfmpeg(ffmpegPath, args, {
|
|
115
|
+
timeoutMs: 6e5,
|
|
116
|
+
failureMessage: "ffmpeg failed",
|
|
117
|
+
signal: options.signal
|
|
97
118
|
});
|
|
98
119
|
onProgress?.(100, "done");
|
|
120
|
+
options.signal?.throwIfAborted();
|
|
99
121
|
} finally {
|
|
100
122
|
await rm(workDir, { recursive: true, force: true });
|
|
101
123
|
}
|
|
102
124
|
}
|
|
103
125
|
async function framesToMp4NativeBytes(ffmpegPath, frames, audio, options = {}) {
|
|
126
|
+
options.signal?.throwIfAborted();
|
|
104
127
|
const tmpId = randomBytes(8).toString("hex");
|
|
105
128
|
const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);
|
|
106
129
|
try {
|
|
107
130
|
await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);
|
|
131
|
+
options.signal?.throwIfAborted();
|
|
108
132
|
const data = await readFile(tmpOutput);
|
|
133
|
+
options.signal?.throwIfAborted();
|
|
109
134
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
110
135
|
} finally {
|
|
111
136
|
await rm(tmpOutput, { force: true });
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
139
|
async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
140
|
+
options.signal?.throwIfAborted();
|
|
115
141
|
const fps = resolveGifFps(options);
|
|
116
142
|
const { width, height } = resolveGifDimensions(options);
|
|
117
143
|
const onProgress = options.onProgress;
|
|
@@ -130,11 +156,14 @@ async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
|
130
156
|
const workDir = join(tmpdir(), `squisq-gif-${tmpId}`);
|
|
131
157
|
await mkdir(workDir, { recursive: true });
|
|
132
158
|
try {
|
|
159
|
+
options.signal?.throwIfAborted();
|
|
133
160
|
onProgress?.(0, "writing frames");
|
|
134
161
|
const padLen = String(frames.length).length;
|
|
135
162
|
for (let i = 0; i < frames.length; i++) {
|
|
163
|
+
options.signal?.throwIfAborted();
|
|
136
164
|
const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
|
|
137
165
|
await writeFile(join(workDir, name), frames[i]);
|
|
166
|
+
options.signal?.throwIfAborted();
|
|
138
167
|
if (onProgress && i % 10 === 0) {
|
|
139
168
|
onProgress(Math.round(i / frames.length * 30), "writing frames");
|
|
140
169
|
}
|
|
@@ -142,23 +171,26 @@ async function framesToGifNative(ffmpegPath, frames, outputPath, options = {}) {
|
|
|
142
171
|
onProgress?.(30, "encoding");
|
|
143
172
|
const padPattern = join(workDir, `frame-%0${padLen}d.png`);
|
|
144
173
|
const args = ["-y", "-framerate", String(fps), "-i", padPattern, ...gifOutputArgs, outputPath];
|
|
145
|
-
await
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
174
|
+
await runFfmpeg(ffmpegPath, args, {
|
|
175
|
+
timeoutMs: 6e5,
|
|
176
|
+
failureMessage: "ffmpeg GIF encoding failed",
|
|
177
|
+
signal: options.signal
|
|
150
178
|
});
|
|
151
179
|
onProgress?.(100, "done");
|
|
180
|
+
options.signal?.throwIfAborted();
|
|
152
181
|
} finally {
|
|
153
182
|
await rm(workDir, { recursive: true, force: true });
|
|
154
183
|
}
|
|
155
184
|
}
|
|
156
185
|
async function framesToGifNativeBytes(ffmpegPath, frames, options = {}) {
|
|
186
|
+
options.signal?.throwIfAborted();
|
|
157
187
|
const tmpId = randomBytes(8).toString("hex");
|
|
158
188
|
const tmpOutput = join(tmpdir(), `squisq-gif-out-${tmpId}.gif`);
|
|
159
189
|
try {
|
|
160
190
|
await framesToGifNative(ffmpegPath, frames, tmpOutput, options);
|
|
191
|
+
options.signal?.throwIfAborted();
|
|
161
192
|
const data = await readFile(tmpOutput);
|
|
193
|
+
options.signal?.throwIfAborted();
|
|
162
194
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
163
195
|
} finally {
|
|
164
196
|
await rm(tmpOutput, { force: true });
|
|
@@ -166,10 +198,11 @@ async function framesToGifNativeBytes(ffmpegPath, frames, options = {}) {
|
|
|
166
198
|
}
|
|
167
199
|
|
|
168
200
|
export {
|
|
201
|
+
runFfmpeg,
|
|
169
202
|
GIF_EXPORT_DEFAULTS,
|
|
170
203
|
framesToMp4Native,
|
|
171
204
|
framesToMp4NativeBytes,
|
|
172
205
|
framesToGifNative,
|
|
173
206
|
framesToGifNativeBytes
|
|
174
207
|
};
|
|
175
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-OEN2RADG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/nativeEncoder.ts","../src/util/runFfmpeg.ts"],"sourcesContent":["/**\n * Native FFmpeg Encoder\n *\n * Encodes PNG frames to MP4 or animated GIF using a locally installed ffmpeg\n * binary. Writes frames to a temporary directory, invokes ffmpeg as a child\n * process, and reads the resulting media file.\n *\n * This is the Node fast path used after the CLI detects native FFmpeg. Browser\n * callers use the WebCodecs/ffmpeg.wasm paths in the video packages instead.\n */\n\nimport { writeFile, readFile, mkdir, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomBytes } from 'node:crypto';\n\nimport type { GifDither, VideoExportOptions, VideoOrientation } from '@bendyline/squisq-video';\nimport {\n resolveDimensions,\n ffmpegVideoQualityArgs,\n audioBitrateArg,\n ffmpegAudioMuxArgs,\n ffmpegGifOutputArgs,\n} from '@bendyline/squisq-video';\nimport { runFfmpeg } from './runFfmpeg.js';\n\n/** Node-native video options with cooperative and child-process cancellation. */\nexport interface NativeVideoExportOptions extends VideoExportOptions {\n signal?: AbortSignal;\n}\n\n/** Options for native animated-GIF encoding. */\nexport interface GifExportOptions {\n /** Cancel staging or encoding and terminate the native FFmpeg process. */\n signal?: AbortSignal;\n /** Frames per second (default: 10; GIF timing is centisecond-based). */\n fps?: number;\n /** Output width in pixels (default: 960 landscape / 540 portrait). */\n width?: number;\n /** Output height in pixels (default: 540 landscape / 960 portrait). */\n height?: number;\n /** Viewport orientation used for default dimensions. */\n orientation?: VideoOrientation;\n /** Number of repeats; 0 loops forever and -1 disables looping. */\n loop?: number;\n /** Palette size, between 2 and 256 (default: 256). */\n maxColors?: number;\n /** Palette dithering algorithm (default: sierra2_4a). */\n dither?: GifDither;\n /** Bayer strength when dither is bayer (0-5, default: 3). */\n bayerScale?: number;\n /** Encoding progress callback. */\n onProgress?: (percent: number, phase: string) => void;\n}\n\n/** Compression-friendly defaults for animated GIF output. */\nexport const GIF_EXPORT_DEFAULTS = {\n fps: 10,\n width: 960,\n height: 540,\n loop: 0,\n maxColors: 256,\n dither: 'sierra2_4a' as GifDither,\n} as const;\n\nfunction resolveGifDimensions(options: GifExportOptions): { width: number; height: number } {\n const portrait = options.orientation === 'portrait';\n const defaults = portrait\n ? { width: GIF_EXPORT_DEFAULTS.height, height: GIF_EXPORT_DEFAULTS.width }\n : { width: GIF_EXPORT_DEFAULTS.width, height: GIF_EXPORT_DEFAULTS.height };\n const width = options.width ?? defaults.width;\n const height = options.height ?? defaults.height;\n for (const [label, value] of [\n ['width', width],\n ['height', height],\n ] as const) {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new RangeError(`GIF ${label} must be a positive integer.`);\n }\n }\n return { width, height };\n}\n\nfunction resolveGifFps(options: GifExportOptions): number {\n const fps = options.fps ?? GIF_EXPORT_DEFAULTS.fps;\n // GIF delays are stored in centiseconds, so rates above 100 cannot be\n // represented without timestamp rounding.\n if (!Number.isFinite(fps) || fps <= 0 || fps > 100) {\n throw new RangeError('GIF FPS must be a finite number between 1 and 100.');\n }\n return fps;\n}\n\n/**\n * Encode frame PNGs to MP4 using native ffmpeg.\n *\n * @param ffmpegPath - Absolute path to the ffmpeg binary\n * @param frames - Array of PNG image bytes (one per frame)\n * @param audio - Optional audio file bytes to mux\n * @param outputPath - Where to write the final MP4\n * @param options - Encoding options (fps, quality, dimensions, progress)\n */\nexport async function framesToMp4Native(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n outputPath: string,\n options: NativeVideoExportOptions = {},\n): Promise<void> {\n options.signal?.throwIfAborted();\n const fps = options.fps ?? 30;\n const quality = options.quality ?? 'normal';\n const { width, height } = resolveDimensions(options);\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n // Create a temp working directory\n const tmpId = randomBytes(8).toString('hex');\n const workDir = join(tmpdir(), `squisq-video-${tmpId}`);\n await mkdir(workDir, { recursive: true });\n\n try {\n options.signal?.throwIfAborted();\n onProgress?.(0, 'writing frames');\n\n // Write frame PNGs to temp directory\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n options.signal?.throwIfAborted();\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await writeFile(join(workDir, name), frames[i]);\n options.signal?.throwIfAborted();\n\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 30), 'writing frames');\n }\n }\n\n // Write audio if provided\n let audioPath: string | null = null;\n if (audio) {\n options.signal?.throwIfAborted();\n audioPath = join(workDir, 'audio-input');\n await writeFile(audioPath, audio);\n options.signal?.throwIfAborted();\n }\n\n onProgress?.(30, 'encoding');\n\n // Build ffmpeg arguments\n const padPattern = join(workDir, `frame-%0${padLen}d.png`);\n const args = ['-y', '-framerate', String(fps), '-i', padPattern];\n\n if (audioPath) {\n args.push('-i', audioPath);\n }\n\n args.push(\n '-c:v',\n 'libx264',\n ...ffmpegVideoQualityArgs(quality),\n '-pix_fmt',\n 'yuv420p',\n '-vf',\n `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`,\n );\n\n if (audioPath) {\n args.push(...ffmpegAudioMuxArgs(audioBitrateArg(quality)));\n }\n\n args.push(outputPath);\n\n // Run ffmpeg. execFile buffers stdout/stderr internally for the callback,\n // so the child's pipes are already drained — no manual stderr listener needed.\n await runFfmpeg(ffmpegPath, args, {\n timeoutMs: 600_000,\n failureMessage: 'ffmpeg failed',\n signal: options.signal,\n });\n\n onProgress?.(100, 'done');\n options.signal?.throwIfAborted();\n } finally {\n // Clean up temp directory\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Encode frames using native ffmpeg and return the MP4 bytes (instead of writing to disk).\n * Useful when the caller needs the bytes in memory.\n */\nexport async function framesToMp4NativeBytes(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n options: NativeVideoExportOptions = {},\n): Promise<Uint8Array> {\n options.signal?.throwIfAborted();\n const tmpId = randomBytes(8).toString('hex');\n const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);\n\n try {\n await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);\n options.signal?.throwIfAborted();\n const data = await readFile(tmpOutput);\n options.signal?.throwIfAborted();\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(tmpOutput, { force: true });\n }\n}\n\n/**\n * Encode PNG frames to an animated GIF using native FFmpeg.\n *\n * The shared palette graph uses changed-frame statistics and rectangle-scoped\n * palette application so static slide regions remain stable and compress well.\n * GIF has no audio track; callers should surface that limitation before calling.\n */\nexport async function framesToGifNative(\n ffmpegPath: string,\n frames: Uint8Array[],\n outputPath: string,\n options: GifExportOptions = {},\n): Promise<void> {\n options.signal?.throwIfAborted();\n const fps = resolveGifFps(options);\n const { width, height } = resolveGifDimensions(options);\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n const gifOutputArgs = ffmpegGifOutputArgs({\n width,\n height,\n loop: options.loop ?? GIF_EXPORT_DEFAULTS.loop,\n maxColors: options.maxColors ?? GIF_EXPORT_DEFAULTS.maxColors,\n dither: options.dither ?? GIF_EXPORT_DEFAULTS.dither,\n bayerScale: options.bayerScale,\n });\n\n const tmpId = randomBytes(8).toString('hex');\n const workDir = join(tmpdir(), `squisq-gif-${tmpId}`);\n await mkdir(workDir, { recursive: true });\n\n try {\n options.signal?.throwIfAborted();\n onProgress?.(0, 'writing frames');\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n options.signal?.throwIfAborted();\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await writeFile(join(workDir, name), frames[i]);\n options.signal?.throwIfAborted();\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 30), 'writing frames');\n }\n }\n\n onProgress?.(30, 'encoding');\n const padPattern = join(workDir, `frame-%0${padLen}d.png`);\n const args = ['-y', '-framerate', String(fps), '-i', padPattern, ...gifOutputArgs, outputPath];\n\n await runFfmpeg(ffmpegPath, args, {\n timeoutMs: 600_000,\n failureMessage: 'ffmpeg GIF encoding failed',\n signal: options.signal,\n });\n onProgress?.(100, 'done');\n options.signal?.throwIfAborted();\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\n/** Encode frames with native FFmpeg and return animated-GIF bytes. */\nexport async function framesToGifNativeBytes(\n ffmpegPath: string,\n frames: Uint8Array[],\n options: GifExportOptions = {},\n): Promise<Uint8Array> {\n options.signal?.throwIfAborted();\n const tmpId = randomBytes(8).toString('hex');\n const tmpOutput = join(tmpdir(), `squisq-gif-out-${tmpId}.gif`);\n try {\n await framesToGifNative(ffmpegPath, frames, tmpOutput, options);\n options.signal?.throwIfAborted();\n const data = await readFile(tmpOutput);\n options.signal?.throwIfAborted();\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(tmpOutput, { force: true });\n }\n}\n","import { execFile } from 'node:child_process';\n\nexport interface RunFfmpegOptions {\n timeoutMs: number;\n failureMessage: string;\n signal?: AbortSignal;\n}\n\n/** Run one bounded FFmpeg-style child process while preserving caller cancellation. */\nexport async function runFfmpeg(\n command: string,\n args: readonly string[],\n options: RunFfmpegOptions,\n): Promise<void> {\n options.signal?.throwIfAborted();\n await new Promise<void>((resolve, reject) => {\n execFile(command, [...args], { timeout: options.timeoutMs, signal: options.signal }, (err) => {\n if (options.signal?.aborted) {\n reject(options.signal.reason);\n } else if (err) {\n reject(new Error(`${options.failureMessage}: ${err.message}`));\n } else {\n resolve();\n }\n });\n });\n options.signal?.throwIfAborted();\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED