@bendyline/squisq-video-react 2.4.11 → 2.4.13
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-4NI2QIFE.js → chunk-2C4BEAWT.js} +369 -31
- package/dist/chunk-4GFZ3GUE.js +24645 -0
- package/dist/{chunk-32QCPFXE.js → chunk-GBIYYDMY.js} +31 -1
- package/dist/chunk-IFQJAW4V.js +321 -0
- package/dist/{chunk-BRE3IRIM.js → chunk-M5AK4MMV.js} +1 -1
- package/dist/{chunk-JGSJQZGU.js → chunk-RO52S6GO.js} +39 -3
- package/dist/chunk-SLLH2RTX.js +452 -0
- package/dist/chunk-TL2IMDIZ.js +27 -0
- package/dist/{chunk-BGS2NEYC.js → chunk-TUIVBMAK.js} +5 -3
- 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",
|
|
@@ -21,7 +267,10 @@ var MIME_MAP = {
|
|
|
21
267
|
webm: "video/webm"
|
|
22
268
|
};
|
|
23
269
|
var VISUAL_UPDATE_FALLBACK_MS = 100;
|
|
270
|
+
var CAPTURE_URL_COMMIT_MAX_WAITS = 10;
|
|
24
271
|
var CAPTURE_ASSET_TIMEOUT_MS = 15e3;
|
|
272
|
+
var RENDER_API_TIMEOUT_MS = 15e3;
|
|
273
|
+
var CAPTURE_SETUP_TIMEOUT_MS = 6e4;
|
|
25
274
|
var RENDER_TIME_EPSILON_SECONDS = 1e-6;
|
|
26
275
|
var POTENTIALLY_ANIMATED_IMAGE_URL = /(?:^data:image\/(?:gif|webp|avif)[;,]|\.(?:gif|webp|avif)(?:[?#]|$))/i;
|
|
27
276
|
var CAPTURE_SVG_SELECTOR = "svg.block-svg";
|
|
@@ -147,6 +396,7 @@ function waitForCaptureVideoState(video, description, isReady, update) {
|
|
|
147
396
|
});
|
|
148
397
|
}
|
|
149
398
|
function captureVideoNeedsPriming(video, primedVideos) {
|
|
399
|
+
if (video.dataset.captureDecoded === "true") return false;
|
|
150
400
|
if (video.readyState >= HTMLMediaElement.HAVE_METADATA && video.videoWidth <= 0 && video.videoHeight <= 0) {
|
|
151
401
|
return false;
|
|
152
402
|
}
|
|
@@ -172,8 +422,8 @@ async function primeIndeterminateCaptureVideos(captureRoot, primedVideos = /* @_
|
|
|
172
422
|
primedVideos.add(video);
|
|
173
423
|
return;
|
|
174
424
|
}
|
|
425
|
+
video.dataset.captureSequential = "true";
|
|
175
426
|
if (Number.isFinite(video.duration)) {
|
|
176
|
-
video.dataset.captureSequential = "true";
|
|
177
427
|
primedVideos.add(video);
|
|
178
428
|
return;
|
|
179
429
|
}
|
|
@@ -196,7 +446,6 @@ async function primeIndeterminateCaptureVideos(captureRoot, primedVideos = /* @_
|
|
|
196
446
|
video.currentTime = reachableRestoreTime;
|
|
197
447
|
}
|
|
198
448
|
);
|
|
199
|
-
video.dataset.captureSequential = "true";
|
|
200
449
|
primedVideos.add(video);
|
|
201
450
|
primedCount += 1;
|
|
202
451
|
})
|
|
@@ -240,6 +489,7 @@ function createInlineProvider(images) {
|
|
|
240
489
|
}
|
|
241
490
|
function createCaptureMediaResolutionTracker(provider) {
|
|
242
491
|
const pending = /* @__PURE__ */ new Set();
|
|
492
|
+
const resolvedUrls = /* @__PURE__ */ new Map();
|
|
243
493
|
let resolutionGeneration = 0;
|
|
244
494
|
let observedGeneration = 0;
|
|
245
495
|
const trackedProvider = {
|
|
@@ -247,7 +497,8 @@ function createCaptureMediaResolutionTracker(provider) {
|
|
|
247
497
|
resolutionGeneration += 1;
|
|
248
498
|
const resolution = provider.resolveUrl(relativePath);
|
|
249
499
|
const completion = resolution.then(
|
|
250
|
-
() => {
|
|
500
|
+
(url) => {
|
|
501
|
+
resolvedUrls.set(relativePath, url);
|
|
251
502
|
pending.delete(completion);
|
|
252
503
|
},
|
|
253
504
|
() => {
|
|
@@ -274,13 +525,37 @@ function createCaptureMediaResolutionTracker(provider) {
|
|
|
274
525
|
const changed = resolutionGeneration !== observedGeneration;
|
|
275
526
|
observedGeneration = resolutionGeneration;
|
|
276
527
|
return changed;
|
|
277
|
-
}
|
|
528
|
+
},
|
|
529
|
+
resolvedUrls: () => resolvedUrls
|
|
278
530
|
};
|
|
279
531
|
}
|
|
280
|
-
|
|
281
|
-
|
|
532
|
+
function stripRelativePrefix(path) {
|
|
533
|
+
return path.replace(/^(?:\.\/)+/, "");
|
|
534
|
+
}
|
|
535
|
+
function showsUncommittedMediaUrl(root, resolved) {
|
|
536
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
537
|
+
for (const [path, url] of resolved) {
|
|
538
|
+
if (url !== path) mapped.add(stripRelativePrefix(path));
|
|
539
|
+
}
|
|
540
|
+
if (mapped.size === 0) return false;
|
|
541
|
+
return Array.from(root.querySelectorAll("img[src], video[src], audio[src], source[src]")).some(
|
|
542
|
+
(element) => mapped.has(stripRelativePrefix(element.getAttribute("src") ?? ""))
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
async function waitForCaptureMediaResolutions(tracker, captureRoot) {
|
|
546
|
+
if (!tracker) return;
|
|
547
|
+
let changed = false;
|
|
548
|
+
while (await tracker.waitForSettled()) {
|
|
549
|
+
changed = true;
|
|
282
550
|
await waitForVisualUpdate();
|
|
283
551
|
}
|
|
552
|
+
if (!changed || !captureRoot) return;
|
|
553
|
+
for (let attempt = 0; attempt < CAPTURE_URL_COMMIT_MAX_WAITS && showsUncommittedMediaUrl(captureRoot, tracker.resolvedUrls()); attempt++) {
|
|
554
|
+
await waitForVisualUpdate();
|
|
555
|
+
while (await tracker.waitForSettled()) {
|
|
556
|
+
await waitForVisualUpdate();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
284
559
|
}
|
|
285
560
|
function shouldIgnoreCaptureSibling(element, captureRoot) {
|
|
286
561
|
const { head } = captureRoot.ownerDocument;
|
|
@@ -350,6 +625,30 @@ function videoFrameRect(sourceWidth, sourceHeight, destinationWidth, destination
|
|
|
350
625
|
dh: destinationHeight
|
|
351
626
|
};
|
|
352
627
|
}
|
|
628
|
+
function croppedVideoFrameRect(sourceWidth, sourceHeight, crop, destinationWidth, destinationHeight, objectFit) {
|
|
629
|
+
if (!crop) {
|
|
630
|
+
return videoFrameRect(
|
|
631
|
+
sourceWidth,
|
|
632
|
+
sourceHeight,
|
|
633
|
+
destinationWidth,
|
|
634
|
+
destinationHeight,
|
|
635
|
+
objectFit
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const cx = crop.x * sourceWidth;
|
|
639
|
+
const cy = crop.y * sourceHeight;
|
|
640
|
+
const rect = videoFrameRect(
|
|
641
|
+
crop.w * sourceWidth,
|
|
642
|
+
crop.h * sourceHeight,
|
|
643
|
+
destinationWidth,
|
|
644
|
+
destinationHeight,
|
|
645
|
+
objectFit
|
|
646
|
+
);
|
|
647
|
+
return { ...rect, sx: rect.sx + cx, sy: rect.sy + cy };
|
|
648
|
+
}
|
|
649
|
+
function videoCrop(video) {
|
|
650
|
+
return parseMediaCrop(video.getAttribute("data-crop") ?? void 0);
|
|
651
|
+
}
|
|
353
652
|
function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
354
653
|
const captureFamilies = [
|
|
355
654
|
{
|
|
@@ -381,15 +680,17 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
381
680
|
canvas.dataset.videoCaptureClone = "true";
|
|
382
681
|
const destinationWidth = Math.round(video.clientWidth || video.offsetWidth);
|
|
383
682
|
const destinationHeight = Math.round(video.clientHeight || video.offsetHeight);
|
|
384
|
-
|
|
683
|
+
const image = resolveCaptureVideoImage(video);
|
|
684
|
+
if (image.width <= 0 || image.height <= 0 || destinationWidth <= 0 || destinationHeight <= 0) {
|
|
385
685
|
return;
|
|
386
686
|
}
|
|
387
687
|
try {
|
|
388
688
|
const view = video.ownerDocument.defaultView;
|
|
389
689
|
const objectFit = video.style.objectFit || view?.getComputedStyle(video).objectFit || "fill";
|
|
390
|
-
const frame =
|
|
391
|
-
|
|
392
|
-
|
|
690
|
+
const frame = croppedVideoFrameRect(
|
|
691
|
+
image.width,
|
|
692
|
+
image.height,
|
|
693
|
+
videoCrop(video),
|
|
393
694
|
destinationWidth,
|
|
394
695
|
destinationHeight,
|
|
395
696
|
objectFit
|
|
@@ -399,7 +700,7 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
399
700
|
canvas.width = destinationWidth;
|
|
400
701
|
canvas.height = destinationHeight;
|
|
401
702
|
context.drawImage(
|
|
402
|
-
|
|
703
|
+
image.source,
|
|
403
704
|
frame.sx,
|
|
404
705
|
frame.sy,
|
|
405
706
|
frame.sw,
|
|
@@ -650,7 +951,8 @@ function releaseCaptureCloneCanvases(canvases) {
|
|
|
650
951
|
});
|
|
651
952
|
}
|
|
652
953
|
function scheduledVideoIsVisual(video) {
|
|
653
|
-
|
|
954
|
+
const { width, height } = resolveCaptureVideoImage(video);
|
|
955
|
+
return width > 0 && height > 0 && video.dataset.active === "true";
|
|
654
956
|
}
|
|
655
957
|
function scheduledVideoPresentation(video) {
|
|
656
958
|
return video.closest(SCHEDULED_MEDIA_SELECTOR)?.dataset.presentation;
|
|
@@ -786,9 +1088,11 @@ function drawScheduledVideosOnto(destination, captureRoot, videos) {
|
|
|
786
1088
|
0,
|
|
787
1089
|
outerRadius - Math.max(borderLeft * scaleX, borderTop * scaleY)
|
|
788
1090
|
);
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
1091
|
+
const image = resolveCaptureVideoImage(video);
|
|
1092
|
+
const source = croppedVideoFrameRect(
|
|
1093
|
+
image.width,
|
|
1094
|
+
image.height,
|
|
1095
|
+
videoCrop(video),
|
|
792
1096
|
contentWidth,
|
|
793
1097
|
contentHeight,
|
|
794
1098
|
style.objectFit || "fill"
|
|
@@ -806,7 +1110,7 @@ function drawScheduledVideosOnto(destination, captureRoot, videos) {
|
|
|
806
1110
|
addRoundedRect(context, innerX, innerY, innerWidth, innerHeight, innerRadius);
|
|
807
1111
|
context.clip();
|
|
808
1112
|
context.drawImage(
|
|
809
|
-
|
|
1113
|
+
image.source,
|
|
810
1114
|
source.sx,
|
|
811
1115
|
source.sy,
|
|
812
1116
|
source.sw,
|
|
@@ -852,10 +1156,20 @@ function getFrameVisualStateKey(captureRoot, timelineTime, options = {}) {
|
|
|
852
1156
|
});
|
|
853
1157
|
if (POTENTIALLY_ANIMATED_IMAGE_URL.test(markup)) needsTimelineKey = true;
|
|
854
1158
|
const videoStates = Array.from(captureRoot.querySelectorAll("video")).filter(
|
|
855
|
-
(video) =>
|
|
856
|
-
).
|
|
857
|
-
|
|
858
|
-
|
|
1159
|
+
(video) => !options.ignoreScheduledVideoFrames || !video.closest(SCHEDULED_MEDIA_SELECTOR)
|
|
1160
|
+
).flatMap((video) => {
|
|
1161
|
+
const src = video.currentSrc || video.src;
|
|
1162
|
+
const decoded = getDecodedCaptureFrame(video);
|
|
1163
|
+
if (decoded) {
|
|
1164
|
+
return [
|
|
1165
|
+
`${src}:decoded:${finiteMediaTime(decoded.timestamp)}:${decoded.width}x${decoded.height}`
|
|
1166
|
+
];
|
|
1167
|
+
}
|
|
1168
|
+
if (video.videoWidth <= 0 || video.videoHeight <= 0) return [];
|
|
1169
|
+
return [
|
|
1170
|
+
`${src}:${finiteMediaTime(video.currentTime)}:${video.readyState}:${video.videoWidth}x${video.videoHeight}`
|
|
1171
|
+
];
|
|
1172
|
+
});
|
|
859
1173
|
if (captureRoot.querySelector(
|
|
860
1174
|
"canvas, iframe, object, embed, animate, animateMotion, animateTransform, set"
|
|
861
1175
|
)) {
|
|
@@ -886,13 +1200,16 @@ function useFrameCapture() {
|
|
|
886
1200
|
const captureImageDataUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
887
1201
|
const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
|
|
888
1202
|
const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
1203
|
+
const videoFramesRef = useRef(null);
|
|
889
1204
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
890
1205
|
const captionLayerCanvasRef = useRef(null);
|
|
891
1206
|
const captionLayerKeyRef = useRef(null);
|
|
892
1207
|
const captionLayerHasContentRef = useRef(false);
|
|
893
1208
|
const init = useCallback(
|
|
894
1209
|
async (doc, renderOptions, captionMode) => {
|
|
895
|
-
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
|
|
1210
|
+
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current || videoFramesRef.current) {
|
|
1211
|
+
const oldVideoFrames = videoFramesRef.current;
|
|
1212
|
+
videoFramesRef.current = null;
|
|
896
1213
|
const oldRoot = rootRef.current;
|
|
897
1214
|
const oldContainer = containerRef.current;
|
|
898
1215
|
const oldMediaProvider = mediaProviderRef.current;
|
|
@@ -922,6 +1239,7 @@ function useFrameCapture() {
|
|
|
922
1239
|
setTimeout(() => {
|
|
923
1240
|
if (oldRoot) oldRoot.unmount();
|
|
924
1241
|
if (oldContainer) oldContainer.remove();
|
|
1242
|
+
oldVideoFrames?.dispose();
|
|
925
1243
|
if (oldOwnsMediaProvider) oldMediaProvider?.dispose();
|
|
926
1244
|
if (oldCaptureCanvas) {
|
|
927
1245
|
oldCaptureCanvas.width = 0;
|
|
@@ -979,6 +1297,8 @@ function useFrameCapture() {
|
|
|
979
1297
|
ownsMediaProviderRef.current = !!renderOptions.images;
|
|
980
1298
|
const root = createRoot(renderRoot);
|
|
981
1299
|
rootRef.current = root;
|
|
1300
|
+
const videoFrames = new CaptureVideoFrameRegistry();
|
|
1301
|
+
videoFramesRef.current = videoFrames;
|
|
982
1302
|
const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
|
|
983
1303
|
const captionStyle = captionMode === "social" ? "social" : "standard";
|
|
984
1304
|
let resolveRenderAPI;
|
|
@@ -1013,6 +1333,7 @@ function useFrameCapture() {
|
|
|
1013
1333
|
coverSlideTemplate: renderOptions.coverSlideTemplate,
|
|
1014
1334
|
captionsEnabled,
|
|
1015
1335
|
captionStyle,
|
|
1336
|
+
renderVideoFrameSelector: videoFrames.selectFrame,
|
|
1016
1337
|
onRenderAPIReady: (api) => {
|
|
1017
1338
|
if (containerRef.current !== container) return;
|
|
1018
1339
|
renderAPIRef.current = api;
|
|
@@ -1032,7 +1353,7 @@ function useFrameCapture() {
|
|
|
1032
1353
|
root.render(playerElement);
|
|
1033
1354
|
}
|
|
1034
1355
|
return new Promise((resolve, reject) => {
|
|
1035
|
-
const
|
|
1356
|
+
const apiTimeout = setTimeout(() => {
|
|
1036
1357
|
const api = renderAPIRef.current;
|
|
1037
1358
|
const hasSeek = typeof api?.seekTo === "function";
|
|
1038
1359
|
const hasDur = typeof api?.getDuration === "function";
|
|
@@ -1040,23 +1361,36 @@ function useFrameCapture() {
|
|
|
1040
1361
|
const hasPlayer = rootEl ? rootEl.querySelector(".doc-player") !== null : false;
|
|
1041
1362
|
reject(
|
|
1042
1363
|
new Error(
|
|
1043
|
-
`Render API did not initialize within
|
|
1364
|
+
`Render API did not initialize within ${RENDER_API_TIMEOUT_MS / 1e3}s. seekTo=${hasSeek}, getDuration=${hasDur}, player=${hasPlayer}, root=${!!rootEl}`
|
|
1044
1365
|
)
|
|
1045
1366
|
);
|
|
1046
|
-
},
|
|
1367
|
+
}, RENDER_API_TIMEOUT_MS);
|
|
1047
1368
|
void renderAPIReady.then(async (api) => {
|
|
1369
|
+
clearTimeout(apiTimeout);
|
|
1370
|
+
let stage = "resolving media URLs";
|
|
1371
|
+
const setupTimeout = setTimeout(() => {
|
|
1372
|
+
reject(
|
|
1373
|
+
new Error(
|
|
1374
|
+
`Frame capture setup did not finish within ${CAPTURE_SETUP_TIMEOUT_MS / 1e3}s while ${stage}.`
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
}, CAPTURE_SETUP_TIMEOUT_MS);
|
|
1048
1378
|
try {
|
|
1049
1379
|
const captureRoot = container.querySelector("#squisq-capture-root");
|
|
1050
1380
|
if (!(captureRoot instanceof HTMLElement)) {
|
|
1051
1381
|
throw new Error("Capture root element not found after player initialization.");
|
|
1052
1382
|
}
|
|
1053
|
-
await waitForCaptureMediaResolutions(mediaResolutionTrackerRef.current);
|
|
1383
|
+
await waitForCaptureMediaResolutions(mediaResolutionTrackerRef.current, captureRoot);
|
|
1384
|
+
stage = "loading fonts and images";
|
|
1054
1385
|
await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
|
|
1386
|
+
stage = "opening video decoders";
|
|
1387
|
+
await videoFrames.attach(captureRoot);
|
|
1388
|
+
stage = "indexing video sources";
|
|
1055
1389
|
await primeIndeterminateCaptureVideos(captureRoot, primedCaptureVideosRef.current);
|
|
1056
|
-
clearTimeout(
|
|
1390
|
+
clearTimeout(setupTimeout);
|
|
1057
1391
|
resolve(api.getDuration());
|
|
1058
1392
|
} catch (assetError) {
|
|
1059
|
-
clearTimeout(
|
|
1393
|
+
clearTimeout(setupTimeout);
|
|
1060
1394
|
reject(assetError);
|
|
1061
1395
|
}
|
|
1062
1396
|
});
|
|
@@ -1085,16 +1419,18 @@ function useFrameCapture() {
|
|
|
1085
1419
|
if (!root) {
|
|
1086
1420
|
throw new Error("Capture root element not found");
|
|
1087
1421
|
}
|
|
1088
|
-
|
|
1422
|
+
const videoFrames = videoFramesRef.current;
|
|
1423
|
+
const prepareCaptureVideos = async () => (await videoFrames?.attach(root) ?? 0) + await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current);
|
|
1424
|
+
await prepareCaptureVideos();
|
|
1089
1425
|
try {
|
|
1090
1426
|
await api.seekTo(time);
|
|
1091
1427
|
} catch (seekError) {
|
|
1092
|
-
if (!await
|
|
1428
|
+
if (!await prepareCaptureVideos()) {
|
|
1093
1429
|
throw seekError;
|
|
1094
1430
|
}
|
|
1095
1431
|
await api.seekTo(time);
|
|
1096
1432
|
}
|
|
1097
|
-
if (await
|
|
1433
|
+
if (await prepareCaptureVideos()) {
|
|
1098
1434
|
await api.seekTo(time);
|
|
1099
1435
|
}
|
|
1100
1436
|
const renderedTime = api.getRenderedTime();
|
|
@@ -1103,7 +1439,7 @@ function useFrameCapture() {
|
|
|
1103
1439
|
`Player committed ${renderedTime.toFixed(6)}s while capture requested ${time.toFixed(6)}s.`
|
|
1104
1440
|
);
|
|
1105
1441
|
}
|
|
1106
|
-
await waitForCaptureMediaResolutions(mediaResolutionTrackerRef.current);
|
|
1442
|
+
await waitForCaptureMediaResolutions(mediaResolutionTrackerRef.current, root);
|
|
1107
1443
|
await waitForCaptureAssets(root, decodedImagesRef.current);
|
|
1108
1444
|
const compositePlan = resolveScheduledVideoCompositePlan(root, {
|
|
1109
1445
|
includeFullFrameOverlays: true
|
|
@@ -1280,6 +1616,8 @@ function useFrameCapture() {
|
|
|
1280
1616
|
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
1281
1617
|
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
1282
1618
|
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
1619
|
+
videoFramesRef.current?.dispose();
|
|
1620
|
+
videoFramesRef.current = null;
|
|
1283
1621
|
renderAPIRef.current = null;
|
|
1284
1622
|
}, []);
|
|
1285
1623
|
return useMemo(
|