@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
@@ -0,0 +1,300 @@
1
+ import {
2
+ createMp4Muxer
3
+ } from "./chunk-VI5AIVOQ.js";
4
+
5
+ // src/mainThreadEncoder.ts
6
+ import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
7
+ function supportsWebCodecs() {
8
+ return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
9
+ }
10
+ async function supportsWebCodecsH264(config) {
11
+ if (!supportsWebCodecs()) return false;
12
+ try {
13
+ const support = await VideoEncoder.isConfigSupported({
14
+ codec: "avc1.640028",
15
+ width: config.width,
16
+ height: config.height,
17
+ bitrate: bitrateForQuality(config.quality, config.width, config.height),
18
+ framerate: config.fps
19
+ });
20
+ return support.supported === true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ function createEncoder(config) {
26
+ validateVideoExportOptions(config);
27
+ if (!supportsWebCodecs()) {
28
+ throw new Error(
29
+ "WebCodecs is not available in this browser. Video export requires Chrome 94+, Edge 94+, or another Chromium-based browser."
30
+ );
31
+ }
32
+ const muxer = createMp4Muxer({
33
+ width: config.width,
34
+ height: config.height,
35
+ fps: config.fps,
36
+ ...config.audio ? { audio: config.audio } : {}
37
+ });
38
+ let closed = false;
39
+ let fatalError = null;
40
+ const frameDuration = 1e6 / config.fps;
41
+ function fail(err) {
42
+ closed = true;
43
+ if (encoder.state !== "closed") encoder.close();
44
+ return fatalError ?? err;
45
+ }
46
+ const encoder = new VideoEncoder({
47
+ output(chunk, meta) {
48
+ if (closed) return;
49
+ muxer.addVideoChunk(chunk, meta ?? void 0);
50
+ },
51
+ error(err) {
52
+ fatalError ?? (fatalError = new Error(`WebCodecs encoder error: ${err.message}`));
53
+ }
54
+ });
55
+ encoder.configure({
56
+ // Deliberate profile split from the fallback worker (avc1.42001f, Baseline):
57
+ // this primary WebCodecs path targets H.264 High@4.0 for better quality up
58
+ // to 1080p; the wasm-fallback worker uses Baseline for max decoder compat.
59
+ codec: "avc1.640028",
60
+ // H.264 High profile, level 4.0 (supports up to 1080p)
61
+ width: config.width,
62
+ height: config.height,
63
+ bitrate: bitrateForQuality(config.quality, config.width, config.height),
64
+ framerate: config.fps
65
+ });
66
+ return {
67
+ async encodeFrame(bitmap, frameIndex) {
68
+ if (fatalError) {
69
+ bitmap.close();
70
+ throw fail(fatalError);
71
+ }
72
+ if (closed) {
73
+ bitmap.close();
74
+ throw new Error("Encoder already closed");
75
+ }
76
+ const timestamp = Math.round(frameIndex * frameDuration);
77
+ const frame = new VideoFrame(bitmap, { timestamp });
78
+ const keyFrame = frameIndex % 30 === 0;
79
+ encoder.encode(frame, { keyFrame });
80
+ frame.close();
81
+ bitmap.close();
82
+ },
83
+ addAudioChunk(chunk, meta) {
84
+ if (closed || !muxer.hasAudioTrack) return;
85
+ muxer.addAudioChunk(chunk, meta);
86
+ },
87
+ async finalize() {
88
+ if (closed) throw new Error("Encoder already closed");
89
+ if (fatalError) throw fail(fatalError);
90
+ try {
91
+ await encoder.flush();
92
+ } catch (err) {
93
+ throw fail(err instanceof Error ? err : new Error(String(err)));
94
+ }
95
+ if (fatalError) throw fail(fatalError);
96
+ encoder.close();
97
+ closed = true;
98
+ return muxer.finalize();
99
+ },
100
+ close() {
101
+ if (closed) return;
102
+ closed = true;
103
+ if (encoder.state !== "closed") {
104
+ encoder.close();
105
+ }
106
+ }
107
+ };
108
+ }
109
+
110
+ // src/audioTrack.ts
111
+ import {
112
+ ffmpegAudioMuxArgs,
113
+ resolveFfmpegWasmLoad
114
+ } from "@bendyline/squisq-video";
115
+ var EXPORT_AUDIO_SAMPLE_RATE = 48e3;
116
+ var EXPORT_AUDIO_CHANNELS = 2;
117
+ var REASON_NO_AAC_NO_SAB = "This browser cannot encode AAC audio (WebCodecs AudioEncoder unavailable) and the ffmpeg.wasm fallback requires cross-origin isolation (SharedArrayBuffer).";
118
+ async function supportsWebCodecsAac(sampleRate = EXPORT_AUDIO_SAMPLE_RATE, channels = EXPORT_AUDIO_CHANNELS) {
119
+ if (typeof AudioEncoder === "undefined") return false;
120
+ try {
121
+ const support = await AudioEncoder.isConfigSupported({
122
+ codec: "mp4a.40.2",
123
+ sampleRate,
124
+ numberOfChannels: channels,
125
+ bitrate: 128e3
126
+ });
127
+ return support.supported === true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function selectAudioTier(inputs) {
133
+ if (!inputs.hasClips) {
134
+ return { tier: 3, reason: null };
135
+ }
136
+ if (inputs.aacSupported && inputs.canUseMainThreadWebCodecs) {
137
+ return { tier: 1, reason: null };
138
+ }
139
+ if (inputs.sharedArrayBufferAvailable) {
140
+ return { tier: 2, reason: null };
141
+ }
142
+ return { tier: 3, reason: REASON_NO_AAC_NO_SAB };
143
+ }
144
+ function resolveOfflineAudioContext() {
145
+ const g = globalThis;
146
+ const ctor = g.OfflineAudioContext ?? g.webkitOfflineAudioContext;
147
+ if (!ctor) {
148
+ throw new Error("OfflineAudioContext is not available in this environment.");
149
+ }
150
+ return ctor;
151
+ }
152
+ async function renderAudioTimeline(clips, buffers, totalDurationSec, sampleRate = EXPORT_AUDIO_SAMPLE_RATE) {
153
+ const Ctor = resolveOfflineAudioContext();
154
+ const channels = EXPORT_AUDIO_CHANNELS;
155
+ const length = Math.max(1, Math.ceil(totalDurationSec * sampleRate));
156
+ const ctx = new Ctor(channels, length, sampleRate);
157
+ const decoded = /* @__PURE__ */ new Map();
158
+ for (const [src, data] of buffers) {
159
+ try {
160
+ decoded.set(src, await ctx.decodeAudioData(data.slice(0)));
161
+ } catch {
162
+ }
163
+ }
164
+ for (const clip of clips) {
165
+ const buffer = decoded.get(clip.src);
166
+ if (!buffer) continue;
167
+ const node = ctx.createBufferSource();
168
+ node.buffer = buffer;
169
+ node.connect(ctx.destination);
170
+ const when = Math.max(0, clip.startSec);
171
+ const offset = Math.max(0, clip.sourceInSec);
172
+ const duration = Math.max(0, clip.durationSec);
173
+ node.start(when, offset, duration);
174
+ }
175
+ return ctx.startRendering();
176
+ }
177
+ async function encodeAacTrack(audioBuffer, sink, bitrate) {
178
+ if (typeof AudioEncoder === "undefined" || typeof AudioData === "undefined") {
179
+ throw new Error("WebCodecs AudioEncoder is not available.");
180
+ }
181
+ const sampleRate = audioBuffer.sampleRate;
182
+ const channels = audioBuffer.numberOfChannels;
183
+ let encodeError = null;
184
+ const encoder = new AudioEncoder({
185
+ output: (chunk, meta) => sink.addAudioChunk(chunk, meta ?? void 0),
186
+ error: (err) => {
187
+ encodeError = err instanceof Error ? err : new Error(String(err));
188
+ }
189
+ });
190
+ encoder.configure({ codec: "mp4a.40.2", sampleRate, numberOfChannels: channels, bitrate });
191
+ const FRAME = 1024;
192
+ const total = audioBuffer.length;
193
+ const channelData = [];
194
+ for (let ch = 0; ch < channels; ch++) {
195
+ channelData.push(audioBuffer.getChannelData(ch));
196
+ }
197
+ for (let offset = 0; offset < total; offset += FRAME) {
198
+ if (encodeError) break;
199
+ const count = Math.min(FRAME, total - offset);
200
+ const planar = new Float32Array(count * channels);
201
+ for (let ch = 0; ch < channels; ch++) {
202
+ planar.set(channelData[ch].subarray(offset, offset + count), ch * count);
203
+ }
204
+ const timestamp = Math.round(offset / sampleRate * 1e6);
205
+ const audioData = new AudioData({
206
+ format: "f32-planar",
207
+ sampleRate,
208
+ numberOfFrames: count,
209
+ numberOfChannels: channels,
210
+ timestamp,
211
+ data: planar
212
+ });
213
+ encoder.encode(audioData);
214
+ audioData.close();
215
+ }
216
+ await encoder.flush();
217
+ encoder.close();
218
+ if (encodeError) throw encodeError;
219
+ }
220
+ function audioBufferToWav(buffer) {
221
+ const channels = buffer.numberOfChannels;
222
+ const sampleRate = buffer.sampleRate;
223
+ const frames = buffer.length;
224
+ const bytesPerSample = 2;
225
+ const blockAlign = channels * bytesPerSample;
226
+ const dataSize = frames * blockAlign;
227
+ const out = new ArrayBuffer(44 + dataSize);
228
+ const view = new DataView(out);
229
+ const writeStr = (offset, str) => {
230
+ for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
231
+ };
232
+ writeStr(0, "RIFF");
233
+ view.setUint32(4, 36 + dataSize, true);
234
+ writeStr(8, "WAVE");
235
+ writeStr(12, "fmt ");
236
+ view.setUint32(16, 16, true);
237
+ view.setUint16(20, 1, true);
238
+ view.setUint16(22, channels, true);
239
+ view.setUint32(24, sampleRate, true);
240
+ view.setUint32(28, sampleRate * blockAlign, true);
241
+ view.setUint16(32, blockAlign, true);
242
+ view.setUint16(34, bytesPerSample * 8, true);
243
+ writeStr(36, "data");
244
+ view.setUint32(40, dataSize, true);
245
+ const channelData = [];
246
+ for (let ch = 0; ch < channels; ch++) channelData.push(buffer.getChannelData(ch));
247
+ let pos = 44;
248
+ for (let i = 0; i < frames; i++) {
249
+ for (let ch = 0; ch < channels; ch++) {
250
+ let sample = channelData[ch][i];
251
+ sample = Math.max(-1, Math.min(1, sample));
252
+ view.setInt16(pos, sample < 0 ? sample * 32768 : sample * 32767, true);
253
+ pos += 2;
254
+ }
255
+ }
256
+ return new Uint8Array(out);
257
+ }
258
+ async function muxAudioWithFfmpegWasm(videoMp4, wav, audioBitrate, loadConfig) {
259
+ const load = resolveFfmpegWasmLoad(loadConfig, "ffmpeg.wasm audio muxing", {
260
+ classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
261
+ });
262
+ const { FFmpeg } = await import("@ffmpeg/ffmpeg");
263
+ const ffmpeg = new FFmpeg();
264
+ try {
265
+ await ffmpeg.load(load);
266
+ await ffmpeg.writeFile("video.mp4", videoMp4);
267
+ await ffmpeg.writeFile("audio.wav", wav);
268
+ const exitCode = await ffmpeg.exec([
269
+ "-i",
270
+ "video.mp4",
271
+ "-i",
272
+ "audio.wav",
273
+ "-c:v",
274
+ "copy",
275
+ ...ffmpegAudioMuxArgs(audioBitrate),
276
+ "out.mp4"
277
+ ]);
278
+ if (exitCode !== 0) {
279
+ throw new Error(`ffmpeg.wasm audio mux failed with exit code ${exitCode}`);
280
+ }
281
+ const data = await ffmpeg.readFile("out.mp4");
282
+ return data instanceof Uint8Array ? data : new TextEncoder().encode(data);
283
+ } finally {
284
+ ffmpeg.terminate();
285
+ }
286
+ }
287
+
288
+ export {
289
+ supportsWebCodecs,
290
+ supportsWebCodecsH264,
291
+ createEncoder,
292
+ EXPORT_AUDIO_SAMPLE_RATE,
293
+ EXPORT_AUDIO_CHANNELS,
294
+ supportsWebCodecsAac,
295
+ selectAudioTier,
296
+ renderAudioTimeline,
297
+ encodeAacTrack,
298
+ audioBufferToWav,
299
+ muxAudioWithFfmpegWasm
300
+ };
@@ -0,0 +1,90 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Doc, MediaProvider } from '@bendyline/squisq/schemas';
3
+ import { a as VideoExportConfig } from '../useVideoExport-NtqZVgaz.js';
4
+ import '@bendyline/squisq/markdown';
5
+ import '@bendyline/squisq-video';
6
+ import '@bendyline/squisq-react';
7
+
8
+ interface VideoExportModalProps {
9
+ /** The document to export */
10
+ doc: Doc;
11
+ /**
12
+ * Player IIFE bundle source. Unused by the browser export path (frames
13
+ * are captured from a live in-page DocPlayer); only forwarded for
14
+ * CLI/Playwright-style pipelines that render standalone HTML.
15
+ */
16
+ playerScript?: string;
17
+ /** Optional media provider for resolving images/audio */
18
+ mediaProvider?: MediaProvider;
19
+ /** Pre-collected images map (alternative to mediaProvider) */
20
+ images?: Map<string, ArrayBuffer>;
21
+ /** Pre-collected audio map */
22
+ audio?: Map<string, ArrayBuffer>;
23
+ /**
24
+ * Seeds the modal's initial format, motion, quality, FPS, orientation, and
25
+ * caption selections. It is merged (as a base) into the config passed to the
26
+ * export hook, so a host can share one config shape with `useVideoExport`.
27
+ * The individual `images`/`audio`/`mediaProvider`/`playerScript` props still
28
+ * take precedence over any matching key here.
29
+ */
30
+ defaultConfig?: Partial<VideoExportConfig>;
31
+ /** Visual color scheme for the portaled dialog. Defaults to light. */
32
+ colorScheme?: 'light' | 'dark';
33
+ /** Optional host overrides for dialog surfaces, controls, status, and accent colors. */
34
+ uiPalette?: Partial<VideoExportPalette>;
35
+ /** Called when the modal should close */
36
+ onClose: () => void;
37
+ }
38
+ interface VideoExportPalette {
39
+ overlay: string;
40
+ surface: string;
41
+ control: string;
42
+ border: string;
43
+ text: string;
44
+ heading: string;
45
+ label: string;
46
+ muted: string;
47
+ secondary: string;
48
+ primary: string;
49
+ primaryBorder: string;
50
+ primaryText: string;
51
+ success: string;
52
+ danger: string;
53
+ }
54
+ declare function VideoExportModal({ doc, playerScript, mediaProvider, images, audio, defaultConfig, colorScheme, uiPalette, onClose, }: VideoExportModalProps): react_jsx_runtime.JSX.Element;
55
+
56
+ interface VideoExportButtonProps {
57
+ /** The document to export */
58
+ doc: Doc;
59
+ /**
60
+ * Player IIFE bundle source. Unused by the browser export path (frames
61
+ * are captured from a live in-page DocPlayer); only forwarded for
62
+ * CLI/Playwright-style pipelines that render standalone HTML.
63
+ */
64
+ playerScript?: string;
65
+ /** Optional media provider for resolving images/audio */
66
+ mediaProvider?: MediaProvider;
67
+ /** Pre-collected images map */
68
+ images?: Map<string, ArrayBuffer>;
69
+ /** Pre-collected audio map */
70
+ audio?: Map<string, ArrayBuffer>;
71
+ /**
72
+ * Seeds the modal's initial output format and export settings and is merged
73
+ * as a base into the config passed to the export hook. Forwarded to
74
+ * {@link VideoExportModal}.
75
+ */
76
+ defaultConfig?: Partial<VideoExportConfig>;
77
+ /** Visual color scheme forwarded to the portaled modal. Defaults to light. */
78
+ colorScheme?: 'light' | 'dark';
79
+ /** Optional host palette overrides forwarded to the portaled modal. */
80
+ uiPalette?: Partial<VideoExportPalette>;
81
+ /** Button label (defaults to "Export Video", or "Export GIF" for a GIF default config) */
82
+ label?: string;
83
+ /** Additional inline styles for the button */
84
+ style?: React.CSSProperties;
85
+ /** Whether the button is disabled */
86
+ disabled?: boolean;
87
+ }
88
+ declare function VideoExportButton({ doc, playerScript, mediaProvider, images, audio, defaultConfig, colorScheme, uiPalette, label, style, disabled, }: VideoExportButtonProps): react_jsx_runtime.JSX.Element;
89
+
90
+ export { VideoExportButton, type VideoExportButtonProps, VideoExportModal, type VideoExportModalProps, type VideoExportPalette };
@@ -0,0 +1,11 @@
1
+ import {
2
+ VideoExportButton,
3
+ VideoExportModal
4
+ } from "../chunk-KQG6DZ7G.js";
5
+ import "../chunk-VILZP3GL.js";
6
+ import "../chunk-YZN7D4IC.js";
7
+ import "../chunk-VI5AIVOQ.js";
8
+ export {
9
+ VideoExportButton,
10
+ VideoExportModal
11
+ };
@@ -0,0 +1,95 @@
1
+ export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
2
+
3
+ /**
4
+ * Main-thread WebCodecs encoder.
5
+ *
6
+ * Encodes video frames to MP4 using the WebCodecs API and mp4-muxer,
7
+ * running directly on the main thread. This is simpler and avoids
8
+ * worker module-resolution issues with bundlers. Since frame capture
9
+ * via html2canvas (~100-200ms per frame) is the bottleneck — not
10
+ * encoding (~1ms per frame with hardware-accelerated WebCodecs) —
11
+ * worker offloading provides minimal benefit.
12
+ *
13
+ * Requirements: Chrome 94+ / Edge 94+ (WebCodecs support).
14
+ */
15
+ interface EncoderConfig {
16
+ width: number;
17
+ height: number;
18
+ fps: number;
19
+ quality: 'draft' | 'normal' | 'high';
20
+ /**
21
+ * Total frames the caller intends to submit, when known. Unused by the
22
+ * main-thread encoder (the caller owns the progress bar) but forwarded by
23
+ * {@link createWorkerEncoder} so the worker can report real progress instead
24
+ * of guessing from the frames it happens to have seen.
25
+ */
26
+ totalFrames?: number;
27
+ /**
28
+ * When present, the underlying muxer is configured with an AAC audio track
29
+ * and {@link MainThreadEncoder.addAudioChunk} becomes usable. Absent → the
30
+ * encoder produces a video-only MP4 exactly as before.
31
+ */
32
+ audio?: {
33
+ numberOfChannels: number;
34
+ sampleRate: number;
35
+ };
36
+ }
37
+ interface MainThreadEncoder {
38
+ /** Encode a single frame. The bitmap is closed after encoding. */
39
+ encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void>;
40
+ /**
41
+ * Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.
42
+ * Only valid when the encoder was created with an `audio` config; otherwise a
43
+ * no-op. Must be called before {@link finalize}.
44
+ */
45
+ addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
46
+ /** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */
47
+ finalize(): Promise<ArrayBuffer>;
48
+ /** Close the encoder without producing output (e.g., on cancel). */
49
+ close(): void;
50
+ }
51
+ /**
52
+ * Check whether the browser supports WebCodecs video encoding.
53
+ */
54
+ declare function supportsWebCodecs(): boolean;
55
+ /**
56
+ * Probe whether the WebCodecs encoder actually supports the H.264 profile
57
+ * we use. The `VideoEncoder` global can exist while the specific codec is
58
+ * unavailable — this is the case on Linux Chromium, which ships without
59
+ * the proprietary H.264 encoder.
60
+ */
61
+ declare function supportsWebCodecsH264(config: EncoderConfig): Promise<boolean>;
62
+ /**
63
+ * Create a main-thread WebCodecs encoder.
64
+ *
65
+ * Throws if WebCodecs is not available.
66
+ */
67
+ declare function createEncoder(config: EncoderConfig): MainThreadEncoder;
68
+
69
+ /**
70
+ * audioTrack — In-browser audio rendering + AAC encoding for MP4 export.
71
+ *
72
+ * Replaces the CLI's ffmpeg `adelay`/`atrim`/`amix` graph with browser-native
73
+ * primitives:
74
+ *
75
+ * - {@link supportsWebCodecsAac} probes whether the browser can encode AAC
76
+ * (WebCodecs `AudioEncoder`).
77
+ * - {@link renderAudioTimeline} mixes an {@link AudioTimelineClip} list into a
78
+ * single `AudioBuffer` using an `OfflineAudioContext` (decode → schedule →
79
+ * render).
80
+ * - {@link encodeAacTrack} feeds that buffer through a WebCodecs
81
+ * `AudioEncoder` and hands the chunks to an mp4-muxer audio track (tier 1).
82
+ * - {@link audioBufferToWav} + {@link muxAudioWithFfmpegWasm} cover the
83
+ * ffmpeg.wasm fallback (tier 2).
84
+ * - {@link selectAudioTier} is the pure capability→tier decision shared with
85
+ * `useVideoExport` (and unit-tested at the module boundary).
86
+ */
87
+
88
+ /**
89
+ * Whether the browser can encode AAC (`mp4a.40.2`) via WebCodecs.
90
+ * Returns false when `AudioEncoder` is undefined (e.g. Node/jsdom, Safari,
91
+ * older Chromium) or the config is unsupported.
92
+ */
93
+ declare function supportsWebCodecsAac(sampleRate?: number, channels?: number): Promise<boolean>;
94
+
95
+ export { type EncoderConfig, type MainThreadEncoder, createEncoder, supportsWebCodecs, supportsWebCodecsAac, supportsWebCodecsH264 };
@@ -0,0 +1,13 @@
1
+ import {
2
+ createEncoder,
3
+ supportsWebCodecs,
4
+ supportsWebCodecsAac,
5
+ supportsWebCodecsH264
6
+ } from "../chunk-YZN7D4IC.js";
7
+ import "../chunk-VI5AIVOQ.js";
8
+ export {
9
+ createEncoder,
10
+ supportsWebCodecs,
11
+ supportsWebCodecsAac,
12
+ supportsWebCodecsH264
13
+ };
@@ -0,0 +1,32 @@
1
+ export { V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportResult, c as VideoExportState, d as VideoOutputFormat, u as useVideoExport } from '../useVideoExport-NtqZVgaz.js';
2
+ import { Doc } from '@bendyline/squisq/schemas';
3
+ import { RenderHtmlOptions } from '@bendyline/squisq-video';
4
+ import { CaptionMode } from '@bendyline/squisq-react';
5
+ import '@bendyline/squisq/markdown';
6
+
7
+ /**
8
+ * useFrameCapture — Hidden div + html2canvas frame capture.
9
+ *
10
+ * Mounts a DocPlayer in renderMode inside a hidden div (same document),
11
+ * then captures individual frames by seeking the player and rendering
12
+ * the DOM to a canvas via html2canvas.
13
+ *
14
+ * Uses React directly — no script injection, no iframes, no eval.
15
+ *
16
+ * Returns an ImageBitmap for each frame (transferable to a Worker).
17
+ */
18
+
19
+ interface FrameCaptureHandle {
20
+ /** Initialize the hidden player. Returns the video duration in seconds. */
21
+ init: (doc: Doc, renderOptions: Omit<RenderHtmlOptions, 'playerScript'>, captionMode?: CaptionMode) => Promise<number>;
22
+ /** Capture a single frame at the given time (seconds). Returns an ImageBitmap. */
23
+ captureFrame: (time: number) => Promise<ImageBitmap>;
24
+ /** Clean up resources. */
25
+ destroy: () => void;
26
+ }
27
+ /**
28
+ * Hook that manages a hidden div for frame capture.
29
+ */
30
+ declare function useFrameCapture(): FrameCaptureHandle;
31
+
32
+ export { type FrameCaptureHandle, useFrameCapture };
@@ -0,0 +1,10 @@
1
+ import {
2
+ useFrameCapture,
3
+ useVideoExport
4
+ } from "../chunk-VILZP3GL.js";
5
+ import "../chunk-YZN7D4IC.js";
6
+ import "../chunk-VI5AIVOQ.js";
7
+ export {
8
+ useFrameCapture,
9
+ useVideoExport
10
+ };