@bendyline/squisq-video-react 2.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/COPYING.GPL-2.0.txt +339 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +39 -0
  4. package/README.md +34 -7
  5. package/dist/chunk-KQG6DZ7G.js +549 -0
  6. package/dist/chunk-VI5AIVOQ.js +1951 -0
  7. package/dist/chunk-VILZP3GL.js +856 -0
  8. package/dist/chunk-YZN7D4IC.js +300 -0
  9. package/dist/components/index.d.ts +90 -0
  10. package/dist/components/index.js +11 -0
  11. package/dist/encoder/index.d.ts +95 -0
  12. package/dist/encoder/index.js +13 -0
  13. package/dist/hooks/index.d.ts +32 -0
  14. package/dist/hooks/index.js +10 -0
  15. package/dist/index.d.ts +8 -277
  16. package/dist/index.js +12 -1503
  17. package/dist/useVideoExport-NtqZVgaz.d.ts +115 -0
  18. package/dist/workers/encode.worker.js +37 -20
  19. package/dist/workers/ffmpeg.class-worker.js +0 -1
  20. package/package.json +26 -8
  21. package/dist/chunk-6DKLHXX3.js +0 -47
  22. package/dist/chunk-6DKLHXX3.js.map +0 -1
  23. package/dist/index.js.map +0 -1
  24. package/dist/workers/encode.worker.js.map +0 -1
  25. package/dist/workers/ffmpeg.class-worker.js.map +0 -1
  26. package/src/VideoExportButton.tsx +0 -87
  27. package/src/VideoExportModal.tsx +0 -553
  28. package/src/__tests__/audioTrack.test.ts +0 -108
  29. package/src/__tests__/gifTranscode.test.ts +0 -118
  30. package/src/__tests__/mp4MuxAudio.test.ts +0 -72
  31. package/src/__tests__/useVideoExportGif.test.ts +0 -155
  32. package/src/__tests__/videoExportProps.test.tsx +0 -148
  33. package/src/__tests__/workerEncoder.test.ts +0 -143
  34. package/src/audioTrack.ts +0 -332
  35. package/src/gifTranscode.ts +0 -75
  36. package/src/hooks/useFrameCapture.ts +0 -268
  37. package/src/hooks/useVideoExport.ts +0 -647
  38. package/src/index.ts +0 -40
  39. package/src/mainThreadEncoder.ts +0 -158
  40. package/src/mp4Mux.ts +0 -123
  41. package/src/workerEncoder.ts +0 -175
  42. package/src/workers/encode.worker.ts +0 -439
  43. package/src/workers/ffmpeg.class-worker.ts +0 -11
  44. package/src/workers/workerTypes.ts +0 -89
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/VideoExportModal.tsx","../src/hooks/useVideoExport.ts","../src/mainThreadEncoder.ts","../src/workerEncoder.ts","../src/audioTrack.ts","../src/gifTranscode.ts","../src/hooks/useFrameCapture.ts","../src/VideoExportButton.tsx"],"sourcesContent":["/**\n * VideoExportModal — Modal dialog for configuring and monitoring video export.\n *\n * States:\n * configure → exporting (capturing + encoding) → complete | error\n *\n * Hosts may supply a resolved color scheme so this portaled dialog matches\n * the surface that opened it.\n */\n\nimport { useState, useCallback } from 'react';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { MediaProvider } from '@bendyline/squisq/schemas';\nimport type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\nimport type { CaptionMode } from '@bendyline/squisq-react';\nimport {\n useVideoExport,\n type VideoExportConfig,\n type VideoOutputFormat,\n} from './hooks/useVideoExport.js';\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface VideoExportModalProps {\n /** The document to export */\n doc: Doc;\n /**\n * Player IIFE bundle source. Unused by the browser export path (frames\n * are captured from a live in-page DocPlayer); only forwarded for\n * CLI/Playwright-style pipelines that render standalone HTML.\n */\n playerScript?: string;\n /** Optional media provider for resolving images/audio */\n mediaProvider?: MediaProvider;\n /** Pre-collected images map (alternative to mediaProvider) */\n images?: Map<string, ArrayBuffer>;\n /** Pre-collected audio map */\n audio?: Map<string, ArrayBuffer>;\n /**\n * Seeds the modal's initial format, motion, quality, FPS, orientation, and\n * caption selections. It is merged (as a base) into the config passed to the\n * export hook, so a host can share one config shape with `useVideoExport`.\n * The individual `images`/`audio`/`mediaProvider`/`playerScript` props still\n * take precedence over any matching key here.\n */\n defaultConfig?: Partial<VideoExportConfig>;\n /** Visual color scheme for the portaled dialog. Defaults to light. */\n colorScheme?: 'light' | 'dark';\n /** Called when the modal should close */\n onClose: () => void;\n}\n\n// ── Helpers ────────────────────────────────────────────────────\n\nfunction formatDuration(seconds: number): string {\n if (seconds < 60) return `${seconds}s`;\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${m}m ${s}s`;\n}\n\nfunction encoderLabel(\n outputFormat: VideoOutputFormat,\n backend: 'webcodecs' | 'ffmpeg-wasm',\n): string {\n if (outputFormat === 'gif') {\n return backend === 'webcodecs'\n ? 'WebCodecs (H.264) → ffmpeg.wasm (GIF)'\n : 'ffmpeg.wasm (H.264 → GIF)';\n }\n return backend === 'webcodecs' ? 'WebCodecs (H.264)' : 'ffmpeg.wasm (H.264)';\n}\n\n// ── Styles ─────────────────────────────────────────────────────────\n\ninterface VideoExportPalette {\n overlay: string;\n surface: string;\n control: string;\n border: string;\n text: string;\n heading: string;\n label: string;\n muted: string;\n secondary: string;\n primary: string;\n primaryBorder: string;\n success: string;\n danger: string;\n}\n\nconst VIDEO_EXPORT_PALETTES: Record<'light' | 'dark', VideoExportPalette> = {\n light: {\n overlay: 'rgba(0, 0, 0, 0.5)',\n surface: '#FFFDF7',\n control: '#ffffff',\n border: '#c9b98a',\n text: '#4a3c1f',\n heading: '#2d2310',\n label: '#5a4a2a',\n muted: '#8a7a5a',\n secondary: '#E8DFC6',\n primary: '#8B6914',\n primaryBorder: '#7a5c10',\n success: '#2d6a10',\n danger: '#a03020',\n },\n dark: {\n overlay: 'rgba(2, 6, 23, 0.72)',\n surface: '#111827',\n control: '#0f172a',\n border: '#475569',\n text: '#e5e7eb',\n heading: '#f8fafc',\n label: '#cbd5e1',\n muted: '#94a3b8',\n secondary: '#1e293b',\n primary: '#9a7416',\n primaryBorder: '#d1a73b',\n success: '#86efac',\n danger: '#fca5a5',\n },\n};\n\nconst overlayStyle: React.CSSProperties = {\n position: 'fixed',\n inset: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 10000,\n};\n\nconst modalStyle: React.CSSProperties = {\n borderRadius: 0,\n padding: '24px 28px',\n minWidth: 380,\n maxWidth: 480,\n boxShadow: '0 8px 32px rgba(0,0,0,0.18)',\n fontFamily: 'system-ui, -apple-system, sans-serif',\n};\n\nconst titleStyle: React.CSSProperties = {\n margin: '0 0 16px 0',\n fontSize: 18,\n fontWeight: 600,\n};\n\nconst labelStyle: React.CSSProperties = {\n display: 'block',\n fontSize: 13,\n fontWeight: 500,\n marginBottom: 4,\n};\n\nconst selectStyle: React.CSSProperties = {\n width: '100%',\n padding: '6px 8px',\n fontSize: 13,\n fontFamily: 'inherit',\n borderRadius: 0,\n marginBottom: 12,\n};\n\nconst btnPrimary: React.CSSProperties = {\n padding: '8px 20px',\n fontSize: 14,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n color: '#fff',\n borderRadius: 0,\n};\n\nconst btnSecondary: React.CSSProperties = {\n padding: '8px 20px',\n fontSize: 14,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n borderRadius: 0,\n};\n\nconst progressBarOuterStyle: React.CSSProperties = {\n width: '100%',\n height: 8,\n borderRadius: 0,\n overflow: 'hidden',\n marginBottom: 8,\n};\n\nconst footerStyle: React.CSSProperties = {\n display: 'flex',\n justifyContent: 'flex-end',\n gap: 8,\n marginTop: 20,\n};\n\n// ── Component ──────────────────────────────────────────────────────\n\nexport function VideoExportModal({\n doc,\n playerScript,\n mediaProvider,\n images,\n audio,\n defaultConfig,\n colorScheme = 'light',\n onClose,\n}: VideoExportModalProps) {\n const initialOutputFormat = defaultConfig?.outputFormat ?? 'mp4';\n const [outputFormat, setOutputFormat] = useState<VideoOutputFormat>(initialOutputFormat);\n const [quality, setQuality] = useState<VideoQuality>(defaultConfig?.quality ?? 'normal');\n const [fps, setFps] = useState(defaultConfig?.fps ?? (initialOutputFormat === 'gif' ? 10 : 24));\n const [orientation, setOrientation] = useState<VideoOrientation>(\n defaultConfig?.orientation ?? 'landscape',\n );\n const [captionMode, setCaptionMode] = useState<CaptionMode>(defaultConfig?.captionMode ?? 'off');\n const [animationsEnabled, setAnimationsEnabled] = useState(\n defaultConfig?.animationsEnabled ?? initialOutputFormat === 'mp4',\n );\n const palette = VIDEO_EXPORT_PALETTES[colorScheme];\n const themedModalStyle: React.CSSProperties = {\n ...modalStyle,\n background: palette.surface,\n border: `1px solid ${palette.border}`,\n color: palette.text,\n colorScheme,\n };\n const themedTitleStyle: React.CSSProperties = { ...titleStyle, color: palette.heading };\n const themedLabelStyle: React.CSSProperties = { ...labelStyle, color: palette.label };\n const themedSelectStyle: React.CSSProperties = {\n ...selectStyle,\n border: `1px solid ${palette.border}`,\n background: palette.control,\n color: palette.text,\n colorScheme,\n };\n const themedPrimaryButtonStyle: React.CSSProperties = {\n ...btnPrimary,\n background: palette.primary,\n border: `1px solid ${palette.primaryBorder}`,\n };\n const themedSecondaryButtonStyle: React.CSSProperties = {\n ...btnSecondary,\n background: palette.secondary,\n color: palette.text,\n border: `1px solid ${palette.border}`,\n };\n\n const exportHook = useVideoExport();\n const {\n state,\n progress,\n backend,\n outputFormat: completedOutputFormat,\n downloadUrl,\n fileSize,\n audioIncluded,\n audioSkippedReason,\n error,\n elapsed,\n estimatedRemaining,\n startExport,\n cancel: cancelExport,\n reset: resetExport,\n } = exportHook;\n\n const handleOutputFormatChange = useCallback((next: VideoOutputFormat) => {\n setOutputFormat(next);\n if (next === 'gif') {\n setFps(10);\n setAnimationsEnabled(false);\n } else {\n setFps(24);\n setAnimationsEnabled(true);\n }\n }, []);\n\n const handleExport = useCallback(async () => {\n const config: VideoExportConfig = {\n // defaultConfig is the base; explicit props/selections win over it.\n ...defaultConfig,\n outputFormat,\n animationsEnabled,\n quality,\n fps,\n orientation,\n captionMode,\n images,\n audio,\n mediaProvider,\n // Only thread the bundle through when the host actually supplied one.\n ...(playerScript !== undefined ? { playerScript } : {}),\n };\n await startExport(doc, config);\n }, [\n doc,\n outputFormat,\n animationsEnabled,\n quality,\n fps,\n orientation,\n captionMode,\n images,\n audio,\n mediaProvider,\n playerScript,\n defaultConfig,\n startExport,\n ]);\n\n const handleDownload = useCallback(() => {\n if (!downloadUrl) return;\n const a = document.createElement('a');\n a.href = downloadUrl;\n const ts = new Date().toISOString().slice(0, 10);\n a.download = `document-${ts}.${completedOutputFormat}`;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }, [downloadUrl, completedOutputFormat]);\n\n const handleClose = useCallback(() => {\n if (state === 'capturing' || state === 'encoding' || state === 'preparing') {\n cancelExport();\n }\n resetExport();\n onClose();\n }, [state, cancelExport, resetExport, onClose]);\n\n const isExporting = state === 'preparing' || state === 'capturing' || state === 'encoding';\n\n return (\n <div\n style={{ ...overlayStyle, background: palette.overlay }}\n data-color-scheme={colorScheme}\n onClick={handleClose}\n >\n <div style={themedModalStyle} onClick={(e) => e.stopPropagation()}>\n <h2 style={themedTitleStyle}>\n {outputFormat === 'gif' ? 'Export Animated GIF' : 'Export Video'}\n </h2>\n\n {/* ── Configure State ── */}\n {state === 'idle' && (\n <>\n <div>\n <label style={themedLabelStyle}>Format</label>\n <select\n aria-label=\"Format\"\n style={themedSelectStyle}\n value={outputFormat}\n onChange={(e) => handleOutputFormatChange(e.target.value as VideoOutputFormat)}\n >\n <option value=\"mp4\">MP4 video</option>\n <option value=\"gif\">Animated GIF</option>\n </select>\n </div>\n\n <div>\n <label style={themedLabelStyle}>Quality</label>\n <select\n aria-label=\"Quality\"\n style={themedSelectStyle}\n value={quality}\n onChange={(e) => setQuality(e.target.value as VideoQuality)}\n >\n <option value=\"draft\">Draft — fast, lower quality</option>\n <option value=\"normal\">Normal — balanced</option>\n <option value=\"high\">High — best quality, slower</option>\n </select>\n </div>\n\n <div>\n <label style={themedLabelStyle}>Frame Rate</label>\n <select\n aria-label=\"Frame Rate\"\n style={themedSelectStyle}\n value={fps}\n onChange={(e) => setFps(Number(e.target.value))}\n >\n <option value={10}>10 fps — recommended for GIF</option>\n <option value={15}>15 fps — fast export</option>\n <option value={24}>24 fps — cinematic</option>\n <option value={30}>30 fps — smooth</option>\n </select>\n </div>\n\n <div>\n <label style={themedLabelStyle}>Orientation</label>\n <select\n aria-label=\"Orientation\"\n style={themedSelectStyle}\n value={orientation}\n onChange={(e) => setOrientation(e.target.value as VideoOrientation)}\n >\n <option value=\"landscape\">\n Landscape ({outputFormat === 'gif' ? '960 × 540' : '1920 × 1080'})\n </option>\n <option value=\"portrait\">\n Portrait ({outputFormat === 'gif' ? '540 × 960' : '1080 × 1920'})\n </option>\n </select>\n </div>\n\n <div>\n <label style={themedLabelStyle}>Captions</label>\n <select\n aria-label=\"Captions\"\n style={themedSelectStyle}\n value={captionMode}\n onChange={(e) => setCaptionMode(e.target.value as CaptionMode)}\n >\n <option value=\"off\">None</option>\n <option value=\"standard\">Standard (top bar)</option>\n <option value=\"social\">Social media (large words)</option>\n </select>\n </div>\n\n <div>\n <label style={themedLabelStyle}>Animations &amp; transitions</label>\n <select\n aria-label=\"Animations and transitions\"\n style={themedSelectStyle}\n value={animationsEnabled ? 'enabled' : 'disabled'}\n onChange={(e) => setAnimationsEnabled(e.target.value === 'enabled')}\n >\n <option value=\"enabled\">Enabled</option>\n <option value=\"disabled\">Disabled — smaller files</option>\n </select>\n {outputFormat === 'gif' && animationsEnabled && (\n <p style={{ fontSize: 12, color: palette.muted, margin: '-6px 0 12px' }}>\n Disabling motion is recommended for much smaller GIFs.\n </p>\n )}\n </div>\n\n <div style={footerStyle}>\n <button style={themedSecondaryButtonStyle} onClick={handleClose}>\n Cancel\n </button>\n <button style={themedPrimaryButtonStyle} onClick={handleExport}>\n {outputFormat === 'gif' ? 'Export GIF' : 'Export Video'}\n </button>\n </div>\n </>\n )}\n\n {/* ── Exporting State ── */}\n {isExporting && (\n <>\n {backend && (\n <p style={{ fontSize: 12, color: palette.muted, margin: '0 0 8px 0' }}>\n Encoder: {encoderLabel(completedOutputFormat, backend)}\n </p>\n )}\n\n <div style={{ ...progressBarOuterStyle, background: palette.secondary }}>\n <div\n style={{\n width: `${progress}%`,\n height: '100%',\n background: palette.primary,\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n\n <p style={{ fontSize: 13, margin: '0 0 4px 0' }}>{progress}% complete</p>\n <p style={{ fontSize: 12, color: palette.muted, margin: 0 }}>\n {formatDuration(elapsed)} elapsed\n {estimatedRemaining > 0 && ` · ~${formatDuration(estimatedRemaining)} remaining`}\n </p>\n\n <div style={footerStyle}>\n <button style={themedSecondaryButtonStyle} onClick={cancelExport}>\n Cancel\n </button>\n </div>\n </>\n )}\n\n {/* ── Complete State ── */}\n {state === 'complete' && (\n <>\n <p style={{ fontSize: 14, margin: '0 0 8px 0', color: palette.success }}>\n Export complete!\n </p>\n <p style={{ fontSize: 13, color: palette.label, margin: '0 0 4px 0' }}>\n File size: {(fileSize / (1024 * 1024)).toFixed(1)} MB\n </p>\n {completedOutputFormat === 'gif' ? (\n <p style={{ fontSize: 12, color: palette.muted, margin: '0 0 4px 0' }}>\n Animated GIF does not include audio.\n </p>\n ) : audioIncluded ? (\n <p style={{ fontSize: 12, color: palette.success, margin: '0 0 4px 0' }}>\n Audio included ✓\n </p>\n ) : (\n <p style={{ fontSize: 12, color: palette.muted, margin: '0 0 4px 0' }}>\n Video only{audioSkippedReason ? ` — ${audioSkippedReason}` : ''}\n </p>\n )}\n {backend && (\n <p style={{ fontSize: 12, color: palette.muted, margin: '0 0 12px 0' }}>\n Encoded with {encoderLabel(completedOutputFormat, backend)}\n </p>\n )}\n\n <div style={footerStyle}>\n <button style={themedSecondaryButtonStyle} onClick={handleClose}>\n Close\n </button>\n <button style={themedPrimaryButtonStyle} onClick={handleDownload}>\n Download {completedOutputFormat.toUpperCase()}\n </button>\n </div>\n </>\n )}\n\n {/* ── Error State ── */}\n {state === 'error' && (\n <>\n <p style={{ fontSize: 14, margin: '0 0 8px 0', color: palette.danger }}>\n Export failed\n </p>\n <p\n style={{\n fontSize: 13,\n color: palette.label,\n margin: '0 0 12px 0',\n wordBreak: 'break-word',\n }}\n >\n {error}\n </p>\n\n <div style={footerStyle}>\n <button style={themedSecondaryButtonStyle} onClick={handleClose}>\n Close\n </button>\n <button style={themedPrimaryButtonStyle} onClick={handleExport}>\n Retry {outputFormat === 'gif' ? 'GIF' : 'video'}\n </button>\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n","/**\n * useVideoExport — Main orchestration hook for browser video export.\n *\n * Coordinates frame capture (hidden iframe + html2canvas) with\n * main-thread WebCodecs encoding via mp4-muxer. Manages the full\n * lifecycle: prepare → capture + encode → download.\n *\n * Encoding runs on the main thread because frame capture via html2canvas\n * (~100-200ms per frame) is the bottleneck, not encoding (~1ms per frame\n * with hardware-accelerated WebCodecs). Worker offloading would add\n * complexity with minimal benefit.\n *\n * Usage:\n * const { state, progress, phase, startExport, cancel, downloadUrl } = useVideoExport();\n * <button onClick={() => startExport(doc, options)}>Export</button>\n */\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { MediaProvider } from '@bendyline/squisq/schemas';\nimport type {\n VideoQuality,\n VideoOrientation,\n AudioTimelineClip,\n FfmpegWasmLoadConfig,\n} from '@bendyline/squisq-video';\nimport { resolveDimensions, computeAudioTimeline, QUALITY_PRESETS } from '@bendyline/squisq-video';\nimport type { CaptionMode } from '@bendyline/squisq-react';\nimport {\n createEncoder,\n supportsWebCodecs,\n supportsWebCodecsH264,\n type MainThreadEncoder,\n} from '../mainThreadEncoder.js';\nimport { createWorkerEncoder } from '../workerEncoder.js';\nimport {\n supportsWebCodecsAac,\n selectAudioTier,\n renderAudioTimeline,\n encodeAacTrack,\n audioBufferToWav,\n muxAudioWithFfmpegWasm,\n EXPORT_AUDIO_SAMPLE_RATE,\n EXPORT_AUDIO_CHANNELS,\n} from '../audioTrack.js';\nimport { transcodeMp4ToGifWithFfmpegWasm } from '../gifTranscode.js';\nimport { useFrameCapture } from './useFrameCapture.js';\n\n// ── Audio resolution ───────────────────────────────────────────────\n\n/**\n * Resolve the raw bytes for every unique source in the audio timeline from\n * (in order) the pre-collected audio map, the images map, then the\n * MediaProvider. Sources that can't be resolved are simply omitted — the\n * caller degrades gracefully rather than failing.\n */\nasync function resolveAudioBuffers(\n clips: AudioTimelineClip[],\n sources: {\n audio?: Map<string, ArrayBuffer>;\n images?: Map<string, ArrayBuffer>;\n mediaProvider?: MediaProvider;\n },\n): Promise<Map<string, ArrayBuffer>> {\n const srcs = new Set(clips.map((c) => c.src));\n const out = new Map<string, ArrayBuffer>();\n for (const src of srcs) {\n let data = sources.audio?.get(src) ?? sources.images?.get(src);\n if (!data && sources.mediaProvider) {\n try {\n const url = await sources.mediaProvider.resolveUrl(src);\n const res = await fetch(url);\n if (res.ok) data = await res.arrayBuffer();\n } catch {\n // Unresolvable source; skip it.\n }\n }\n if (data) out.set(src, data);\n }\n return out;\n}\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport type VideoExportState =\n | 'idle'\n | 'preparing'\n | 'capturing'\n | 'encoding'\n | 'complete'\n | 'error';\n\n/** Browser export container format. */\nexport type VideoOutputFormat = 'mp4' | 'gif';\n\nexport interface VideoExportConfig {\n /** Output container (default: 'mp4') */\n outputFormat?: VideoOutputFormat;\n /** Render authored animations and slide transitions (default: true for MP4, false for GIF). */\n animationsEnabled?: boolean;\n /** Encoding quality preset (default: 'normal') */\n quality?: VideoQuality;\n /** Frames per second (default: 30) */\n fps?: number;\n /** Viewport orientation (default: 'landscape') */\n orientation?: VideoOrientation;\n /** Explicit output width. GIF defaults to 960 landscape / 540 portrait. */\n width?: number;\n /** Explicit output height. GIF defaults to 540 landscape / 960 portrait. */\n height?: number;\n /**\n * Map of relative image paths to binary data.\n * Used to embed images into the render HTML.\n */\n images?: Map<string, ArrayBuffer>;\n /**\n * Map of audio segment names to binary data.\n * Used to embed audio into the render HTML.\n */\n audio?: Map<string, ArrayBuffer>;\n /** MediaProvider to resolve media URLs (alternative to passing images directly) */\n mediaProvider?: MediaProvider;\n /** Caption mode for the exported video (default: 'off') */\n captionMode?: CaptionMode;\n /** Player IIFE bundle (unused in browser export, kept for CLI/Playwright path) */\n playerScript?: string;\n /** Optional self-hosted ffmpeg.wasm core URLs for fallback/offline/CSP use. */\n ffmpegWasm?: FfmpegWasmLoadConfig;\n}\n\nexport interface VideoExportResult {\n /** Current export state */\n state: VideoExportState;\n /** 0–100 progress percentage */\n progress: number;\n /** Human-readable description of the current phase */\n phase: string;\n /** Video duration detected from the doc (seconds) */\n duration: number;\n /** Effective output format for the current or most recent export. */\n outputFormat: VideoOutputFormat;\n /** Encoder backend ('webcodecs' when WebCodecs H.264 active, 'ffmpeg-wasm' when worker fallback active, null when idle) */\n backend: 'webcodecs' | 'ffmpeg-wasm' | null;\n /** Blob download URL (populated when state === 'complete') */\n downloadUrl: string | null;\n /** File size in bytes (populated when state === 'complete') */\n fileSize: number;\n /**\n * Whether an audio track was muxed into the exported MP4 (populated when\n * state === 'complete'). False when the doc had no audio or when audio was\n * skipped/degraded — see {@link audioSkippedReason}.\n */\n audioIncluded: boolean;\n /**\n * Why audio is absent, when `audioIncluded` is false. `null` means the doc\n * simply had no audio (not a failure); a non-null string explains a genuine\n * capability shortfall or runtime failure. Audio problems never fail the\n * export — the video always completes.\n */\n audioSkippedReason: string | null;\n /** Error message (populated when state === 'error') */\n error: string | null;\n /** Seconds elapsed since export started */\n elapsed: number;\n /** Estimated seconds remaining (0 when idle or complete) */\n estimatedRemaining: number;\n /** Start a new export */\n startExport: (doc: Doc, config: VideoExportConfig) => Promise<void>;\n /** Cancel an in-progress export */\n cancel: () => void;\n /** Reset state back to idle (e.g., after complete or error) */\n reset: () => void;\n}\n\n// ── Hook ───────────────────────────────────────────────────────────\n\nexport function useVideoExport(): VideoExportResult {\n const [state, setState] = useState<VideoExportState>('idle');\n const [progress, setProgress] = useState(0);\n const [phase, setPhase] = useState('');\n const [duration, setDuration] = useState(0);\n const [outputFormat, setOutputFormat] = useState<VideoOutputFormat>('mp4');\n const [backend, setBackend] = useState<'webcodecs' | 'ffmpeg-wasm' | null>(null);\n const [downloadUrl, setDownloadUrl] = useState<string | null>(null);\n const [fileSize, setFileSize] = useState(0);\n const [audioIncluded, setAudioIncluded] = useState(false);\n const [audioSkippedReason, setAudioSkippedReason] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const [elapsed, setElapsed] = useState(0);\n const [estimatedRemaining, setEstimatedRemaining] = useState(0);\n\n const encoderRef = useRef<MainThreadEncoder | null>(null);\n const gifAbortRef = useRef<AbortController | null>(null);\n const cancelledRef = useRef(false);\n const downloadUrlRef = useRef<string | null>(null);\n const startTimeRef = useRef<number>(0);\n const elapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const frameCapture = useFrameCapture();\n\n // Clean up on unmount\n useEffect(() => {\n return () => {\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n if (downloadUrlRef.current) {\n URL.revokeObjectURL(downloadUrlRef.current);\n }\n if (encoderRef.current) {\n encoderRef.current.close();\n }\n gifAbortRef.current?.abort();\n frameCapture.destroy();\n };\n }, [frameCapture]);\n\n const reset = useCallback(() => {\n if (downloadUrlRef.current) {\n URL.revokeObjectURL(downloadUrlRef.current);\n downloadUrlRef.current = null;\n }\n if (encoderRef.current) {\n encoderRef.current.close();\n encoderRef.current = null;\n }\n gifAbortRef.current?.abort();\n gifAbortRef.current = null;\n frameCapture.destroy();\n setState('idle');\n setProgress(0);\n setPhase('');\n setDuration(0);\n setOutputFormat('mp4');\n setBackend(null);\n setDownloadUrl(null);\n setFileSize(0);\n setAudioIncluded(false);\n setAudioSkippedReason(null);\n setError(null);\n setElapsed(0);\n setEstimatedRemaining(0);\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n cancelledRef.current = false;\n }, [frameCapture]);\n\n const cancel = useCallback(() => {\n cancelledRef.current = true;\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n if (encoderRef.current) {\n encoderRef.current.close();\n encoderRef.current = null;\n }\n gifAbortRef.current?.abort();\n gifAbortRef.current = null;\n frameCapture.destroy();\n setState('idle');\n setProgress(0);\n setPhase('Cancelled');\n }, [frameCapture]);\n\n const startExport = useCallback(\n async (doc: Doc, config: VideoExportConfig) => {\n // Clear previous state\n cancelledRef.current = false;\n if (downloadUrlRef.current) {\n URL.revokeObjectURL(downloadUrlRef.current);\n downloadUrlRef.current = null;\n }\n setDownloadUrl(null);\n setFileSize(0);\n setAudioIncluded(false);\n setAudioSkippedReason(null);\n setError(null);\n\n const quality = config.quality ?? 'normal';\n const effectiveOutputFormat = config.outputFormat ?? 'mp4';\n const fps = config.fps ?? (effectiveOutputFormat === 'gif' ? 10 : 30);\n const orientation = config.orientation ?? 'landscape';\n const animationsEnabled = config.animationsEnabled ?? effectiveOutputFormat === 'mp4';\n setOutputFormat(effectiveOutputFormat);\n\n try {\n const gifDefaults =\n orientation === 'portrait' ? { width: 540, height: 960 } : { width: 960, height: 540 };\n const { width, height } = resolveDimensions({\n orientation,\n fps,\n quality,\n ...(config.width !== undefined\n ? { width: config.width }\n : effectiveOutputFormat === 'gif'\n ? { width: gifDefaults.width }\n : {}),\n ...(config.height !== undefined\n ? { height: config.height }\n : effectiveOutputFormat === 'gif'\n ? { height: gifDefaults.height }\n : {}),\n });\n // ── Check browser support ─────────────────────────────────\n const webCodecsAvailable = supportsWebCodecs();\n const sharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined';\n if (effectiveOutputFormat === 'gif' && !sharedArrayBufferAvailable) {\n throw new Error(\n 'Animated GIF export requires ffmpeg.wasm and SharedArrayBuffer ' +\n '(Cross-Origin-Isolation headers).',\n );\n }\n if (!webCodecsAvailable && !sharedArrayBufferAvailable) {\n throw new Error(\n 'No video encoder available. WebCodecs requires Chrome 94+ / Edge 94+, ' +\n 'and the ffmpeg.wasm fallback requires SharedArrayBuffer ' +\n '(Cross-Origin-Isolation headers).',\n );\n }\n\n // ── Step 1: Prepare ───────────────────────────────────────\n setState('preparing');\n setPhase('Loading document…');\n setProgress(0);\n setElapsed(0);\n setEstimatedRemaining(0);\n\n // Start elapsed timer\n startTimeRef.current = performance.now();\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n elapsedTimerRef.current = setInterval(() => {\n setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1000));\n }, 1000);\n\n // Collect images from MediaProvider if provided and images not passed directly\n let images = config.images;\n if (!images && config.mediaProvider) {\n images = new Map<string, ArrayBuffer>();\n const entries = await config.mediaProvider.listMedia();\n for (const entry of entries) {\n const url = await config.mediaProvider.resolveUrl(entry.name);\n const res = await fetch(url);\n if (res.ok) {\n const data = await res.arrayBuffer();\n images.set(entry.name, data);\n }\n }\n }\n\n const docDuration = await frameCapture.init(\n doc,\n { images, audio: config.audio, width, height, animationsEnabled },\n config.captionMode,\n );\n\n if (cancelledRef.current) return;\n\n setDuration(docDuration);\n if (docDuration <= 0) {\n throw new Error('Document has zero duration — nothing to export');\n }\n\n // ── Step 2: Create encoder ────────────────────────────────\n setPhase('Starting encoder…');\n setProgress(5);\n\n // Prefer main-thread WebCodecs (fast), but probe whether H.264\n // is actually supported. Linux Chromium has VideoEncoder but\n // no proprietary H.264 codec — fall back to the worker, which\n // loads ffmpeg.wasm in that case.\n const canUseWebCodecs =\n webCodecsAvailable && (await supportsWebCodecsH264({ width, height, fps, quality }));\n\n // ── Audio: tier selection + (best-effort) render ──────────\n // The browser frame-capture path has no cover pre-roll (Playwright\n // only), so the timeline is unshifted. Every audio operation below is\n // wrapped so a failure degrades to a silent video with a reason —\n // audio never aborts the export.\n const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;\n // GIF has no audio track. An empty timeline skips preparation and\n // muxing without reporting the format limitation as an export error.\n const timeline = effectiveOutputFormat === 'mp4' ? computeAudioTimeline(doc, 0) : [];\n const aacSupported =\n timeline.length > 0\n ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS)\n : false;\n const tierDecision = selectAudioTier({\n hasClips: timeline.length > 0,\n aacSupported,\n sharedArrayBufferAvailable,\n canUseMainThreadWebCodecs: canUseWebCodecs,\n });\n\n let renderedAudio: AudioBuffer | null = null;\n let audioIncludedLocal = false;\n let audioReasonLocal: string | null = tierDecision.reason;\n\n if (tierDecision.tier === 1 || tierDecision.tier === 2) {\n setPhase('Preparing audio…');\n try {\n const buffers = await resolveAudioBuffers(timeline, {\n audio: config.audio,\n images,\n mediaProvider: config.mediaProvider,\n });\n if (buffers.size === 0) {\n audioReasonLocal = 'Audio files for this document could not be loaded.';\n } else {\n const totalAudioDur = timeline.reduce(\n (max, c) => Math.max(max, c.startSec + c.durationSec),\n docDuration,\n );\n renderedAudio = await renderAudioTimeline(\n timeline,\n buffers,\n totalAudioDur,\n EXPORT_AUDIO_SAMPLE_RATE,\n );\n }\n } catch (audioErr: unknown) {\n renderedAudio = null;\n audioReasonLocal = `Audio could not be prepared: ${\n audioErr instanceof Error ? audioErr.message : String(audioErr)\n }`;\n }\n }\n\n const useInlineAudio = renderedAudio !== null && tierDecision.tier === 1;\n const useFfmpegAudio = renderedAudio !== null && tierDecision.tier === 2;\n\n if (cancelledRef.current) return;\n\n let encoder: MainThreadEncoder;\n if (canUseWebCodecs) {\n encoder = createEncoder({\n width,\n height,\n fps,\n quality,\n ...(useInlineAudio && renderedAudio\n ? {\n audio: {\n numberOfChannels: renderedAudio.numberOfChannels,\n sampleRate: renderedAudio.sampleRate,\n },\n }\n : {}),\n });\n setBackend('webcodecs');\n } else if (sharedArrayBufferAvailable) {\n const workerEncoder = createWorkerEncoder({\n width,\n height,\n fps,\n quality,\n ffmpegWasm: config.ffmpegWasm,\n });\n encoder = workerEncoder;\n const selectedBackend = await workerEncoder.ready;\n setBackend(selectedBackend);\n setPhase(\n selectedBackend === 'ffmpeg-wasm'\n ? 'Starting encoder (ffmpeg.wasm)…'\n : 'Starting encoder…',\n );\n } else {\n throw new Error(\n 'WebCodecs H.264 is unavailable in this browser and the ffmpeg.wasm ' +\n 'fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers).',\n );\n }\n encoderRef.current = encoder;\n\n if (cancelledRef.current) return;\n\n // ── Step 3: Capture frames and encode ─────────────────────\n setState('capturing');\n const totalFrames = Math.ceil(docDuration * fps);\n\n const captureStartTime = performance.now();\n // Throttle UI updates to every ~10 frames to avoid excessive re-renders.\n // Each setState between awaits triggers a separate render cycle.\n const UI_UPDATE_INTERVAL = 10;\n\n for (let i = 0; i < totalFrames; i++) {\n if (cancelledRef.current) return;\n\n const time = i / fps;\n\n // Update UI periodically (not every frame)\n if (i % UI_UPDATE_INTERVAL === 0 || i === totalFrames - 1) {\n const captureProgress = Math.round((i / totalFrames) * 90);\n setProgress(5 + captureProgress);\n setPhase(`Capturing frame ${i + 1}/${totalFrames} (${time.toFixed(1)}s)`);\n\n if (i > 0) {\n const elapsedCapture = (performance.now() - captureStartTime) / 1000;\n const avgPerFrame = elapsedCapture / i;\n const remaining = Math.round(avgPerFrame * (totalFrames - i));\n setEstimatedRemaining(remaining);\n }\n }\n\n const bitmap = await frameCapture.captureFrame(time);\n\n if (cancelledRef.current) {\n bitmap.close();\n return;\n }\n\n // Encode immediately — WebCodecs is fast and async internally\n await encoder.encodeFrame(bitmap, i);\n }\n\n if (cancelledRef.current) return;\n\n // ── Step 4a: Inline audio (tier 1) ────────────────────────\n // Encode the rendered audio into the same muxer before finalizing.\n if (useInlineAudio && renderedAudio && encoder.addAudioChunk) {\n setState('encoding');\n setPhase('Encoding audio…');\n try {\n await encodeAacTrack(\n renderedAudio,\n { addAudioChunk: encoder.addAudioChunk.bind(encoder) },\n audioBitrate,\n );\n audioIncludedLocal = true;\n } catch (audioErr: unknown) {\n audioIncludedLocal = false;\n audioReasonLocal = `Audio encoding failed: ${\n audioErr instanceof Error ? audioErr.message : String(audioErr)\n }`;\n }\n }\n\n // ── Step 4: Finalize MP4 (or GIF's MP4 intermediate) ─────\n setState('encoding');\n setPhase(effectiveOutputFormat === 'gif' ? 'Finalizing GIF frames…' : 'Finalizing video…');\n setProgress(95);\n\n let outputBytes: ArrayBuffer | Uint8Array = await encoder.finalize();\n encoderRef.current = null;\n\n if (cancelledRef.current) return;\n\n // ── Step 4b: GIF palette transcode or audio mux ───────────\n if (effectiveOutputFormat === 'gif') {\n setPhase('Generating GIF palette…');\n const videoOnly =\n outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);\n const gifAbort = new AbortController();\n gifAbortRef.current = gifAbort;\n try {\n outputBytes = await transcodeMp4ToGifWithFfmpegWasm(\n videoOnly,\n { width, height, loop: 0 },\n config.ffmpegWasm,\n gifAbort.signal,\n );\n } finally {\n if (gifAbortRef.current === gifAbort) gifAbortRef.current = null;\n }\n } else if (useFfmpegAudio && renderedAudio) {\n // Video is finalized; add audio in a single copy-video pass.\n setPhase('Muxing audio…');\n try {\n const wav = audioBufferToWav(renderedAudio);\n const videoOnly =\n outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);\n outputBytes = await muxAudioWithFfmpegWasm(\n videoOnly,\n wav,\n audioBitrate,\n config.ffmpegWasm,\n );\n audioIncludedLocal = true;\n } catch (audioErr: unknown) {\n audioIncludedLocal = false;\n audioReasonLocal = `Audio muxing failed: ${\n audioErr instanceof Error ? audioErr.message : String(audioErr)\n }`;\n }\n }\n\n if (cancelledRef.current) return;\n\n // ── Step 5: Create download URL ───────────────────────────\n // Normalize to a plain ArrayBuffer-backed view for Blob (ffmpeg.wasm\n // output may be typed over SharedArrayBuffer).\n const finalBytes =\n outputBytes instanceof Uint8Array ? outputBytes.slice() : new Uint8Array(outputBytes);\n const mimeType = effectiveOutputFormat === 'gif' ? 'image/gif' : 'video/mp4';\n const blob = new Blob([finalBytes], { type: mimeType });\n const url = URL.createObjectURL(blob);\n downloadUrlRef.current = url;\n\n setDownloadUrl(url);\n setFileSize(finalBytes.byteLength);\n setAudioIncluded(audioIncludedLocal);\n setAudioSkippedReason(\n effectiveOutputFormat === 'gif' || audioIncludedLocal ? null : audioReasonLocal,\n );\n setState('complete');\n setProgress(100);\n setPhase('Export complete');\n setEstimatedRemaining(0);\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n\n // Clean up\n frameCapture.destroy();\n } catch (err: unknown) {\n if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);\n if (cancelledRef.current) return;\n const message = err instanceof Error ? err.message : String(err);\n setState('error');\n setError(message);\n setPhase('Export failed');\n\n // Clean up on error\n if (encoderRef.current) {\n encoderRef.current.close();\n encoderRef.current = null;\n }\n gifAbortRef.current?.abort();\n gifAbortRef.current = null;\n frameCapture.destroy();\n }\n },\n [frameCapture],\n );\n\n return {\n state,\n progress,\n phase,\n duration,\n outputFormat,\n backend,\n downloadUrl,\n fileSize,\n audioIncluded,\n audioSkippedReason,\n error,\n elapsed,\n estimatedRemaining,\n startExport,\n cancel,\n reset,\n };\n}\n","/**\n * Main-thread WebCodecs encoder.\n *\n * Encodes video frames to MP4 using the WebCodecs API and mp4-muxer,\n * running directly on the main thread. This is simpler and avoids\n * worker module-resolution issues with bundlers. Since frame capture\n * via html2canvas (~100-200ms per frame) is the bottleneck — not\n * encoding (~1ms per frame with hardware-accelerated WebCodecs) —\n * worker offloading provides minimal benefit.\n *\n * Requirements: Chrome 94+ / Edge 94+ (WebCodecs support).\n */\n\nimport { bitrateForQuality, validateVideoExportOptions } from '@bendyline/squisq-video';\n\nimport { createMp4Muxer, type Mp4MuxerHandle } from './mp4Mux.js';\n\nexport interface EncoderConfig {\n width: number;\n height: number;\n fps: number;\n quality: 'draft' | 'normal' | 'high';\n /**\n * When present, the underlying muxer is configured with an AAC audio track\n * and {@link MainThreadEncoder.addAudioChunk} becomes usable. Absent → the\n * encoder produces a video-only MP4 exactly as before.\n */\n audio?: {\n numberOfChannels: number;\n sampleRate: number;\n };\n}\n\nexport interface MainThreadEncoder {\n /** Encode a single frame. The bitmap is closed after encoding. */\n encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void>;\n /**\n * Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.\n * Only valid when the encoder was created with an `audio` config; otherwise a\n * no-op. Must be called before {@link finalize}.\n */\n addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;\n /** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */\n finalize(): Promise<ArrayBuffer>;\n /** Close the encoder without producing output (e.g., on cancel). */\n close(): void;\n}\n\n/**\n * Check whether the browser supports WebCodecs video encoding.\n */\nexport function supportsWebCodecs(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined';\n}\n\n/**\n * Probe whether the WebCodecs encoder actually supports the H.264 profile\n * we use. The `VideoEncoder` global can exist while the specific codec is\n * unavailable — this is the case on Linux Chromium, which ships without\n * the proprietary H.264 encoder.\n */\nexport async function supportsWebCodecsH264(config: EncoderConfig): Promise<boolean> {\n if (!supportsWebCodecs()) return false;\n try {\n const support = await VideoEncoder.isConfigSupported({\n codec: 'avc1.640028',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n return support.supported === true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a main-thread WebCodecs encoder.\n *\n * Throws if WebCodecs is not available.\n */\nexport function createEncoder(config: EncoderConfig): MainThreadEncoder {\n validateVideoExportOptions(config);\n if (!supportsWebCodecs()) {\n throw new Error(\n 'WebCodecs is not available in this browser. ' +\n 'Video export requires Chrome 94+, Edge 94+, or another Chromium-based browser.',\n );\n }\n\n const muxer: Mp4MuxerHandle = createMp4Muxer({\n width: config.width,\n height: config.height,\n fps: config.fps,\n ...(config.audio ? { audio: config.audio } : {}),\n });\n\n let closed = false;\n const frameDuration = 1_000_000 / config.fps; // microseconds per frame\n\n const encoder = new VideoEncoder({\n output(chunk, meta) {\n if (closed) return;\n muxer.addVideoChunk(chunk, meta ?? undefined);\n },\n error(err) {\n console.error('WebCodecs encoder error:', err.message);\n },\n });\n\n encoder.configure({\n // Deliberate profile split from the fallback worker (avc1.42001f, Baseline):\n // this primary WebCodecs path targets H.264 High@4.0 for better quality up\n // to 1080p; the wasm-fallback worker uses Baseline for max decoder compat.\n codec: 'avc1.640028', // H.264 High profile, level 4.0 (supports up to 1080p)\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n\n return {\n async encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void> {\n if (closed) {\n bitmap.close();\n throw new Error('Encoder already closed');\n }\n const timestamp = Math.round(frameIndex * frameDuration);\n const frame = new VideoFrame(bitmap, { timestamp });\n const keyFrame = frameIndex % 30 === 0;\n encoder.encode(frame, { keyFrame });\n frame.close();\n bitmap.close();\n },\n\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {\n if (closed || !muxer.hasAudioTrack) return;\n muxer.addAudioChunk(chunk, meta);\n },\n\n async finalize(): Promise<ArrayBuffer> {\n if (closed) throw new Error('Encoder already closed');\n await encoder.flush();\n encoder.close();\n closed = true;\n return muxer.finalize();\n },\n\n close() {\n if (closed) return;\n closed = true;\n if (encoder.state !== 'closed') {\n encoder.close();\n }\n },\n };\n}\n","/**\n * Worker-backed video encoder.\n *\n * Spawns `workers/encode.worker.ts` and exposes the same shape as\n * `MainThreadEncoder` so `useVideoExport` can treat both backends\n * uniformly. Used as a fallback when WebCodecs H.264 isn't supported\n * (typical on Linux Chromium): the worker auto-selects its ffmpeg.wasm\n * path in that case.\n */\n\nimport type { MainThreadEncoder, EncoderConfig } from './mainThreadEncoder.js';\nimport type { MainToWorkerMessage, WorkerToMainMessage } from './workers/workerTypes.js';\nimport type { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';\nimport { validateVideoExportOptions } from '@bendyline/squisq-video';\n\n/** Resolves once the worker has reported which backend it picked. */\nexport interface WorkerEncoder extends MainThreadEncoder {\n /** Backend the worker selected ('webcodecs' or 'ffmpeg-wasm'). */\n readonly ready: Promise<'webcodecs' | 'ffmpeg-wasm'>;\n}\n\nexport function createWorkerEncoder(\n config: EncoderConfig & { ffmpegWasm?: FfmpegWasmLoadConfig },\n): WorkerEncoder {\n validateVideoExportOptions(config);\n\n const worker = new Worker(new URL('./workers/encode.worker.js', import.meta.url), {\n type: 'module',\n });\n\n let state: 'open' | 'finalizing' | 'closed' = 'open';\n let fatalError: Error | null = null;\n let finalizeResolve: ((buffer: ArrayBuffer) => void) | null = null;\n let finalizeReject: ((err: Error) => void) | null = null;\n let readyResolve: ((backend: 'webcodecs' | 'ffmpeg-wasm') => void) | null = null;\n let readyReject: ((err: Error) => void) | null = null;\n let readySettled = false;\n const frameWaiters = new Map<\n number,\n { promise: Promise<void>; resolve: () => void; reject: (err: Error) => void }\n >();\n\n const ready = new Promise<'webcodecs' | 'ffmpeg-wasm'>((resolve, reject) => {\n readyResolve = resolve;\n readyReject = reject;\n });\n\n const frameDuration = 1_000_000 / config.fps; // microseconds per frame\n\n function post(msg: MainToWorkerMessage, transfer?: Transferable[]) {\n worker.postMessage(msg, transfer ?? []);\n }\n\n // Keep async lifecycle checks observable to TypeScript: event handlers can\n // change state while finalize() is awaiting pending frame acknowledgements.\n const currentState = () => state;\n\n worker.onmessage = (event: MessageEvent<WorkerToMainMessage>) => {\n const msg = event.data;\n switch (msg.type) {\n case 'capabilities':\n readySettled = true;\n readyResolve?.(msg.backend);\n readyResolve = readyReject = null;\n break;\n case 'frame-complete': {\n const waiter = frameWaiters.get(msg.frameIndex);\n waiter?.resolve();\n frameWaiters.delete(msg.frameIndex);\n break;\n }\n case 'complete':\n state = 'closed';\n finalizeResolve?.(msg.data);\n finalizeResolve = finalizeReject = null;\n worker.terminate();\n break;\n case 'error': {\n const err = new Error(msg.message);\n fatalError = err;\n state = 'closed';\n readySettled = true;\n readyReject?.(err);\n finalizeReject?.(err);\n for (const waiter of frameWaiters.values()) waiter.reject(err);\n frameWaiters.clear();\n readyResolve = readyReject = null;\n finalizeResolve = finalizeReject = null;\n worker.terminate();\n break;\n }\n // 'progress' is informational; ignored at this layer\n }\n };\n\n worker.onerror = (event) => {\n const err = new Error(event.message || 'Worker error');\n fatalError = err;\n state = 'closed';\n readySettled = true;\n readyReject?.(err);\n finalizeReject?.(err);\n for (const waiter of frameWaiters.values()) waiter.reject(err);\n frameWaiters.clear();\n readyResolve = readyReject = null;\n finalizeResolve = finalizeReject = null;\n worker.terminate();\n };\n\n post({\n type: 'init',\n width: config.width,\n height: config.height,\n fps: config.fps,\n quality: config.quality,\n ...(config.ffmpegWasm ? { ffmpegWasm: config.ffmpegWasm } : {}),\n });\n\n return {\n ready,\n\n encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void> {\n if (state !== 'open' || fatalError) {\n bitmap.close();\n return Promise.reject(fatalError ?? new Error('Encoder is not accepting frames'));\n }\n if (frameWaiters.has(frameIndex)) {\n bitmap.close();\n return Promise.reject(new Error(`Frame ${frameIndex} was submitted more than once`));\n }\n let resolveFrame!: () => void;\n let rejectFrame!: (err: Error) => void;\n const promise = new Promise<void>((resolve, reject) => {\n resolveFrame = resolve;\n rejectFrame = reject;\n });\n frameWaiters.set(frameIndex, { promise, resolve: resolveFrame, reject: rejectFrame });\n const timestamp = Math.round(frameIndex * frameDuration);\n post({ type: 'frame', bitmap, frameIndex, timestamp }, [bitmap]);\n return promise;\n },\n\n async finalize(): Promise<ArrayBuffer> {\n if (state !== 'open') throw new Error('Encoder already closed or finalizing');\n if (fatalError) throw fatalError;\n state = 'finalizing';\n await Promise.all(Array.from(frameWaiters.values(), (waiter) => waiter.promise));\n if (currentState() === 'closed') {\n throw fatalError ?? new Error('Encoder closed during finalization');\n }\n return new Promise<ArrayBuffer>((resolve, reject) => {\n finalizeResolve = resolve;\n finalizeReject = reject;\n post({ type: 'finalize' });\n });\n },\n\n close() {\n if (state === 'closed') return;\n state = 'closed';\n const err = new Error('Encoder closed');\n if (!readySettled) {\n readySettled = true;\n readyReject?.(err);\n }\n finalizeReject?.(err);\n for (const waiter of frameWaiters.values()) waiter.reject(err);\n frameWaiters.clear();\n readyResolve = readyReject = null;\n finalizeResolve = finalizeReject = null;\n post({ type: 'cancel' });\n worker.terminate();\n },\n };\n}\n","/**\n * audioTrack — In-browser audio rendering + AAC encoding for MP4 export.\n *\n * Replaces the CLI's ffmpeg `adelay`/`atrim`/`amix` graph with browser-native\n * primitives:\n *\n * - {@link supportsWebCodecsAac} probes whether the browser can encode AAC\n * (WebCodecs `AudioEncoder`).\n * - {@link renderAudioTimeline} mixes an {@link AudioTimelineClip} list into a\n * single `AudioBuffer` using an `OfflineAudioContext` (decode → schedule →\n * render).\n * - {@link encodeAacTrack} feeds that buffer through a WebCodecs\n * `AudioEncoder` and hands the chunks to an mp4-muxer audio track (tier 1).\n * - {@link audioBufferToWav} + {@link muxAudioWithFfmpegWasm} cover the\n * ffmpeg.wasm fallback (tier 2).\n * - {@link selectAudioTier} is the pure capability→tier decision shared with\n * `useVideoExport` (and unit-tested at the module boundary).\n */\n\nimport {\n ffmpegAudioMuxArgs,\n type AudioTimelineClip,\n type FfmpegWasmLoadConfig,\n} from '@bendyline/squisq-video';\n\n/** Sample rate the export renders + encodes audio at. */\nexport const EXPORT_AUDIO_SAMPLE_RATE = 48_000;\n/** Channel count the export renders + encodes audio at (stereo). */\nexport const EXPORT_AUDIO_CHANNELS = 2;\n\n/** Tier-3 reason used when AAC encoding is impossible and no ffmpeg fallback. */\nexport const REASON_NO_AAC_NO_SAB =\n 'This browser cannot encode AAC audio (WebCodecs AudioEncoder unavailable) ' +\n 'and the ffmpeg.wasm fallback requires cross-origin isolation (SharedArrayBuffer).';\n\n// ── Capability probe ───────────────────────────────────────────────\n\n/**\n * Whether the browser can encode AAC (`mp4a.40.2`) via WebCodecs.\n * Returns false when `AudioEncoder` is undefined (e.g. Node/jsdom, Safari,\n * older Chromium) or the config is unsupported.\n */\nexport async function supportsWebCodecsAac(\n sampleRate: number = EXPORT_AUDIO_SAMPLE_RATE,\n channels: number = EXPORT_AUDIO_CHANNELS,\n): Promise<boolean> {\n if (typeof AudioEncoder === 'undefined') return false;\n try {\n const support = await AudioEncoder.isConfigSupported({\n codec: 'mp4a.40.2',\n sampleRate,\n numberOfChannels: channels,\n bitrate: 128_000,\n });\n return support.supported === true;\n } catch {\n return false;\n }\n}\n\n// ── Tier selection (pure) ──────────────────────────────────────────\n\nexport interface AudioTierInputs {\n /** Whether the doc has any audio timeline clips to include at all. */\n hasClips: boolean;\n /** Whether WebCodecs AAC encoding is available. */\n aacSupported: boolean;\n /** Whether `SharedArrayBuffer` (ffmpeg.wasm path) is available. */\n sharedArrayBufferAvailable: boolean;\n /** Whether the main-thread WebCodecs H.264 encoder is in use (tier-1 host). */\n canUseMainThreadWebCodecs: boolean;\n}\n\nexport interface AudioTierDecision {\n /**\n * 1 → inline AAC into the video muxer.\n * 2 → finalize video-only then ffmpeg.wasm mux pass.\n * 3 → video-only (no audio).\n */\n tier: 1 | 2 | 3;\n /**\n * Reason audio will be absent, when tier 3. `null` means \"there was simply\n * no audio to include\" (not a failure). Non-null for a genuine capability\n * shortfall. Always `null` for tiers 1 and 2 (the attempt is expected to\n * succeed; the caller downgrades with a fresh reason on runtime failure).\n */\n reason: string | null;\n}\n\n/**\n * Decide which audio tier to run from capability booleans. Pure — the single\n * source of truth for tier selection, shared with `useVideoExport`.\n */\nexport function selectAudioTier(inputs: AudioTierInputs): AudioTierDecision {\n if (!inputs.hasClips) {\n return { tier: 3, reason: null };\n }\n if (inputs.aacSupported && inputs.canUseMainThreadWebCodecs) {\n return { tier: 1, reason: null };\n }\n if (inputs.sharedArrayBufferAvailable) {\n return { tier: 2, reason: null };\n }\n return { tier: 3, reason: REASON_NO_AAC_NO_SAB };\n}\n\n// ── OfflineAudioContext rendering ──────────────────────────────────\n\ntype OfflineCtor = new (\n numberOfChannels: number,\n length: number,\n sampleRate: number,\n) => OfflineAudioContext;\n\nfunction resolveOfflineAudioContext(): OfflineCtor {\n const g = globalThis as unknown as {\n OfflineAudioContext?: OfflineCtor;\n webkitOfflineAudioContext?: OfflineCtor;\n };\n const ctor = g.OfflineAudioContext ?? g.webkitOfflineAudioContext;\n if (!ctor) {\n throw new Error('OfflineAudioContext is not available in this environment.');\n }\n return ctor;\n}\n\n/**\n * Mix a list of timeline clips into one `AudioBuffer`.\n *\n * Decodes each unique source once, then schedules an `AudioBufferSourceNode`\n * per clip at its `startSec` with the trim window (`offset = sourceInSec`,\n * `duration = durationSec`). Undecodable sources are skipped rather than\n * failing the whole render.\n */\nexport async function renderAudioTimeline(\n clips: AudioTimelineClip[],\n buffers: Map<string, ArrayBuffer>,\n totalDurationSec: number,\n sampleRate: number = EXPORT_AUDIO_SAMPLE_RATE,\n): Promise<AudioBuffer> {\n const Ctor = resolveOfflineAudioContext();\n const channels = EXPORT_AUDIO_CHANNELS;\n const length = Math.max(1, Math.ceil(totalDurationSec * sampleRate));\n const ctx = new Ctor(channels, length, sampleRate);\n\n // Decode each unique source once. decodeAudioData detaches the buffer, so\n // hand it a copy to keep the caller's ArrayBuffer reusable.\n const decoded = new Map<string, AudioBuffer>();\n for (const [src, data] of buffers) {\n try {\n decoded.set(src, await ctx.decodeAudioData(data.slice(0)));\n } catch {\n // Skip undecodable sources; the clip is simply omitted.\n }\n }\n\n for (const clip of clips) {\n const buffer = decoded.get(clip.src);\n if (!buffer) continue;\n const node = ctx.createBufferSource();\n node.buffer = buffer;\n node.connect(ctx.destination);\n const when = Math.max(0, clip.startSec);\n const offset = Math.max(0, clip.sourceInSec);\n const duration = Math.max(0, clip.durationSec);\n node.start(when, offset, duration);\n }\n\n return ctx.startRendering();\n}\n\n// ── WebCodecs AAC encode (tier 1) ──────────────────────────────────\n\n/** Minimal muxer surface {@link encodeAacTrack} needs. */\nexport interface AudioChunkSink {\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;\n}\n\n/**\n * Encode an `AudioBuffer` to AAC via WebCodecs and feed the chunks to `sink`.\n * Resolves once every chunk has been flushed to the muxer. Throws only if the\n * `AudioEncoder` errors — the caller wraps this so audio failure never aborts\n * the video export.\n */\nexport async function encodeAacTrack(\n audioBuffer: AudioBuffer,\n sink: AudioChunkSink,\n bitrate: number,\n): Promise<void> {\n if (typeof AudioEncoder === 'undefined' || typeof AudioData === 'undefined') {\n throw new Error('WebCodecs AudioEncoder is not available.');\n }\n\n const sampleRate = audioBuffer.sampleRate;\n const channels = audioBuffer.numberOfChannels;\n\n let encodeError: Error | null = null;\n const encoder = new AudioEncoder({\n output: (chunk, meta) => sink.addAudioChunk(chunk, meta ?? undefined),\n error: (err) => {\n encodeError = err instanceof Error ? err : new Error(String(err));\n },\n });\n\n encoder.configure({ codec: 'mp4a.40.2', sampleRate, numberOfChannels: channels, bitrate });\n\n const FRAME = 1024; // samples per AudioData chunk\n const total = audioBuffer.length;\n const channelData: Float32Array[] = [];\n for (let ch = 0; ch < channels; ch++) {\n channelData.push(audioBuffer.getChannelData(ch));\n }\n\n for (let offset = 0; offset < total; offset += FRAME) {\n if (encodeError) break;\n const count = Math.min(FRAME, total - offset);\n // Planar layout: all of channel 0, then all of channel 1, …\n const planar = new Float32Array(count * channels);\n for (let ch = 0; ch < channels; ch++) {\n planar.set(channelData[ch].subarray(offset, offset + count), ch * count);\n }\n const timestamp = Math.round((offset / sampleRate) * 1_000_000);\n const audioData = new AudioData({\n format: 'f32-planar',\n sampleRate,\n numberOfFrames: count,\n numberOfChannels: channels,\n timestamp,\n data: planar,\n });\n encoder.encode(audioData);\n audioData.close();\n }\n\n await encoder.flush();\n encoder.close();\n if (encodeError) throw encodeError;\n}\n\n// ── WAV serialization (tier 2 input) ───────────────────────────────\n\n/**\n * Serialize an `AudioBuffer` to a 16-bit PCM WAV byte array. Used to hand the\n * rendered audio to the ffmpeg.wasm fallback for muxing.\n */\nexport function audioBufferToWav(buffer: AudioBuffer): Uint8Array {\n const channels = buffer.numberOfChannels;\n const sampleRate = buffer.sampleRate;\n const frames = buffer.length;\n const bytesPerSample = 2;\n const blockAlign = channels * bytesPerSample;\n const dataSize = frames * blockAlign;\n const out = new ArrayBuffer(44 + dataSize);\n const view = new DataView(out);\n\n const writeStr = (offset: number, str: string) => {\n for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));\n };\n\n writeStr(0, 'RIFF');\n view.setUint32(4, 36 + dataSize, true);\n writeStr(8, 'WAVE');\n writeStr(12, 'fmt ');\n view.setUint32(16, 16, true); // PCM chunk size\n view.setUint16(20, 1, true); // PCM format\n view.setUint16(22, channels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * blockAlign, true); // byte rate\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bytesPerSample * 8, true);\n writeStr(36, 'data');\n view.setUint32(40, dataSize, true);\n\n const channelData: Float32Array[] = [];\n for (let ch = 0; ch < channels; ch++) channelData.push(buffer.getChannelData(ch));\n\n let pos = 44;\n for (let i = 0; i < frames; i++) {\n for (let ch = 0; ch < channels; ch++) {\n let sample = channelData[ch][i];\n sample = Math.max(-1, Math.min(1, sample));\n view.setInt16(pos, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);\n pos += 2;\n }\n }\n\n return new Uint8Array(out);\n}\n\n// ── ffmpeg.wasm mux fallback (tier 2) ──────────────────────────────\n\n/**\n * Mux a pre-encoded video-only MP4 with a WAV audio track in a single\n * ffmpeg.wasm pass (`-c:v copy -c:a aac -af apad -shortest`). Requires\n * `SharedArrayBuffer` (cross-origin isolation). Returns the muxed MP4 bytes.\n */\nexport async function muxAudioWithFfmpegWasm(\n videoMp4: Uint8Array,\n wav: Uint8Array,\n audioBitrate: number,\n loadConfig?: FfmpegWasmLoadConfig,\n): Promise<Uint8Array> {\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n const ffmpeg = new FFmpeg();\n try {\n await ffmpeg.load({\n ...loadConfig,\n classWorkerURL:\n loadConfig?.classWorkerURL ??\n new URL('./workers/ffmpeg.class-worker.js', import.meta.url).href,\n });\n await ffmpeg.writeFile('video.mp4', videoMp4);\n await ffmpeg.writeFile('audio.wav', wav);\n const exitCode = await ffmpeg.exec([\n '-i',\n 'video.mp4',\n '-i',\n 'audio.wav',\n '-c:v',\n 'copy',\n ...ffmpegAudioMuxArgs(audioBitrate),\n 'out.mp4',\n ]);\n if (exitCode !== 0) {\n throw new Error(`ffmpeg.wasm audio mux failed with exit code ${exitCode}`);\n }\n const data = await ffmpeg.readFile('out.mp4');\n return data instanceof Uint8Array ? data : new TextEncoder().encode(data as string);\n } finally {\n ffmpeg.terminate();\n }\n}\n","/**\n * Animated GIF finalization for the browser export path.\n *\n * The frame-capture pipeline already produces a compact, video-only MP4. For\n * the GIF MVP we use that MP4 as a bounded-memory intermediate, then run a\n * palette generation/application pass through ffmpeg.wasm.\n */\n\nimport {\n ffmpegGifOutputArgs,\n type FfmpegWasmLoadConfig,\n type GifOutputOptions,\n} from '@bendyline/squisq-video';\n\n/** Build the deterministic ffmpeg arguments used for MP4 -> animated GIF. */\nexport function buildGifFfmpegArgs(options: GifOutputOptions): string[] {\n return ['-y', '-i', 'video.mp4', ...ffmpegGifOutputArgs(options), 'out.gif'];\n}\n\n/**\n * Convert a video-only MP4 into a looping animated GIF with a generated\n * palette. The ffmpeg runtime is always terminated, including on failure.\n */\nexport async function transcodeMp4ToGifWithFfmpegWasm(\n videoMp4: Uint8Array,\n options: GifOutputOptions,\n loadConfig?: FfmpegWasmLoadConfig,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n if (videoMp4.byteLength === 0) {\n throw new Error('Cannot create an animated GIF from an empty MP4.');\n }\n if (signal?.aborted) {\n throw new DOMException('Animated GIF export was cancelled.', 'AbortError');\n }\n\n const args = buildGifFfmpegArgs(options);\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n const ffmpeg = new FFmpeg();\n let terminated = false;\n const terminate = () => {\n if (terminated) return;\n terminated = true;\n ffmpeg.terminate();\n };\n const handleAbort = () => terminate();\n signal?.addEventListener('abort', handleAbort, { once: true });\n try {\n await ffmpeg.load({\n ...loadConfig,\n classWorkerURL:\n loadConfig?.classWorkerURL ??\n new URL('./workers/ffmpeg.class-worker.js', import.meta.url).href,\n });\n if (signal?.aborted) {\n throw new DOMException('Animated GIF export was cancelled.', 'AbortError');\n }\n await ffmpeg.writeFile('video.mp4', videoMp4);\n if (signal?.aborted) {\n throw new DOMException('Animated GIF export was cancelled.', 'AbortError');\n }\n const exitCode = await ffmpeg.exec(args);\n if (signal?.aborted) {\n throw new DOMException('Animated GIF export was cancelled.', 'AbortError');\n }\n if (exitCode !== 0) {\n throw new Error(`ffmpeg.wasm GIF transcode failed with exit code ${exitCode}`);\n }\n const data = await ffmpeg.readFile('out.gif');\n return data instanceof Uint8Array ? data : new TextEncoder().encode(data as string);\n } finally {\n signal?.removeEventListener('abort', handleAbort);\n terminate();\n }\n}\n","/**\n * useFrameCapture — Hidden div + html2canvas frame capture.\n *\n * Mounts a DocPlayer in renderMode inside a hidden div (same document),\n * then captures individual frames by seeking the player and rendering\n * the DOM to a canvas via html2canvas.\n *\n * Uses React directly — no script injection, no iframes, no eval.\n *\n * Returns an ImageBitmap for each frame (transferable to a Worker).\n */\n\nimport { createElement } from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\nimport { useRef, useCallback, useMemo } from 'react';\nimport type { Doc, MediaProvider } from '@bendyline/squisq/schemas';\nimport type { RenderHtmlOptions } from '@bendyline/squisq-video';\nimport { DocPlayer, MediaContext } from '@bendyline/squisq-react';\nimport type { SquisqRenderAPI, CaptionMode, CaptionStyle } from '@bendyline/squisq-react';\nimport html2canvas from 'html2canvas';\n\nexport interface FrameCaptureHandle {\n /** Initialize the hidden player. Returns the video duration in seconds. */\n init: (\n doc: Doc,\n renderOptions: Omit<RenderHtmlOptions, 'playerScript'>,\n captionMode?: CaptionMode,\n ) => Promise<number>;\n /** Capture a single frame at the given time (seconds). Returns an ImageBitmap. */\n captureFrame: (time: number) => Promise<ImageBitmap>;\n /** Clean up resources. */\n destroy: () => void;\n}\n\n/** Extension → MIME type map (hoisted to avoid per-image allocation). */\nconst MIME_MAP: Record<string, string> = {\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 bmp: 'image/bmp',\n avif: 'image/avif',\n};\n\n/** Convert an ArrayBuffer to a base64 data URI using chunked encoding (O(n)). */\nfunction arrayBufferToDataUrl(buffer: ArrayBuffer, mime: string): string {\n const bytes = new Uint8Array(buffer);\n const chunks: string[] = [];\n const CHUNK = 8192;\n for (let i = 0; i < bytes.length; i += CHUNK) {\n chunks.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));\n }\n return `data:${mime};base64,${btoa(chunks.join(''))}`;\n}\n\n/**\n * Create an inline MediaProvider from a map of paths to ArrayBuffers.\n */\nfunction createInlineProvider(images: Map<string, ArrayBuffer>): MediaProvider {\n const dataUrls = new Map<string, string>();\n const mimeTypes = new Map<string, string>();\n for (const [path, buffer] of images) {\n const ext = path.split('.').pop()?.toLowerCase() ?? '';\n const mime = MIME_MAP[ext] ?? 'application/octet-stream';\n dataUrls.set(path, arrayBufferToDataUrl(buffer, mime));\n mimeTypes.set(path, mime);\n }\n\n return {\n async resolveUrl(relativePath: string): Promise<string> {\n return dataUrls.get(relativePath) ?? relativePath;\n },\n async listMedia() {\n return [...dataUrls.keys()].map((name) => ({\n name,\n mimeType: mimeTypes.get(name) ?? 'application/octet-stream',\n size: 0,\n }));\n },\n async addMedia() {\n throw new Error('Read-only');\n },\n async removeMedia() {\n throw new Error('Read-only');\n },\n dispose() {},\n };\n}\n\n/**\n * Hook that manages a hidden div for frame capture.\n */\nexport function useFrameCapture(): FrameCaptureHandle {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const rootRef = useRef<Root | null>(null);\n const renderAPIRef = useRef<SquisqRenderAPI | null>(null);\n const dimensionsRef = useRef<{ width: number; height: number }>({ width: 1920, height: 1080 });\n\n const init = useCallback(\n async (\n doc: Doc,\n renderOptions: Omit<RenderHtmlOptions, 'playerScript'>,\n captionMode?: CaptionMode,\n ): Promise<number> => {\n // Clean up any existing container.\n // Defer unmount to avoid \"synchronously unmount a root while React\n // was already rendering\" when init() is called from a React handler.\n if (rootRef.current || containerRef.current) {\n const oldRoot = rootRef.current;\n const oldContainer = containerRef.current;\n rootRef.current = null;\n containerRef.current = null;\n renderAPIRef.current = null;\n await new Promise<void>((resolve) => {\n setTimeout(() => {\n if (oldRoot) oldRoot.unmount();\n if (oldContainer) oldContainer.remove();\n resolve();\n }, 0);\n });\n }\n\n const width = renderOptions.width ?? 1920;\n const height = renderOptions.height ?? 1080;\n const animationsEnabled = renderOptions.animationsEnabled ?? true;\n dimensionsRef.current = { width, height };\n\n // Create a hidden container\n const container = document.createElement('div');\n container.style.cssText =\n `position:fixed;left:0;top:0;width:${width}px;height:${height}px;` +\n 'opacity:0;pointer-events:none;z-index:-1;overflow:hidden;';\n document.body.appendChild(container);\n containerRef.current = container;\n\n // Create render root\n const renderRoot = document.createElement('div');\n renderRoot.id = 'squisq-capture-root';\n renderRoot.style.cssText = `width:${width}px;height:${height}px;`;\n container.appendChild(renderRoot);\n\n // Build media provider from images\n const mediaProvider = renderOptions.images\n ? createInlineProvider(renderOptions.images)\n : null;\n\n // Mount DocPlayer in renderMode via React\n const root = createRoot(renderRoot);\n rootRef.current = root;\n\n // Derive caption props from captionMode\n const captionsEnabled = captionMode !== undefined && captionMode !== 'off';\n const captionStyle: CaptionStyle = captionMode === 'social' ? 'social' : 'standard';\n let resolveRenderAPI!: (api: SquisqRenderAPI) => void;\n const renderAPIReady = new Promise<SquisqRenderAPI>((resolve) => {\n resolveRenderAPI = resolve;\n });\n\n const playerElement = createElement(DocPlayer, {\n doc,\n basePath: '.',\n renderMode: true,\n animationsEnabled,\n showControls: false,\n autoPlay: false,\n forceViewport: { width, height, name: 'export' },\n captionsEnabled,\n captionStyle,\n onRenderAPIReady: (api: SquisqRenderAPI | null) => {\n if (containerRef.current !== container) return;\n renderAPIRef.current = api;\n if (api) resolveRenderAPI(api);\n },\n });\n\n // Defer rendering to the next microtask to avoid \"synchronously unmount\n // a root while React was already rendering\" when init() is called during\n // a React render cycle (e.g., from startExport in VideoExportModal).\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n\n if (mediaProvider) {\n root.render(createElement(MediaContext.Provider, { value: mediaProvider }, playerElement));\n } else {\n root.render(playerElement);\n }\n\n // Wait for this exact player's instance API.\n return new Promise<number>((resolve, reject) => {\n const timeout = setTimeout(() => {\n const api = renderAPIRef.current;\n const hasSeek = typeof api?.seekTo === 'function';\n const hasDur = typeof api?.getDuration === 'function';\n const rootEl = containerRef.current?.querySelector('#squisq-capture-root');\n const hasPlayer = rootEl ? rootEl.querySelector('.doc-player') !== null : false;\n reject(\n new Error(\n `Render API did not initialize within 15s. ` +\n `seekTo=${hasSeek}, getDuration=${hasDur}, player=${hasPlayer}, root=${!!rootEl}`,\n ),\n );\n }, 15000);\n\n void renderAPIReady.then((api) => {\n clearTimeout(timeout);\n resolve(api.getDuration());\n });\n });\n },\n [],\n );\n\n const captureFrame = useCallback(async (time: number): Promise<ImageBitmap> => {\n const container = containerRef.current;\n const api = renderAPIRef.current;\n if (!container || !api) {\n throw new Error('Frame capture not initialized — call init() first');\n }\n\n const { width, height } = dimensionsRef.current;\n\n // Seek the player to the target time\n await api.seekTo(time);\n\n // Wait for the DOM to update after seek\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n\n const root = container.querySelector('#squisq-capture-root') as HTMLElement;\n if (!root) {\n throw new Error('Capture root element not found');\n }\n\n // Render the DOM to a canvas via html2canvas.\n // We're in the same document (no iframe), so COEP doesn't block cloning.\n const canvas = await html2canvas(root, {\n width,\n height,\n scale: 1,\n useCORS: true,\n allowTaint: true,\n backgroundColor: '#000000',\n logging: false,\n });\n\n // Convert to ImageBitmap (transferable to worker — zero-copy)\n const bitmap = await createImageBitmap(canvas);\n return bitmap;\n }, []);\n\n const destroy = useCallback(() => {\n if (rootRef.current) {\n rootRef.current.unmount();\n rootRef.current = null;\n }\n if (containerRef.current) {\n containerRef.current.remove();\n containerRef.current = null;\n }\n renderAPIRef.current = null;\n }, []);\n\n // Return a stable object to prevent useEffect cleanup loops\n // in consumers that depend on the handle reference.\n return useMemo(() => ({ init, captureFrame, destroy }), [init, captureFrame, destroy]);\n}\n","/**\n * VideoExportButton — Simple button that opens the VideoExportModal in a portal.\n *\n * Convenience wrapper for consumers who want a drop-in button.\n * The modal renders via `createPortal` into `document.body`.\n */\n\nimport { useState, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { MediaProvider } from '@bendyline/squisq/schemas';\nimport { VideoExportModal } from './VideoExportModal.js';\nimport type { VideoExportConfig } from './hooks/useVideoExport.js';\n\nexport interface VideoExportButtonProps {\n /** The document to export */\n doc: Doc;\n /**\n * Player IIFE bundle source. Unused by the browser export path (frames\n * are captured from a live in-page DocPlayer); only forwarded for\n * CLI/Playwright-style pipelines that render standalone HTML.\n */\n playerScript?: string;\n /** Optional media provider for resolving images/audio */\n mediaProvider?: MediaProvider;\n /** Pre-collected images map */\n images?: Map<string, ArrayBuffer>;\n /** Pre-collected audio map */\n audio?: Map<string, ArrayBuffer>;\n /**\n * Seeds the modal's initial output format and export settings and is merged\n * as a base into the config passed to the export hook. Forwarded to\n * {@link VideoExportModal}.\n */\n defaultConfig?: Partial<VideoExportConfig>;\n /** Visual color scheme forwarded to the portaled modal. Defaults to light. */\n colorScheme?: 'light' | 'dark';\n /** Button label (defaults to \"Export Video\", or \"Export GIF\" for a GIF default config) */\n label?: string;\n /** Additional inline styles for the button */\n style?: React.CSSProperties;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\nexport function VideoExportButton({\n doc,\n playerScript,\n mediaProvider,\n images,\n audio,\n defaultConfig,\n colorScheme,\n label,\n style,\n disabled,\n}: VideoExportButtonProps) {\n const [showModal, setShowModal] = useState(false);\n const resolvedLabel =\n label ?? (defaultConfig?.outputFormat === 'gif' ? 'Export GIF' : 'Export Video');\n\n const handleOpen = useCallback(() => setShowModal(true), []);\n const handleClose = useCallback(() => setShowModal(false), []);\n\n return (\n <>\n <button onClick={handleOpen} disabled={disabled} style={style}>\n {resolvedLabel}\n </button>\n\n {showModal &&\n createPortal(\n <VideoExportModal\n doc={doc}\n playerScript={playerScript}\n mediaProvider={mediaProvider}\n images={images}\n audio={audio}\n defaultConfig={defaultConfig}\n colorScheme={colorScheme}\n onClose={handleClose}\n />,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":["useState","useCallback","useRef","useCallback","validateVideoExportOptions","useRef","useCallback","url","useState","useCallback","useState","useCallback","Fragment","jsx","jsxs","useState","useCallback"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/workers/encode.worker.ts"],"sourcesContent":["/**\n * Video Encoding Web Worker\n *\n * Encodes video frames to MP4 using one of two backends:\n * - **WebCodecs** (preferred): Streaming H.264 encoding via VideoEncoder + mp4-muxer.\n * Each frame is encoded as it arrives — minimal memory footprint.\n * - **ffmpeg.wasm** (fallback): Batch encoding in ~10-second segments.\n * Used when WebCodecs is unavailable (older browsers).\n *\n * Frames arrive as ImageBitmap (zero-copy transfer from main thread).\n */\n\nimport type {\n MainToWorkerMessage,\n WorkerToMainMessage,\n InitMessage,\n FrameMessage,\n} from './workerTypes.js';\nimport { bitrateForQuality, ffmpegVideoQualityArgs } from '@bendyline/squisq-video';\nimport type { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';\n\nimport { createMp4Muxer, type Mp4MuxerHandle } from '../mp4Mux.js';\n\n// ── State ──────────────────────────────────────────────────────────\n\nlet backend: 'webcodecs' | 'ffmpeg-wasm' | null = null;\nlet cancelled = false;\n\n// WebCodecs state\nlet videoEncoder: VideoEncoder | null = null;\nlet muxer: Mp4MuxerHandle | null = null;\n\n// ffmpeg.wasm state\nlet ffmpegInstance: unknown = null;\nlet ffmpegFrames: Array<{ data: Uint8Array; index: number }> = [];\nlet ffmpegConfig: InitMessage | null = null;\n\n// Frame tracking\nlet totalFramesReceived = 0;\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction post(msg: WorkerToMainMessage, transfer?: Transferable[]) {\n self.postMessage(msg, { transfer: transfer ?? [] });\n}\n\nfunction postProgress(percent: number, phase: string) {\n post({ type: 'progress', percent, phase });\n}\n\nfunction postError(message: string) {\n post({ type: 'error', message });\n}\n\nfunction disposeFfmpeg(): void {\n const instance = ffmpegInstance as { terminate?: () => void } | null;\n try {\n instance?.terminate?.();\n } finally {\n ffmpegInstance = null;\n }\n}\n\n// ── Feature Detection ──────────────────────────────────────────────\n\nfunction hasWebCodecs(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined';\n}\n\nfunction hasSharedArrayBuffer(): boolean {\n return typeof SharedArrayBuffer !== 'undefined';\n}\n\n/**\n * Check whether the browser's WebCodecs implementation actually supports\n * the H.264 Baseline profile we use. Linux Chromium ships without the\n * proprietary H.264 encoder, so `VideoEncoder` exists but `configure()`\n * for `avc1.*` fails asynchronously.\n */\nasync function supportsWebCodecsH264(config: InitMessage): Promise<boolean> {\n if (!hasWebCodecs()) return false;\n try {\n const support = await VideoEncoder.isConfigSupported({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n return support.supported === true;\n } catch {\n return false;\n }\n}\n\n// ── WebCodecs Backend ──────────────────────────────────────────────\n\nfunction initWebCodecs(config: InitMessage) {\n muxer = createMp4Muxer({\n width: config.width,\n height: config.height,\n fps: config.fps,\n });\n\n videoEncoder = new VideoEncoder({\n output(chunk, meta) {\n if (cancelled) return;\n muxer!.addVideoChunk(chunk, meta ?? undefined);\n },\n error(err) {\n postError(`WebCodecs encoder error: ${err.message}`);\n },\n });\n\n // Deliberate profile split from mainThreadEncoder (avc1.640028, High@4.0):\n // this worker is the max-compatibility fallback path, so it uses H.264\n // Baseline (avc1.42001f) — widest decoder support at a small quality cost.\n videoEncoder.configure({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n}\n\nasync function encodeFrameWebCodecs(msg: FrameMessage) {\n if (!videoEncoder || cancelled) {\n msg.bitmap.close();\n return;\n }\n\n const frame = new VideoFrame(msg.bitmap, { timestamp: msg.timestamp });\n const keyFrame = msg.frameIndex % 30 === 0; // Key frame every 30 frames\n videoEncoder.encode(frame, { keyFrame });\n frame.close();\n msg.bitmap.close();\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 50),\n `Encoding frame ${totalFramesReceived}`,\n );\n}\n\nasync function finalizeWebCodecs() {\n if (!videoEncoder || !muxer || cancelled) return;\n\n postProgress(50, 'Flushing encoder…');\n await videoEncoder.flush();\n videoEncoder.close();\n videoEncoder = null;\n\n postProgress(90, 'Finalizing MP4…');\n const buffer = muxer.finalize();\n muxer = null;\n\n post({ type: 'complete', data: buffer, size: buffer.byteLength }, [buffer]);\n}\n\n// ── ffmpeg.wasm Backend ────────────────────────────────────────────\n\nasync function initFfmpegWasm(config: InitMessage) {\n ffmpegConfig = config;\n ffmpegFrames = [];\n ffmpegSegments.length = 0;\n totalFramesReceived = 0;\n\n // Lazy-load ffmpeg.wasm\n let ffmpeg: {\n load: (config?: FfmpegWasmLoadConfig) => Promise<unknown>;\n terminate: () => void;\n } | null = null;\n try {\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n ffmpeg = new FFmpeg();\n await ffmpeg.load({\n ...config.ffmpegWasm,\n classWorkerURL:\n config.ffmpegWasm?.classWorkerURL ??\n new URL('./ffmpeg.class-worker.js', import.meta.url).href,\n });\n ffmpegInstance = ffmpeg;\n } catch (err: unknown) {\n ffmpeg?.terminate();\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Failed to load ffmpeg.wasm: ${message}. ` +\n 'Ensure @ffmpeg/ffmpeg and @ffmpeg/util are installed and ' +\n 'Cross-Origin-Isolation headers (COOP/COEP) are set.',\n );\n }\n}\n\nasync function encodeFrameFfmpeg(msg: FrameMessage) {\n if (cancelled) {\n msg.bitmap.close();\n return;\n }\n\n // Convert ImageBitmap to PNG bytes via OffscreenCanvas\n const canvas = new OffscreenCanvas(msg.bitmap.width, msg.bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) {\n msg.bitmap.close();\n postError('Failed to create OffscreenCanvas 2D context');\n return;\n }\n ctx.drawImage(msg.bitmap, 0, 0);\n msg.bitmap.close();\n\n const blob = await canvas.convertToBlob({ type: 'image/png' });\n const arrayBuffer = await blob.arrayBuffer();\n ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 40),\n `Collecting frame ${totalFramesReceived}`,\n );\n\n // Batch encode every ~10 seconds worth of frames\n const batchSize = (ffmpegConfig?.fps ?? 30) * 10;\n if (ffmpegFrames.length >= batchSize) {\n await encodeFfmpegBatch();\n }\n}\n\n/** Segments already encoded as MP4 data. */\nconst ffmpegSegments: Uint8Array[] = [];\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function execOrThrow(ffmpeg: any, args: string[], operation: string): Promise<void> {\n const exitCode = await ffmpeg.exec(args);\n if (exitCode !== 0) {\n throw new Error(`${operation} failed with ffmpeg exit code ${exitCode}`);\n }\n}\n\nasync function encodeFfmpegBatch() {\n if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n const config = ffmpegConfig!;\n const batchIndex = ffmpegSegments.length;\n\n postProgress(40 + batchIndex * 5, `Encoding batch ${batchIndex + 1}…`);\n\n // Write frames to virtual filesystem\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);\n }\n\n const firstIndex = ffmpegFrames[0].index;\n const segmentName = `segment_${batchIndex}.mp4`;\n\n // Encode batch to MP4 segment\n await execOrThrow(\n ffmpeg,\n [\n '-framerate',\n String(config.fps),\n '-start_number',\n String(firstIndex),\n '-i',\n `frame_%06d.png`,\n '-frames:v',\n String(ffmpegFrames.length),\n '-c:v',\n 'libx264',\n '-pix_fmt',\n 'yuv420p',\n ...ffmpegVideoQualityArgs(config.quality),\n segmentName,\n ],\n `Encoding video segment ${batchIndex + 1}`,\n );\n\n // Read segment and clean up frames\n const segmentData = await ffmpeg.readFile(segmentName);\n ffmpegSegments.push(segmentData);\n\n // Clean up frame files\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.deleteFile(`frame_${paddedIndex}.png`);\n }\n await ffmpeg.deleteFile(segmentName);\n\n // Free batch memory\n ffmpegFrames = [];\n}\n\nasync function finalizeFfmpeg() {\n if (!ffmpegInstance || cancelled) return;\n\n // Encode any remaining frames\n await encodeFfmpegBatch();\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n\n postProgress(85, 'Concatenating segments…');\n\n let finalData: Uint8Array;\n\n if (ffmpegSegments.length === 1) {\n // Single segment — no concat needed\n finalData = ffmpegSegments[0];\n } else if (ffmpegSegments.length > 1) {\n // Write segments and concat list\n const concatList: string[] = [];\n for (let i = 0; i < ffmpegSegments.length; i++) {\n const name = `seg_${i}.mp4`;\n await ffmpeg.writeFile(name, ffmpegSegments[i]);\n concatList.push(`file '${name}'`);\n }\n await ffmpeg.writeFile('concat.txt', new TextEncoder().encode(concatList.join('\\n')));\n\n await execOrThrow(\n ffmpeg,\n ['-f', 'concat', '-safe', '0', '-i', 'concat.txt', '-c', 'copy', 'output.mp4'],\n 'Concatenating video segments',\n );\n\n finalData = await ffmpeg.readFile('output.mp4');\n\n // Clean up\n for (let i = 0; i < ffmpegSegments.length; i++) {\n await ffmpeg.deleteFile(`seg_${i}.mp4`);\n }\n await ffmpeg.deleteFile('concat.txt');\n await ffmpeg.deleteFile('output.mp4');\n } else {\n postError('No frames were encoded');\n return;\n }\n\n postProgress(95, 'Preparing download…');\n\n // Convert Uint8Array to a transferable ArrayBuffer\n const sliced = new Uint8Array(finalData).buffer as ArrayBuffer;\n post({ type: 'complete', data: sliced, size: sliced.byteLength }, [sliced]);\n\n // Clean up\n disposeFfmpeg();\n ffmpegSegments.length = 0;\n}\n\n// ── Message Handler ────────────────────────────────────────────────\n\nasync function handleMessage(msg: MainToWorkerMessage): Promise<void> {\n switch (msg.type) {\n case 'init': {\n cancelled = false;\n totalFramesReceived = 0;\n\n if (await supportsWebCodecsH264(msg)) {\n backend = 'webcodecs';\n initWebCodecs(msg);\n } else if (hasSharedArrayBuffer()) {\n backend = 'ffmpeg-wasm';\n await initFfmpegWasm(msg);\n } else {\n throw new Error(\n 'No video encoding support available. ' +\n 'WebCodecs H.264 is unavailable (typical on Linux Chromium without proprietary codecs) ' +\n 'and ffmpeg.wasm requires SharedArrayBuffer (Cross-Origin-Isolation headers).',\n );\n }\n\n post({ type: 'capabilities', backend });\n postProgress(0, 'Encoder ready');\n break;\n }\n\n case 'frame': {\n if (cancelled) {\n msg.bitmap.close();\n return;\n }\n if (backend === 'webcodecs') {\n await encodeFrameWebCodecs(msg);\n } else if (backend === 'ffmpeg-wasm') {\n await encodeFrameFfmpeg(msg);\n } else {\n msg.bitmap.close();\n throw new Error('Encoder received a frame before it was initialized');\n }\n post({ type: 'frame-complete', frameIndex: msg.frameIndex });\n break;\n }\n\n case 'finalize': {\n if (backend === 'webcodecs') {\n await finalizeWebCodecs();\n } else if (backend === 'ffmpeg-wasm') {\n await finalizeFfmpeg();\n } else {\n throw new Error('Encoder cannot finalize before it is initialized');\n }\n break;\n }\n\n case 'cancel': {\n if (videoEncoder && videoEncoder.state !== 'closed') {\n videoEncoder.close();\n videoEncoder = null;\n }\n muxer = null;\n disposeFfmpeg();\n ffmpegFrames = [];\n ffmpegSegments.length = 0;\n backend = null;\n break;\n }\n }\n}\n\n// MessageEvent callbacks are not implicitly serialized: an async frame handler\n// can still be running when finalize arrives. Keep one queue so frame ownership,\n// ffmpeg's virtual filesystem, and finalization have a single ordering boundary.\nlet messageQueue: Promise<void> = Promise.resolve();\n\nself.onmessage = (event: MessageEvent<MainToWorkerMessage>) => {\n const msg = event.data;\n // Cancellation must become visible to the currently-running operation without\n // waiting behind all queued frames. Cleanup itself remains ordered.\n if (msg.type === 'cancel') cancelled = true;\n\n messageQueue = messageQueue\n .then(() => handleMessage(msg))\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n postError(message);\n });\n};\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/const.js","../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/errors.js","../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js"],"sourcesContent":["export const MIME_TYPE_JAVASCRIPT = \"text/javascript\";\nexport const MIME_TYPE_WASM = \"application/wasm\";\nexport const CORE_VERSION = \"0.12.9\";\nexport const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;\nexport var FFMessageType;\n(function (FFMessageType) {\n FFMessageType[\"LOAD\"] = \"LOAD\";\n FFMessageType[\"EXEC\"] = \"EXEC\";\n FFMessageType[\"FFPROBE\"] = \"FFPROBE\";\n FFMessageType[\"WRITE_FILE\"] = \"WRITE_FILE\";\n FFMessageType[\"READ_FILE\"] = \"READ_FILE\";\n FFMessageType[\"DELETE_FILE\"] = \"DELETE_FILE\";\n FFMessageType[\"RENAME\"] = \"RENAME\";\n FFMessageType[\"CREATE_DIR\"] = \"CREATE_DIR\";\n FFMessageType[\"LIST_DIR\"] = \"LIST_DIR\";\n FFMessageType[\"DELETE_DIR\"] = \"DELETE_DIR\";\n FFMessageType[\"ERROR\"] = \"ERROR\";\n FFMessageType[\"DOWNLOAD\"] = \"DOWNLOAD\";\n FFMessageType[\"PROGRESS\"] = \"PROGRESS\";\n FFMessageType[\"LOG\"] = \"LOG\";\n FFMessageType[\"MOUNT\"] = \"MOUNT\";\n FFMessageType[\"UNMOUNT\"] = \"UNMOUNT\";\n})(FFMessageType || (FFMessageType = {}));\n","export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error(\"unknown message type\");\nexport const ERROR_NOT_LOADED = new Error(\"ffmpeg is not loaded, call `await ffmpeg.load()` first\");\nexport const ERROR_TERMINATED = new Error(\"called FFmpeg.terminate()\");\nexport const ERROR_IMPORT_FAILURE = new Error(\"failed to import ffmpeg-core.js\");\n","/// <reference no-default-lib=\"true\" />\n/// <reference lib=\"esnext\" />\n/// <reference lib=\"webworker\" />\nimport { CORE_URL, FFMessageType } from \"./const.js\";\nimport { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, } from \"./errors.js\";\nlet ffmpeg;\nconst load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL, }) => {\n const first = !ffmpeg;\n try {\n if (!_coreURL)\n _coreURL = CORE_URL;\n // when web worker type is `classic`.\n importScripts(_coreURL);\n }\n catch {\n if (!_coreURL || _coreURL === CORE_URL)\n _coreURL = CORE_URL.replace('/umd/', '/esm/');\n // when web worker type is `module`.\n self.createFFmpegCore = (await import(\n /* @vite-ignore */ _coreURL)).default;\n if (!self.createFFmpegCore) {\n throw ERROR_IMPORT_FAILURE;\n }\n }\n const coreURL = _coreURL;\n const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, \".wasm\");\n const workerURL = _workerURL\n ? _workerURL\n : _coreURL.replace(/.js$/g, \".worker.js\");\n ffmpeg = await self.createFFmpegCore({\n // Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core.\n // Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue.\n mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`,\n });\n ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));\n ffmpeg.setProgress((data) => self.postMessage({\n type: FFMessageType.PROGRESS,\n data,\n }));\n return first;\n};\nconst exec = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.exec(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst ffprobe = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.ffprobe(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst writeFile = ({ path, data }) => {\n ffmpeg.FS.writeFile(path, data);\n return true;\n};\nconst readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });\n// TODO: check if deletion works.\nconst deleteFile = ({ path }) => {\n ffmpeg.FS.unlink(path);\n return true;\n};\nconst rename = ({ oldPath, newPath }) => {\n ffmpeg.FS.rename(oldPath, newPath);\n return true;\n};\n// TODO: check if creation works.\nconst createDir = ({ path }) => {\n ffmpeg.FS.mkdir(path);\n return true;\n};\nconst listDir = ({ path }) => {\n const names = ffmpeg.FS.readdir(path);\n const nodes = [];\n for (const name of names) {\n const stat = ffmpeg.FS.stat(`${path}/${name}`);\n const isDir = ffmpeg.FS.isDir(stat.mode);\n nodes.push({ name, isDir });\n }\n return nodes;\n};\n// TODO: check if deletion works.\nconst deleteDir = ({ path }) => {\n ffmpeg.FS.rmdir(path);\n return true;\n};\nconst mount = ({ fsType, options, mountPoint }) => {\n const str = fsType;\n const fs = ffmpeg.FS.filesystems[str];\n if (!fs)\n return false;\n ffmpeg.FS.mount(fs, options, mountPoint);\n return true;\n};\nconst unmount = ({ mountPoint }) => {\n ffmpeg.FS.unmount(mountPoint);\n return true;\n};\nself.onmessage = async ({ data: { id, type, data: _data }, }) => {\n const trans = [];\n let data;\n try {\n if (type !== FFMessageType.LOAD && !ffmpeg)\n throw ERROR_NOT_LOADED; // eslint-disable-line\n switch (type) {\n case FFMessageType.LOAD:\n data = await load(_data);\n break;\n case FFMessageType.EXEC:\n data = exec(_data);\n break;\n case FFMessageType.FFPROBE:\n data = ffprobe(_data);\n break;\n case FFMessageType.WRITE_FILE:\n data = writeFile(_data);\n break;\n case FFMessageType.READ_FILE:\n data = readFile(_data);\n break;\n case FFMessageType.DELETE_FILE:\n data = deleteFile(_data);\n break;\n case FFMessageType.RENAME:\n data = rename(_data);\n break;\n case FFMessageType.CREATE_DIR:\n data = createDir(_data);\n break;\n case FFMessageType.LIST_DIR:\n data = listDir(_data);\n break;\n case FFMessageType.DELETE_DIR:\n data = deleteDir(_data);\n break;\n case FFMessageType.MOUNT:\n data = mount(_data);\n break;\n case FFMessageType.UNMOUNT:\n data = unmount(_data);\n break;\n default:\n throw ERROR_UNKNOWN_MESSAGE_TYPE;\n }\n }\n catch (e) {\n self.postMessage({\n id,\n type: FFMessageType.ERROR,\n data: e.toString(),\n });\n return;\n }\n if (data instanceof Uint8Array) {\n trans.push(data.buffer);\n }\n self.postMessage({ id, type, data }, trans);\n};\n"],"mappings":"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","names":["FFMessageType"]}
@@ -1,87 +0,0 @@
1
- /**
2
- * VideoExportButton — Simple button that opens the VideoExportModal in a portal.
3
- *
4
- * Convenience wrapper for consumers who want a drop-in button.
5
- * The modal renders via `createPortal` into `document.body`.
6
- */
7
-
8
- import { useState, useCallback } from 'react';
9
- import { createPortal } from 'react-dom';
10
- import type { Doc } from '@bendyline/squisq/schemas';
11
- import type { MediaProvider } from '@bendyline/squisq/schemas';
12
- import { VideoExportModal } from './VideoExportModal.js';
13
- import type { VideoExportConfig } from './hooks/useVideoExport.js';
14
-
15
- export interface VideoExportButtonProps {
16
- /** The document to export */
17
- doc: Doc;
18
- /**
19
- * Player IIFE bundle source. Unused by the browser export path (frames
20
- * are captured from a live in-page DocPlayer); only forwarded for
21
- * CLI/Playwright-style pipelines that render standalone HTML.
22
- */
23
- playerScript?: string;
24
- /** Optional media provider for resolving images/audio */
25
- mediaProvider?: MediaProvider;
26
- /** Pre-collected images map */
27
- images?: Map<string, ArrayBuffer>;
28
- /** Pre-collected audio map */
29
- audio?: Map<string, ArrayBuffer>;
30
- /**
31
- * Seeds the modal's initial output format and export settings and is merged
32
- * as a base into the config passed to the export hook. Forwarded to
33
- * {@link VideoExportModal}.
34
- */
35
- defaultConfig?: Partial<VideoExportConfig>;
36
- /** Visual color scheme forwarded to the portaled modal. Defaults to light. */
37
- colorScheme?: 'light' | 'dark';
38
- /** Button label (defaults to "Export Video", or "Export GIF" for a GIF default config) */
39
- label?: string;
40
- /** Additional inline styles for the button */
41
- style?: React.CSSProperties;
42
- /** Whether the button is disabled */
43
- disabled?: boolean;
44
- }
45
-
46
- export function VideoExportButton({
47
- doc,
48
- playerScript,
49
- mediaProvider,
50
- images,
51
- audio,
52
- defaultConfig,
53
- colorScheme,
54
- label,
55
- style,
56
- disabled,
57
- }: VideoExportButtonProps) {
58
- const [showModal, setShowModal] = useState(false);
59
- const resolvedLabel =
60
- label ?? (defaultConfig?.outputFormat === 'gif' ? 'Export GIF' : 'Export Video');
61
-
62
- const handleOpen = useCallback(() => setShowModal(true), []);
63
- const handleClose = useCallback(() => setShowModal(false), []);
64
-
65
- return (
66
- <>
67
- <button onClick={handleOpen} disabled={disabled} style={style}>
68
- {resolvedLabel}
69
- </button>
70
-
71
- {showModal &&
72
- createPortal(
73
- <VideoExportModal
74
- doc={doc}
75
- playerScript={playerScript}
76
- mediaProvider={mediaProvider}
77
- images={images}
78
- audio={audio}
79
- defaultConfig={defaultConfig}
80
- colorScheme={colorScheme}
81
- onClose={handleClose}
82
- />,
83
- document.body,
84
- )}
85
- </>
86
- );
87
- }