@bendyline/squisq-video-react 2.2.10 → 2.2.11

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.
@@ -1905,8 +1905,73 @@ ensureNotFinalized_fn = function() {
1905
1905
  };
1906
1906
 
1907
1907
  // src/mp4Mux.ts
1908
+ var ChunkedMp4Output = class {
1909
+ constructor() {
1910
+ this.writes = [];
1911
+ this.length = 0;
1912
+ }
1913
+ write(data, position) {
1914
+ const owned = new Uint8Array(data);
1915
+ const end = position + owned.byteLength;
1916
+ if (position >= this.length) {
1917
+ this.writes.push({ position, data: owned });
1918
+ this.length = end;
1919
+ return;
1920
+ }
1921
+ const updated = [];
1922
+ for (const existing of this.writes) {
1923
+ const existingEnd = existing.position + existing.data.byteLength;
1924
+ if (existingEnd <= position || existing.position >= end) {
1925
+ updated.push(existing);
1926
+ continue;
1927
+ }
1928
+ if (existing.position < position) {
1929
+ updated.push({
1930
+ position: existing.position,
1931
+ data: existing.data.subarray(0, position - existing.position)
1932
+ });
1933
+ }
1934
+ if (existingEnd > end) {
1935
+ updated.push({
1936
+ position: end,
1937
+ data: existing.data.subarray(end - existing.position)
1938
+ });
1939
+ }
1940
+ }
1941
+ updated.push({ position, data: owned });
1942
+ updated.sort((left, right) => left.position - right.position);
1943
+ this.writes = updated;
1944
+ this.length = Math.max(this.length, end);
1945
+ }
1946
+ toArrayBuffer() {
1947
+ const output = new Uint8Array(this.length);
1948
+ for (const write of this.writes) output.set(write.data, write.position);
1949
+ this.release();
1950
+ return output.buffer;
1951
+ }
1952
+ toBlob() {
1953
+ const parts = [];
1954
+ let position = 0;
1955
+ for (const write of this.writes) {
1956
+ if (write.position > position) parts.push(new Uint8Array(write.position - position));
1957
+ parts.push(write.data);
1958
+ position = write.position + write.data.byteLength;
1959
+ }
1960
+ if (position < this.length) parts.push(new Uint8Array(this.length - position));
1961
+ const blob = new Blob(parts, { type: "video/mp4" });
1962
+ this.release();
1963
+ return blob;
1964
+ }
1965
+ release() {
1966
+ this.writes = [];
1967
+ this.length = 0;
1968
+ }
1969
+ };
1908
1970
  function createMp4Muxer(options) {
1909
- const target = new ArrayBufferTarget();
1971
+ const output = new ChunkedMp4Output();
1972
+ const target = new StreamTarget({
1973
+ onData: (data, position) => output.write(data, position)
1974
+ });
1910
1975
  const muxer = new Muxer({
1911
1976
  target,
1912
1977
  video: {
@@ -1930,6 +1995,12 @@ function createMp4Muxer(options) {
1930
1995
  // releases the sample payload immediately.
1931
1996
  fastStart: false
1932
1997
  });
1998
+ let finalized = false;
1999
+ const finalizeMuxer = () => {
2000
+ if (finalized) throw new Error("MP4 muxer already finalized");
2001
+ muxer.finalize();
2002
+ finalized = true;
2003
+ };
1933
2004
  return {
1934
2005
  hasAudioTrack: options.audio !== void 0,
1935
2006
  addVideoChunk(chunk, meta) {
@@ -1945,8 +2016,12 @@ function createMp4Muxer(options) {
1945
2016
  muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);
1946
2017
  },
1947
2018
  finalize() {
1948
- muxer.finalize();
1949
- return target.buffer;
2019
+ finalizeMuxer();
2020
+ return output.toArrayBuffer();
2021
+ },
2022
+ finalizeBlob() {
2023
+ finalizeMuxer();
2024
+ return output.toBlob();
1950
2025
  }
1951
2026
  };
1952
2027
  }
@@ -2,7 +2,7 @@ import {
2
2
  applyWebCodecsBackpressure,
3
3
  createMp4Muxer,
4
4
  resolveWebCodecsQueueLimit
5
- } from "./chunk-MEPETH5V.js";
5
+ } from "./chunk-I4SXMCDF.js";
6
6
 
7
7
  // src/mainThreadEncoder.ts
8
8
  import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
@@ -24,6 +24,14 @@ async function supportsWebCodecsH264(config) {
24
24
  return false;
25
25
  }
26
26
  }
27
+ var RECLAIMED_CODEC_ERROR = /codec reclaimed due to inactivity/i;
28
+ var MAX_CODEC_RECOVERY_ATTEMPTS = 2;
29
+ function toError(caught) {
30
+ return caught instanceof Error ? caught : new Error(String(caught));
31
+ }
32
+ function isReclaimedCodecError(error) {
33
+ return RECLAIMED_CODEC_ERROR.test(error.message);
34
+ }
27
35
  function createEncoder(config) {
28
36
  validateVideoExportOptions(config);
29
37
  if (!supportsWebCodecs()) {
@@ -39,60 +47,136 @@ function createEncoder(config) {
39
47
  });
40
48
  let closed = false;
41
49
  let fatalError = null;
50
+ let recoverableError = null;
42
51
  const frameDuration = 1e6 / config.fps;
43
52
  const queueLimit = resolveWebCodecsQueueLimit(config);
44
53
  let framesSinceFlush = 0;
54
+ let forceNextKeyFrame = false;
55
+ let encoderGeneration = 0;
56
+ let encoder;
45
57
  function fail(err) {
46
58
  closed = true;
47
59
  if (encoder.state !== "closed") encoder.close();
48
60
  return fatalError ?? err;
49
61
  }
50
- const encoder = new VideoEncoder({
51
- output(chunk, meta) {
52
- if (closed) return;
53
- muxer.addVideoChunk(chunk, meta ?? void 0);
54
- },
55
- error(err) {
56
- fatalError ?? (fatalError = new Error(`WebCodecs encoder error: ${err.message}`));
57
- }
58
- });
59
- encoder.configure({
60
- // Deliberate profile split from the fallback worker (avc1.42001f, Baseline):
61
- // this primary WebCodecs path targets H.264 High@4.0 for better quality up
62
- // to 1080p; the wasm-fallback worker uses Baseline for max decoder compat.
62
+ const videoEncoderConfig = {
63
+ // Deliberate profile split from the fallback worker (avc1.42001f,
64
+ // Baseline): this primary WebCodecs path targets H.264 High@4.0 for
65
+ // better quality up to 1080p; the wasm-fallback worker uses Baseline for
66
+ // maximum decoder compatibility.
63
67
  codec: "avc1.640028",
64
- // H.264 High profile, level 4.0 (supports up to 1080p)
65
68
  width: config.width,
66
69
  height: config.height,
67
70
  bitrate: bitrateForQuality(config.quality, config.width, config.height),
68
71
  framerate: config.fps
69
- });
70
- return {
71
- async encodeFrame(source, frameIndex) {
72
- if (fatalError) {
73
- if ("close" in source) source.close();
74
- throw fail(fatalError);
72
+ };
73
+ function createConfiguredVideoEncoder() {
74
+ const generation = ++encoderGeneration;
75
+ const nextEncoder = new VideoEncoder({
76
+ output(chunk, meta) {
77
+ if (closed || generation !== encoderGeneration) return;
78
+ muxer.addVideoChunk(chunk, meta ?? void 0);
79
+ },
80
+ error(err) {
81
+ if (closed || generation !== encoderGeneration || fatalError || recoverableError) return;
82
+ const wrapped = new Error(`WebCodecs encoder error: ${err.message}`);
83
+ if (isReclaimedCodecError(wrapped)) {
84
+ recoverableError = wrapped;
85
+ } else {
86
+ fatalError = wrapped;
87
+ }
75
88
  }
76
- if (closed) {
77
- if ("close" in source) source.close();
78
- throw new Error("Encoder already closed");
89
+ });
90
+ nextEncoder.configure(videoEncoderConfig);
91
+ return nextEncoder;
92
+ }
93
+ function recoverCodec() {
94
+ const previousEncoder = encoder;
95
+ recoverableError = null;
96
+ if (previousEncoder.state !== "closed") previousEncoder.close();
97
+ encoder = createConfiguredVideoEncoder();
98
+ framesSinceFlush = 0;
99
+ forceNextKeyFrame = true;
100
+ }
101
+ encoder = createConfiguredVideoEncoder();
102
+ async function finishVideoEncoding() {
103
+ if (closed) throw new Error("Encoder already closed");
104
+ let recoveryAttempts = 0;
105
+ while (true) {
106
+ if (fatalError) throw fail(fatalError);
107
+ if (recoverableError) {
108
+ if (recoveryAttempts >= MAX_CODEC_RECOVERY_ATTEMPTS) {
109
+ throw fail(recoverableError);
110
+ }
111
+ recoverCodec();
112
+ recoveryAttempts++;
79
113
  }
80
114
  try {
81
- const drained = await applyWebCodecsBackpressure(encoder, queueLimit, framesSinceFlush);
82
- if (drained) framesSinceFlush = 0;
83
- if (fatalError) throw fail(fatalError);
84
- if (closed) throw new Error("Encoder already closed");
85
- const timestamp = Math.round(frameIndex * frameDuration);
86
- const frame = new VideoFrame(source, { timestamp });
115
+ await encoder.flush();
116
+ } catch (caught) {
117
+ const error = toError(caught);
118
+ if (!recoverableError && isReclaimedCodecError(error)) {
119
+ recoverableError = error;
120
+ }
121
+ if (recoverableError && recoveryAttempts < MAX_CODEC_RECOVERY_ATTEMPTS) continue;
122
+ throw fail(fatalError ?? recoverableError ?? error);
123
+ }
124
+ if (fatalError) throw fail(fatalError);
125
+ if (recoverableError) continue;
126
+ break;
127
+ }
128
+ encoder.close();
129
+ closed = true;
130
+ }
131
+ return {
132
+ async encodeFrame(source, frameIndex) {
133
+ try {
87
134
  try {
88
- const keyFrame = frameIndex % 30 === 0;
89
- encoder.encode(frame, { keyFrame });
90
- framesSinceFlush++;
91
- } finally {
92
- frame.close();
135
+ let recoveryAttempts = 0;
136
+ while (true) {
137
+ if (fatalError) throw fail(fatalError);
138
+ if (closed) throw new Error("Encoder already closed");
139
+ if (recoverableError) {
140
+ if (recoveryAttempts >= MAX_CODEC_RECOVERY_ATTEMPTS) {
141
+ throw fail(recoverableError);
142
+ }
143
+ recoverCodec();
144
+ recoveryAttempts++;
145
+ }
146
+ try {
147
+ const drained = await applyWebCodecsBackpressure(
148
+ encoder,
149
+ queueLimit,
150
+ framesSinceFlush
151
+ );
152
+ if (drained) framesSinceFlush = 0;
153
+ if (fatalError) throw fail(fatalError);
154
+ if (recoverableError) continue;
155
+ if (closed) throw new Error("Encoder already closed");
156
+ const timestamp = Math.round(frameIndex * frameDuration);
157
+ const frame = new VideoFrame(source, { timestamp });
158
+ try {
159
+ const keyFrame = forceNextKeyFrame || frameIndex % 30 === 0;
160
+ encoder.encode(frame, { keyFrame });
161
+ framesSinceFlush++;
162
+ forceNextKeyFrame = false;
163
+ } finally {
164
+ frame.close();
165
+ }
166
+ if (recoverableError) continue;
167
+ return;
168
+ } catch (caught) {
169
+ const error = toError(caught);
170
+ if (!recoverableError && isReclaimedCodecError(error)) {
171
+ recoverableError = error;
172
+ }
173
+ if (recoverableError && recoveryAttempts < MAX_CODEC_RECOVERY_ATTEMPTS) continue;
174
+ throw fail(fatalError ?? recoverableError ?? error);
175
+ }
176
+ }
177
+ } catch (caught) {
178
+ throw fail(toError(caught));
93
179
  }
94
- } catch (err) {
95
- throw fail(err instanceof Error ? err : new Error(String(err)));
96
180
  } finally {
97
181
  if ("close" in source) source.close();
98
182
  }
@@ -102,18 +186,13 @@ function createEncoder(config) {
102
186
  muxer.addAudioChunk(chunk, meta);
103
187
  },
104
188
  async finalize() {
105
- if (closed) throw new Error("Encoder already closed");
106
- if (fatalError) throw fail(fatalError);
107
- try {
108
- await encoder.flush();
109
- } catch (err) {
110
- throw fail(err instanceof Error ? err : new Error(String(err)));
111
- }
112
- if (fatalError) throw fail(fatalError);
113
- encoder.close();
114
- closed = true;
189
+ await finishVideoEncoding();
115
190
  return muxer.finalize();
116
191
  },
192
+ async finalizeBlob() {
193
+ await finishVideoEncoding();
194
+ return muxer.finalizeBlob();
195
+ },
117
196
  close() {
118
197
  if (closed) return;
119
198
  closed = true;
@@ -186,25 +265,35 @@ async function renderAudioTimeline(clips, buffers, totalDurationSec, sampleRate
186
265
  if (requiredFailures.length > 0) {
187
266
  throw new Error(`No decodable audio track was found in: ${requiredFailures.join(", ")}`);
188
267
  }
189
- let scheduledNodes = 0;
190
- for (const clip of clips) {
191
- const buffer = decoded.get(clip.src);
192
- if (!buffer) {
193
- if (clip.sourceKind === "video") continue;
194
- throw new Error(`Audio source was not decoded: ${clip.src}`);
268
+ const scheduledNodes = [];
269
+ try {
270
+ for (const clip of clips) {
271
+ const buffer = decoded.get(clip.src);
272
+ if (!buffer) {
273
+ if (clip.sourceKind === "video") continue;
274
+ throw new Error(`Audio source was not decoded: ${clip.src}`);
275
+ }
276
+ const node = ctx.createBufferSource();
277
+ node.buffer = buffer;
278
+ node.connect(ctx.destination);
279
+ const when = Math.max(0, clip.startSec);
280
+ const offset = Math.max(0, clip.sourceInSec);
281
+ const duration = Math.max(0, clip.durationSec);
282
+ node.start(when, offset, duration);
283
+ scheduledNodes.push(node);
195
284
  }
196
- const node = ctx.createBufferSource();
197
- node.buffer = buffer;
198
- node.connect(ctx.destination);
199
- const when = Math.max(0, clip.startSec);
200
- const offset = Math.max(0, clip.sourceInSec);
201
- const duration = Math.max(0, clip.durationSec);
202
- node.start(when, offset, duration);
203
- scheduledNodes++;
285
+ if (scheduledNodes.length === 0) return null;
286
+ return await ctx.startRendering();
287
+ } finally {
288
+ for (const node of scheduledNodes) {
289
+ node.disconnect();
290
+ node.buffer = null;
291
+ }
292
+ decoded.clear();
204
293
  }
205
- if (scheduledNodes === 0) return null;
206
- return ctx.startRendering();
207
294
  }
295
+ var AAC_FRAME_SAMPLES = 1024;
296
+ var MAX_AAC_QUEUE_SECONDS = 2;
208
297
  async function encodeAacTrack(audioBuffer, sink, bitrate) {
209
298
  if (typeof AudioEncoder === "undefined" || typeof AudioData === "undefined") {
210
299
  throw new Error("WebCodecs AudioEncoder is not available.");
@@ -219,34 +308,47 @@ async function encodeAacTrack(audioBuffer, sink, bitrate) {
219
308
  }
220
309
  });
221
310
  encoder.configure({ codec: "mp4a.40.2", sampleRate, numberOfChannels: channels, bitrate });
222
- const FRAME = 1024;
223
311
  const total = audioBuffer.length;
312
+ const queueLimit = Math.max(
313
+ 1,
314
+ Math.ceil(sampleRate * MAX_AAC_QUEUE_SECONDS / AAC_FRAME_SAMPLES)
315
+ );
224
316
  const channelData = [];
225
317
  for (let ch = 0; ch < channels; ch++) {
226
318
  channelData.push(audioBuffer.getChannelData(ch));
227
319
  }
228
- for (let offset = 0; offset < total; offset += FRAME) {
229
- if (encodeError) break;
230
- const count = Math.min(FRAME, total - offset);
231
- const planar = new Float32Array(count * channels);
232
- for (let ch = 0; ch < channels; ch++) {
233
- planar.set(channelData[ch].subarray(offset, offset + count), ch * count);
320
+ try {
321
+ for (let offset = 0; offset < total; offset += AAC_FRAME_SAMPLES) {
322
+ if (encodeError) throw encodeError;
323
+ if (encoder.encodeQueueSize >= queueLimit) {
324
+ await encoder.flush();
325
+ if (encodeError) throw encodeError;
326
+ }
327
+ const count = Math.min(AAC_FRAME_SAMPLES, total - offset);
328
+ const planar = new Float32Array(count * channels);
329
+ for (let ch = 0; ch < channels; ch++) {
330
+ planar.set(channelData[ch].subarray(offset, offset + count), ch * count);
331
+ }
332
+ const timestamp = Math.round(offset / sampleRate * 1e6);
333
+ const audioData = new AudioData({
334
+ format: "f32-planar",
335
+ sampleRate,
336
+ numberOfFrames: count,
337
+ numberOfChannels: channels,
338
+ timestamp,
339
+ data: planar
340
+ });
341
+ try {
342
+ encoder.encode(audioData);
343
+ } finally {
344
+ audioData.close();
345
+ }
234
346
  }
235
- const timestamp = Math.round(offset / sampleRate * 1e6);
236
- const audioData = new AudioData({
237
- format: "f32-planar",
238
- sampleRate,
239
- numberOfFrames: count,
240
- numberOfChannels: channels,
241
- timestamp,
242
- data: planar
243
- });
244
- encoder.encode(audioData);
245
- audioData.close();
347
+ await encoder.flush();
348
+ if (encodeError) throw encodeError;
349
+ } finally {
350
+ if (encoder.state !== "closed") encoder.close();
246
351
  }
247
- await encoder.flush();
248
- encoder.close();
249
- if (encodeError) throw encodeError;
250
352
  }
251
353
  function audioBufferToWav(buffer) {
252
354
  const channels = buffer.numberOfChannels;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useVideoExport
3
- } from "./chunk-GLOLS2CQ.js";
3
+ } from "./chunk-F2XUI32B.js";
4
4
 
5
5
  // src/VideoExportModal.tsx
6
6
  import { useState, useCallback, useId, useRef } from "react";
@@ -174,7 +174,7 @@ function VideoExportModal({
174
174
  const initialOutputFormat = defaultConfig?.outputFormat ?? "mp4";
175
175
  const [outputFormat, setOutputFormat] = useState(initialOutputFormat);
176
176
  const [quality, setQuality] = useState(defaultConfig?.quality ?? "normal");
177
- const [fps, setFps] = useState(defaultConfig?.fps ?? (initialOutputFormat === "gif" ? 10 : 24));
177
+ const [fps, setFps] = useState(defaultConfig?.fps ?? (initialOutputFormat === "gif" ? 10 : 30));
178
178
  const [orientation, setOrientation] = useState(
179
179
  defaultConfig?.orientation ?? "landscape"
180
180
  );
@@ -251,7 +251,7 @@ function VideoExportModal({
251
251
  setAnimationsEnabled(false);
252
252
  setCaptionMode("standard");
253
253
  } else {
254
- setFps(24);
254
+ setFps(30);
255
255
  setAnimationsEnabled(true);
256
256
  setCaptionMode("off");
257
257
  }
@@ -1,10 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Doc, MediaProvider } from '@bendyline/squisq/schemas';
3
- import { a as VideoExportConfig, e as VideoOutputFormat } from '../useVideoExport-raCbwbwb.js';
3
+ import { a as VideoExportConfig, e as VideoOutputFormat } from '../useVideoExport-CM5XiM6Z.js';
4
4
  import '@bendyline/squisq/markdown';
5
5
  import '@bendyline/squisq-video';
6
6
  import '@bendyline/squisq-react';
7
- import '../mainThreadEncoder-BgcFyYvO.js';
7
+ import '../mainThreadEncoder-CiVsL1Bf.js';
8
8
 
9
9
  interface VideoExportModalProps {
10
10
  /** The document to export */
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "../chunk-ERG6OLLO.js";
5
- import "../chunk-GLOLS2CQ.js";
6
- import "../chunk-2XACUF6E.js";
7
- import "../chunk-MEPETH5V.js";
4
+ } from "../chunk-KW5BBYKP.js";
5
+ import "../chunk-F2XUI32B.js";
6
+ import "../chunk-KJ5RKG67.js";
7
+ import "../chunk-I4SXMCDF.js";
8
8
  export {
9
9
  VideoExportButton,
10
10
  VideoExportModal
@@ -1,4 +1,4 @@
1
- export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from '../mainThreadEncoder-BgcFyYvO.js';
1
+ export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from '../mainThreadEncoder-CiVsL1Bf.js';
2
2
  export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
3
3
 
4
4
  /**
@@ -3,8 +3,8 @@ import {
3
3
  supportsWebCodecs,
4
4
  supportsWebCodecsAac,
5
5
  supportsWebCodecsH264
6
- } from "../chunk-2XACUF6E.js";
7
- import "../chunk-MEPETH5V.js";
6
+ } from "../chunk-KJ5RKG67.js";
7
+ import "../chunk-I4SXMCDF.js";
8
8
  export {
9
9
  createEncoder,
10
10
  supportsWebCodecs,
@@ -1,9 +1,9 @@
1
- export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from '../useVideoExport-raCbwbwb.js';
1
+ export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from '../useVideoExport-CM5XiM6Z.js';
2
2
  import { Doc } from '@bendyline/squisq/schemas';
3
3
  import { RenderHtmlOptions } from '@bendyline/squisq-video';
4
4
  import { CaptionMode } from '@bendyline/squisq-react';
5
5
  import '@bendyline/squisq/markdown';
6
- import '../mainThreadEncoder-BgcFyYvO.js';
6
+ import '../mainThreadEncoder-CiVsL1Bf.js';
7
7
 
8
8
  /**
9
9
  * useFrameCapture — Hidden div + html2canvas frame capture.
@@ -3,9 +3,9 @@ import {
3
3
  resolveVideoExportCover,
4
4
  useFrameCapture,
5
5
  useVideoExport
6
- } from "../chunk-GLOLS2CQ.js";
7
- import "../chunk-2XACUF6E.js";
8
- import "../chunk-MEPETH5V.js";
6
+ } from "../chunk-F2XUI32B.js";
7
+ import "../chunk-KJ5RKG67.js";
8
+ import "../chunk-I4SXMCDF.js";
9
9
  export {
10
10
  DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
11
11
  resolveVideoExportCover,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { VideoExportButton, VideoExportButtonProps, VideoExportModal, VideoExportModalProps, VideoExportPalette } from './components/index.js';
2
- export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from './useVideoExport-raCbwbwb.js';
2
+ export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from './useVideoExport-CM5XiM6Z.js';
3
3
  export { FrameCaptureHandle, FrameCaptureOptions, FrameCaptureRenderOptions, useFrameCapture } from './hooks/index.js';
4
- export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from './mainThreadEncoder-BgcFyYvO.js';
4
+ export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from './mainThreadEncoder-CiVsL1Bf.js';
5
5
  export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
6
6
  export { supportsWebCodecsAac } from './encoder/index.js';
7
7
  import 'react/jsx-runtime';
package/dist/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "./chunk-ERG6OLLO.js";
4
+ } from "./chunk-KW5BBYKP.js";
5
5
  import {
6
6
  DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
7
7
  resolveVideoExportCover,
8
8
  useFrameCapture,
9
9
  useVideoExport
10
- } from "./chunk-GLOLS2CQ.js";
10
+ } from "./chunk-F2XUI32B.js";
11
11
  import {
12
12
  createEncoder,
13
13
  supportsWebCodecs,
14
14
  supportsWebCodecsAac,
15
15
  supportsWebCodecsH264
16
- } from "./chunk-2XACUF6E.js";
17
- import "./chunk-MEPETH5V.js";
16
+ } from "./chunk-KJ5RKG67.js";
17
+ import "./chunk-I4SXMCDF.js";
18
18
  export {
19
19
  DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
20
20
  VideoExportButton,
@@ -44,6 +44,8 @@ interface MainThreadEncoder {
44
44
  addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
45
45
  /** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */
46
46
  finalize(): Promise<ArrayBuffer>;
47
+ /** Finalize directly into a Blob, avoiding a second full-file allocation. */
48
+ finalizeBlob?(): Promise<Blob>;
47
49
  /** Close the encoder without producing output (e.g., on cancel). */
48
50
  close(): void;
49
51
  }
@@ -2,7 +2,7 @@ import { MediaProvider, Theme, VideoPresentation, VideoPipSize, VideoPipShape, V
2
2
  import { ResourcePolicy } from '@bendyline/squisq/markdown';
3
3
  import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
4
4
  import { CaptionMode } from '@bendyline/squisq-react';
5
- import { a as EncoderFrameSource } from './mainThreadEncoder-BgcFyYvO.js';
5
+ import { a as EncoderFrameSource } from './mainThreadEncoder-CiVsL1Bf.js';
6
6
 
7
7
  /**
8
8
  * useVideoExport — Main orchestration hook for browser video export.
@@ -3,7 +3,7 @@ import {
3
3
  createMp4Muxer,
4
4
  resolveWebCodecsQueueLimit,
5
5
  shouldEncodeFfmpegBatch
6
- } from "../chunk-MEPETH5V.js";
6
+ } from "../chunk-I4SXMCDF.js";
7
7
 
8
8
  // src/workers/encode.worker.ts
9
9
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video-react",
3
- "version": "2.2.10",
3
+ "version": "2.2.11",
4
4
  "description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -67,7 +67,7 @@
67
67
  "dependencies": {
68
68
  "@bendyline/squisq": "2.4.4",
69
69
  "@bendyline/squisq-video": "2.2.8",
70
- "@bendyline/squisq-react": "2.4.6",
70
+ "@bendyline/squisq-react": "2.4.7",
71
71
  "@ffmpeg/core": "0.12.9",
72
72
  "@ffmpeg/ffmpeg": "0.12.15",
73
73
  "@ffmpeg/util": "0.12.2",