@bendyline/squisq-video-react 2.0.1 → 2.1.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.
- package/COPYING.GPL-2.0.txt +339 -0
- package/LICENSE +21 -0
- package/NOTICE.md +49 -0
- package/README.md +34 -7
- package/dist/{chunk-6DKLHXX3.js → chunk-2PGWBGAT.js} +0 -1
- package/dist/index.d.ts +41 -3
- package/dist/index.js +395 -234
- package/dist/workers/encode.worker.js +37 -20
- package/dist/workers/ffmpeg.class-worker.js +0 -1
- package/package.json +8 -5
- package/dist/chunk-6DKLHXX3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/workers/encode.worker.js.map +0 -1
- package/dist/workers/ffmpeg.class-worker.js.map +0 -1
- package/src/VideoExportButton.tsx +0 -87
- package/src/VideoExportModal.tsx +0 -553
- package/src/__tests__/audioTrack.test.ts +0 -108
- package/src/__tests__/gifTranscode.test.ts +0 -118
- package/src/__tests__/mp4MuxAudio.test.ts +0 -72
- package/src/__tests__/useVideoExportGif.test.ts +0 -155
- package/src/__tests__/videoExportProps.test.tsx +0 -148
- package/src/__tests__/workerEncoder.test.ts +0 -143
- package/src/audioTrack.ts +0 -332
- package/src/gifTranscode.ts +0 -75
- package/src/hooks/useFrameCapture.ts +0 -268
- package/src/hooks/useVideoExport.ts +0 -647
- package/src/index.ts +0 -40
- package/src/mainThreadEncoder.ts +0 -158
- package/src/mp4Mux.ts +0 -123
- package/src/workerEncoder.ts +0 -175
- package/src/workers/encode.worker.ts +0 -439
- package/src/workers/ffmpeg.class-worker.ts +0 -11
- package/src/workers/workerTypes.ts +0 -89
package/dist/index.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMp4Muxer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2PGWBGAT.js";
|
|
4
4
|
|
|
5
5
|
// src/VideoExportModal.tsx
|
|
6
|
-
import { useState as useState2, useCallback as useCallback3 } from "react";
|
|
6
|
+
import { useState as useState2, useCallback as useCallback3, useId, useRef as useRef3 } from "react";
|
|
7
|
+
import { useModalDialog } from "@bendyline/squisq-react";
|
|
7
8
|
|
|
8
9
|
// src/hooks/useVideoExport.ts
|
|
9
10
|
import { useState, useRef as useRef2, useCallback as useCallback2, useEffect } from "react";
|
|
10
|
-
import {
|
|
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";
|
|
11
21
|
|
|
12
22
|
// src/mainThreadEncoder.ts
|
|
13
23
|
import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
|
|
@@ -43,14 +53,20 @@ function createEncoder(config) {
|
|
|
43
53
|
...config.audio ? { audio: config.audio } : {}
|
|
44
54
|
});
|
|
45
55
|
let closed = false;
|
|
56
|
+
let fatalError = null;
|
|
46
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
|
+
}
|
|
47
63
|
const encoder = new VideoEncoder({
|
|
48
64
|
output(chunk, meta) {
|
|
49
65
|
if (closed) return;
|
|
50
66
|
muxer.addVideoChunk(chunk, meta ?? void 0);
|
|
51
67
|
},
|
|
52
68
|
error(err) {
|
|
53
|
-
|
|
69
|
+
fatalError ?? (fatalError = new Error(`WebCodecs encoder error: ${err.message}`));
|
|
54
70
|
}
|
|
55
71
|
});
|
|
56
72
|
encoder.configure({
|
|
@@ -66,6 +82,10 @@ function createEncoder(config) {
|
|
|
66
82
|
});
|
|
67
83
|
return {
|
|
68
84
|
async encodeFrame(bitmap, frameIndex) {
|
|
85
|
+
if (fatalError) {
|
|
86
|
+
bitmap.close();
|
|
87
|
+
throw fail(fatalError);
|
|
88
|
+
}
|
|
69
89
|
if (closed) {
|
|
70
90
|
bitmap.close();
|
|
71
91
|
throw new Error("Encoder already closed");
|
|
@@ -83,7 +103,13 @@ function createEncoder(config) {
|
|
|
83
103
|
},
|
|
84
104
|
async finalize() {
|
|
85
105
|
if (closed) throw new Error("Encoder already closed");
|
|
86
|
-
|
|
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);
|
|
87
113
|
encoder.close();
|
|
88
114
|
closed = true;
|
|
89
115
|
return muxer.finalize();
|
|
@@ -177,6 +203,7 @@ function createWorkerEncoder(config) {
|
|
|
177
203
|
height: config.height,
|
|
178
204
|
fps: config.fps,
|
|
179
205
|
quality: config.quality,
|
|
206
|
+
...config.totalFrames !== void 0 ? { totalFrames: config.totalFrames } : {},
|
|
180
207
|
...config.ffmpegWasm ? { ffmpegWasm: config.ffmpegWasm } : {}
|
|
181
208
|
});
|
|
182
209
|
return {
|
|
@@ -236,7 +263,8 @@ function createWorkerEncoder(config) {
|
|
|
236
263
|
|
|
237
264
|
// src/audioTrack.ts
|
|
238
265
|
import {
|
|
239
|
-
ffmpegAudioMuxArgs
|
|
266
|
+
ffmpegAudioMuxArgs,
|
|
267
|
+
resolveFfmpegWasmLoad
|
|
240
268
|
} from "@bendyline/squisq-video";
|
|
241
269
|
var EXPORT_AUDIO_SAMPLE_RATE = 48e3;
|
|
242
270
|
var EXPORT_AUDIO_CHANNELS = 2;
|
|
@@ -382,13 +410,13 @@ function audioBufferToWav(buffer) {
|
|
|
382
410
|
return new Uint8Array(out);
|
|
383
411
|
}
|
|
384
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
|
+
});
|
|
385
416
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
386
417
|
const ffmpeg = new FFmpeg();
|
|
387
418
|
try {
|
|
388
|
-
await ffmpeg.load(
|
|
389
|
-
...loadConfig,
|
|
390
|
-
classWorkerURL: loadConfig?.classWorkerURL ?? new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
|
|
391
|
-
});
|
|
419
|
+
await ffmpeg.load(load);
|
|
392
420
|
await ffmpeg.writeFile("video.mp4", videoMp4);
|
|
393
421
|
await ffmpeg.writeFile("audio.wav", wav);
|
|
394
422
|
const exitCode = await ffmpeg.exec([
|
|
@@ -413,7 +441,8 @@ async function muxAudioWithFfmpegWasm(videoMp4, wav, audioBitrate, loadConfig) {
|
|
|
413
441
|
|
|
414
442
|
// src/gifTranscode.ts
|
|
415
443
|
import {
|
|
416
|
-
ffmpegGifOutputArgs
|
|
444
|
+
ffmpegGifOutputArgs,
|
|
445
|
+
resolveFfmpegWasmLoad as resolveFfmpegWasmLoad2
|
|
417
446
|
} from "@bendyline/squisq-video";
|
|
418
447
|
function buildGifFfmpegArgs(options) {
|
|
419
448
|
return ["-y", "-i", "video.mp4", ...ffmpegGifOutputArgs(options), "out.gif"];
|
|
@@ -425,6 +454,9 @@ async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, si
|
|
|
425
454
|
if (signal?.aborted) {
|
|
426
455
|
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
427
456
|
}
|
|
457
|
+
const load = resolveFfmpegWasmLoad2(loadConfig, "Animated GIF export", {
|
|
458
|
+
classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
|
|
459
|
+
});
|
|
428
460
|
const args = buildGifFfmpegArgs(options);
|
|
429
461
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
430
462
|
const ffmpeg = new FFmpeg();
|
|
@@ -437,10 +469,7 @@ async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, si
|
|
|
437
469
|
const handleAbort = () => terminate();
|
|
438
470
|
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
439
471
|
try {
|
|
440
|
-
await ffmpeg.load(
|
|
441
|
-
...loadConfig,
|
|
442
|
-
classWorkerURL: loadConfig?.classWorkerURL ?? new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
|
|
443
|
-
});
|
|
472
|
+
await ffmpeg.load(load);
|
|
444
473
|
if (signal?.aborted) {
|
|
445
474
|
throw new DOMException("Animated GIF export was cancelled.", "AbortError");
|
|
446
475
|
}
|
|
@@ -479,33 +508,24 @@ var MIME_MAP = {
|
|
|
479
508
|
bmp: "image/bmp",
|
|
480
509
|
avif: "image/avif"
|
|
481
510
|
};
|
|
482
|
-
function arrayBufferToDataUrl(buffer, mime) {
|
|
483
|
-
const bytes = new Uint8Array(buffer);
|
|
484
|
-
const chunks = [];
|
|
485
|
-
const CHUNK = 8192;
|
|
486
|
-
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
487
|
-
chunks.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
|
|
488
|
-
}
|
|
489
|
-
return `data:${mime};base64,${btoa(chunks.join(""))}`;
|
|
490
|
-
}
|
|
491
511
|
function createInlineProvider(images) {
|
|
492
|
-
const
|
|
512
|
+
const blobUrls = /* @__PURE__ */ new Map();
|
|
493
513
|
const mimeTypes = /* @__PURE__ */ new Map();
|
|
494
514
|
for (const [path, buffer] of images) {
|
|
495
515
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
496
516
|
const mime = MIME_MAP[ext] ?? "application/octet-stream";
|
|
497
|
-
|
|
517
|
+
blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
|
|
498
518
|
mimeTypes.set(path, mime);
|
|
499
519
|
}
|
|
500
520
|
return {
|
|
501
521
|
async resolveUrl(relativePath) {
|
|
502
|
-
return
|
|
522
|
+
return blobUrls.get(relativePath) ?? relativePath;
|
|
503
523
|
},
|
|
504
524
|
async listMedia() {
|
|
505
|
-
return [...
|
|
525
|
+
return [...blobUrls.keys()].map((name) => ({
|
|
506
526
|
name,
|
|
507
527
|
mimeType: mimeTypes.get(name) ?? "application/octet-stream",
|
|
508
|
-
size: 0
|
|
528
|
+
size: images.get(name)?.byteLength ?? 0
|
|
509
529
|
}));
|
|
510
530
|
},
|
|
511
531
|
async addMedia() {
|
|
@@ -515,6 +535,8 @@ function createInlineProvider(images) {
|
|
|
515
535
|
throw new Error("Read-only");
|
|
516
536
|
},
|
|
517
537
|
dispose() {
|
|
538
|
+
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
539
|
+
blobUrls.clear();
|
|
518
540
|
}
|
|
519
541
|
};
|
|
520
542
|
}
|
|
@@ -522,19 +544,23 @@ function useFrameCapture() {
|
|
|
522
544
|
const containerRef = useRef(null);
|
|
523
545
|
const rootRef = useRef(null);
|
|
524
546
|
const renderAPIRef = useRef(null);
|
|
547
|
+
const mediaProviderRef = useRef(null);
|
|
525
548
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
526
549
|
const init = useCallback(
|
|
527
550
|
async (doc, renderOptions, captionMode) => {
|
|
528
|
-
if (rootRef.current || containerRef.current) {
|
|
551
|
+
if (rootRef.current || containerRef.current || mediaProviderRef.current) {
|
|
529
552
|
const oldRoot = rootRef.current;
|
|
530
553
|
const oldContainer = containerRef.current;
|
|
554
|
+
const oldMediaProvider = mediaProviderRef.current;
|
|
531
555
|
rootRef.current = null;
|
|
532
556
|
containerRef.current = null;
|
|
533
557
|
renderAPIRef.current = null;
|
|
558
|
+
mediaProviderRef.current = null;
|
|
534
559
|
await new Promise((resolve) => {
|
|
535
560
|
setTimeout(() => {
|
|
536
561
|
if (oldRoot) oldRoot.unmount();
|
|
537
562
|
if (oldContainer) oldContainer.remove();
|
|
563
|
+
oldMediaProvider?.dispose();
|
|
538
564
|
resolve();
|
|
539
565
|
}, 0);
|
|
540
566
|
});
|
|
@@ -552,6 +578,7 @@ function useFrameCapture() {
|
|
|
552
578
|
renderRoot.style.cssText = `width:${width}px;height:${height}px;`;
|
|
553
579
|
container.appendChild(renderRoot);
|
|
554
580
|
const mediaProvider = renderOptions.images ? createInlineProvider(renderOptions.images) : null;
|
|
581
|
+
mediaProviderRef.current = mediaProvider;
|
|
555
582
|
const root = createRoot(renderRoot);
|
|
556
583
|
rootRef.current = root;
|
|
557
584
|
const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
|
|
@@ -639,12 +666,40 @@ function useFrameCapture() {
|
|
|
639
666
|
containerRef.current.remove();
|
|
640
667
|
containerRef.current = null;
|
|
641
668
|
}
|
|
669
|
+
mediaProviderRef.current?.dispose();
|
|
670
|
+
mediaProviderRef.current = null;
|
|
642
671
|
renderAPIRef.current = null;
|
|
643
672
|
}, []);
|
|
644
673
|
return useMemo(() => ({ init, captureFrame, destroy }), [init, captureFrame, destroy]);
|
|
645
674
|
}
|
|
646
675
|
|
|
647
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
|
+
}
|
|
648
703
|
async function resolveAudioBuffers(clips, sources) {
|
|
649
704
|
const srcs = new Set(clips.map((c) => c.src));
|
|
650
705
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -653,8 +708,10 @@ async function resolveAudioBuffers(clips, sources) {
|
|
|
653
708
|
if (!data && sources.mediaProvider) {
|
|
654
709
|
try {
|
|
655
710
|
const url = await sources.mediaProvider.resolveUrl(src);
|
|
656
|
-
const
|
|
657
|
-
|
|
711
|
+
const resource = await fetchResourceBytes(url, {
|
|
712
|
+
policy: sources.resourcePolicy
|
|
713
|
+
});
|
|
714
|
+
data = toArrayBuffer(resource.bytes);
|
|
658
715
|
} catch {
|
|
659
716
|
}
|
|
660
717
|
}
|
|
@@ -755,6 +812,7 @@ function useVideoExport() {
|
|
|
755
812
|
const fps = config.fps ?? (effectiveOutputFormat === "gif" ? 10 : 30);
|
|
756
813
|
const orientation = config.orientation ?? "landscape";
|
|
757
814
|
const animationsEnabled = config.animationsEnabled ?? effectiveOutputFormat === "mp4";
|
|
815
|
+
const audioPolicy = config.audioPolicy ?? "require";
|
|
758
816
|
setOutputFormat(effectiveOutputFormat);
|
|
759
817
|
try {
|
|
760
818
|
const gifDefaults = orientation === "portrait" ? { width: 540, height: 960 } : { width: 960, height: 540 };
|
|
@@ -772,6 +830,9 @@ function useVideoExport() {
|
|
|
772
830
|
"Animated GIF export requires ffmpeg.wasm and SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
773
831
|
);
|
|
774
832
|
}
|
|
833
|
+
if (effectiveOutputFormat === "gif") {
|
|
834
|
+
resolveFfmpegWasmLoad3(config.ffmpegWasm, "Animated GIF export");
|
|
835
|
+
}
|
|
775
836
|
if (!webCodecsAvailable && !sharedArrayBufferAvailable) {
|
|
776
837
|
throw new Error(
|
|
777
838
|
"No video encoder available. WebCodecs requires Chrome 94+ / Edge 94+, and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
@@ -791,13 +852,38 @@ function useVideoExport() {
|
|
|
791
852
|
if (!images && config.mediaProvider) {
|
|
792
853
|
images = /* @__PURE__ */ new Map();
|
|
793
854
|
const entries = await config.mediaProvider.listMedia();
|
|
794
|
-
|
|
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
|
+
}
|
|
795
870
|
const url2 = await config.mediaProvider.resolveUrl(entry.name);
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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.");
|
|
800
885
|
}
|
|
886
|
+
images.set(entry.name, data);
|
|
801
887
|
}
|
|
802
888
|
}
|
|
803
889
|
const docDuration = await frameCapture.init(
|
|
@@ -810,11 +896,12 @@ function useVideoExport() {
|
|
|
810
896
|
if (docDuration <= 0) {
|
|
811
897
|
throw new Error("Document has zero duration \u2014 nothing to export");
|
|
812
898
|
}
|
|
899
|
+
const totalFrames = Math.ceil(docDuration * fps);
|
|
813
900
|
setPhase("Starting encoder\u2026");
|
|
814
901
|
setProgress(5);
|
|
815
902
|
const canUseWebCodecs = webCodecsAvailable && await supportsWebCodecsH264({ width, height, fps, quality });
|
|
816
903
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
817
|
-
const timeline = effectiveOutputFormat === "mp4" ? computeAudioTimeline(doc, 0) : [];
|
|
904
|
+
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, 0) : [];
|
|
818
905
|
const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
|
|
819
906
|
const tierDecision = selectAudioTier({
|
|
820
907
|
hasClips: timeline.length > 0,
|
|
@@ -825,16 +912,27 @@ function useVideoExport() {
|
|
|
825
912
|
let renderedAudio = null;
|
|
826
913
|
let audioIncludedLocal = false;
|
|
827
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
|
+
}
|
|
828
918
|
if (tierDecision.tier === 1 || tierDecision.tier === 2) {
|
|
829
919
|
setPhase("Preparing audio\u2026");
|
|
830
920
|
try {
|
|
831
921
|
const buffers = await resolveAudioBuffers(timeline, {
|
|
832
922
|
audio: config.audio,
|
|
833
923
|
images,
|
|
834
|
-
mediaProvider: config.mediaProvider
|
|
924
|
+
mediaProvider: config.mediaProvider,
|
|
925
|
+
resourcePolicy: config.resourcePolicy
|
|
835
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
|
+
}
|
|
836
934
|
if (buffers.size === 0) {
|
|
837
|
-
audioReasonLocal = "Audio files for this document could not be loaded.";
|
|
935
|
+
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
838
936
|
} else {
|
|
839
937
|
const totalAudioDur = timeline.reduce(
|
|
840
938
|
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
@@ -850,6 +948,7 @@ function useVideoExport() {
|
|
|
850
948
|
} catch (audioErr) {
|
|
851
949
|
renderedAudio = null;
|
|
852
950
|
audioReasonLocal = `Audio could not be prepared: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
951
|
+
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
853
952
|
}
|
|
854
953
|
}
|
|
855
954
|
const useInlineAudio = renderedAudio !== null && tierDecision.tier === 1;
|
|
@@ -876,6 +975,7 @@ function useVideoExport() {
|
|
|
876
975
|
height,
|
|
877
976
|
fps,
|
|
878
977
|
quality,
|
|
978
|
+
totalFrames,
|
|
879
979
|
ffmpegWasm: config.ffmpegWasm
|
|
880
980
|
});
|
|
881
981
|
encoder = workerEncoder;
|
|
@@ -892,7 +992,6 @@ function useVideoExport() {
|
|
|
892
992
|
encoderRef.current = encoder;
|
|
893
993
|
if (cancelledRef.current) return;
|
|
894
994
|
setState("capturing");
|
|
895
|
-
const totalFrames = Math.ceil(docDuration * fps);
|
|
896
995
|
const captureStartTime = performance.now();
|
|
897
996
|
const UI_UPDATE_INTERVAL = 10;
|
|
898
997
|
for (let i = 0; i < totalFrames; i++) {
|
|
@@ -930,6 +1029,7 @@ function useVideoExport() {
|
|
|
930
1029
|
} catch (audioErr) {
|
|
931
1030
|
audioIncludedLocal = false;
|
|
932
1031
|
audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1032
|
+
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
933
1033
|
}
|
|
934
1034
|
}
|
|
935
1035
|
setState("encoding");
|
|
@@ -968,6 +1068,7 @@ function useVideoExport() {
|
|
|
968
1068
|
} catch (audioErr) {
|
|
969
1069
|
audioIncludedLocal = false;
|
|
970
1070
|
audioReasonLocal = `Audio muxing failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1071
|
+
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
971
1072
|
}
|
|
972
1073
|
}
|
|
973
1074
|
if (cancelledRef.current) return;
|
|
@@ -1053,6 +1154,7 @@ var VIDEO_EXPORT_PALETTES = {
|
|
|
1053
1154
|
secondary: "#E8DFC6",
|
|
1054
1155
|
primary: "#8B6914",
|
|
1055
1156
|
primaryBorder: "#7a5c10",
|
|
1157
|
+
primaryText: "#ffffff",
|
|
1056
1158
|
success: "#2d6a10",
|
|
1057
1159
|
danger: "#a03020"
|
|
1058
1160
|
},
|
|
@@ -1068,6 +1170,7 @@ var VIDEO_EXPORT_PALETTES = {
|
|
|
1068
1170
|
secondary: "#1e293b",
|
|
1069
1171
|
primary: "#9a7416",
|
|
1070
1172
|
primaryBorder: "#d1a73b",
|
|
1173
|
+
primaryText: "#ffffff",
|
|
1071
1174
|
success: "#86efac",
|
|
1072
1175
|
danger: "#fca5a5"
|
|
1073
1176
|
}
|
|
@@ -1113,7 +1216,6 @@ var btnPrimary = {
|
|
|
1113
1216
|
fontFamily: "inherit",
|
|
1114
1217
|
fontWeight: 500,
|
|
1115
1218
|
cursor: "pointer",
|
|
1116
|
-
color: "#fff",
|
|
1117
1219
|
borderRadius: 0
|
|
1118
1220
|
};
|
|
1119
1221
|
var btnSecondary = {
|
|
@@ -1145,8 +1247,12 @@ function VideoExportModal({
|
|
|
1145
1247
|
audio,
|
|
1146
1248
|
defaultConfig,
|
|
1147
1249
|
colorScheme = "light",
|
|
1250
|
+
uiPalette,
|
|
1148
1251
|
onClose
|
|
1149
1252
|
}) {
|
|
1253
|
+
const overlayRef = useRef3(null);
|
|
1254
|
+
const dialogRef = useRef3(null);
|
|
1255
|
+
const titleId = useId();
|
|
1150
1256
|
const initialOutputFormat = defaultConfig?.outputFormat ?? "mp4";
|
|
1151
1257
|
const [outputFormat, setOutputFormat] = useState2(initialOutputFormat);
|
|
1152
1258
|
const [quality, setQuality] = useState2(defaultConfig?.quality ?? "normal");
|
|
@@ -1158,7 +1264,13 @@ function VideoExportModal({
|
|
|
1158
1264
|
const [animationsEnabled, setAnimationsEnabled] = useState2(
|
|
1159
1265
|
defaultConfig?.animationsEnabled ?? initialOutputFormat === "mp4"
|
|
1160
1266
|
);
|
|
1161
|
-
const
|
|
1267
|
+
const [audioPolicy, setAudioPolicy] = useState2(
|
|
1268
|
+
defaultConfig?.audioPolicy ?? "require"
|
|
1269
|
+
);
|
|
1270
|
+
const palette = {
|
|
1271
|
+
...VIDEO_EXPORT_PALETTES[colorScheme],
|
|
1272
|
+
...uiPalette
|
|
1273
|
+
};
|
|
1162
1274
|
const themedModalStyle = {
|
|
1163
1275
|
...modalStyle,
|
|
1164
1276
|
background: palette.surface,
|
|
@@ -1178,7 +1290,8 @@ function VideoExportModal({
|
|
|
1178
1290
|
const themedPrimaryButtonStyle = {
|
|
1179
1291
|
...btnPrimary,
|
|
1180
1292
|
background: palette.primary,
|
|
1181
|
-
border: `1px solid ${palette.primaryBorder}
|
|
1293
|
+
border: `1px solid ${palette.primaryBorder}`,
|
|
1294
|
+
color: palette.primaryText
|
|
1182
1295
|
};
|
|
1183
1296
|
const themedSecondaryButtonStyle = {
|
|
1184
1297
|
...btnSecondary,
|
|
@@ -1223,6 +1336,7 @@ function VideoExportModal({
|
|
|
1223
1336
|
fps,
|
|
1224
1337
|
orientation,
|
|
1225
1338
|
captionMode,
|
|
1339
|
+
audioPolicy,
|
|
1226
1340
|
images,
|
|
1227
1341
|
audio,
|
|
1228
1342
|
mediaProvider,
|
|
@@ -1238,6 +1352,7 @@ function VideoExportModal({
|
|
|
1238
1352
|
fps,
|
|
1239
1353
|
orientation,
|
|
1240
1354
|
captionMode,
|
|
1355
|
+
audioPolicy,
|
|
1241
1356
|
images,
|
|
1242
1357
|
audio,
|
|
1243
1358
|
mediaProvider,
|
|
@@ -1263,202 +1378,247 @@ function VideoExportModal({
|
|
|
1263
1378
|
onClose();
|
|
1264
1379
|
}, [state, cancelExport, resetExport, onClose]);
|
|
1265
1380
|
const isExporting = state === "preparing" || state === "capturing" || state === "encoding";
|
|
1381
|
+
useModalDialog({ rootRef: overlayRef, dialogRef, onClose: handleClose });
|
|
1266
1382
|
return /* @__PURE__ */ jsx(
|
|
1267
1383
|
"div",
|
|
1268
1384
|
{
|
|
1385
|
+
ref: overlayRef,
|
|
1269
1386
|
style: { ...overlayStyle, background: palette.overlay },
|
|
1270
1387
|
"data-color-scheme": colorScheme,
|
|
1271
1388
|
onClick: handleClose,
|
|
1272
|
-
children: /* @__PURE__ */ jsxs(
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
"
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
},
|
|
1450
|
-
children:
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
+
] })
|
|
1458
1618
|
] })
|
|
1459
|
-
]
|
|
1460
|
-
|
|
1461
|
-
|
|
1619
|
+
]
|
|
1620
|
+
}
|
|
1621
|
+
)
|
|
1462
1622
|
}
|
|
1463
1623
|
);
|
|
1464
1624
|
}
|
|
@@ -1475,6 +1635,7 @@ function VideoExportButton({
|
|
|
1475
1635
|
audio,
|
|
1476
1636
|
defaultConfig,
|
|
1477
1637
|
colorScheme,
|
|
1638
|
+
uiPalette,
|
|
1478
1639
|
label,
|
|
1479
1640
|
style,
|
|
1480
1641
|
disabled
|
|
@@ -1496,6 +1657,7 @@ function VideoExportButton({
|
|
|
1496
1657
|
audio,
|
|
1497
1658
|
defaultConfig,
|
|
1498
1659
|
colorScheme,
|
|
1660
|
+
uiPalette,
|
|
1499
1661
|
onClose: handleClose
|
|
1500
1662
|
}
|
|
1501
1663
|
),
|
|
@@ -1513,4 +1675,3 @@ export {
|
|
|
1513
1675
|
useFrameCapture,
|
|
1514
1676
|
useVideoExport
|
|
1515
1677
|
};
|
|
1516
|
-
//# sourceMappingURL=index.js.map
|