@bendyline/squisq-video-react 2.4.10 → 2.4.12
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 +20 -9
- package/THIRD_PARTY_LICENSES.txt +378 -0
- package/dist/{chunk-BRE3IRIM.js → chunk-3EJKBFEK.js} +1 -1
- package/dist/chunk-4GFZ3GUE.js +24645 -0
- package/dist/{chunk-4NI2QIFE.js → chunk-7ZMJ25DC.js} +316 -18
- package/dist/{chunk-JGSJQZGU.js → chunk-BNUM3BXO.js} +39 -3
- package/dist/{chunk-BGS2NEYC.js → chunk-ED7QK6VC.js} +5 -3
- package/dist/{chunk-32QCPFXE.js → chunk-GBIYYDMY.js} +31 -1
- package/dist/chunk-IFQJAW4V.js +321 -0
- package/dist/chunk-SLLH2RTX.js +452 -0
- package/dist/chunk-TL2IMDIZ.js +27 -0
- package/dist/components/index.d.ts +9 -2
- package/dist/components/index.js +6 -5
- package/dist/cover-image/index.js +3 -2
- package/dist/dashboard-image/index.js +2 -1
- package/dist/encoder/index.js +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +4 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +19 -5
- package/dist/media-edit/index.d.ts +29 -0
- package/dist/media-edit/index.js +15 -0
- package/dist/mediaEditEngine-ZLG7D3TB.js +13 -0
- package/dist/mediaEditRenderManager-W_o6RRhT.d.ts +167 -0
- package/dist/{useVideoExport-B2eVCvTX.d.ts → useVideoExport-DsNDpjmO.d.ts} +7 -1
- package/dist/workers/mediaEdit.worker.d.ts +2 -0
- package/dist/workers/mediaEdit.worker.js +63 -0
- package/package.json +11 -5
|
@@ -1,10 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlobSource,
|
|
3
|
+
Input,
|
|
4
|
+
MATROSKA,
|
|
5
|
+
MP4,
|
|
6
|
+
QTFF,
|
|
7
|
+
VideoSampleSink,
|
|
8
|
+
WEBM
|
|
9
|
+
} from "./chunk-4GFZ3GUE.js";
|
|
10
|
+
|
|
1
11
|
// src/hooks/useFrameCapture.ts
|
|
2
12
|
import { createElement } from "react";
|
|
3
13
|
import { createRoot } from "react-dom/client";
|
|
4
14
|
import { useRef, useCallback, useMemo } from "react";
|
|
15
|
+
import { parseMediaCrop } from "@bendyline/squisq/mediaEdit";
|
|
5
16
|
import { resolveDashboardStyleId } from "@bendyline/squisq/doc";
|
|
6
17
|
import { DocPlayer, MediaContext } from "@bendyline/squisq-react";
|
|
7
18
|
import html2canvas from "html2canvas";
|
|
19
|
+
|
|
20
|
+
// src/hooks/captureVideoFrames.ts
|
|
21
|
+
var FRAME_TIME_EPSILON_SECONDS = 1e-4;
|
|
22
|
+
var MAX_SEQUENTIAL_DECODE_GAP_SECONDS = 3;
|
|
23
|
+
var DECODABLE_SOURCE_URL = /^(?:blob|data):/i;
|
|
24
|
+
var INPUT_FORMATS = [MP4, QTFF, WEBM, MATROSKA];
|
|
25
|
+
var decodedFrames = /* @__PURE__ */ new WeakMap();
|
|
26
|
+
function getDecodedCaptureFrame(video) {
|
|
27
|
+
const frame = decodedFrames.get(video);
|
|
28
|
+
if (!frame || frame.timestamp === null) return null;
|
|
29
|
+
return {
|
|
30
|
+
canvas: frame.canvas,
|
|
31
|
+
width: frame.width,
|
|
32
|
+
height: frame.height,
|
|
33
|
+
timestamp: frame.timestamp
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function resolveCaptureVideoImage(video) {
|
|
37
|
+
const decoded = getDecodedCaptureFrame(video);
|
|
38
|
+
if (decoded) return { source: decoded.canvas, width: decoded.width, height: decoded.height };
|
|
39
|
+
return { source: video, width: video.videoWidth, height: video.videoHeight };
|
|
40
|
+
}
|
|
41
|
+
function captureVideoSource(video) {
|
|
42
|
+
return video.currentSrc || video.src;
|
|
43
|
+
}
|
|
44
|
+
var DecodedVideoCursor = class _DecodedVideoCursor {
|
|
45
|
+
/**
|
|
46
|
+
* @param source Sequential sample iterator factory for one video track.
|
|
47
|
+
* @param releaseSource Frees the demuxer behind `source`.
|
|
48
|
+
*/
|
|
49
|
+
constructor(source, releaseSource, canvas, width, height) {
|
|
50
|
+
this.source = source;
|
|
51
|
+
this.releaseSource = releaseSource;
|
|
52
|
+
this.iterator = null;
|
|
53
|
+
this.lookahead = null;
|
|
54
|
+
/** True while showing a track's first frame for a target before it. */
|
|
55
|
+
this.showingLeadingFrame = false;
|
|
56
|
+
this.queue = Promise.resolve();
|
|
57
|
+
this.disposed = false;
|
|
58
|
+
canvas.width = width;
|
|
59
|
+
canvas.height = height;
|
|
60
|
+
this.frame = { canvas, width, height, timestamp: null };
|
|
61
|
+
}
|
|
62
|
+
/** Open a local source, or null when it has no decodable video track. */
|
|
63
|
+
static async open(blob, ownerDocument) {
|
|
64
|
+
const input = new Input({ source: new BlobSource(blob), formats: INPUT_FORMATS });
|
|
65
|
+
try {
|
|
66
|
+
const track = await input.getPrimaryVideoTrack();
|
|
67
|
+
if (!track || !await track.canDecode()) {
|
|
68
|
+
input.dispose();
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const width = track.displayWidth;
|
|
72
|
+
const height = track.displayHeight;
|
|
73
|
+
if (width <= 0 || height <= 0) {
|
|
74
|
+
input.dispose();
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return new _DecodedVideoCursor(
|
|
78
|
+
new VideoSampleSink(track),
|
|
79
|
+
() => input.dispose(),
|
|
80
|
+
ownerDocument.createElement("canvas"),
|
|
81
|
+
width,
|
|
82
|
+
height
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
input.dispose();
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Draw the frame displayed at `targetTime` (seconds). Calls run in order. */
|
|
90
|
+
selectFrame(targetTime) {
|
|
91
|
+
const selection = this.queue.then(() => this.advanceTo(targetTime));
|
|
92
|
+
this.queue = selection.catch(() => void 0);
|
|
93
|
+
return selection;
|
|
94
|
+
}
|
|
95
|
+
dispose() {
|
|
96
|
+
if (this.disposed) return;
|
|
97
|
+
this.disposed = true;
|
|
98
|
+
this.lookahead?.close();
|
|
99
|
+
this.lookahead = null;
|
|
100
|
+
void this.iterator?.return();
|
|
101
|
+
this.iterator = null;
|
|
102
|
+
this.releaseSource();
|
|
103
|
+
this.frame.canvas.width = 0;
|
|
104
|
+
this.frame.canvas.height = 0;
|
|
105
|
+
this.frame.timestamp = null;
|
|
106
|
+
}
|
|
107
|
+
async advanceTo(targetTime) {
|
|
108
|
+
if (this.disposed) throw new Error("Decoded capture video was released.");
|
|
109
|
+
const target = targetTime + FRAME_TIME_EPSILON_SECONDS;
|
|
110
|
+
const shown = this.frame.timestamp;
|
|
111
|
+
const continues = this.iterator !== null && shown !== null && target - shown <= MAX_SEQUENTIAL_DECODE_GAP_SECONDS && (target >= shown || this.showingLeadingFrame);
|
|
112
|
+
if (continues && target < shown) return;
|
|
113
|
+
let candidate = null;
|
|
114
|
+
try {
|
|
115
|
+
if (!continues) {
|
|
116
|
+
await this.restartAt(target);
|
|
117
|
+
candidate = await this.pull();
|
|
118
|
+
if (!candidate) return;
|
|
119
|
+
}
|
|
120
|
+
for (; ; ) {
|
|
121
|
+
const next = this.lookahead ?? await this.pull();
|
|
122
|
+
this.lookahead = null;
|
|
123
|
+
if (!next) break;
|
|
124
|
+
if (next.timestamp > target) {
|
|
125
|
+
this.lookahead = next;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
candidate?.close();
|
|
129
|
+
candidate = next;
|
|
130
|
+
}
|
|
131
|
+
if (candidate && !this.disposed) {
|
|
132
|
+
this.present(candidate);
|
|
133
|
+
this.showingLeadingFrame = candidate.timestamp > target;
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
candidate?.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async restartAt(target) {
|
|
140
|
+
this.lookahead?.close();
|
|
141
|
+
this.lookahead = null;
|
|
142
|
+
await this.iterator?.return();
|
|
143
|
+
this.iterator = this.source.samples(target);
|
|
144
|
+
}
|
|
145
|
+
async pull() {
|
|
146
|
+
const result = await this.iterator?.next();
|
|
147
|
+
return result && !result.done ? result.value : null;
|
|
148
|
+
}
|
|
149
|
+
present(sample) {
|
|
150
|
+
const { frame } = this;
|
|
151
|
+
if (sample.displayWidth !== frame.width || sample.displayHeight !== frame.height) {
|
|
152
|
+
frame.width = sample.displayWidth;
|
|
153
|
+
frame.height = sample.displayHeight;
|
|
154
|
+
frame.canvas.width = frame.width;
|
|
155
|
+
frame.canvas.height = frame.height;
|
|
156
|
+
}
|
|
157
|
+
const context = frame.canvas.getContext("2d");
|
|
158
|
+
if (!context) throw new Error("Could not create the decoded video frame canvas context");
|
|
159
|
+
sample.draw(context, 0, 0, frame.width, frame.height);
|
|
160
|
+
frame.timestamp = sample.timestamp;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var CaptureVideoFrameRegistry = class {
|
|
164
|
+
constructor() {
|
|
165
|
+
this.bindings = /* @__PURE__ */ new Map();
|
|
166
|
+
this.blobs = /* @__PURE__ */ new Map();
|
|
167
|
+
/** Sources that failed to open or decode; they keep the element path. */
|
|
168
|
+
this.undecodableSources = /* @__PURE__ */ new Set();
|
|
169
|
+
this.opening = /* @__PURE__ */ new Map();
|
|
170
|
+
this.disposed = false;
|
|
171
|
+
this.selectFrame = async (video, targetTime) => {
|
|
172
|
+
const binding = this.bindings.get(video);
|
|
173
|
+
if (!binding || binding.src !== captureVideoSource(video)) return false;
|
|
174
|
+
try {
|
|
175
|
+
await binding.cursor.selectFrame(targetTime);
|
|
176
|
+
return true;
|
|
177
|
+
} catch {
|
|
178
|
+
this.undecodableSources.add(binding.src);
|
|
179
|
+
this.release(video);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Bind decoders to every video under `root` that lacks one for its current
|
|
186
|
+
* source, and release bindings whose element left the DOM or changed
|
|
187
|
+
* source. Returns how many elements were newly bound: their pixels now
|
|
188
|
+
* come from the decoder, so a caller must re-run its seek barrier.
|
|
189
|
+
*/
|
|
190
|
+
async attach(root) {
|
|
191
|
+
for (const [video, binding] of this.bindings) {
|
|
192
|
+
if (!video.isConnected || captureVideoSource(video) !== binding.src) this.release(video);
|
|
193
|
+
}
|
|
194
|
+
const results = await Promise.all(
|
|
195
|
+
Array.from(root.querySelectorAll("video")).filter((video) => !this.bindings.has(video)).map((video) => this.bind(video))
|
|
196
|
+
);
|
|
197
|
+
return results.filter(Boolean).length;
|
|
198
|
+
}
|
|
199
|
+
/** Whether capture draws this element from decoded frames. */
|
|
200
|
+
isDecoded(video) {
|
|
201
|
+
return this.bindings.has(video);
|
|
202
|
+
}
|
|
203
|
+
dispose() {
|
|
204
|
+
this.disposed = true;
|
|
205
|
+
for (const video of [...this.bindings.keys()]) this.release(video);
|
|
206
|
+
this.blobs.clear();
|
|
207
|
+
this.opening.clear();
|
|
208
|
+
}
|
|
209
|
+
bind(video) {
|
|
210
|
+
const pending = this.opening.get(video);
|
|
211
|
+
if (pending) return pending;
|
|
212
|
+
const opening = this.open(video).finally(() => this.opening.delete(video));
|
|
213
|
+
this.opening.set(video, opening);
|
|
214
|
+
return opening;
|
|
215
|
+
}
|
|
216
|
+
async open(video) {
|
|
217
|
+
const src = captureVideoSource(video);
|
|
218
|
+
if (!src || !DECODABLE_SOURCE_URL.test(src) || this.undecodableSources.has(src)) return false;
|
|
219
|
+
const blob = await this.loadBlob(src);
|
|
220
|
+
const cursor = blob ? await DecodedVideoCursor.open(blob, video.ownerDocument) : null;
|
|
221
|
+
if (!cursor) {
|
|
222
|
+
this.undecodableSources.add(src);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (this.disposed || !video.isConnected || captureVideoSource(video) !== src) {
|
|
226
|
+
cursor.dispose();
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
this.bindings.set(video, { src, cursor });
|
|
230
|
+
decodedFrames.set(video, cursor.frame);
|
|
231
|
+
video.pause();
|
|
232
|
+
video.dataset.captureDecoded = "true";
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
loadBlob(src) {
|
|
236
|
+
let blob = this.blobs.get(src);
|
|
237
|
+
if (!blob) {
|
|
238
|
+
blob = fetch(src).then((response) => response.ok ? response.blob() : null).catch(() => null);
|
|
239
|
+
this.blobs.set(src, blob);
|
|
240
|
+
}
|
|
241
|
+
return blob;
|
|
242
|
+
}
|
|
243
|
+
release(video) {
|
|
244
|
+
const binding = this.bindings.get(video);
|
|
245
|
+
if (!binding) return;
|
|
246
|
+
this.bindings.delete(video);
|
|
247
|
+
if (decodedFrames.get(video) === binding.cursor.frame) decodedFrames.delete(video);
|
|
248
|
+
delete video.dataset.captureDecoded;
|
|
249
|
+
binding.cursor.dispose();
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/hooks/useFrameCapture.ts
|
|
8
254
|
var MIME_MAP = {
|
|
9
255
|
jpg: "image/jpeg",
|
|
10
256
|
jpeg: "image/jpeg",
|
|
@@ -147,6 +393,7 @@ function waitForCaptureVideoState(video, description, isReady, update) {
|
|
|
147
393
|
});
|
|
148
394
|
}
|
|
149
395
|
function captureVideoNeedsPriming(video, primedVideos) {
|
|
396
|
+
if (video.dataset.captureDecoded === "true") return false;
|
|
150
397
|
if (video.readyState >= HTMLMediaElement.HAVE_METADATA && video.videoWidth <= 0 && video.videoHeight <= 0) {
|
|
151
398
|
return false;
|
|
152
399
|
}
|
|
@@ -350,6 +597,30 @@ function videoFrameRect(sourceWidth, sourceHeight, destinationWidth, destination
|
|
|
350
597
|
dh: destinationHeight
|
|
351
598
|
};
|
|
352
599
|
}
|
|
600
|
+
function croppedVideoFrameRect(sourceWidth, sourceHeight, crop, destinationWidth, destinationHeight, objectFit) {
|
|
601
|
+
if (!crop) {
|
|
602
|
+
return videoFrameRect(
|
|
603
|
+
sourceWidth,
|
|
604
|
+
sourceHeight,
|
|
605
|
+
destinationWidth,
|
|
606
|
+
destinationHeight,
|
|
607
|
+
objectFit
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
const cx = crop.x * sourceWidth;
|
|
611
|
+
const cy = crop.y * sourceHeight;
|
|
612
|
+
const rect = videoFrameRect(
|
|
613
|
+
crop.w * sourceWidth,
|
|
614
|
+
crop.h * sourceHeight,
|
|
615
|
+
destinationWidth,
|
|
616
|
+
destinationHeight,
|
|
617
|
+
objectFit
|
|
618
|
+
);
|
|
619
|
+
return { ...rect, sx: rect.sx + cx, sy: rect.sy + cy };
|
|
620
|
+
}
|
|
621
|
+
function videoCrop(video) {
|
|
622
|
+
return parseMediaCrop(video.getAttribute("data-crop") ?? void 0);
|
|
623
|
+
}
|
|
353
624
|
function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
354
625
|
const captureFamilies = [
|
|
355
626
|
{
|
|
@@ -381,15 +652,17 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
381
652
|
canvas.dataset.videoCaptureClone = "true";
|
|
382
653
|
const destinationWidth = Math.round(video.clientWidth || video.offsetWidth);
|
|
383
654
|
const destinationHeight = Math.round(video.clientHeight || video.offsetHeight);
|
|
384
|
-
|
|
655
|
+
const image = resolveCaptureVideoImage(video);
|
|
656
|
+
if (image.width <= 0 || image.height <= 0 || destinationWidth <= 0 || destinationHeight <= 0) {
|
|
385
657
|
return;
|
|
386
658
|
}
|
|
387
659
|
try {
|
|
388
660
|
const view = video.ownerDocument.defaultView;
|
|
389
661
|
const objectFit = video.style.objectFit || view?.getComputedStyle(video).objectFit || "fill";
|
|
390
|
-
const frame =
|
|
391
|
-
|
|
392
|
-
|
|
662
|
+
const frame = croppedVideoFrameRect(
|
|
663
|
+
image.width,
|
|
664
|
+
image.height,
|
|
665
|
+
videoCrop(video),
|
|
393
666
|
destinationWidth,
|
|
394
667
|
destinationHeight,
|
|
395
668
|
objectFit
|
|
@@ -399,7 +672,7 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
399
672
|
canvas.width = destinationWidth;
|
|
400
673
|
canvas.height = destinationHeight;
|
|
401
674
|
context.drawImage(
|
|
402
|
-
|
|
675
|
+
image.source,
|
|
403
676
|
frame.sx,
|
|
404
677
|
frame.sy,
|
|
405
678
|
frame.sw,
|
|
@@ -650,7 +923,8 @@ function releaseCaptureCloneCanvases(canvases) {
|
|
|
650
923
|
});
|
|
651
924
|
}
|
|
652
925
|
function scheduledVideoIsVisual(video) {
|
|
653
|
-
|
|
926
|
+
const { width, height } = resolveCaptureVideoImage(video);
|
|
927
|
+
return width > 0 && height > 0 && video.dataset.active === "true";
|
|
654
928
|
}
|
|
655
929
|
function scheduledVideoPresentation(video) {
|
|
656
930
|
return video.closest(SCHEDULED_MEDIA_SELECTOR)?.dataset.presentation;
|
|
@@ -786,9 +1060,11 @@ function drawScheduledVideosOnto(destination, captureRoot, videos) {
|
|
|
786
1060
|
0,
|
|
787
1061
|
outerRadius - Math.max(borderLeft * scaleX, borderTop * scaleY)
|
|
788
1062
|
);
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
1063
|
+
const image = resolveCaptureVideoImage(video);
|
|
1064
|
+
const source = croppedVideoFrameRect(
|
|
1065
|
+
image.width,
|
|
1066
|
+
image.height,
|
|
1067
|
+
videoCrop(video),
|
|
792
1068
|
contentWidth,
|
|
793
1069
|
contentHeight,
|
|
794
1070
|
style.objectFit || "fill"
|
|
@@ -806,7 +1082,7 @@ function drawScheduledVideosOnto(destination, captureRoot, videos) {
|
|
|
806
1082
|
addRoundedRect(context, innerX, innerY, innerWidth, innerHeight, innerRadius);
|
|
807
1083
|
context.clip();
|
|
808
1084
|
context.drawImage(
|
|
809
|
-
|
|
1085
|
+
image.source,
|
|
810
1086
|
source.sx,
|
|
811
1087
|
source.sy,
|
|
812
1088
|
source.sw,
|
|
@@ -852,10 +1128,20 @@ function getFrameVisualStateKey(captureRoot, timelineTime, options = {}) {
|
|
|
852
1128
|
});
|
|
853
1129
|
if (POTENTIALLY_ANIMATED_IMAGE_URL.test(markup)) needsTimelineKey = true;
|
|
854
1130
|
const videoStates = Array.from(captureRoot.querySelectorAll("video")).filter(
|
|
855
|
-
(video) =>
|
|
856
|
-
).
|
|
857
|
-
|
|
858
|
-
|
|
1131
|
+
(video) => !options.ignoreScheduledVideoFrames || !video.closest(SCHEDULED_MEDIA_SELECTOR)
|
|
1132
|
+
).flatMap((video) => {
|
|
1133
|
+
const src = video.currentSrc || video.src;
|
|
1134
|
+
const decoded = getDecodedCaptureFrame(video);
|
|
1135
|
+
if (decoded) {
|
|
1136
|
+
return [
|
|
1137
|
+
`${src}:decoded:${finiteMediaTime(decoded.timestamp)}:${decoded.width}x${decoded.height}`
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
if (video.videoWidth <= 0 || video.videoHeight <= 0) return [];
|
|
1141
|
+
return [
|
|
1142
|
+
`${src}:${finiteMediaTime(video.currentTime)}:${video.readyState}:${video.videoWidth}x${video.videoHeight}`
|
|
1143
|
+
];
|
|
1144
|
+
});
|
|
859
1145
|
if (captureRoot.querySelector(
|
|
860
1146
|
"canvas, iframe, object, embed, animate, animateMotion, animateTransform, set"
|
|
861
1147
|
)) {
|
|
@@ -886,13 +1172,16 @@ function useFrameCapture() {
|
|
|
886
1172
|
const captureImageDataUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
887
1173
|
const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
|
|
888
1174
|
const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
1175
|
+
const videoFramesRef = useRef(null);
|
|
889
1176
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
890
1177
|
const captionLayerCanvasRef = useRef(null);
|
|
891
1178
|
const captionLayerKeyRef = useRef(null);
|
|
892
1179
|
const captionLayerHasContentRef = useRef(false);
|
|
893
1180
|
const init = useCallback(
|
|
894
1181
|
async (doc, renderOptions, captionMode) => {
|
|
895
|
-
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
|
|
1182
|
+
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current || videoFramesRef.current) {
|
|
1183
|
+
const oldVideoFrames = videoFramesRef.current;
|
|
1184
|
+
videoFramesRef.current = null;
|
|
896
1185
|
const oldRoot = rootRef.current;
|
|
897
1186
|
const oldContainer = containerRef.current;
|
|
898
1187
|
const oldMediaProvider = mediaProviderRef.current;
|
|
@@ -922,6 +1211,7 @@ function useFrameCapture() {
|
|
|
922
1211
|
setTimeout(() => {
|
|
923
1212
|
if (oldRoot) oldRoot.unmount();
|
|
924
1213
|
if (oldContainer) oldContainer.remove();
|
|
1214
|
+
oldVideoFrames?.dispose();
|
|
925
1215
|
if (oldOwnsMediaProvider) oldMediaProvider?.dispose();
|
|
926
1216
|
if (oldCaptureCanvas) {
|
|
927
1217
|
oldCaptureCanvas.width = 0;
|
|
@@ -979,6 +1269,8 @@ function useFrameCapture() {
|
|
|
979
1269
|
ownsMediaProviderRef.current = !!renderOptions.images;
|
|
980
1270
|
const root = createRoot(renderRoot);
|
|
981
1271
|
rootRef.current = root;
|
|
1272
|
+
const videoFrames = new CaptureVideoFrameRegistry();
|
|
1273
|
+
videoFramesRef.current = videoFrames;
|
|
982
1274
|
const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
|
|
983
1275
|
const captionStyle = captionMode === "social" ? "social" : "standard";
|
|
984
1276
|
let resolveRenderAPI;
|
|
@@ -1013,6 +1305,7 @@ function useFrameCapture() {
|
|
|
1013
1305
|
coverSlideTemplate: renderOptions.coverSlideTemplate,
|
|
1014
1306
|
captionsEnabled,
|
|
1015
1307
|
captionStyle,
|
|
1308
|
+
renderVideoFrameSelector: videoFrames.selectFrame,
|
|
1016
1309
|
onRenderAPIReady: (api) => {
|
|
1017
1310
|
if (containerRef.current !== container) return;
|
|
1018
1311
|
renderAPIRef.current = api;
|
|
@@ -1052,6 +1345,7 @@ function useFrameCapture() {
|
|
|
1052
1345
|
}
|
|
1053
1346
|
await waitForCaptureMediaResolutions(mediaResolutionTrackerRef.current);
|
|
1054
1347
|
await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
|
|
1348
|
+
await videoFrames.attach(captureRoot);
|
|
1055
1349
|
await primeIndeterminateCaptureVideos(captureRoot, primedCaptureVideosRef.current);
|
|
1056
1350
|
clearTimeout(timeout);
|
|
1057
1351
|
resolve(api.getDuration());
|
|
@@ -1085,16 +1379,18 @@ function useFrameCapture() {
|
|
|
1085
1379
|
if (!root) {
|
|
1086
1380
|
throw new Error("Capture root element not found");
|
|
1087
1381
|
}
|
|
1088
|
-
|
|
1382
|
+
const videoFrames = videoFramesRef.current;
|
|
1383
|
+
const prepareCaptureVideos = async () => (await videoFrames?.attach(root) ?? 0) + await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current);
|
|
1384
|
+
await prepareCaptureVideos();
|
|
1089
1385
|
try {
|
|
1090
1386
|
await api.seekTo(time);
|
|
1091
1387
|
} catch (seekError) {
|
|
1092
|
-
if (!await
|
|
1388
|
+
if (!await prepareCaptureVideos()) {
|
|
1093
1389
|
throw seekError;
|
|
1094
1390
|
}
|
|
1095
1391
|
await api.seekTo(time);
|
|
1096
1392
|
}
|
|
1097
|
-
if (await
|
|
1393
|
+
if (await prepareCaptureVideos()) {
|
|
1098
1394
|
await api.seekTo(time);
|
|
1099
1395
|
}
|
|
1100
1396
|
const renderedTime = api.getRenderedTime();
|
|
@@ -1280,6 +1576,8 @@ function useFrameCapture() {
|
|
|
1280
1576
|
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
1281
1577
|
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
1282
1578
|
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
1579
|
+
videoFramesRef.current?.dispose();
|
|
1580
|
+
videoFramesRef.current = null;
|
|
1283
1581
|
renderAPIRef.current = null;
|
|
1284
1582
|
}, []);
|
|
1285
1583
|
return useMemo(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useVideoExport
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ED7QK6VC.js";
|
|
4
4
|
|
|
5
5
|
// src/VideoExportModal.tsx
|
|
6
|
-
import { useState, useCallback, useId, useRef } from "react";
|
|
6
|
+
import { useState, useCallback, useEffect, useId, useRef } from "react";
|
|
7
7
|
import { useModalDialog } from "@bendyline/squisq-react";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
function formatDuration(seconds) {
|
|
@@ -160,6 +160,7 @@ function VideoExportModal({
|
|
|
160
160
|
mediaProvider,
|
|
161
161
|
images,
|
|
162
162
|
audio,
|
|
163
|
+
mediaEditRenders,
|
|
163
164
|
defaultConfig,
|
|
164
165
|
colorScheme = "light",
|
|
165
166
|
uiPalette,
|
|
@@ -256,7 +257,23 @@ function VideoExportModal({
|
|
|
256
257
|
setCaptionMode("off");
|
|
257
258
|
}
|
|
258
259
|
}, []);
|
|
260
|
+
const [preparingEdits, setPreparingEdits] = useState(false);
|
|
261
|
+
const editsWaitRef = useRef(null);
|
|
262
|
+
useEffect(() => () => editsWaitRef.current?.abort(), []);
|
|
259
263
|
const handleExport = useCallback(async () => {
|
|
264
|
+
if (includeAudio && mediaEditRenders) {
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
editsWaitRef.current = controller;
|
|
267
|
+
setPreparingEdits(true);
|
|
268
|
+
try {
|
|
269
|
+
await mediaEditRenders.whenReady(doc, controller.signal);
|
|
270
|
+
} catch {
|
|
271
|
+
return;
|
|
272
|
+
} finally {
|
|
273
|
+
editsWaitRef.current = null;
|
|
274
|
+
setPreparingEdits(false);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
260
277
|
const previewCanvas = previewCanvasRef.current;
|
|
261
278
|
const previewContext = previewCanvas?.getContext("2d");
|
|
262
279
|
if (previewCanvas && previewContext) {
|
|
@@ -276,6 +293,7 @@ function VideoExportModal({
|
|
|
276
293
|
images,
|
|
277
294
|
audio,
|
|
278
295
|
mediaProvider,
|
|
296
|
+
...mediaEditRenders ? { processedAudio: mediaEditRenders.processedAudio } : {},
|
|
279
297
|
// Only thread the bundle through when the host actually supplied one.
|
|
280
298
|
...playerScript !== void 0 ? { playerScript } : {}
|
|
281
299
|
};
|
|
@@ -292,6 +310,7 @@ function VideoExportModal({
|
|
|
292
310
|
images,
|
|
293
311
|
audio,
|
|
294
312
|
mediaProvider,
|
|
313
|
+
mediaEditRenders,
|
|
295
314
|
playerScript,
|
|
296
315
|
defaultConfig,
|
|
297
316
|
startExport
|
|
@@ -505,9 +524,26 @@ function VideoExportModal({
|
|
|
505
524
|
),
|
|
506
525
|
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." })
|
|
507
526
|
] }),
|
|
527
|
+
preparingEdits && /* @__PURE__ */ jsx(
|
|
528
|
+
"p",
|
|
529
|
+
{
|
|
530
|
+
role: "status",
|
|
531
|
+
"data-squisq-video-export-preparing-edits": true,
|
|
532
|
+
style: { fontSize: 12, color: palette.muted, margin: "0 0 12px" },
|
|
533
|
+
children: "Processing audio edits\u2026"
|
|
534
|
+
}
|
|
535
|
+
),
|
|
508
536
|
/* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
|
|
509
537
|
/* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Cancel" }),
|
|
510
|
-
/* @__PURE__ */ jsx(
|
|
538
|
+
/* @__PURE__ */ jsx(
|
|
539
|
+
"button",
|
|
540
|
+
{
|
|
541
|
+
style: themedPrimaryButtonStyle,
|
|
542
|
+
onClick: handleExport,
|
|
543
|
+
disabled: preparingEdits,
|
|
544
|
+
children: outputFormat === "gif" ? "Export GIF" : "Export Video"
|
|
545
|
+
}
|
|
546
|
+
)
|
|
511
547
|
] })
|
|
512
548
|
] }),
|
|
513
549
|
isExporting && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useFrameCapture
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7ZMJ25DC.js";
|
|
4
4
|
import {
|
|
5
5
|
EXPORT_AUDIO_CHANNELS,
|
|
6
6
|
EXPORT_AUDIO_SAMPLE_RATE,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
supportsWebCodecs,
|
|
14
14
|
supportsWebCodecsAac,
|
|
15
15
|
supportsWebCodecsH264
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-GBIYYDMY.js";
|
|
17
17
|
|
|
18
18
|
// src/hooks/useVideoExport.ts
|
|
19
19
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
@@ -677,7 +677,9 @@ function useVideoExport(options = {}) {
|
|
|
677
677
|
document
|
|
678
678
|
).catch(() => false);
|
|
679
679
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
680
|
-
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, coverPlan.audioOffset
|
|
680
|
+
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? config.processedAudio ? computeAudioTimeline(doc, coverPlan.audioOffset, {
|
|
681
|
+
processedAudio: config.processedAudio
|
|
682
|
+
}) : computeAudioTimeline(doc, coverPlan.audioOffset) : [];
|
|
681
683
|
const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
|
|
682
684
|
const tierDecision = selectAudioTier({
|
|
683
685
|
hasClips: timeline.length > 0,
|
|
@@ -247,6 +247,25 @@ function resolveOfflineAudioContext() {
|
|
|
247
247
|
}
|
|
248
248
|
return ctor;
|
|
249
249
|
}
|
|
250
|
+
function clipGainEnvelope(clip, when, duration) {
|
|
251
|
+
const level = clip.gainDb ? Math.pow(10, clip.gainDb / 20) : 1;
|
|
252
|
+
let fadeIn = Math.max(0, clip.fadeInSec ?? 0);
|
|
253
|
+
let fadeOut = Math.max(0, clip.fadeOutSec ?? 0);
|
|
254
|
+
if (level === 1 && fadeIn === 0 && fadeOut === 0) return null;
|
|
255
|
+
if (duration <= 0) return [{ time: when, value: level }];
|
|
256
|
+
if (fadeIn + fadeOut > duration) {
|
|
257
|
+
const scale = duration / (fadeIn + fadeOut);
|
|
258
|
+
fadeIn *= scale;
|
|
259
|
+
fadeOut *= scale;
|
|
260
|
+
}
|
|
261
|
+
const points = [{ time: when, value: fadeIn > 0 ? 0 : level }];
|
|
262
|
+
if (fadeIn > 0) points.push({ time: when + fadeIn, value: level });
|
|
263
|
+
if (fadeOut > 0) {
|
|
264
|
+
points.push({ time: when + duration - fadeOut, value: level });
|
|
265
|
+
points.push({ time: when + duration, value: 0 });
|
|
266
|
+
}
|
|
267
|
+
return points;
|
|
268
|
+
}
|
|
250
269
|
async function renderAudioTimeline(clips, buffers, totalDurationSec, sampleRate = EXPORT_AUDIO_SAMPLE_RATE) {
|
|
251
270
|
const Ctor = resolveOfflineAudioContext();
|
|
252
271
|
const channels = EXPORT_AUDIO_CHANNELS;
|
|
@@ -277,10 +296,21 @@ async function renderAudioTimeline(clips, buffers, totalDurationSec, sampleRate
|
|
|
277
296
|
}
|
|
278
297
|
const node = ctx.createBufferSource();
|
|
279
298
|
node.buffer = buffer;
|
|
280
|
-
node.connect(ctx.destination);
|
|
281
299
|
const when = Math.max(0, clip.startSec);
|
|
282
300
|
const offset = Math.max(0, clip.sourceInSec);
|
|
283
301
|
const duration = Math.max(0, clip.durationSec);
|
|
302
|
+
const envelope = clipGainEnvelope(clip, when, duration);
|
|
303
|
+
if (envelope) {
|
|
304
|
+
const gain = ctx.createGain();
|
|
305
|
+
gain.gain.setValueAtTime(envelope[0].value, envelope[0].time);
|
|
306
|
+
for (const point of envelope.slice(1)) {
|
|
307
|
+
gain.gain.linearRampToValueAtTime(point.value, point.time);
|
|
308
|
+
}
|
|
309
|
+
node.connect(gain);
|
|
310
|
+
gain.connect(ctx.destination);
|
|
311
|
+
} else {
|
|
312
|
+
node.connect(ctx.destination);
|
|
313
|
+
}
|
|
284
314
|
node.start(when, offset, duration);
|
|
285
315
|
scheduledNodes.push(node);
|
|
286
316
|
}
|