@bendyline/squisq-video-react 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +24 -33
- package/THIRD_PARTY_LICENSES.txt +60 -0
- package/dist/chunk-KQG6DZ7G.js +549 -0
- package/dist/chunk-VI5AIVOQ.js +1951 -0
- package/dist/chunk-VILZP3GL.js +856 -0
- package/dist/chunk-YZN7D4IC.js +300 -0
- package/dist/components/index.d.ts +90 -0
- package/dist/components/index.js +11 -0
- package/dist/encoder/index.d.ts +95 -0
- package/dist/encoder/index.js +13 -0
- package/dist/hooks/index.d.ts +32 -0
- package/dist/hooks/index.js +10 -0
- package/dist/index.d.ts +8 -315
- package/dist/index.js +12 -1664
- package/dist/useVideoExport-NtqZVgaz.d.ts +115 -0
- package/dist/workers/encode.worker.js +1 -1
- package/package.json +23 -7
- package/dist/chunk-2PGWBGAT.js +0 -46
package/dist/index.js
CHANGED
|
@@ -1,1670 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// src/VideoExportModal.tsx
|
|
6
|
-
import { useState as useState2, useCallback as useCallback3, useId, useRef as useRef3 } from "react";
|
|
7
|
-
import { useModalDialog } from "@bendyline/squisq-react";
|
|
8
|
-
|
|
9
|
-
// src/hooks/useVideoExport.ts
|
|
10
|
-
import { useState, useRef as useRef2, useCallback as useCallback2, useEffect } from "react";
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_INTERACTIVE_RESOURCE_POLICY,
|
|
13
|
-
fetchResourceBytes
|
|
14
|
-
} from "@bendyline/squisq/markdown";
|
|
15
|
-
import {
|
|
16
|
-
resolveDimensions,
|
|
17
|
-
computeAudioTimeline,
|
|
18
|
-
resolveFfmpegWasmLoad as resolveFfmpegWasmLoad3,
|
|
19
|
-
QUALITY_PRESETS
|
|
20
|
-
} from "@bendyline/squisq-video";
|
|
21
|
-
|
|
22
|
-
// src/mainThreadEncoder.ts
|
|
23
|
-
import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
|
|
24
|
-
function supportsWebCodecs() {
|
|
25
|
-
return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
|
|
26
|
-
}
|
|
27
|
-
async function supportsWebCodecsH264(config) {
|
|
28
|
-
if (!supportsWebCodecs()) return false;
|
|
29
|
-
try {
|
|
30
|
-
const support = await VideoEncoder.isConfigSupported({
|
|
31
|
-
codec: "avc1.640028",
|
|
32
|
-
width: config.width,
|
|
33
|
-
height: config.height,
|
|
34
|
-
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
35
|
-
framerate: config.fps
|
|
36
|
-
});
|
|
37
|
-
return support.supported === true;
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function createEncoder(config) {
|
|
43
|
-
validateVideoExportOptions(config);
|
|
44
|
-
if (!supportsWebCodecs()) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
"WebCodecs is not available in this browser. Video export requires Chrome 94+, Edge 94+, or another Chromium-based browser."
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
const muxer = createMp4Muxer({
|
|
50
|
-
width: config.width,
|
|
51
|
-
height: config.height,
|
|
52
|
-
fps: config.fps,
|
|
53
|
-
...config.audio ? { audio: config.audio } : {}
|
|
54
|
-
});
|
|
55
|
-
let closed = false;
|
|
56
|
-
let fatalError = null;
|
|
57
|
-
const frameDuration = 1e6 / config.fps;
|
|
58
|
-
function fail(err) {
|
|
59
|
-
closed = true;
|
|
60
|
-
if (encoder.state !== "closed") encoder.close();
|
|
61
|
-
return fatalError ?? err;
|
|
62
|
-
}
|
|
63
|
-
const encoder = new VideoEncoder({
|
|
64
|
-
output(chunk, meta) {
|
|
65
|
-
if (closed) return;
|
|
66
|
-
muxer.addVideoChunk(chunk, meta ?? void 0);
|
|
67
|
-
},
|
|
68
|
-
error(err) {
|
|
69
|
-
fatalError ?? (fatalError = new Error(`WebCodecs encoder error: ${err.message}`));
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
encoder.configure({
|
|
73
|
-
// Deliberate profile split from the fallback worker (avc1.42001f, Baseline):
|
|
74
|
-
// this primary WebCodecs path targets H.264 High@4.0 for better quality up
|
|
75
|
-
// to 1080p; the wasm-fallback worker uses Baseline for max decoder compat.
|
|
76
|
-
codec: "avc1.640028",
|
|
77
|
-
// H.264 High profile, level 4.0 (supports up to 1080p)
|
|
78
|
-
width: config.width,
|
|
79
|
-
height: config.height,
|
|
80
|
-
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
81
|
-
framerate: config.fps
|
|
82
|
-
});
|
|
83
|
-
return {
|
|
84
|
-
async encodeFrame(bitmap, frameIndex) {
|
|
85
|
-
if (fatalError) {
|
|
86
|
-
bitmap.close();
|
|
87
|
-
throw fail(fatalError);
|
|
88
|
-
}
|
|
89
|
-
if (closed) {
|
|
90
|
-
bitmap.close();
|
|
91
|
-
throw new Error("Encoder already closed");
|
|
92
|
-
}
|
|
93
|
-
const timestamp = Math.round(frameIndex * frameDuration);
|
|
94
|
-
const frame = new VideoFrame(bitmap, { timestamp });
|
|
95
|
-
const keyFrame = frameIndex % 30 === 0;
|
|
96
|
-
encoder.encode(frame, { keyFrame });
|
|
97
|
-
frame.close();
|
|
98
|
-
bitmap.close();
|
|
99
|
-
},
|
|
100
|
-
addAudioChunk(chunk, meta) {
|
|
101
|
-
if (closed || !muxer.hasAudioTrack) return;
|
|
102
|
-
muxer.addAudioChunk(chunk, meta);
|
|
103
|
-
},
|
|
104
|
-
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;
|
|
115
|
-
return muxer.finalize();
|
|
116
|
-
},
|
|
117
|
-
close() {
|
|
118
|
-
if (closed) return;
|
|
119
|
-
closed = true;
|
|
120
|
-
if (encoder.state !== "closed") {
|
|
121
|
-
encoder.close();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/workerEncoder.ts
|
|
128
|
-
import { validateVideoExportOptions as validateVideoExportOptions2 } from "@bendyline/squisq-video";
|
|
129
|
-
function createWorkerEncoder(config) {
|
|
130
|
-
validateVideoExportOptions2(config);
|
|
131
|
-
const worker = new Worker(new URL("./workers/encode.worker.js", import.meta.url), {
|
|
132
|
-
type: "module"
|
|
133
|
-
});
|
|
134
|
-
let state = "open";
|
|
135
|
-
let fatalError = null;
|
|
136
|
-
let finalizeResolve = null;
|
|
137
|
-
let finalizeReject = null;
|
|
138
|
-
let readyResolve = null;
|
|
139
|
-
let readyReject = null;
|
|
140
|
-
let readySettled = false;
|
|
141
|
-
const frameWaiters = /* @__PURE__ */ new Map();
|
|
142
|
-
const ready = new Promise((resolve, reject) => {
|
|
143
|
-
readyResolve = resolve;
|
|
144
|
-
readyReject = reject;
|
|
145
|
-
});
|
|
146
|
-
const frameDuration = 1e6 / config.fps;
|
|
147
|
-
function post(msg, transfer) {
|
|
148
|
-
worker.postMessage(msg, transfer ?? []);
|
|
149
|
-
}
|
|
150
|
-
const currentState = () => state;
|
|
151
|
-
worker.onmessage = (event) => {
|
|
152
|
-
const msg = event.data;
|
|
153
|
-
switch (msg.type) {
|
|
154
|
-
case "capabilities":
|
|
155
|
-
readySettled = true;
|
|
156
|
-
readyResolve?.(msg.backend);
|
|
157
|
-
readyResolve = readyReject = null;
|
|
158
|
-
break;
|
|
159
|
-
case "frame-complete": {
|
|
160
|
-
const waiter = frameWaiters.get(msg.frameIndex);
|
|
161
|
-
waiter?.resolve();
|
|
162
|
-
frameWaiters.delete(msg.frameIndex);
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
case "complete":
|
|
166
|
-
state = "closed";
|
|
167
|
-
finalizeResolve?.(msg.data);
|
|
168
|
-
finalizeResolve = finalizeReject = null;
|
|
169
|
-
worker.terminate();
|
|
170
|
-
break;
|
|
171
|
-
case "error": {
|
|
172
|
-
const err = new Error(msg.message);
|
|
173
|
-
fatalError = err;
|
|
174
|
-
state = "closed";
|
|
175
|
-
readySettled = true;
|
|
176
|
-
readyReject?.(err);
|
|
177
|
-
finalizeReject?.(err);
|
|
178
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
179
|
-
frameWaiters.clear();
|
|
180
|
-
readyResolve = readyReject = null;
|
|
181
|
-
finalizeResolve = finalizeReject = null;
|
|
182
|
-
worker.terminate();
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
worker.onerror = (event) => {
|
|
188
|
-
const err = new Error(event.message || "Worker error");
|
|
189
|
-
fatalError = err;
|
|
190
|
-
state = "closed";
|
|
191
|
-
readySettled = true;
|
|
192
|
-
readyReject?.(err);
|
|
193
|
-
finalizeReject?.(err);
|
|
194
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
195
|
-
frameWaiters.clear();
|
|
196
|
-
readyResolve = readyReject = null;
|
|
197
|
-
finalizeResolve = finalizeReject = null;
|
|
198
|
-
worker.terminate();
|
|
199
|
-
};
|
|
200
|
-
post({
|
|
201
|
-
type: "init",
|
|
202
|
-
width: config.width,
|
|
203
|
-
height: config.height,
|
|
204
|
-
fps: config.fps,
|
|
205
|
-
quality: config.quality,
|
|
206
|
-
...config.totalFrames !== void 0 ? { totalFrames: config.totalFrames } : {},
|
|
207
|
-
...config.ffmpegWasm ? { ffmpegWasm: config.ffmpegWasm } : {}
|
|
208
|
-
});
|
|
209
|
-
return {
|
|
210
|
-
ready,
|
|
211
|
-
encodeFrame(bitmap, frameIndex) {
|
|
212
|
-
if (state !== "open" || fatalError) {
|
|
213
|
-
bitmap.close();
|
|
214
|
-
return Promise.reject(fatalError ?? new Error("Encoder is not accepting frames"));
|
|
215
|
-
}
|
|
216
|
-
if (frameWaiters.has(frameIndex)) {
|
|
217
|
-
bitmap.close();
|
|
218
|
-
return Promise.reject(new Error(`Frame ${frameIndex} was submitted more than once`));
|
|
219
|
-
}
|
|
220
|
-
let resolveFrame;
|
|
221
|
-
let rejectFrame;
|
|
222
|
-
const promise = new Promise((resolve, reject) => {
|
|
223
|
-
resolveFrame = resolve;
|
|
224
|
-
rejectFrame = reject;
|
|
225
|
-
});
|
|
226
|
-
frameWaiters.set(frameIndex, { promise, resolve: resolveFrame, reject: rejectFrame });
|
|
227
|
-
const timestamp = Math.round(frameIndex * frameDuration);
|
|
228
|
-
post({ type: "frame", bitmap, frameIndex, timestamp }, [bitmap]);
|
|
229
|
-
return promise;
|
|
230
|
-
},
|
|
231
|
-
async finalize() {
|
|
232
|
-
if (state !== "open") throw new Error("Encoder already closed or finalizing");
|
|
233
|
-
if (fatalError) throw fatalError;
|
|
234
|
-
state = "finalizing";
|
|
235
|
-
await Promise.all(Array.from(frameWaiters.values(), (waiter) => waiter.promise));
|
|
236
|
-
if (currentState() === "closed") {
|
|
237
|
-
throw fatalError ?? new Error("Encoder closed during finalization");
|
|
238
|
-
}
|
|
239
|
-
return new Promise((resolve, reject) => {
|
|
240
|
-
finalizeResolve = resolve;
|
|
241
|
-
finalizeReject = reject;
|
|
242
|
-
post({ type: "finalize" });
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
close() {
|
|
246
|
-
if (state === "closed") return;
|
|
247
|
-
state = "closed";
|
|
248
|
-
const err = new Error("Encoder closed");
|
|
249
|
-
if (!readySettled) {
|
|
250
|
-
readySettled = true;
|
|
251
|
-
readyReject?.(err);
|
|
252
|
-
}
|
|
253
|
-
finalizeReject?.(err);
|
|
254
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
255
|
-
frameWaiters.clear();
|
|
256
|
-
readyResolve = readyReject = null;
|
|
257
|
-
finalizeResolve = finalizeReject = null;
|
|
258
|
-
post({ type: "cancel" });
|
|
259
|
-
worker.terminate();
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// src/audioTrack.ts
|
|
2
|
+
VideoExportButton,
|
|
3
|
+
VideoExportModal
|
|
4
|
+
} from "./chunk-KQG6DZ7G.js";
|
|
265
5
|
import {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} from "
|
|
269
|
-
var EXPORT_AUDIO_SAMPLE_RATE = 48e3;
|
|
270
|
-
var EXPORT_AUDIO_CHANNELS = 2;
|
|
271
|
-
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).";
|
|
272
|
-
async function supportsWebCodecsAac(sampleRate = EXPORT_AUDIO_SAMPLE_RATE, channels = EXPORT_AUDIO_CHANNELS) {
|
|
273
|
-
if (typeof AudioEncoder === "undefined") return false;
|
|
274
|
-
try {
|
|
275
|
-
const support = await AudioEncoder.isConfigSupported({
|
|
276
|
-
codec: "mp4a.40.2",
|
|
277
|
-
sampleRate,
|
|
278
|
-
numberOfChannels: channels,
|
|
279
|
-
bitrate: 128e3
|
|
280
|
-
});
|
|
281
|
-
return support.supported === true;
|
|
282
|
-
} catch {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
function selectAudioTier(inputs) {
|
|
287
|
-
if (!inputs.hasClips) {
|
|
288
|
-
return { tier: 3, reason: null };
|
|
289
|
-
}
|
|
290
|
-
if (inputs.aacSupported && inputs.canUseMainThreadWebCodecs) {
|
|
291
|
-
return { tier: 1, reason: null };
|
|
292
|
-
}
|
|
293
|
-
if (inputs.sharedArrayBufferAvailable) {
|
|
294
|
-
return { tier: 2, reason: null };
|
|
295
|
-
}
|
|
296
|
-
return { tier: 3, reason: REASON_NO_AAC_NO_SAB };
|
|
297
|
-
}
|
|
298
|
-
function resolveOfflineAudioContext() {
|
|
299
|
-
const g = globalThis;
|
|
300
|
-
const ctor = g.OfflineAudioContext ?? g.webkitOfflineAudioContext;
|
|
301
|
-
if (!ctor) {
|
|
302
|
-
throw new Error("OfflineAudioContext is not available in this environment.");
|
|
303
|
-
}
|
|
304
|
-
return ctor;
|
|
305
|
-
}
|
|
306
|
-
async function renderAudioTimeline(clips, buffers, totalDurationSec, sampleRate = EXPORT_AUDIO_SAMPLE_RATE) {
|
|
307
|
-
const Ctor = resolveOfflineAudioContext();
|
|
308
|
-
const channels = EXPORT_AUDIO_CHANNELS;
|
|
309
|
-
const length = Math.max(1, Math.ceil(totalDurationSec * sampleRate));
|
|
310
|
-
const ctx = new Ctor(channels, length, sampleRate);
|
|
311
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
312
|
-
for (const [src, data] of buffers) {
|
|
313
|
-
try {
|
|
314
|
-
decoded.set(src, await ctx.decodeAudioData(data.slice(0)));
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
for (const clip of clips) {
|
|
319
|
-
const buffer = decoded.get(clip.src);
|
|
320
|
-
if (!buffer) continue;
|
|
321
|
-
const node = ctx.createBufferSource();
|
|
322
|
-
node.buffer = buffer;
|
|
323
|
-
node.connect(ctx.destination);
|
|
324
|
-
const when = Math.max(0, clip.startSec);
|
|
325
|
-
const offset = Math.max(0, clip.sourceInSec);
|
|
326
|
-
const duration = Math.max(0, clip.durationSec);
|
|
327
|
-
node.start(when, offset, duration);
|
|
328
|
-
}
|
|
329
|
-
return ctx.startRendering();
|
|
330
|
-
}
|
|
331
|
-
async function encodeAacTrack(audioBuffer, sink, bitrate) {
|
|
332
|
-
if (typeof AudioEncoder === "undefined" || typeof AudioData === "undefined") {
|
|
333
|
-
throw new Error("WebCodecs AudioEncoder is not available.");
|
|
334
|
-
}
|
|
335
|
-
const sampleRate = audioBuffer.sampleRate;
|
|
336
|
-
const channels = audioBuffer.numberOfChannels;
|
|
337
|
-
let encodeError = null;
|
|
338
|
-
const encoder = new AudioEncoder({
|
|
339
|
-
output: (chunk, meta) => sink.addAudioChunk(chunk, meta ?? void 0),
|
|
340
|
-
error: (err) => {
|
|
341
|
-
encodeError = err instanceof Error ? err : new Error(String(err));
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
encoder.configure({ codec: "mp4a.40.2", sampleRate, numberOfChannels: channels, bitrate });
|
|
345
|
-
const FRAME = 1024;
|
|
346
|
-
const total = audioBuffer.length;
|
|
347
|
-
const channelData = [];
|
|
348
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
349
|
-
channelData.push(audioBuffer.getChannelData(ch));
|
|
350
|
-
}
|
|
351
|
-
for (let offset = 0; offset < total; offset += FRAME) {
|
|
352
|
-
if (encodeError) break;
|
|
353
|
-
const count = Math.min(FRAME, total - offset);
|
|
354
|
-
const planar = new Float32Array(count * channels);
|
|
355
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
356
|
-
planar.set(channelData[ch].subarray(offset, offset + count), ch * count);
|
|
357
|
-
}
|
|
358
|
-
const timestamp = Math.round(offset / sampleRate * 1e6);
|
|
359
|
-
const audioData = new AudioData({
|
|
360
|
-
format: "f32-planar",
|
|
361
|
-
sampleRate,
|
|
362
|
-
numberOfFrames: count,
|
|
363
|
-
numberOfChannels: channels,
|
|
364
|
-
timestamp,
|
|
365
|
-
data: planar
|
|
366
|
-
});
|
|
367
|
-
encoder.encode(audioData);
|
|
368
|
-
audioData.close();
|
|
369
|
-
}
|
|
370
|
-
await encoder.flush();
|
|
371
|
-
encoder.close();
|
|
372
|
-
if (encodeError) throw encodeError;
|
|
373
|
-
}
|
|
374
|
-
function audioBufferToWav(buffer) {
|
|
375
|
-
const channels = buffer.numberOfChannels;
|
|
376
|
-
const sampleRate = buffer.sampleRate;
|
|
377
|
-
const frames = buffer.length;
|
|
378
|
-
const bytesPerSample = 2;
|
|
379
|
-
const blockAlign = channels * bytesPerSample;
|
|
380
|
-
const dataSize = frames * blockAlign;
|
|
381
|
-
const out = new ArrayBuffer(44 + dataSize);
|
|
382
|
-
const view = new DataView(out);
|
|
383
|
-
const writeStr = (offset, str) => {
|
|
384
|
-
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
|
|
385
|
-
};
|
|
386
|
-
writeStr(0, "RIFF");
|
|
387
|
-
view.setUint32(4, 36 + dataSize, true);
|
|
388
|
-
writeStr(8, "WAVE");
|
|
389
|
-
writeStr(12, "fmt ");
|
|
390
|
-
view.setUint32(16, 16, true);
|
|
391
|
-
view.setUint16(20, 1, true);
|
|
392
|
-
view.setUint16(22, channels, true);
|
|
393
|
-
view.setUint32(24, sampleRate, true);
|
|
394
|
-
view.setUint32(28, sampleRate * blockAlign, true);
|
|
395
|
-
view.setUint16(32, blockAlign, true);
|
|
396
|
-
view.setUint16(34, bytesPerSample * 8, true);
|
|
397
|
-
writeStr(36, "data");
|
|
398
|
-
view.setUint32(40, dataSize, true);
|
|
399
|
-
const channelData = [];
|
|
400
|
-
for (let ch = 0; ch < channels; ch++) channelData.push(buffer.getChannelData(ch));
|
|
401
|
-
let pos = 44;
|
|
402
|
-
for (let i = 0; i < frames; i++) {
|
|
403
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
404
|
-
let sample = channelData[ch][i];
|
|
405
|
-
sample = Math.max(-1, Math.min(1, sample));
|
|
406
|
-
view.setInt16(pos, sample < 0 ? sample * 32768 : sample * 32767, true);
|
|
407
|
-
pos += 2;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return new Uint8Array(out);
|
|
411
|
-
}
|
|
412
|
-
async function muxAudioWithFfmpegWasm(videoMp4, wav, audioBitrate, loadConfig) {
|
|
413
|
-
const load = resolveFfmpegWasmLoad(loadConfig, "ffmpeg.wasm audio muxing", {
|
|
414
|
-
classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
|
|
415
|
-
});
|
|
416
|
-
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
417
|
-
const ffmpeg = new FFmpeg();
|
|
418
|
-
try {
|
|
419
|
-
await ffmpeg.load(load);
|
|
420
|
-
await ffmpeg.writeFile("video.mp4", videoMp4);
|
|
421
|
-
await ffmpeg.writeFile("audio.wav", wav);
|
|
422
|
-
const exitCode = await ffmpeg.exec([
|
|
423
|
-
"-i",
|
|
424
|
-
"video.mp4",
|
|
425
|
-
"-i",
|
|
426
|
-
"audio.wav",
|
|
427
|
-
"-c:v",
|
|
428
|
-
"copy",
|
|
429
|
-
...ffmpegAudioMuxArgs(audioBitrate),
|
|
430
|
-
"out.mp4"
|
|
431
|
-
]);
|
|
432
|
-
if (exitCode !== 0) {
|
|
433
|
-
throw new Error(`ffmpeg.wasm audio mux failed with exit code ${exitCode}`);
|
|
434
|
-
}
|
|
435
|
-
const data = await ffmpeg.readFile("out.mp4");
|
|
436
|
-
return data instanceof Uint8Array ? data : new TextEncoder().encode(data);
|
|
437
|
-
} finally {
|
|
438
|
-
ffmpeg.terminate();
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/gifTranscode.ts
|
|
6
|
+
useFrameCapture,
|
|
7
|
+
useVideoExport
|
|
8
|
+
} from "./chunk-VILZP3GL.js";
|
|
443
9
|
import {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, signal) {
|
|
451
|
-
if (videoMp4.byteLength === 0) {
|
|
452
|
-
throw new Error("Cannot create an animated GIF from an empty MP4.");
|
|
453
|
-
}
|
|
454
|
-
if (signal?.aborted) {
|
|
455
|
-
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
456
|
-
}
|
|
457
|
-
const load = resolveFfmpegWasmLoad2(loadConfig, "Animated GIF export", {
|
|
458
|
-
classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
|
|
459
|
-
});
|
|
460
|
-
const args = buildGifFfmpegArgs(options);
|
|
461
|
-
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
462
|
-
const ffmpeg = new FFmpeg();
|
|
463
|
-
let terminated = false;
|
|
464
|
-
const terminate = () => {
|
|
465
|
-
if (terminated) return;
|
|
466
|
-
terminated = true;
|
|
467
|
-
ffmpeg.terminate();
|
|
468
|
-
};
|
|
469
|
-
const handleAbort = () => terminate();
|
|
470
|
-
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
471
|
-
try {
|
|
472
|
-
await ffmpeg.load(load);
|
|
473
|
-
if (signal?.aborted) {
|
|
474
|
-
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
475
|
-
}
|
|
476
|
-
await ffmpeg.writeFile("video.mp4", videoMp4);
|
|
477
|
-
if (signal?.aborted) {
|
|
478
|
-
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
479
|
-
}
|
|
480
|
-
const exitCode = await ffmpeg.exec(args);
|
|
481
|
-
if (signal?.aborted) {
|
|
482
|
-
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
483
|
-
}
|
|
484
|
-
if (exitCode !== 0) {
|
|
485
|
-
throw new Error(`ffmpeg.wasm GIF transcode failed with exit code ${exitCode}`);
|
|
486
|
-
}
|
|
487
|
-
const data = await ffmpeg.readFile("out.gif");
|
|
488
|
-
return data instanceof Uint8Array ? data : new TextEncoder().encode(data);
|
|
489
|
-
} finally {
|
|
490
|
-
signal?.removeEventListener("abort", handleAbort);
|
|
491
|
-
terminate();
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// src/hooks/useFrameCapture.ts
|
|
496
|
-
import { createElement } from "react";
|
|
497
|
-
import { createRoot } from "react-dom/client";
|
|
498
|
-
import { useRef, useCallback, useMemo } from "react";
|
|
499
|
-
import { DocPlayer, MediaContext } from "@bendyline/squisq-react";
|
|
500
|
-
import html2canvas from "html2canvas";
|
|
501
|
-
var MIME_MAP = {
|
|
502
|
-
jpg: "image/jpeg",
|
|
503
|
-
jpeg: "image/jpeg",
|
|
504
|
-
png: "image/png",
|
|
505
|
-
gif: "image/gif",
|
|
506
|
-
webp: "image/webp",
|
|
507
|
-
svg: "image/svg+xml",
|
|
508
|
-
bmp: "image/bmp",
|
|
509
|
-
avif: "image/avif"
|
|
510
|
-
};
|
|
511
|
-
function createInlineProvider(images) {
|
|
512
|
-
const blobUrls = /* @__PURE__ */ new Map();
|
|
513
|
-
const mimeTypes = /* @__PURE__ */ new Map();
|
|
514
|
-
for (const [path, buffer] of images) {
|
|
515
|
-
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
516
|
-
const mime = MIME_MAP[ext] ?? "application/octet-stream";
|
|
517
|
-
blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
|
|
518
|
-
mimeTypes.set(path, mime);
|
|
519
|
-
}
|
|
520
|
-
return {
|
|
521
|
-
async resolveUrl(relativePath) {
|
|
522
|
-
return blobUrls.get(relativePath) ?? relativePath;
|
|
523
|
-
},
|
|
524
|
-
async listMedia() {
|
|
525
|
-
return [...blobUrls.keys()].map((name) => ({
|
|
526
|
-
name,
|
|
527
|
-
mimeType: mimeTypes.get(name) ?? "application/octet-stream",
|
|
528
|
-
size: images.get(name)?.byteLength ?? 0
|
|
529
|
-
}));
|
|
530
|
-
},
|
|
531
|
-
async addMedia() {
|
|
532
|
-
throw new Error("Read-only");
|
|
533
|
-
},
|
|
534
|
-
async removeMedia() {
|
|
535
|
-
throw new Error("Read-only");
|
|
536
|
-
},
|
|
537
|
-
dispose() {
|
|
538
|
-
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
539
|
-
blobUrls.clear();
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
function useFrameCapture() {
|
|
544
|
-
const containerRef = useRef(null);
|
|
545
|
-
const rootRef = useRef(null);
|
|
546
|
-
const renderAPIRef = useRef(null);
|
|
547
|
-
const mediaProviderRef = useRef(null);
|
|
548
|
-
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
549
|
-
const init = useCallback(
|
|
550
|
-
async (doc, renderOptions, captionMode) => {
|
|
551
|
-
if (rootRef.current || containerRef.current || mediaProviderRef.current) {
|
|
552
|
-
const oldRoot = rootRef.current;
|
|
553
|
-
const oldContainer = containerRef.current;
|
|
554
|
-
const oldMediaProvider = mediaProviderRef.current;
|
|
555
|
-
rootRef.current = null;
|
|
556
|
-
containerRef.current = null;
|
|
557
|
-
renderAPIRef.current = null;
|
|
558
|
-
mediaProviderRef.current = null;
|
|
559
|
-
await new Promise((resolve) => {
|
|
560
|
-
setTimeout(() => {
|
|
561
|
-
if (oldRoot) oldRoot.unmount();
|
|
562
|
-
if (oldContainer) oldContainer.remove();
|
|
563
|
-
oldMediaProvider?.dispose();
|
|
564
|
-
resolve();
|
|
565
|
-
}, 0);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
const width = renderOptions.width ?? 1920;
|
|
569
|
-
const height = renderOptions.height ?? 1080;
|
|
570
|
-
const animationsEnabled = renderOptions.animationsEnabled ?? true;
|
|
571
|
-
dimensionsRef.current = { width, height };
|
|
572
|
-
const container = document.createElement("div");
|
|
573
|
-
container.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-1;overflow:hidden;`;
|
|
574
|
-
document.body.appendChild(container);
|
|
575
|
-
containerRef.current = container;
|
|
576
|
-
const renderRoot = document.createElement("div");
|
|
577
|
-
renderRoot.id = "squisq-capture-root";
|
|
578
|
-
renderRoot.style.cssText = `width:${width}px;height:${height}px;`;
|
|
579
|
-
container.appendChild(renderRoot);
|
|
580
|
-
const mediaProvider = renderOptions.images ? createInlineProvider(renderOptions.images) : null;
|
|
581
|
-
mediaProviderRef.current = mediaProvider;
|
|
582
|
-
const root = createRoot(renderRoot);
|
|
583
|
-
rootRef.current = root;
|
|
584
|
-
const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
|
|
585
|
-
const captionStyle = captionMode === "social" ? "social" : "standard";
|
|
586
|
-
let resolveRenderAPI;
|
|
587
|
-
const renderAPIReady = new Promise((resolve) => {
|
|
588
|
-
resolveRenderAPI = resolve;
|
|
589
|
-
});
|
|
590
|
-
const playerElement = createElement(DocPlayer, {
|
|
591
|
-
doc,
|
|
592
|
-
basePath: ".",
|
|
593
|
-
renderMode: true,
|
|
594
|
-
animationsEnabled,
|
|
595
|
-
showControls: false,
|
|
596
|
-
autoPlay: false,
|
|
597
|
-
forceViewport: { width, height, name: "export" },
|
|
598
|
-
captionsEnabled,
|
|
599
|
-
captionStyle,
|
|
600
|
-
onRenderAPIReady: (api) => {
|
|
601
|
-
if (containerRef.current !== container) return;
|
|
602
|
-
renderAPIRef.current = api;
|
|
603
|
-
if (api) resolveRenderAPI(api);
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
607
|
-
if (mediaProvider) {
|
|
608
|
-
root.render(createElement(MediaContext.Provider, { value: mediaProvider }, playerElement));
|
|
609
|
-
} else {
|
|
610
|
-
root.render(playerElement);
|
|
611
|
-
}
|
|
612
|
-
return new Promise((resolve, reject) => {
|
|
613
|
-
const timeout = setTimeout(() => {
|
|
614
|
-
const api = renderAPIRef.current;
|
|
615
|
-
const hasSeek = typeof api?.seekTo === "function";
|
|
616
|
-
const hasDur = typeof api?.getDuration === "function";
|
|
617
|
-
const rootEl = containerRef.current?.querySelector("#squisq-capture-root");
|
|
618
|
-
const hasPlayer = rootEl ? rootEl.querySelector(".doc-player") !== null : false;
|
|
619
|
-
reject(
|
|
620
|
-
new Error(
|
|
621
|
-
`Render API did not initialize within 15s. seekTo=${hasSeek}, getDuration=${hasDur}, player=${hasPlayer}, root=${!!rootEl}`
|
|
622
|
-
)
|
|
623
|
-
);
|
|
624
|
-
}, 15e3);
|
|
625
|
-
void renderAPIReady.then((api) => {
|
|
626
|
-
clearTimeout(timeout);
|
|
627
|
-
resolve(api.getDuration());
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
},
|
|
631
|
-
[]
|
|
632
|
-
);
|
|
633
|
-
const captureFrame = useCallback(async (time) => {
|
|
634
|
-
const container = containerRef.current;
|
|
635
|
-
const api = renderAPIRef.current;
|
|
636
|
-
if (!container || !api) {
|
|
637
|
-
throw new Error("Frame capture not initialized \u2014 call init() first");
|
|
638
|
-
}
|
|
639
|
-
const { width, height } = dimensionsRef.current;
|
|
640
|
-
await api.seekTo(time);
|
|
641
|
-
await new Promise(
|
|
642
|
-
(resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
|
643
|
-
);
|
|
644
|
-
const root = container.querySelector("#squisq-capture-root");
|
|
645
|
-
if (!root) {
|
|
646
|
-
throw new Error("Capture root element not found");
|
|
647
|
-
}
|
|
648
|
-
const canvas = await html2canvas(root, {
|
|
649
|
-
width,
|
|
650
|
-
height,
|
|
651
|
-
scale: 1,
|
|
652
|
-
useCORS: true,
|
|
653
|
-
allowTaint: true,
|
|
654
|
-
backgroundColor: "#000000",
|
|
655
|
-
logging: false
|
|
656
|
-
});
|
|
657
|
-
const bitmap = await createImageBitmap(canvas);
|
|
658
|
-
return bitmap;
|
|
659
|
-
}, []);
|
|
660
|
-
const destroy = useCallback(() => {
|
|
661
|
-
if (rootRef.current) {
|
|
662
|
-
rootRef.current.unmount();
|
|
663
|
-
rootRef.current = null;
|
|
664
|
-
}
|
|
665
|
-
if (containerRef.current) {
|
|
666
|
-
containerRef.current.remove();
|
|
667
|
-
containerRef.current = null;
|
|
668
|
-
}
|
|
669
|
-
mediaProviderRef.current?.dispose();
|
|
670
|
-
mediaProviderRef.current = null;
|
|
671
|
-
renderAPIRef.current = null;
|
|
672
|
-
}, []);
|
|
673
|
-
return useMemo(() => ({ init, captureFrame, destroy }), [init, captureFrame, destroy]);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// src/hooks/useVideoExport.ts
|
|
677
|
-
var MAX_EXPORT_MEDIA_FILES = 256;
|
|
678
|
-
var MAX_EXPORT_MEDIA_FILE_BYTES = 64 * 1024 * 1024;
|
|
679
|
-
var MAX_EXPORT_MEDIA_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
680
|
-
function toArrayBuffer(bytes) {
|
|
681
|
-
return bytes.slice().buffer;
|
|
682
|
-
}
|
|
683
|
-
function collectDocumentMediaReferences(doc) {
|
|
684
|
-
const references = /* @__PURE__ */ new Set();
|
|
685
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
686
|
-
const visit = (value) => {
|
|
687
|
-
if (typeof value === "string") {
|
|
688
|
-
references.add(value);
|
|
689
|
-
if (value.startsWith("./")) references.add(value.slice(2));
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
693
|
-
seen.add(value);
|
|
694
|
-
if (Array.isArray(value)) {
|
|
695
|
-
value.forEach(visit);
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
Object.values(value).forEach(visit);
|
|
699
|
-
};
|
|
700
|
-
visit(doc);
|
|
701
|
-
return references;
|
|
702
|
-
}
|
|
703
|
-
async function resolveAudioBuffers(clips, sources) {
|
|
704
|
-
const srcs = new Set(clips.map((c) => c.src));
|
|
705
|
-
const out = /* @__PURE__ */ new Map();
|
|
706
|
-
for (const src of srcs) {
|
|
707
|
-
let data = sources.audio?.get(src) ?? sources.images?.get(src);
|
|
708
|
-
if (!data && sources.mediaProvider) {
|
|
709
|
-
try {
|
|
710
|
-
const url = await sources.mediaProvider.resolveUrl(src);
|
|
711
|
-
const resource = await fetchResourceBytes(url, {
|
|
712
|
-
policy: sources.resourcePolicy
|
|
713
|
-
});
|
|
714
|
-
data = toArrayBuffer(resource.bytes);
|
|
715
|
-
} catch {
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (data) out.set(src, data);
|
|
719
|
-
}
|
|
720
|
-
return out;
|
|
721
|
-
}
|
|
722
|
-
function useVideoExport() {
|
|
723
|
-
const [state, setState] = useState("idle");
|
|
724
|
-
const [progress, setProgress] = useState(0);
|
|
725
|
-
const [phase, setPhase] = useState("");
|
|
726
|
-
const [duration, setDuration] = useState(0);
|
|
727
|
-
const [outputFormat, setOutputFormat] = useState("mp4");
|
|
728
|
-
const [backend, setBackend] = useState(null);
|
|
729
|
-
const [downloadUrl, setDownloadUrl] = useState(null);
|
|
730
|
-
const [fileSize, setFileSize] = useState(0);
|
|
731
|
-
const [audioIncluded, setAudioIncluded] = useState(false);
|
|
732
|
-
const [audioSkippedReason, setAudioSkippedReason] = useState(null);
|
|
733
|
-
const [error, setError] = useState(null);
|
|
734
|
-
const [elapsed, setElapsed] = useState(0);
|
|
735
|
-
const [estimatedRemaining, setEstimatedRemaining] = useState(0);
|
|
736
|
-
const encoderRef = useRef2(null);
|
|
737
|
-
const gifAbortRef = useRef2(null);
|
|
738
|
-
const cancelledRef = useRef2(false);
|
|
739
|
-
const downloadUrlRef = useRef2(null);
|
|
740
|
-
const startTimeRef = useRef2(0);
|
|
741
|
-
const elapsedTimerRef = useRef2(null);
|
|
742
|
-
const frameCapture = useFrameCapture();
|
|
743
|
-
useEffect(() => {
|
|
744
|
-
return () => {
|
|
745
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
746
|
-
if (downloadUrlRef.current) {
|
|
747
|
-
URL.revokeObjectURL(downloadUrlRef.current);
|
|
748
|
-
}
|
|
749
|
-
if (encoderRef.current) {
|
|
750
|
-
encoderRef.current.close();
|
|
751
|
-
}
|
|
752
|
-
gifAbortRef.current?.abort();
|
|
753
|
-
frameCapture.destroy();
|
|
754
|
-
};
|
|
755
|
-
}, [frameCapture]);
|
|
756
|
-
const reset = useCallback2(() => {
|
|
757
|
-
if (downloadUrlRef.current) {
|
|
758
|
-
URL.revokeObjectURL(downloadUrlRef.current);
|
|
759
|
-
downloadUrlRef.current = null;
|
|
760
|
-
}
|
|
761
|
-
if (encoderRef.current) {
|
|
762
|
-
encoderRef.current.close();
|
|
763
|
-
encoderRef.current = null;
|
|
764
|
-
}
|
|
765
|
-
gifAbortRef.current?.abort();
|
|
766
|
-
gifAbortRef.current = null;
|
|
767
|
-
frameCapture.destroy();
|
|
768
|
-
setState("idle");
|
|
769
|
-
setProgress(0);
|
|
770
|
-
setPhase("");
|
|
771
|
-
setDuration(0);
|
|
772
|
-
setOutputFormat("mp4");
|
|
773
|
-
setBackend(null);
|
|
774
|
-
setDownloadUrl(null);
|
|
775
|
-
setFileSize(0);
|
|
776
|
-
setAudioIncluded(false);
|
|
777
|
-
setAudioSkippedReason(null);
|
|
778
|
-
setError(null);
|
|
779
|
-
setElapsed(0);
|
|
780
|
-
setEstimatedRemaining(0);
|
|
781
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
782
|
-
cancelledRef.current = false;
|
|
783
|
-
}, [frameCapture]);
|
|
784
|
-
const cancel = useCallback2(() => {
|
|
785
|
-
cancelledRef.current = true;
|
|
786
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
787
|
-
if (encoderRef.current) {
|
|
788
|
-
encoderRef.current.close();
|
|
789
|
-
encoderRef.current = null;
|
|
790
|
-
}
|
|
791
|
-
gifAbortRef.current?.abort();
|
|
792
|
-
gifAbortRef.current = null;
|
|
793
|
-
frameCapture.destroy();
|
|
794
|
-
setState("idle");
|
|
795
|
-
setProgress(0);
|
|
796
|
-
setPhase("Cancelled");
|
|
797
|
-
}, [frameCapture]);
|
|
798
|
-
const startExport = useCallback2(
|
|
799
|
-
async (doc, config) => {
|
|
800
|
-
cancelledRef.current = false;
|
|
801
|
-
if (downloadUrlRef.current) {
|
|
802
|
-
URL.revokeObjectURL(downloadUrlRef.current);
|
|
803
|
-
downloadUrlRef.current = null;
|
|
804
|
-
}
|
|
805
|
-
setDownloadUrl(null);
|
|
806
|
-
setFileSize(0);
|
|
807
|
-
setAudioIncluded(false);
|
|
808
|
-
setAudioSkippedReason(null);
|
|
809
|
-
setError(null);
|
|
810
|
-
const quality = config.quality ?? "normal";
|
|
811
|
-
const effectiveOutputFormat = config.outputFormat ?? "mp4";
|
|
812
|
-
const fps = config.fps ?? (effectiveOutputFormat === "gif" ? 10 : 30);
|
|
813
|
-
const orientation = config.orientation ?? "landscape";
|
|
814
|
-
const animationsEnabled = config.animationsEnabled ?? effectiveOutputFormat === "mp4";
|
|
815
|
-
const audioPolicy = config.audioPolicy ?? "require";
|
|
816
|
-
setOutputFormat(effectiveOutputFormat);
|
|
817
|
-
try {
|
|
818
|
-
const gifDefaults = orientation === "portrait" ? { width: 540, height: 960 } : { width: 960, height: 540 };
|
|
819
|
-
const { width, height } = resolveDimensions({
|
|
820
|
-
orientation,
|
|
821
|
-
fps,
|
|
822
|
-
quality,
|
|
823
|
-
...config.width !== void 0 ? { width: config.width } : effectiveOutputFormat === "gif" ? { width: gifDefaults.width } : {},
|
|
824
|
-
...config.height !== void 0 ? { height: config.height } : effectiveOutputFormat === "gif" ? { height: gifDefaults.height } : {}
|
|
825
|
-
});
|
|
826
|
-
const webCodecsAvailable = supportsWebCodecs();
|
|
827
|
-
const sharedArrayBufferAvailable = typeof SharedArrayBuffer !== "undefined";
|
|
828
|
-
if (effectiveOutputFormat === "gif" && !sharedArrayBufferAvailable) {
|
|
829
|
-
throw new Error(
|
|
830
|
-
"Animated GIF export requires ffmpeg.wasm and SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
if (effectiveOutputFormat === "gif") {
|
|
834
|
-
resolveFfmpegWasmLoad3(config.ffmpegWasm, "Animated GIF export");
|
|
835
|
-
}
|
|
836
|
-
if (!webCodecsAvailable && !sharedArrayBufferAvailable) {
|
|
837
|
-
throw new Error(
|
|
838
|
-
"No video encoder available. WebCodecs requires Chrome 94+ / Edge 94+, and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
setState("preparing");
|
|
842
|
-
setPhase("Loading document\u2026");
|
|
843
|
-
setProgress(0);
|
|
844
|
-
setElapsed(0);
|
|
845
|
-
setEstimatedRemaining(0);
|
|
846
|
-
startTimeRef.current = performance.now();
|
|
847
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
848
|
-
elapsedTimerRef.current = setInterval(() => {
|
|
849
|
-
setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1e3));
|
|
850
|
-
}, 1e3);
|
|
851
|
-
let images = config.images;
|
|
852
|
-
if (!images && config.mediaProvider) {
|
|
853
|
-
images = /* @__PURE__ */ new Map();
|
|
854
|
-
const entries = await config.mediaProvider.listMedia();
|
|
855
|
-
const references = collectDocumentMediaReferences(doc);
|
|
856
|
-
const neededEntries = entries.filter(
|
|
857
|
-
(entry) => references.has(entry.name) || references.has(`./${entry.name}`)
|
|
858
|
-
);
|
|
859
|
-
if (neededEntries.length > MAX_EXPORT_MEDIA_FILES) {
|
|
860
|
-
throw new Error(
|
|
861
|
-
`Document references ${neededEntries.length} media files; browser export supports at most ${MAX_EXPORT_MEDIA_FILES}.`
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
let totalMediaBytes = 0;
|
|
865
|
-
for (const entry of neededEntries) {
|
|
866
|
-
if (cancelledRef.current) return;
|
|
867
|
-
if (entry.size > MAX_EXPORT_MEDIA_FILE_BYTES) {
|
|
868
|
-
throw new Error(`Media file "${entry.name}" is too large for browser video export.`);
|
|
869
|
-
}
|
|
870
|
-
const url2 = await config.mediaProvider.resolveUrl(entry.name);
|
|
871
|
-
const resource = await fetchResourceBytes(url2, {
|
|
872
|
-
policy: {
|
|
873
|
-
...DEFAULT_INTERACTIVE_RESOURCE_POLICY,
|
|
874
|
-
...config.resourcePolicy,
|
|
875
|
-
maxBytes: Math.min(
|
|
876
|
-
config.resourcePolicy?.maxBytes ?? MAX_EXPORT_MEDIA_FILE_BYTES,
|
|
877
|
-
MAX_EXPORT_MEDIA_FILE_BYTES
|
|
878
|
-
)
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
const data = toArrayBuffer(resource.bytes);
|
|
882
|
-
totalMediaBytes += data.byteLength;
|
|
883
|
-
if (totalMediaBytes > MAX_EXPORT_MEDIA_TOTAL_BYTES) {
|
|
884
|
-
throw new Error("Referenced media exceeds the browser video export memory limit.");
|
|
885
|
-
}
|
|
886
|
-
images.set(entry.name, data);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
const docDuration = await frameCapture.init(
|
|
890
|
-
doc,
|
|
891
|
-
{ images, audio: config.audio, width, height, animationsEnabled },
|
|
892
|
-
config.captionMode
|
|
893
|
-
);
|
|
894
|
-
if (cancelledRef.current) return;
|
|
895
|
-
setDuration(docDuration);
|
|
896
|
-
if (docDuration <= 0) {
|
|
897
|
-
throw new Error("Document has zero duration \u2014 nothing to export");
|
|
898
|
-
}
|
|
899
|
-
const totalFrames = Math.ceil(docDuration * fps);
|
|
900
|
-
setPhase("Starting encoder\u2026");
|
|
901
|
-
setProgress(5);
|
|
902
|
-
const canUseWebCodecs = webCodecsAvailable && await supportsWebCodecsH264({ width, height, fps, quality });
|
|
903
|
-
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
904
|
-
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, 0) : [];
|
|
905
|
-
const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
|
|
906
|
-
const tierDecision = selectAudioTier({
|
|
907
|
-
hasClips: timeline.length > 0,
|
|
908
|
-
aacSupported,
|
|
909
|
-
sharedArrayBufferAvailable,
|
|
910
|
-
canUseMainThreadWebCodecs: canUseWebCodecs
|
|
911
|
-
});
|
|
912
|
-
let renderedAudio = null;
|
|
913
|
-
let audioIncludedLocal = false;
|
|
914
|
-
let audioReasonLocal = tierDecision.reason;
|
|
915
|
-
if (timeline.length > 0 && tierDecision.tier === 3 && audioPolicy === "require") {
|
|
916
|
-
throw new Error(tierDecision.reason ?? "This browser cannot include the document audio.");
|
|
917
|
-
}
|
|
918
|
-
if (tierDecision.tier === 1 || tierDecision.tier === 2) {
|
|
919
|
-
setPhase("Preparing audio\u2026");
|
|
920
|
-
try {
|
|
921
|
-
const buffers = await resolveAudioBuffers(timeline, {
|
|
922
|
-
audio: config.audio,
|
|
923
|
-
images,
|
|
924
|
-
mediaProvider: config.mediaProvider,
|
|
925
|
-
resourcePolicy: config.resourcePolicy
|
|
926
|
-
});
|
|
927
|
-
const missingSources = [...new Set(timeline.map((clip) => clip.src))].filter(
|
|
928
|
-
(src) => !buffers.has(src)
|
|
929
|
-
);
|
|
930
|
-
if (missingSources.length > 0) {
|
|
931
|
-
audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
|
|
932
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
933
|
-
}
|
|
934
|
-
if (buffers.size === 0) {
|
|
935
|
-
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
936
|
-
} else {
|
|
937
|
-
const totalAudioDur = timeline.reduce(
|
|
938
|
-
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
939
|
-
docDuration
|
|
940
|
-
);
|
|
941
|
-
renderedAudio = await renderAudioTimeline(
|
|
942
|
-
timeline,
|
|
943
|
-
buffers,
|
|
944
|
-
totalAudioDur,
|
|
945
|
-
EXPORT_AUDIO_SAMPLE_RATE
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
} catch (audioErr) {
|
|
949
|
-
renderedAudio = null;
|
|
950
|
-
audioReasonLocal = `Audio could not be prepared: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
951
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
const useInlineAudio = renderedAudio !== null && tierDecision.tier === 1;
|
|
955
|
-
const useFfmpegAudio = renderedAudio !== null && tierDecision.tier === 2;
|
|
956
|
-
if (cancelledRef.current) return;
|
|
957
|
-
let encoder;
|
|
958
|
-
if (canUseWebCodecs) {
|
|
959
|
-
encoder = createEncoder({
|
|
960
|
-
width,
|
|
961
|
-
height,
|
|
962
|
-
fps,
|
|
963
|
-
quality,
|
|
964
|
-
...useInlineAudio && renderedAudio ? {
|
|
965
|
-
audio: {
|
|
966
|
-
numberOfChannels: renderedAudio.numberOfChannels,
|
|
967
|
-
sampleRate: renderedAudio.sampleRate
|
|
968
|
-
}
|
|
969
|
-
} : {}
|
|
970
|
-
});
|
|
971
|
-
setBackend("webcodecs");
|
|
972
|
-
} else if (sharedArrayBufferAvailable) {
|
|
973
|
-
const workerEncoder = createWorkerEncoder({
|
|
974
|
-
width,
|
|
975
|
-
height,
|
|
976
|
-
fps,
|
|
977
|
-
quality,
|
|
978
|
-
totalFrames,
|
|
979
|
-
ffmpegWasm: config.ffmpegWasm
|
|
980
|
-
});
|
|
981
|
-
encoder = workerEncoder;
|
|
982
|
-
const selectedBackend = await workerEncoder.ready;
|
|
983
|
-
setBackend(selectedBackend);
|
|
984
|
-
setPhase(
|
|
985
|
-
selectedBackend === "ffmpeg-wasm" ? "Starting encoder (ffmpeg.wasm)\u2026" : "Starting encoder\u2026"
|
|
986
|
-
);
|
|
987
|
-
} else {
|
|
988
|
-
throw new Error(
|
|
989
|
-
"WebCodecs H.264 is unavailable in this browser and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
encoderRef.current = encoder;
|
|
993
|
-
if (cancelledRef.current) return;
|
|
994
|
-
setState("capturing");
|
|
995
|
-
const captureStartTime = performance.now();
|
|
996
|
-
const UI_UPDATE_INTERVAL = 10;
|
|
997
|
-
for (let i = 0; i < totalFrames; i++) {
|
|
998
|
-
if (cancelledRef.current) return;
|
|
999
|
-
const time = i / fps;
|
|
1000
|
-
if (i % UI_UPDATE_INTERVAL === 0 || i === totalFrames - 1) {
|
|
1001
|
-
const captureProgress = Math.round(i / totalFrames * 90);
|
|
1002
|
-
setProgress(5 + captureProgress);
|
|
1003
|
-
setPhase(`Capturing frame ${i + 1}/${totalFrames} (${time.toFixed(1)}s)`);
|
|
1004
|
-
if (i > 0) {
|
|
1005
|
-
const elapsedCapture = (performance.now() - captureStartTime) / 1e3;
|
|
1006
|
-
const avgPerFrame = elapsedCapture / i;
|
|
1007
|
-
const remaining = Math.round(avgPerFrame * (totalFrames - i));
|
|
1008
|
-
setEstimatedRemaining(remaining);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
const bitmap = await frameCapture.captureFrame(time);
|
|
1012
|
-
if (cancelledRef.current) {
|
|
1013
|
-
bitmap.close();
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
await encoder.encodeFrame(bitmap, i);
|
|
1017
|
-
}
|
|
1018
|
-
if (cancelledRef.current) return;
|
|
1019
|
-
if (useInlineAudio && renderedAudio && encoder.addAudioChunk) {
|
|
1020
|
-
setState("encoding");
|
|
1021
|
-
setPhase("Encoding audio\u2026");
|
|
1022
|
-
try {
|
|
1023
|
-
await encodeAacTrack(
|
|
1024
|
-
renderedAudio,
|
|
1025
|
-
{ addAudioChunk: encoder.addAudioChunk.bind(encoder) },
|
|
1026
|
-
audioBitrate
|
|
1027
|
-
);
|
|
1028
|
-
audioIncludedLocal = true;
|
|
1029
|
-
} catch (audioErr) {
|
|
1030
|
-
audioIncludedLocal = false;
|
|
1031
|
-
audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1032
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
setState("encoding");
|
|
1036
|
-
setPhase(effectiveOutputFormat === "gif" ? "Finalizing GIF frames\u2026" : "Finalizing video\u2026");
|
|
1037
|
-
setProgress(95);
|
|
1038
|
-
let outputBytes = await encoder.finalize();
|
|
1039
|
-
encoderRef.current = null;
|
|
1040
|
-
if (cancelledRef.current) return;
|
|
1041
|
-
if (effectiveOutputFormat === "gif") {
|
|
1042
|
-
setPhase("Generating GIF palette\u2026");
|
|
1043
|
-
const videoOnly = outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1044
|
-
const gifAbort = new AbortController();
|
|
1045
|
-
gifAbortRef.current = gifAbort;
|
|
1046
|
-
try {
|
|
1047
|
-
outputBytes = await transcodeMp4ToGifWithFfmpegWasm(
|
|
1048
|
-
videoOnly,
|
|
1049
|
-
{ width, height, loop: 0 },
|
|
1050
|
-
config.ffmpegWasm,
|
|
1051
|
-
gifAbort.signal
|
|
1052
|
-
);
|
|
1053
|
-
} finally {
|
|
1054
|
-
if (gifAbortRef.current === gifAbort) gifAbortRef.current = null;
|
|
1055
|
-
}
|
|
1056
|
-
} else if (useFfmpegAudio && renderedAudio) {
|
|
1057
|
-
setPhase("Muxing audio\u2026");
|
|
1058
|
-
try {
|
|
1059
|
-
const wav = audioBufferToWav(renderedAudio);
|
|
1060
|
-
const videoOnly = outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1061
|
-
outputBytes = await muxAudioWithFfmpegWasm(
|
|
1062
|
-
videoOnly,
|
|
1063
|
-
wav,
|
|
1064
|
-
audioBitrate,
|
|
1065
|
-
config.ffmpegWasm
|
|
1066
|
-
);
|
|
1067
|
-
audioIncludedLocal = true;
|
|
1068
|
-
} catch (audioErr) {
|
|
1069
|
-
audioIncludedLocal = false;
|
|
1070
|
-
audioReasonLocal = `Audio muxing failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1071
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
if (cancelledRef.current) return;
|
|
1075
|
-
const finalBytes = outputBytes instanceof Uint8Array ? outputBytes.slice() : new Uint8Array(outputBytes);
|
|
1076
|
-
const mimeType = effectiveOutputFormat === "gif" ? "image/gif" : "video/mp4";
|
|
1077
|
-
const blob = new Blob([finalBytes], { type: mimeType });
|
|
1078
|
-
const url = URL.createObjectURL(blob);
|
|
1079
|
-
downloadUrlRef.current = url;
|
|
1080
|
-
setDownloadUrl(url);
|
|
1081
|
-
setFileSize(finalBytes.byteLength);
|
|
1082
|
-
setAudioIncluded(audioIncludedLocal);
|
|
1083
|
-
setAudioSkippedReason(
|
|
1084
|
-
effectiveOutputFormat === "gif" || audioIncludedLocal ? null : audioReasonLocal
|
|
1085
|
-
);
|
|
1086
|
-
setState("complete");
|
|
1087
|
-
setProgress(100);
|
|
1088
|
-
setPhase("Export complete");
|
|
1089
|
-
setEstimatedRemaining(0);
|
|
1090
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
1091
|
-
frameCapture.destroy();
|
|
1092
|
-
} catch (err) {
|
|
1093
|
-
if (elapsedTimerRef.current) clearInterval(elapsedTimerRef.current);
|
|
1094
|
-
if (cancelledRef.current) return;
|
|
1095
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1096
|
-
setState("error");
|
|
1097
|
-
setError(message);
|
|
1098
|
-
setPhase("Export failed");
|
|
1099
|
-
if (encoderRef.current) {
|
|
1100
|
-
encoderRef.current.close();
|
|
1101
|
-
encoderRef.current = null;
|
|
1102
|
-
}
|
|
1103
|
-
gifAbortRef.current?.abort();
|
|
1104
|
-
gifAbortRef.current = null;
|
|
1105
|
-
frameCapture.destroy();
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
[frameCapture]
|
|
1109
|
-
);
|
|
1110
|
-
return {
|
|
1111
|
-
state,
|
|
1112
|
-
progress,
|
|
1113
|
-
phase,
|
|
1114
|
-
duration,
|
|
1115
|
-
outputFormat,
|
|
1116
|
-
backend,
|
|
1117
|
-
downloadUrl,
|
|
1118
|
-
fileSize,
|
|
1119
|
-
audioIncluded,
|
|
1120
|
-
audioSkippedReason,
|
|
1121
|
-
error,
|
|
1122
|
-
elapsed,
|
|
1123
|
-
estimatedRemaining,
|
|
1124
|
-
startExport,
|
|
1125
|
-
cancel,
|
|
1126
|
-
reset
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// src/VideoExportModal.tsx
|
|
1131
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
1132
|
-
function formatDuration(seconds) {
|
|
1133
|
-
if (seconds < 60) return `${seconds}s`;
|
|
1134
|
-
const m = Math.floor(seconds / 60);
|
|
1135
|
-
const s = seconds % 60;
|
|
1136
|
-
return `${m}m ${s}s`;
|
|
1137
|
-
}
|
|
1138
|
-
function encoderLabel(outputFormat, backend) {
|
|
1139
|
-
if (outputFormat === "gif") {
|
|
1140
|
-
return backend === "webcodecs" ? "WebCodecs (H.264) \u2192 ffmpeg.wasm (GIF)" : "ffmpeg.wasm (H.264 \u2192 GIF)";
|
|
1141
|
-
}
|
|
1142
|
-
return backend === "webcodecs" ? "WebCodecs (H.264)" : "ffmpeg.wasm (H.264)";
|
|
1143
|
-
}
|
|
1144
|
-
var VIDEO_EXPORT_PALETTES = {
|
|
1145
|
-
light: {
|
|
1146
|
-
overlay: "rgba(0, 0, 0, 0.5)",
|
|
1147
|
-
surface: "#FFFDF7",
|
|
1148
|
-
control: "#ffffff",
|
|
1149
|
-
border: "#c9b98a",
|
|
1150
|
-
text: "#4a3c1f",
|
|
1151
|
-
heading: "#2d2310",
|
|
1152
|
-
label: "#5a4a2a",
|
|
1153
|
-
muted: "#8a7a5a",
|
|
1154
|
-
secondary: "#E8DFC6",
|
|
1155
|
-
primary: "#8B6914",
|
|
1156
|
-
primaryBorder: "#7a5c10",
|
|
1157
|
-
primaryText: "#ffffff",
|
|
1158
|
-
success: "#2d6a10",
|
|
1159
|
-
danger: "#a03020"
|
|
1160
|
-
},
|
|
1161
|
-
dark: {
|
|
1162
|
-
overlay: "rgba(2, 6, 23, 0.72)",
|
|
1163
|
-
surface: "#111827",
|
|
1164
|
-
control: "#0f172a",
|
|
1165
|
-
border: "#475569",
|
|
1166
|
-
text: "#e5e7eb",
|
|
1167
|
-
heading: "#f8fafc",
|
|
1168
|
-
label: "#cbd5e1",
|
|
1169
|
-
muted: "#94a3b8",
|
|
1170
|
-
secondary: "#1e293b",
|
|
1171
|
-
primary: "#9a7416",
|
|
1172
|
-
primaryBorder: "#d1a73b",
|
|
1173
|
-
primaryText: "#ffffff",
|
|
1174
|
-
success: "#86efac",
|
|
1175
|
-
danger: "#fca5a5"
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
var overlayStyle = {
|
|
1179
|
-
position: "fixed",
|
|
1180
|
-
inset: 0,
|
|
1181
|
-
display: "flex",
|
|
1182
|
-
alignItems: "center",
|
|
1183
|
-
justifyContent: "center",
|
|
1184
|
-
zIndex: 1e4
|
|
1185
|
-
};
|
|
1186
|
-
var modalStyle = {
|
|
1187
|
-
borderRadius: 0,
|
|
1188
|
-
padding: "24px 28px",
|
|
1189
|
-
minWidth: 380,
|
|
1190
|
-
maxWidth: 480,
|
|
1191
|
-
boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
|
|
1192
|
-
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
1193
|
-
};
|
|
1194
|
-
var titleStyle = {
|
|
1195
|
-
margin: "0 0 16px 0",
|
|
1196
|
-
fontSize: 18,
|
|
1197
|
-
fontWeight: 600
|
|
1198
|
-
};
|
|
1199
|
-
var labelStyle = {
|
|
1200
|
-
display: "block",
|
|
1201
|
-
fontSize: 13,
|
|
1202
|
-
fontWeight: 500,
|
|
1203
|
-
marginBottom: 4
|
|
1204
|
-
};
|
|
1205
|
-
var selectStyle = {
|
|
1206
|
-
width: "100%",
|
|
1207
|
-
padding: "6px 8px",
|
|
1208
|
-
fontSize: 13,
|
|
1209
|
-
fontFamily: "inherit",
|
|
1210
|
-
borderRadius: 0,
|
|
1211
|
-
marginBottom: 12
|
|
1212
|
-
};
|
|
1213
|
-
var btnPrimary = {
|
|
1214
|
-
padding: "8px 20px",
|
|
1215
|
-
fontSize: 14,
|
|
1216
|
-
fontFamily: "inherit",
|
|
1217
|
-
fontWeight: 500,
|
|
1218
|
-
cursor: "pointer",
|
|
1219
|
-
borderRadius: 0
|
|
1220
|
-
};
|
|
1221
|
-
var btnSecondary = {
|
|
1222
|
-
padding: "8px 20px",
|
|
1223
|
-
fontSize: 14,
|
|
1224
|
-
fontFamily: "inherit",
|
|
1225
|
-
fontWeight: 500,
|
|
1226
|
-
cursor: "pointer",
|
|
1227
|
-
borderRadius: 0
|
|
1228
|
-
};
|
|
1229
|
-
var progressBarOuterStyle = {
|
|
1230
|
-
width: "100%",
|
|
1231
|
-
height: 8,
|
|
1232
|
-
borderRadius: 0,
|
|
1233
|
-
overflow: "hidden",
|
|
1234
|
-
marginBottom: 8
|
|
1235
|
-
};
|
|
1236
|
-
var footerStyle = {
|
|
1237
|
-
display: "flex",
|
|
1238
|
-
justifyContent: "flex-end",
|
|
1239
|
-
gap: 8,
|
|
1240
|
-
marginTop: 20
|
|
1241
|
-
};
|
|
1242
|
-
function VideoExportModal({
|
|
1243
|
-
doc,
|
|
1244
|
-
playerScript,
|
|
1245
|
-
mediaProvider,
|
|
1246
|
-
images,
|
|
1247
|
-
audio,
|
|
1248
|
-
defaultConfig,
|
|
1249
|
-
colorScheme = "light",
|
|
1250
|
-
uiPalette,
|
|
1251
|
-
onClose
|
|
1252
|
-
}) {
|
|
1253
|
-
const overlayRef = useRef3(null);
|
|
1254
|
-
const dialogRef = useRef3(null);
|
|
1255
|
-
const titleId = useId();
|
|
1256
|
-
const initialOutputFormat = defaultConfig?.outputFormat ?? "mp4";
|
|
1257
|
-
const [outputFormat, setOutputFormat] = useState2(initialOutputFormat);
|
|
1258
|
-
const [quality, setQuality] = useState2(defaultConfig?.quality ?? "normal");
|
|
1259
|
-
const [fps, setFps] = useState2(defaultConfig?.fps ?? (initialOutputFormat === "gif" ? 10 : 24));
|
|
1260
|
-
const [orientation, setOrientation] = useState2(
|
|
1261
|
-
defaultConfig?.orientation ?? "landscape"
|
|
1262
|
-
);
|
|
1263
|
-
const [captionMode, setCaptionMode] = useState2(defaultConfig?.captionMode ?? "off");
|
|
1264
|
-
const [animationsEnabled, setAnimationsEnabled] = useState2(
|
|
1265
|
-
defaultConfig?.animationsEnabled ?? initialOutputFormat === "mp4"
|
|
1266
|
-
);
|
|
1267
|
-
const [audioPolicy, setAudioPolicy] = useState2(
|
|
1268
|
-
defaultConfig?.audioPolicy ?? "require"
|
|
1269
|
-
);
|
|
1270
|
-
const palette = {
|
|
1271
|
-
...VIDEO_EXPORT_PALETTES[colorScheme],
|
|
1272
|
-
...uiPalette
|
|
1273
|
-
};
|
|
1274
|
-
const themedModalStyle = {
|
|
1275
|
-
...modalStyle,
|
|
1276
|
-
background: palette.surface,
|
|
1277
|
-
border: `1px solid ${palette.border}`,
|
|
1278
|
-
color: palette.text,
|
|
1279
|
-
colorScheme
|
|
1280
|
-
};
|
|
1281
|
-
const themedTitleStyle = { ...titleStyle, color: palette.heading };
|
|
1282
|
-
const themedLabelStyle = { ...labelStyle, color: palette.label };
|
|
1283
|
-
const themedSelectStyle = {
|
|
1284
|
-
...selectStyle,
|
|
1285
|
-
border: `1px solid ${palette.border}`,
|
|
1286
|
-
background: palette.control,
|
|
1287
|
-
color: palette.text,
|
|
1288
|
-
colorScheme
|
|
1289
|
-
};
|
|
1290
|
-
const themedPrimaryButtonStyle = {
|
|
1291
|
-
...btnPrimary,
|
|
1292
|
-
background: palette.primary,
|
|
1293
|
-
border: `1px solid ${palette.primaryBorder}`,
|
|
1294
|
-
color: palette.primaryText
|
|
1295
|
-
};
|
|
1296
|
-
const themedSecondaryButtonStyle = {
|
|
1297
|
-
...btnSecondary,
|
|
1298
|
-
background: palette.secondary,
|
|
1299
|
-
color: palette.text,
|
|
1300
|
-
border: `1px solid ${palette.border}`
|
|
1301
|
-
};
|
|
1302
|
-
const exportHook = useVideoExport();
|
|
1303
|
-
const {
|
|
1304
|
-
state,
|
|
1305
|
-
progress,
|
|
1306
|
-
backend,
|
|
1307
|
-
outputFormat: completedOutputFormat,
|
|
1308
|
-
downloadUrl,
|
|
1309
|
-
fileSize,
|
|
1310
|
-
audioIncluded,
|
|
1311
|
-
audioSkippedReason,
|
|
1312
|
-
error,
|
|
1313
|
-
elapsed,
|
|
1314
|
-
estimatedRemaining,
|
|
1315
|
-
startExport,
|
|
1316
|
-
cancel: cancelExport,
|
|
1317
|
-
reset: resetExport
|
|
1318
|
-
} = exportHook;
|
|
1319
|
-
const handleOutputFormatChange = useCallback3((next) => {
|
|
1320
|
-
setOutputFormat(next);
|
|
1321
|
-
if (next === "gif") {
|
|
1322
|
-
setFps(10);
|
|
1323
|
-
setAnimationsEnabled(false);
|
|
1324
|
-
} else {
|
|
1325
|
-
setFps(24);
|
|
1326
|
-
setAnimationsEnabled(true);
|
|
1327
|
-
}
|
|
1328
|
-
}, []);
|
|
1329
|
-
const handleExport = useCallback3(async () => {
|
|
1330
|
-
const config = {
|
|
1331
|
-
// defaultConfig is the base; explicit props/selections win over it.
|
|
1332
|
-
...defaultConfig,
|
|
1333
|
-
outputFormat,
|
|
1334
|
-
animationsEnabled,
|
|
1335
|
-
quality,
|
|
1336
|
-
fps,
|
|
1337
|
-
orientation,
|
|
1338
|
-
captionMode,
|
|
1339
|
-
audioPolicy,
|
|
1340
|
-
images,
|
|
1341
|
-
audio,
|
|
1342
|
-
mediaProvider,
|
|
1343
|
-
// Only thread the bundle through when the host actually supplied one.
|
|
1344
|
-
...playerScript !== void 0 ? { playerScript } : {}
|
|
1345
|
-
};
|
|
1346
|
-
await startExport(doc, config);
|
|
1347
|
-
}, [
|
|
1348
|
-
doc,
|
|
1349
|
-
outputFormat,
|
|
1350
|
-
animationsEnabled,
|
|
1351
|
-
quality,
|
|
1352
|
-
fps,
|
|
1353
|
-
orientation,
|
|
1354
|
-
captionMode,
|
|
1355
|
-
audioPolicy,
|
|
1356
|
-
images,
|
|
1357
|
-
audio,
|
|
1358
|
-
mediaProvider,
|
|
1359
|
-
playerScript,
|
|
1360
|
-
defaultConfig,
|
|
1361
|
-
startExport
|
|
1362
|
-
]);
|
|
1363
|
-
const handleDownload = useCallback3(() => {
|
|
1364
|
-
if (!downloadUrl) return;
|
|
1365
|
-
const a = document.createElement("a");
|
|
1366
|
-
a.href = downloadUrl;
|
|
1367
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1368
|
-
a.download = `document-${ts}.${completedOutputFormat}`;
|
|
1369
|
-
document.body.appendChild(a);
|
|
1370
|
-
a.click();
|
|
1371
|
-
document.body.removeChild(a);
|
|
1372
|
-
}, [downloadUrl, completedOutputFormat]);
|
|
1373
|
-
const handleClose = useCallback3(() => {
|
|
1374
|
-
if (state === "capturing" || state === "encoding" || state === "preparing") {
|
|
1375
|
-
cancelExport();
|
|
1376
|
-
}
|
|
1377
|
-
resetExport();
|
|
1378
|
-
onClose();
|
|
1379
|
-
}, [state, cancelExport, resetExport, onClose]);
|
|
1380
|
-
const isExporting = state === "preparing" || state === "capturing" || state === "encoding";
|
|
1381
|
-
useModalDialog({ rootRef: overlayRef, dialogRef, onClose: handleClose });
|
|
1382
|
-
return /* @__PURE__ */ jsx(
|
|
1383
|
-
"div",
|
|
1384
|
-
{
|
|
1385
|
-
ref: overlayRef,
|
|
1386
|
-
style: { ...overlayStyle, background: palette.overlay },
|
|
1387
|
-
"data-color-scheme": colorScheme,
|
|
1388
|
-
onClick: handleClose,
|
|
1389
|
-
children: /* @__PURE__ */ jsxs(
|
|
1390
|
-
"div",
|
|
1391
|
-
{
|
|
1392
|
-
ref: dialogRef,
|
|
1393
|
-
role: "dialog",
|
|
1394
|
-
"aria-modal": "true",
|
|
1395
|
-
"aria-labelledby": titleId,
|
|
1396
|
-
tabIndex: -1,
|
|
1397
|
-
style: themedModalStyle,
|
|
1398
|
-
"data-squisq-video-export-modal": true,
|
|
1399
|
-
onClick: (e) => e.stopPropagation(),
|
|
1400
|
-
children: [
|
|
1401
|
-
/* @__PURE__ */ jsx("h2", { id: titleId, style: themedTitleStyle, children: outputFormat === "gif" ? "Export Animated GIF" : "Export Video" }),
|
|
1402
|
-
state === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1403
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1404
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Format" }),
|
|
1405
|
-
/* @__PURE__ */ jsxs(
|
|
1406
|
-
"select",
|
|
1407
|
-
{
|
|
1408
|
-
"aria-label": "Format",
|
|
1409
|
-
style: themedSelectStyle,
|
|
1410
|
-
value: outputFormat,
|
|
1411
|
-
onChange: (e) => handleOutputFormatChange(e.target.value),
|
|
1412
|
-
children: [
|
|
1413
|
-
/* @__PURE__ */ jsx("option", { value: "mp4", children: "MP4 video" }),
|
|
1414
|
-
/* @__PURE__ */ jsx("option", { value: "gif", children: "Animated GIF" })
|
|
1415
|
-
]
|
|
1416
|
-
}
|
|
1417
|
-
)
|
|
1418
|
-
] }),
|
|
1419
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1420
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Quality" }),
|
|
1421
|
-
/* @__PURE__ */ jsxs(
|
|
1422
|
-
"select",
|
|
1423
|
-
{
|
|
1424
|
-
"aria-label": "Quality",
|
|
1425
|
-
style: themedSelectStyle,
|
|
1426
|
-
value: quality,
|
|
1427
|
-
onChange: (e) => setQuality(e.target.value),
|
|
1428
|
-
children: [
|
|
1429
|
-
/* @__PURE__ */ jsx("option", { value: "draft", children: "Draft \u2014 fast, lower quality" }),
|
|
1430
|
-
/* @__PURE__ */ jsx("option", { value: "normal", children: "Normal \u2014 balanced" }),
|
|
1431
|
-
/* @__PURE__ */ jsx("option", { value: "high", children: "High \u2014 best quality, slower" })
|
|
1432
|
-
]
|
|
1433
|
-
}
|
|
1434
|
-
)
|
|
1435
|
-
] }),
|
|
1436
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1437
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Frame Rate" }),
|
|
1438
|
-
/* @__PURE__ */ jsxs(
|
|
1439
|
-
"select",
|
|
1440
|
-
{
|
|
1441
|
-
"aria-label": "Frame Rate",
|
|
1442
|
-
style: themedSelectStyle,
|
|
1443
|
-
value: fps,
|
|
1444
|
-
onChange: (e) => setFps(Number(e.target.value)),
|
|
1445
|
-
children: [
|
|
1446
|
-
/* @__PURE__ */ jsx("option", { value: 10, children: "10 fps \u2014 recommended for GIF" }),
|
|
1447
|
-
/* @__PURE__ */ jsx("option", { value: 15, children: "15 fps \u2014 fast export" }),
|
|
1448
|
-
/* @__PURE__ */ jsx("option", { value: 24, children: "24 fps \u2014 cinematic" }),
|
|
1449
|
-
/* @__PURE__ */ jsx("option", { value: 30, children: "30 fps \u2014 smooth" })
|
|
1450
|
-
]
|
|
1451
|
-
}
|
|
1452
|
-
)
|
|
1453
|
-
] }),
|
|
1454
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1455
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Orientation" }),
|
|
1456
|
-
/* @__PURE__ */ jsxs(
|
|
1457
|
-
"select",
|
|
1458
|
-
{
|
|
1459
|
-
"aria-label": "Orientation",
|
|
1460
|
-
style: themedSelectStyle,
|
|
1461
|
-
value: orientation,
|
|
1462
|
-
onChange: (e) => setOrientation(e.target.value),
|
|
1463
|
-
children: [
|
|
1464
|
-
/* @__PURE__ */ jsxs("option", { value: "landscape", children: [
|
|
1465
|
-
"Landscape (",
|
|
1466
|
-
outputFormat === "gif" ? "960 \xD7 540" : "1920 \xD7 1080",
|
|
1467
|
-
")"
|
|
1468
|
-
] }),
|
|
1469
|
-
/* @__PURE__ */ jsxs("option", { value: "portrait", children: [
|
|
1470
|
-
"Portrait (",
|
|
1471
|
-
outputFormat === "gif" ? "540 \xD7 960" : "1080 \xD7 1920",
|
|
1472
|
-
")"
|
|
1473
|
-
] })
|
|
1474
|
-
]
|
|
1475
|
-
}
|
|
1476
|
-
)
|
|
1477
|
-
] }),
|
|
1478
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1479
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Captions" }),
|
|
1480
|
-
/* @__PURE__ */ jsxs(
|
|
1481
|
-
"select",
|
|
1482
|
-
{
|
|
1483
|
-
"aria-label": "Captions",
|
|
1484
|
-
style: themedSelectStyle,
|
|
1485
|
-
value: captionMode,
|
|
1486
|
-
onChange: (e) => setCaptionMode(e.target.value),
|
|
1487
|
-
children: [
|
|
1488
|
-
/* @__PURE__ */ jsx("option", { value: "off", children: "None" }),
|
|
1489
|
-
/* @__PURE__ */ jsx("option", { value: "standard", children: "Standard (top bar)" }),
|
|
1490
|
-
/* @__PURE__ */ jsx("option", { value: "social", children: "Social media (large words)" })
|
|
1491
|
-
]
|
|
1492
|
-
}
|
|
1493
|
-
)
|
|
1494
|
-
] }),
|
|
1495
|
-
outputFormat === "mp4" && /* @__PURE__ */ jsxs("div", { children: [
|
|
1496
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Audio" }),
|
|
1497
|
-
/* @__PURE__ */ jsxs(
|
|
1498
|
-
"select",
|
|
1499
|
-
{
|
|
1500
|
-
"aria-label": "Audio handling",
|
|
1501
|
-
style: themedSelectStyle,
|
|
1502
|
-
value: audioPolicy,
|
|
1503
|
-
onChange: (e) => setAudioPolicy(e.target.value),
|
|
1504
|
-
children: [
|
|
1505
|
-
/* @__PURE__ */ jsx("option", { value: "require", children: "Require document audio" }),
|
|
1506
|
-
/* @__PURE__ */ jsx("option", { value: "best-effort", children: "Best effort \u2014 allow video-only fallback" }),
|
|
1507
|
-
/* @__PURE__ */ jsx("option", { value: "omit", children: "Omit audio intentionally" })
|
|
1508
|
-
]
|
|
1509
|
-
}
|
|
1510
|
-
)
|
|
1511
|
-
] }),
|
|
1512
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1513
|
-
/* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Animations & transitions" }),
|
|
1514
|
-
/* @__PURE__ */ jsxs(
|
|
1515
|
-
"select",
|
|
1516
|
-
{
|
|
1517
|
-
"aria-label": "Animations and transitions",
|
|
1518
|
-
style: themedSelectStyle,
|
|
1519
|
-
value: animationsEnabled ? "enabled" : "disabled",
|
|
1520
|
-
onChange: (e) => setAnimationsEnabled(e.target.value === "enabled"),
|
|
1521
|
-
children: [
|
|
1522
|
-
/* @__PURE__ */ jsx("option", { value: "enabled", children: "Enabled" }),
|
|
1523
|
-
/* @__PURE__ */ jsx("option", { value: "disabled", children: "Disabled \u2014 smaller files" })
|
|
1524
|
-
]
|
|
1525
|
-
}
|
|
1526
|
-
),
|
|
1527
|
-
outputFormat === "gif" && animationsEnabled && /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "-6px 0 12px" }, children: "Disabling motion is recommended for much smaller GIFs." })
|
|
1528
|
-
] }),
|
|
1529
|
-
/* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
|
|
1530
|
-
/* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Cancel" }),
|
|
1531
|
-
/* @__PURE__ */ jsx("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: outputFormat === "gif" ? "Export GIF" : "Export Video" })
|
|
1532
|
-
] })
|
|
1533
|
-
] }),
|
|
1534
|
-
isExporting && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1535
|
-
backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 8px 0" }, children: [
|
|
1536
|
-
"Encoder: ",
|
|
1537
|
-
encoderLabel(completedOutputFormat, backend)
|
|
1538
|
-
] }),
|
|
1539
|
-
/* @__PURE__ */ jsx(
|
|
1540
|
-
"div",
|
|
1541
|
-
{
|
|
1542
|
-
role: "progressbar",
|
|
1543
|
-
"aria-label": "Video export progress",
|
|
1544
|
-
"aria-valuemin": 0,
|
|
1545
|
-
"aria-valuemax": 100,
|
|
1546
|
-
"aria-valuenow": progress,
|
|
1547
|
-
style: { ...progressBarOuterStyle, background: palette.secondary },
|
|
1548
|
-
"data-squisq-video-export-progress-track": true,
|
|
1549
|
-
children: /* @__PURE__ */ jsx(
|
|
1550
|
-
"div",
|
|
1551
|
-
{
|
|
1552
|
-
style: {
|
|
1553
|
-
width: `${progress}%`,
|
|
1554
|
-
height: "100%",
|
|
1555
|
-
background: palette.primary,
|
|
1556
|
-
transition: "width 0.3s ease"
|
|
1557
|
-
},
|
|
1558
|
-
"data-squisq-video-export-progress": true
|
|
1559
|
-
}
|
|
1560
|
-
)
|
|
1561
|
-
}
|
|
1562
|
-
),
|
|
1563
|
-
/* @__PURE__ */ jsxs("p", { style: { fontSize: 13, margin: "0 0 4px 0" }, children: [
|
|
1564
|
-
progress,
|
|
1565
|
-
"% complete"
|
|
1566
|
-
] }),
|
|
1567
|
-
/* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: 0 }, children: [
|
|
1568
|
-
formatDuration(elapsed),
|
|
1569
|
-
" elapsed",
|
|
1570
|
-
estimatedRemaining > 0 && ` \xB7 ~${formatDuration(estimatedRemaining)} remaining`
|
|
1571
|
-
] }),
|
|
1572
|
-
/* @__PURE__ */ jsx("div", { style: footerStyle, children: /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: cancelExport, children: "Cancel" }) })
|
|
1573
|
-
] }),
|
|
1574
|
-
state === "complete" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1575
|
-
/* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.success }, children: "Export complete!" }),
|
|
1576
|
-
/* @__PURE__ */ jsxs("p", { style: { fontSize: 13, color: palette.label, margin: "0 0 4px 0" }, children: [
|
|
1577
|
-
"File size: ",
|
|
1578
|
-
(fileSize / (1024 * 1024)).toFixed(1),
|
|
1579
|
-
" MB"
|
|
1580
|
-
] }),
|
|
1581
|
-
completedOutputFormat === "gif" ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: "Animated GIF does not include audio." }) : audioIncluded ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.success, margin: "0 0 4px 0" }, children: "Audio included \u2713" }) : /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: [
|
|
1582
|
-
"Video only",
|
|
1583
|
-
audioSkippedReason ? ` \u2014 ${audioSkippedReason}` : ""
|
|
1584
|
-
] }),
|
|
1585
|
-
backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 12px 0" }, children: [
|
|
1586
|
-
"Encoded with ",
|
|
1587
|
-
encoderLabel(completedOutputFormat, backend)
|
|
1588
|
-
] }),
|
|
1589
|
-
/* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
|
|
1590
|
-
/* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
|
|
1591
|
-
/* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleDownload, children: [
|
|
1592
|
-
"Download ",
|
|
1593
|
-
completedOutputFormat.toUpperCase()
|
|
1594
|
-
] })
|
|
1595
|
-
] })
|
|
1596
|
-
] }),
|
|
1597
|
-
state === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1598
|
-
/* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.danger }, children: "Export failed" }),
|
|
1599
|
-
/* @__PURE__ */ jsx(
|
|
1600
|
-
"p",
|
|
1601
|
-
{
|
|
1602
|
-
style: {
|
|
1603
|
-
fontSize: 13,
|
|
1604
|
-
color: palette.label,
|
|
1605
|
-
margin: "0 0 12px 0",
|
|
1606
|
-
wordBreak: "break-word"
|
|
1607
|
-
},
|
|
1608
|
-
children: error
|
|
1609
|
-
}
|
|
1610
|
-
),
|
|
1611
|
-
/* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
|
|
1612
|
-
/* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
|
|
1613
|
-
/* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: [
|
|
1614
|
-
"Retry ",
|
|
1615
|
-
outputFormat === "gif" ? "GIF" : "video"
|
|
1616
|
-
] })
|
|
1617
|
-
] })
|
|
1618
|
-
] })
|
|
1619
|
-
]
|
|
1620
|
-
}
|
|
1621
|
-
)
|
|
1622
|
-
}
|
|
1623
|
-
);
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
// src/VideoExportButton.tsx
|
|
1627
|
-
import { useState as useState3, useCallback as useCallback4 } from "react";
|
|
1628
|
-
import { createPortal } from "react-dom";
|
|
1629
|
-
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1630
|
-
function VideoExportButton({
|
|
1631
|
-
doc,
|
|
1632
|
-
playerScript,
|
|
1633
|
-
mediaProvider,
|
|
1634
|
-
images,
|
|
1635
|
-
audio,
|
|
1636
|
-
defaultConfig,
|
|
1637
|
-
colorScheme,
|
|
1638
|
-
uiPalette,
|
|
1639
|
-
label,
|
|
1640
|
-
style,
|
|
1641
|
-
disabled
|
|
1642
|
-
}) {
|
|
1643
|
-
const [showModal, setShowModal] = useState3(false);
|
|
1644
|
-
const resolvedLabel = label ?? (defaultConfig?.outputFormat === "gif" ? "Export GIF" : "Export Video");
|
|
1645
|
-
const handleOpen = useCallback4(() => setShowModal(true), []);
|
|
1646
|
-
const handleClose = useCallback4(() => setShowModal(false), []);
|
|
1647
|
-
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1648
|
-
/* @__PURE__ */ jsx2("button", { onClick: handleOpen, disabled, style, children: resolvedLabel }),
|
|
1649
|
-
showModal && createPortal(
|
|
1650
|
-
/* @__PURE__ */ jsx2(
|
|
1651
|
-
VideoExportModal,
|
|
1652
|
-
{
|
|
1653
|
-
doc,
|
|
1654
|
-
playerScript,
|
|
1655
|
-
mediaProvider,
|
|
1656
|
-
images,
|
|
1657
|
-
audio,
|
|
1658
|
-
defaultConfig,
|
|
1659
|
-
colorScheme,
|
|
1660
|
-
uiPalette,
|
|
1661
|
-
onClose: handleClose
|
|
1662
|
-
}
|
|
1663
|
-
),
|
|
1664
|
-
document.body
|
|
1665
|
-
)
|
|
1666
|
-
] });
|
|
1667
|
-
}
|
|
10
|
+
createEncoder,
|
|
11
|
+
supportsWebCodecs,
|
|
12
|
+
supportsWebCodecsAac,
|
|
13
|
+
supportsWebCodecsH264
|
|
14
|
+
} from "./chunk-YZN7D4IC.js";
|
|
15
|
+
import "./chunk-VI5AIVOQ.js";
|
|
1668
16
|
export {
|
|
1669
17
|
VideoExportButton,
|
|
1670
18
|
VideoExportModal,
|