@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,115 @@
1
+ import { MediaProvider, Doc } from '@bendyline/squisq/schemas';
2
+ import { ResourcePolicy } from '@bendyline/squisq/markdown';
3
+ import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
4
+ import { CaptionMode } from '@bendyline/squisq-react';
5
+
6
+ /**
7
+ * useVideoExport — Main orchestration hook for browser video export.
8
+ *
9
+ * Coordinates frame capture (hidden iframe + html2canvas) with
10
+ * main-thread WebCodecs encoding via mp4-muxer. Manages the full
11
+ * lifecycle: prepare → capture + encode → download.
12
+ *
13
+ * Encoding runs on the main thread because frame capture via html2canvas
14
+ * (~100-200ms per frame) is the bottleneck, not encoding (~1ms per frame
15
+ * with hardware-accelerated WebCodecs). Worker offloading would add
16
+ * complexity with minimal benefit.
17
+ *
18
+ * Usage:
19
+ * const { state, progress, phase, startExport, cancel, downloadUrl } = useVideoExport();
20
+ * <button onClick={() => startExport(doc, options)}>Export</button>
21
+ */
22
+
23
+ type VideoExportState = 'idle' | 'preparing' | 'capturing' | 'encoding' | 'complete' | 'error';
24
+ /** Browser export container format. */
25
+ type VideoOutputFormat = 'mp4' | 'gif';
26
+ type VideoAudioPolicy = 'require' | 'best-effort' | 'omit';
27
+ interface VideoExportConfig {
28
+ /** Output container (default: 'mp4') */
29
+ outputFormat?: VideoOutputFormat;
30
+ /**
31
+ * How authored MP4 audio is handled. `require` (default) fails before
32
+ * capture when audio cannot be loaded/decoded and fails the export if later
33
+ * encoding or muxing fails. `best-effort` explicitly permits video-only
34
+ * degradation; `omit` intentionally skips audio.
35
+ */
36
+ audioPolicy?: VideoAudioPolicy;
37
+ /** Render authored animations and slide transitions (default: true for MP4, false for GIF). */
38
+ animationsEnabled?: boolean;
39
+ /** Encoding quality preset (default: 'normal') */
40
+ quality?: VideoQuality;
41
+ /** Frames per second (default: 30) */
42
+ fps?: number;
43
+ /** Viewport orientation (default: 'landscape') */
44
+ orientation?: VideoOrientation;
45
+ /** Explicit output width. GIF defaults to 960 landscape / 540 portrait. */
46
+ width?: number;
47
+ /** Explicit output height. GIF defaults to 540 landscape / 960 portrait. */
48
+ height?: number;
49
+ /**
50
+ * Map of relative image paths to binary data.
51
+ * Used to embed images into the render HTML.
52
+ */
53
+ images?: Map<string, ArrayBuffer>;
54
+ /**
55
+ * Map of audio segment names to binary data.
56
+ * Used to embed audio into the render HTML.
57
+ */
58
+ audio?: Map<string, ArrayBuffer>;
59
+ /** MediaProvider to resolve media URLs (alternative to passing images directly) */
60
+ mediaProvider?: MediaProvider;
61
+ /** Policy and byte/time limits for MediaProvider URLs. */
62
+ resourcePolicy?: ResourcePolicy;
63
+ /** Caption mode for the exported video (default: 'off') */
64
+ captionMode?: CaptionMode;
65
+ /** Player IIFE bundle (unused in browser export, kept for CLI/Playwright path) */
66
+ playerScript?: string;
67
+ /** Optional self-hosted ffmpeg.wasm core URLs for fallback/offline/CSP use. */
68
+ ffmpegWasm?: FfmpegWasmLoadConfig;
69
+ }
70
+ interface VideoExportResult {
71
+ /** Current export state */
72
+ state: VideoExportState;
73
+ /** 0–100 progress percentage */
74
+ progress: number;
75
+ /** Human-readable description of the current phase */
76
+ phase: string;
77
+ /** Video duration detected from the doc (seconds) */
78
+ duration: number;
79
+ /** Effective output format for the current or most recent export. */
80
+ outputFormat: VideoOutputFormat;
81
+ /** Encoder backend ('webcodecs' when WebCodecs H.264 active, 'ffmpeg-wasm' when worker fallback active, null when idle) */
82
+ backend: 'webcodecs' | 'ffmpeg-wasm' | null;
83
+ /** Blob download URL (populated when state === 'complete') */
84
+ downloadUrl: string | null;
85
+ /** File size in bytes (populated when state === 'complete') */
86
+ fileSize: number;
87
+ /**
88
+ * Whether an audio track was muxed into the exported MP4 (populated when
89
+ * state === 'complete'). False when the doc had no audio or when audio was
90
+ * skipped/degraded — see {@link audioSkippedReason}.
91
+ */
92
+ audioIncluded: boolean;
93
+ /**
94
+ * Why audio is absent, when `audioIncluded` is false. `null` means the doc
95
+ * simply had no audio (not a failure); a non-null string explains a genuine
96
+ * capability shortfall or runtime failure. Audio problems never fail the
97
+ * export — the video always completes.
98
+ */
99
+ audioSkippedReason: string | null;
100
+ /** Error message (populated when state === 'error') */
101
+ error: string | null;
102
+ /** Seconds elapsed since export started */
103
+ elapsed: number;
104
+ /** Estimated seconds remaining (0 when idle or complete) */
105
+ estimatedRemaining: number;
106
+ /** Start a new export */
107
+ startExport: (doc: Doc, config: VideoExportConfig) => Promise<void>;
108
+ /** Cancel an in-progress export */
109
+ cancel: () => void;
110
+ /** Reset state back to idle (e.g., after complete or error) */
111
+ reset: () => void;
112
+ }
113
+ declare function useVideoExport(): VideoExportResult;
114
+
115
+ export { type VideoAudioPolicy as V, type VideoExportConfig as a, type VideoExportResult as b, type VideoExportState as c, type VideoOutputFormat as d, useVideoExport as u };
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  createMp4Muxer
3
- } from "../chunk-6DKLHXX3.js";
3
+ } from "../chunk-VI5AIVOQ.js";
4
4
 
5
5
  // src/workers/encode.worker.ts
6
- import { bitrateForQuality, ffmpegVideoQualityArgs } from "@bendyline/squisq-video";
6
+ import {
7
+ bitrateForQuality,
8
+ ffmpegVideoQualityArgs,
9
+ resolveFfmpegWasmLoad
10
+ } from "@bendyline/squisq-video";
7
11
  var backend = null;
8
12
  var cancelled = false;
9
13
  var videoEncoder = null;
@@ -12,19 +16,27 @@ var ffmpegInstance = null;
12
16
  var ffmpegFrames = [];
13
17
  var ffmpegConfig = null;
14
18
  var totalFramesReceived = 0;
19
+ var totalFramesExpected = 0;
15
20
  function post(msg, transfer) {
16
21
  self.postMessage(msg, { transfer: transfer ?? [] });
17
22
  }
18
23
  function postProgress(percent, phase) {
19
24
  post({ type: "progress", percent, phase });
20
25
  }
26
+ function postFrameProgress(scale, phase) {
27
+ if (totalFramesExpected <= 0) {
28
+ post({ type: "progress", percent: 0, phase, indeterminate: true });
29
+ return;
30
+ }
31
+ const ratio = Math.min(totalFramesReceived / totalFramesExpected, 1);
32
+ post({ type: "progress", percent: Math.round(ratio * scale), phase });
33
+ }
21
34
  function postError(message) {
22
35
  post({ type: "error", message });
23
36
  }
24
37
  function disposeFfmpeg() {
25
- const instance = ffmpegInstance;
26
38
  try {
27
- instance?.terminate?.();
39
+ ffmpegInstance?.terminate();
28
40
  } finally {
29
41
  ffmpegInstance = null;
30
42
  }
@@ -84,10 +96,10 @@ async function encodeFrameWebCodecs(msg) {
84
96
  frame.close();
85
97
  msg.bitmap.close();
86
98
  totalFramesReceived++;
87
- postProgress(
88
- Math.round(totalFramesReceived / (totalFramesReceived + 1) * 50),
89
- `Encoding frame ${totalFramesReceived}`
90
- );
99
+ postFrameProgress(50, frameLabel("Encoding"));
100
+ }
101
+ function frameLabel(verb) {
102
+ return totalFramesExpected > 0 ? `${verb} frame ${totalFramesReceived}/${totalFramesExpected}` : `${verb} frame ${totalFramesReceived}`;
91
103
  }
92
104
  async function finalizeWebCodecs() {
93
105
  if (!videoEncoder || !muxer || cancelled) return;
@@ -105,14 +117,14 @@ async function initFfmpegWasm(config) {
105
117
  ffmpegFrames = [];
106
118
  ffmpegSegments.length = 0;
107
119
  totalFramesReceived = 0;
120
+ const load = resolveFfmpegWasmLoad(config.ffmpegWasm, "The ffmpeg.wasm encoder fallback", {
121
+ classWorkerURL: new URL("./ffmpeg.class-worker.js", import.meta.url).href
122
+ });
108
123
  let ffmpeg = null;
109
124
  try {
110
125
  const { FFmpeg } = await import("@ffmpeg/ffmpeg");
111
126
  ffmpeg = new FFmpeg();
112
- await ffmpeg.load({
113
- ...config.ffmpegWasm,
114
- classWorkerURL: config.ffmpegWasm?.classWorkerURL ?? new URL("./ffmpeg.class-worker.js", import.meta.url).href
115
- });
127
+ await ffmpeg.load(load);
116
128
  ffmpegInstance = ffmpeg;
117
129
  } catch (err) {
118
130
  ffmpeg?.terminate();
@@ -140,10 +152,7 @@ async function encodeFrameFfmpeg(msg) {
140
152
  const arrayBuffer = await blob.arrayBuffer();
141
153
  ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });
142
154
  totalFramesReceived++;
143
- postProgress(
144
- Math.round(totalFramesReceived / (totalFramesReceived + 1) * 40),
145
- `Collecting frame ${totalFramesReceived}`
146
- );
155
+ postFrameProgress(80, frameLabel("Collecting"));
147
156
  const batchSize = (ffmpegConfig?.fps ?? 30) * 10;
148
157
  if (ffmpegFrames.length >= batchSize) {
149
158
  await encodeFfmpegBatch();
@@ -156,12 +165,19 @@ async function execOrThrow(ffmpeg, args, operation) {
156
165
  throw new Error(`${operation} failed with ffmpeg exit code ${exitCode}`);
157
166
  }
158
167
  }
168
+ async function readBinaryFile(ffmpeg, path) {
169
+ const data = await ffmpeg.readFile(path);
170
+ if (typeof data === "string") {
171
+ throw new Error(`Expected binary data from ffmpeg for ${path}`);
172
+ }
173
+ return data;
174
+ }
159
175
  async function encodeFfmpegBatch() {
160
176
  if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;
161
177
  const ffmpeg = ffmpegInstance;
162
178
  const config = ffmpegConfig;
163
179
  const batchIndex = ffmpegSegments.length;
164
- postProgress(40 + batchIndex * 5, `Encoding batch ${batchIndex + 1}\u2026`);
180
+ postFrameProgress(80, `Encoding batch ${batchIndex + 1}\u2026`);
165
181
  for (const frame of ffmpegFrames) {
166
182
  const paddedIndex = String(frame.index).padStart(6, "0");
167
183
  await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);
@@ -188,7 +204,7 @@ async function encodeFfmpegBatch() {
188
204
  ],
189
205
  `Encoding video segment ${batchIndex + 1}`
190
206
  );
191
- const segmentData = await ffmpeg.readFile(segmentName);
207
+ const segmentData = await readBinaryFile(ffmpeg, segmentName);
192
208
  ffmpegSegments.push(segmentData);
193
209
  for (const frame of ffmpegFrames) {
194
210
  const paddedIndex = String(frame.index).padStart(6, "0");
@@ -218,7 +234,7 @@ async function finalizeFfmpeg() {
218
234
  ["-f", "concat", "-safe", "0", "-i", "concat.txt", "-c", "copy", "output.mp4"],
219
235
  "Concatenating video segments"
220
236
  );
221
- finalData = await ffmpeg.readFile("output.mp4");
237
+ finalData = await readBinaryFile(ffmpeg, "output.mp4");
222
238
  for (let i = 0; i < ffmpegSegments.length; i++) {
223
239
  await ffmpeg.deleteFile(`seg_${i}.mp4`);
224
240
  }
@@ -239,6 +255,7 @@ async function handleMessage(msg) {
239
255
  case "init": {
240
256
  cancelled = false;
241
257
  totalFramesReceived = 0;
258
+ totalFramesExpected = typeof msg.totalFrames === "number" && Number.isFinite(msg.totalFrames) ? Math.max(0, Math.floor(msg.totalFrames)) : 0;
242
259
  if (await supportsWebCodecsH264(msg)) {
243
260
  backend = "webcodecs";
244
261
  initWebCodecs(msg);
@@ -289,6 +306,7 @@ async function handleMessage(msg) {
289
306
  disposeFfmpeg();
290
307
  ffmpegFrames = [];
291
308
  ffmpegSegments.length = 0;
309
+ totalFramesExpected = 0;
292
310
  backend = null;
293
311
  break;
294
312
  }
@@ -303,4 +321,3 @@ self.onmessage = (event) => {
303
321
  postError(message);
304
322
  });
305
323
  };
306
- //# sourceMappingURL=encode.worker.js.map
@@ -177,4 +177,3 @@ self.onmessage = async ({ data: { id, type, data: _data } }) => {
177
177
  }
178
178
  self.postMessage({ id, type, data }, trans);
179
179
  };
180
- //# sourceMappingURL=ffmpeg.class-worker.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video-react",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -23,6 +23,9 @@
23
23
  "access": "public"
24
24
  },
25
25
  "type": "module",
26
+ "engines": {
27
+ "node": ">=22.14.0"
28
+ },
26
29
  "main": "./dist/index.js",
27
30
  "types": "./dist/index.d.ts",
28
31
  "exports": {
@@ -30,14 +33,29 @@
30
33
  "types": "./dist/index.d.ts",
31
34
  "import": "./dist/index.js",
32
35
  "default": "./dist/index.js"
36
+ },
37
+ "./components": {
38
+ "types": "./dist/components/index.d.ts",
39
+ "import": "./dist/components/index.js"
40
+ },
41
+ "./hooks": {
42
+ "types": "./dist/hooks/index.d.ts",
43
+ "import": "./dist/hooks/index.js"
44
+ },
45
+ "./encoder": {
46
+ "types": "./dist/encoder/index.d.ts",
47
+ "import": "./dist/encoder/index.js"
33
48
  }
34
49
  },
35
50
  "files": [
36
51
  "dist",
37
- "src"
52
+ "!dist/**/*.map",
53
+ "LICENSE",
54
+ "NOTICE.md",
55
+ "COPYING.GPL-2.0.txt"
38
56
  ],
39
57
  "scripts": {
40
- "build": "tsup",
58
+ "build": "tsup && node ../../scripts/remove-empty-chunks.mjs dist",
41
59
  "dev": "concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
42
60
  "typecheck": "tsc --noEmit"
43
61
  },
@@ -46,20 +64,20 @@
46
64
  "react-dom": "^18.0.0 || ^19.0.0"
47
65
  },
48
66
  "dependencies": {
49
- "@bendyline/squisq": "2.1.0",
50
- "@bendyline/squisq-video": "2.0.2",
51
- "@bendyline/squisq-react": "2.1.0",
67
+ "@bendyline/squisq": "2.3.0",
68
+ "@bendyline/squisq-video": "2.2.0",
69
+ "@bendyline/squisq-react": "2.3.0",
52
70
  "@ffmpeg/core": "0.12.9",
53
71
  "@ffmpeg/ffmpeg": "0.12.15",
54
72
  "@ffmpeg/util": "0.12.2",
55
- "html2canvas": "1.4.1",
56
- "mp4-muxer": "5.2.2"
73
+ "html2canvas": "1.4.1"
57
74
  },
58
75
  "devDependencies": {
59
76
  "@types/react": "18.3.28",
60
77
  "@types/react-dom": "18.3.7",
61
78
  "react": "18.3.1",
62
79
  "react-dom": "18.3.1",
80
+ "mp4-muxer": "5.2.2",
63
81
  "tsup": "8.5.1",
64
82
  "typescript": "5.9.3"
65
83
  },
@@ -1,47 +0,0 @@
1
- // src/mp4Mux.ts
2
- import { Muxer, ArrayBufferTarget } from "mp4-muxer";
3
- function createMp4Muxer(options) {
4
- const target = new ArrayBufferTarget();
5
- const muxer = new Muxer({
6
- target,
7
- video: {
8
- codec: "avc",
9
- width: options.width,
10
- height: options.height
11
- },
12
- // Only declare the audio track when requested so the video-only
13
- // configuration stays byte-identical to the historical output.
14
- ...options.audio ? {
15
- audio: {
16
- codec: "aac",
17
- numberOfChannels: options.audio.numberOfChannels,
18
- sampleRate: options.audio.sampleRate
19
- }
20
- } : {},
21
- fastStart: "in-memory"
22
- });
23
- return {
24
- hasAudioTrack: options.audio !== void 0,
25
- addVideoChunk(chunk, meta) {
26
- muxer.addVideoChunk(chunk, meta);
27
- },
28
- addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta) {
29
- muxer.addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta);
30
- },
31
- addAudioChunk(chunk, meta) {
32
- muxer.addAudioChunk(chunk, meta);
33
- },
34
- addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta) {
35
- muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);
36
- },
37
- finalize() {
38
- muxer.finalize();
39
- return target.buffer;
40
- }
41
- };
42
- }
43
-
44
- export {
45
- createMp4Muxer
46
- };
47
- //# sourceMappingURL=chunk-6DKLHXX3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/mp4Mux.ts"],"sourcesContent":["/**\n * mp4Mux — Thin wrapper around mp4-muxer for WebCodecs encoding.\n *\n * Creates a Muxer instance configured for H.264 video (and, optionally, an\n * AAC audio track), accumulates encoded chunks, and produces a final MP4\n * ArrayBuffer. When no `audio` option is supplied the muxer configuration is\n * byte-identical to the video-only path.\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer';\n\nexport interface Mp4MuxerOptions {\n width: number;\n height: number;\n fps: number;\n /**\n * When present, declares an AAC audio track alongside the video track.\n * Feed encoded audio via {@link Mp4MuxerHandle.addAudioChunk} (from a\n * WebCodecs `AudioEncoder`) or {@link Mp4MuxerHandle.addAudioChunkRaw}.\n */\n audio?: {\n numberOfChannels: number;\n sampleRate: number;\n };\n}\n\nexport interface Mp4MuxerHandle {\n /** Add an encoded video chunk to the muxer. */\n addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void;\n /**\n * Add a raw video sample (bytes + timing) without an `EncodedVideoChunk`.\n * Useful where no `VideoEncoder` instance is available (e.g. Node tests).\n */\n addVideoChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedVideoChunkMetadata,\n ): void;\n /** Add an encoded audio chunk (from a WebCodecs `AudioEncoder`). */\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;\n /**\n * Add a raw audio sample (bytes + timing) without an `EncodedAudioChunk`.\n * Useful where no `AudioEncoder` instance is available (e.g. Node tests).\n */\n addAudioChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedAudioChunkMetadata,\n ): void;\n /** Whether this muxer was created with an audio track. */\n readonly hasAudioTrack: boolean;\n /** Finalize and return the MP4 as an ArrayBuffer. */\n finalize(): ArrayBuffer;\n}\n\n/**\n * Create an MP4 muxer configured for H.264 video, plus an optional AAC track.\n */\nexport function createMp4Muxer(options: Mp4MuxerOptions): Mp4MuxerHandle {\n const target = new ArrayBufferTarget();\n\n const muxer = new Muxer({\n target,\n video: {\n codec: 'avc',\n width: options.width,\n height: options.height,\n },\n // Only declare the audio track when requested so the video-only\n // configuration stays byte-identical to the historical output.\n ...(options.audio\n ? {\n audio: {\n codec: 'aac' as const,\n numberOfChannels: options.audio.numberOfChannels,\n sampleRate: options.audio.sampleRate,\n },\n }\n : {}),\n fastStart: 'in-memory',\n });\n\n return {\n hasAudioTrack: options.audio !== undefined,\n\n addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {\n muxer.addVideoChunk(chunk, meta);\n },\n\n addVideoChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedVideoChunkMetadata,\n ) {\n muxer.addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta);\n },\n\n addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {\n muxer.addAudioChunk(chunk, meta);\n },\n\n addAudioChunkRaw(\n data: Uint8Array,\n type: 'key' | 'delta',\n timestampMicros: number,\n durationMicros: number,\n meta?: EncodedAudioChunkMetadata,\n ) {\n muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);\n },\n\n finalize(): ArrayBuffer {\n muxer.finalize();\n return target.buffer;\n },\n };\n}\n"],"mappings":";AASA,SAAS,OAAO,yBAAyB;AAqDlC,SAAS,eAAe,SAA0C;AACvE,QAAM,SAAS,IAAI,kBAAkB;AAErC,QAAM,QAAQ,IAAI,MAAM;AAAA,IACtB;AAAA,IACA,OAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,IAClB;AAAA;AAAA;AAAA,IAGA,GAAI,QAAQ,QACR;AAAA,MACE,OAAO;AAAA,QACL,OAAO;AAAA,QACP,kBAAkB,QAAQ,MAAM;AAAA,QAChC,YAAY,QAAQ,MAAM;AAAA,MAC5B;AAAA,IACF,IACA,CAAC;AAAA,IACL,WAAW;AAAA,EACb,CAAC;AAED,SAAO;AAAA,IACL,eAAe,QAAQ,UAAU;AAAA,IAEjC,cAAc,OAA0B,MAAkC;AACxE,YAAM,cAAc,OAAO,IAAI;AAAA,IACjC;AAAA,IAEA,iBACE,MACA,MACA,iBACA,gBACA,MACA;AACA,YAAM,iBAAiB,MAAM,MAAM,iBAAiB,gBAAgB,IAAI;AAAA,IAC1E;AAAA,IAEA,cAAc,OAA0B,MAAkC;AACxE,YAAM,cAAc,OAAO,IAAI;AAAA,IACjC;AAAA,IAEA,iBACE,MACA,MACA,iBACA,gBACA,MACA;AACA,YAAM,iBAAiB,MAAM,MAAM,iBAAiB,gBAAgB,IAAI;AAAA,IAC1E;AAAA,IAEA,WAAwB;AACtB,YAAM,SAAS;AACf,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}