@bendyline/squisq-video-react 2.2.9 → 2.2.11
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/README.md +6 -2
- package/dist/{chunk-NX34XGLL.js → chunk-F2XUI32B.js} +646 -98
- package/dist/{chunk-MEPETH5V.js → chunk-I4SXMCDF.js} +78 -3
- package/dist/{chunk-2XACUF6E.js → chunk-KJ5RKG67.js} +186 -84
- package/dist/{chunk-SO656KT7.js → chunk-KW5BBYKP.js} +48 -17
- package/dist/components/index.d.ts +12 -4
- package/dist/components/index.js +4 -4
- package/dist/encoder/index.d.ts +1 -1
- package/dist/encoder/index.js +2 -2
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/{mainThreadEncoder-BgcFyYvO.d.ts → mainThreadEncoder-CiVsL1Bf.d.ts} +2 -0
- package/dist/{useVideoExport-DKpdXZ0o.d.ts → useVideoExport-CM5XiM6Z.d.ts} +3 -1
- package/dist/workers/encode.worker.js +1 -1
- package/package.json +4 -4
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
supportsWebCodecs,
|
|
11
11
|
supportsWebCodecsAac,
|
|
12
12
|
supportsWebCodecsH264
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-KJ5RKG67.js";
|
|
14
14
|
|
|
15
15
|
// src/hooks/useFrameCapture.ts
|
|
16
16
|
import { createElement } from "react";
|
|
@@ -36,6 +36,11 @@ var MIME_MAP = {
|
|
|
36
36
|
var CAPTURE_ASSET_TIMEOUT_MS = 15e3;
|
|
37
37
|
var RENDER_TIME_EPSILON_SECONDS = 1e-6;
|
|
38
38
|
var POTENTIALLY_ANIMATED_IMAGE_URL = /(?:^data:image\/(?:gif|webp|avif)[;,]|\.(?:gif|webp|avif)(?:[?#]|$))/i;
|
|
39
|
+
var CAPTURE_SVG_SELECTOR = "svg.block-svg";
|
|
40
|
+
var CAPTURE_VIDEO_READINESS_POLL_MS = 16;
|
|
41
|
+
var CAPTURE_VIDEO_END_PROBE_TIME = 1e101;
|
|
42
|
+
var SCHEDULED_MEDIA_SELECTOR = ".doc-player__media-clips";
|
|
43
|
+
var SCHEDULED_VIDEO_SELECTOR = `${SCHEDULED_MEDIA_SELECTOR} video[data-clip-id]`;
|
|
39
44
|
async function waitForImageDecode(image) {
|
|
40
45
|
const src = image.currentSrc || image.src;
|
|
41
46
|
if (!src) return;
|
|
@@ -82,14 +87,111 @@ async function waitForCaptureAssets(captureRoot, decodedImages = /* @__PURE__ */
|
|
|
82
87
|
})
|
|
83
88
|
);
|
|
84
89
|
}
|
|
90
|
+
function waitForCaptureVideoState(video, description, isReady, update) {
|
|
91
|
+
if (isReady()) return Promise.resolve();
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
let settled = false;
|
|
94
|
+
const events = ["loadedmetadata", "durationchange", "loadeddata", "canplay", "seeked"];
|
|
95
|
+
const cleanup = () => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
clearInterval(poll);
|
|
98
|
+
events.forEach((eventName) => video.removeEventListener(eventName, check));
|
|
99
|
+
video.removeEventListener("error", fail);
|
|
100
|
+
};
|
|
101
|
+
const finish = () => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const fail = () => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
cleanup();
|
|
111
|
+
reject(
|
|
112
|
+
new Error(
|
|
113
|
+
`Video did not become ready while ${description} within 15s: ${video.currentSrc || video.src}`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
function check() {
|
|
118
|
+
if (isReady()) finish();
|
|
119
|
+
}
|
|
120
|
+
const timeout = setTimeout(fail, CAPTURE_ASSET_TIMEOUT_MS);
|
|
121
|
+
const poll = setInterval(check, CAPTURE_VIDEO_READINESS_POLL_MS);
|
|
122
|
+
events.forEach((eventName) => video.addEventListener(eventName, check));
|
|
123
|
+
video.addEventListener("error", fail, { once: true });
|
|
124
|
+
try {
|
|
125
|
+
update?.();
|
|
126
|
+
queueMicrotask(check);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
settled = true;
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function primeIndeterminateCaptureVideos(captureRoot, primedVideos = /* @__PURE__ */ new WeakSet()) {
|
|
135
|
+
const videos = Array.from(captureRoot.querySelectorAll("video")).filter(
|
|
136
|
+
(video) => !primedVideos.has(video)
|
|
137
|
+
);
|
|
138
|
+
let primedCount = 0;
|
|
139
|
+
await Promise.all(
|
|
140
|
+
videos.map(async (video) => {
|
|
141
|
+
const source = video.currentSrc || video.src;
|
|
142
|
+
if (!source) {
|
|
143
|
+
primedVideos.add(video);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await waitForCaptureVideoState(
|
|
147
|
+
video,
|
|
148
|
+
"loading capture metadata",
|
|
149
|
+
() => video.readyState >= HTMLMediaElement.HAVE_METADATA
|
|
150
|
+
);
|
|
151
|
+
if (video.videoWidth <= 0 && video.videoHeight <= 0) {
|
|
152
|
+
primedVideos.add(video);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (Number.isFinite(video.duration)) {
|
|
156
|
+
primedVideos.add(video);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const restoreTime = Number.isFinite(video.currentTime) ? Math.max(0, video.currentTime) : 0;
|
|
160
|
+
video.pause();
|
|
161
|
+
await waitForCaptureVideoState(
|
|
162
|
+
video,
|
|
163
|
+
"indexing an indeterminate-duration capture source",
|
|
164
|
+
() => Number.isFinite(video.duration) && video.duration > 0 && !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA,
|
|
165
|
+
() => {
|
|
166
|
+
video.currentTime = CAPTURE_VIDEO_END_PROBE_TIME;
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
const reachableRestoreTime = Math.min(restoreTime, video.duration);
|
|
170
|
+
await waitForCaptureVideoState(
|
|
171
|
+
video,
|
|
172
|
+
"restoring the capture source after indexing",
|
|
173
|
+
() => !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && Math.abs(video.currentTime - reachableRestoreTime) <= 0.01,
|
|
174
|
+
() => {
|
|
175
|
+
video.currentTime = reachableRestoreTime;
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
video.dataset.captureSequential = "true";
|
|
179
|
+
primedVideos.add(video);
|
|
180
|
+
primedCount += 1;
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
return primedCount;
|
|
184
|
+
}
|
|
85
185
|
function createInlineProvider(images) {
|
|
86
186
|
const blobUrls = /* @__PURE__ */ new Map();
|
|
87
187
|
const mimeTypes = /* @__PURE__ */ new Map();
|
|
188
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
88
189
|
for (const [path, buffer] of images) {
|
|
89
190
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
90
191
|
const mime = MIME_MAP[ext] ?? "application/octet-stream";
|
|
91
192
|
blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
|
|
92
193
|
mimeTypes.set(path, mime);
|
|
194
|
+
sizes.set(path, buffer.byteLength);
|
|
93
195
|
}
|
|
94
196
|
return {
|
|
95
197
|
async resolveUrl(relativePath) {
|
|
@@ -99,7 +201,7 @@ function createInlineProvider(images) {
|
|
|
99
201
|
return [...blobUrls.keys()].map((name) => ({
|
|
100
202
|
name,
|
|
101
203
|
mimeType: mimeTypes.get(name) ?? "application/octet-stream",
|
|
102
|
-
size:
|
|
204
|
+
size: sizes.get(name) ?? 0
|
|
103
205
|
}));
|
|
104
206
|
},
|
|
105
207
|
async addMedia() {
|
|
@@ -111,6 +213,7 @@ function createInlineProvider(images) {
|
|
|
111
213
|
dispose() {
|
|
112
214
|
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
113
215
|
blobUrls.clear();
|
|
216
|
+
sizes.clear();
|
|
114
217
|
}
|
|
115
218
|
};
|
|
116
219
|
}
|
|
@@ -201,6 +304,7 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
201
304
|
return canvas ? [{ video, canvas }] : [];
|
|
202
305
|
});
|
|
203
306
|
});
|
|
307
|
+
const preparedCanvases = pairs.map(({ canvas }) => canvas);
|
|
204
308
|
pairs.forEach(({ video, canvas }) => {
|
|
205
309
|
canvas.className = video.className;
|
|
206
310
|
canvas.style.cssText = video.style.cssText;
|
|
@@ -225,13 +329,11 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
225
329
|
destinationHeight,
|
|
226
330
|
objectFit
|
|
227
331
|
);
|
|
228
|
-
const stagingCanvas = canvas.ownerDocument.createElement("canvas");
|
|
229
|
-
stagingCanvas.width = destinationWidth;
|
|
230
|
-
stagingCanvas.height = destinationHeight;
|
|
231
|
-
const stagingContext = stagingCanvas.getContext("2d");
|
|
232
332
|
const context = canvas.getContext("2d");
|
|
233
|
-
if (!
|
|
234
|
-
|
|
333
|
+
if (!context) return;
|
|
334
|
+
canvas.width = destinationWidth;
|
|
335
|
+
canvas.height = destinationHeight;
|
|
336
|
+
context.drawImage(
|
|
235
337
|
video,
|
|
236
338
|
frame.sx,
|
|
237
339
|
frame.sy,
|
|
@@ -242,9 +344,6 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
242
344
|
frame.dw,
|
|
243
345
|
frame.dh
|
|
244
346
|
);
|
|
245
|
-
canvas.width = destinationWidth;
|
|
246
|
-
canvas.height = destinationHeight;
|
|
247
|
-
context.drawImage(stagingCanvas, 0, 0);
|
|
248
347
|
const foreignObject = canvas.closest("foreignObject");
|
|
249
348
|
const svg = canvas.closest("svg");
|
|
250
349
|
if (foreignObject && svg) {
|
|
@@ -275,8 +374,334 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
275
374
|
} catch {
|
|
276
375
|
}
|
|
277
376
|
});
|
|
377
|
+
return preparedCanvases;
|
|
378
|
+
}
|
|
379
|
+
function createCaptureSvgRasterCache() {
|
|
380
|
+
return /* @__PURE__ */ new Map();
|
|
381
|
+
}
|
|
382
|
+
function releaseCaptureSvgRasterEntry(entry) {
|
|
383
|
+
entry.canvas.width = 0;
|
|
384
|
+
entry.canvas.height = 0;
|
|
385
|
+
}
|
|
386
|
+
function releaseCaptureSvgRasterCache(cache) {
|
|
387
|
+
cache.forEach(releaseCaptureSvgRasterEntry);
|
|
388
|
+
cache.clear();
|
|
389
|
+
}
|
|
390
|
+
function parseAbsoluteSvgLength(value) {
|
|
391
|
+
if (!value) return 0;
|
|
392
|
+
const match = /^\s*(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
393
|
+
return match ? Number.parseFloat(match[1]) : 0;
|
|
394
|
+
}
|
|
395
|
+
function svgViewBoxSize(svg) {
|
|
396
|
+
const values = (svg.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/).map(Number);
|
|
397
|
+
if (values.length === 4 && values.every(Number.isFinite)) {
|
|
398
|
+
return { width: Math.max(0, values[2]), height: Math.max(0, values[3]) };
|
|
399
|
+
}
|
|
400
|
+
return { width: 0, height: 0 };
|
|
401
|
+
}
|
|
402
|
+
function captureSvgRasterSize(clonedSvg, originalSvg) {
|
|
403
|
+
const clonedRect = clonedSvg.getBoundingClientRect();
|
|
404
|
+
const originalRect = originalSvg?.getBoundingClientRect();
|
|
405
|
+
const clonedViewBox = svgViewBoxSize(clonedSvg);
|
|
406
|
+
const originalViewBox = originalSvg ? svgViewBoxSize(originalSvg) : { width: 0, height: 0 };
|
|
407
|
+
const width = clonedRect.width || originalRect?.width || parseAbsoluteSvgLength(clonedSvg.getAttribute("width")) || (originalSvg ? parseAbsoluteSvgLength(originalSvg.getAttribute("width")) : 0) || clonedViewBox.width || originalViewBox.width;
|
|
408
|
+
const height = clonedRect.height || originalRect?.height || parseAbsoluteSvgLength(clonedSvg.getAttribute("height")) || (originalSvg ? parseAbsoluteSvgLength(originalSvg.getAttribute("height")) : 0) || clonedViewBox.height || originalViewBox.height;
|
|
409
|
+
return {
|
|
410
|
+
width: Math.max(1, Math.round(width)),
|
|
411
|
+
height: Math.max(1, Math.round(height))
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function copyCaptureSvgPresentation(svg, canvas) {
|
|
415
|
+
for (const attribute of Array.from(svg.attributes)) {
|
|
416
|
+
if (attribute.name === "id" || attribute.name === "class" || attribute.name === "style" || attribute.name.startsWith("data-") || attribute.name.startsWith("aria-")) {
|
|
417
|
+
canvas.setAttribute(attribute.name, attribute.value);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
canvas.dataset.svgCaptureClone = "true";
|
|
421
|
+
}
|
|
422
|
+
function captureImageMimeType(source, blob) {
|
|
423
|
+
if (blob.type) return blob.type;
|
|
424
|
+
const path = source.split(/[?#]/, 1)[0];
|
|
425
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
426
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
427
|
+
}
|
|
428
|
+
function blobToDataUrl(blob, source) {
|
|
429
|
+
const typedBlob = blob.type ? blob : blob.slice(0, blob.size, captureImageMimeType(source, blob));
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const reader = new FileReader();
|
|
432
|
+
reader.addEventListener(
|
|
433
|
+
"load",
|
|
434
|
+
() => {
|
|
435
|
+
if (typeof reader.result === "string") resolve(reader.result);
|
|
436
|
+
else reject(new Error(`Image could not be embedded for SVG capture: ${source}`));
|
|
437
|
+
},
|
|
438
|
+
{ once: true }
|
|
439
|
+
);
|
|
440
|
+
reader.addEventListener(
|
|
441
|
+
"error",
|
|
442
|
+
() => reject(reader.error ?? new Error(`Image could not be read for SVG capture: ${source}`)),
|
|
443
|
+
{ once: true }
|
|
444
|
+
);
|
|
445
|
+
reader.readAsDataURL(typedBlob);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function resolveCaptureImageDataUrl(source, cache) {
|
|
449
|
+
const cached = cache.get(source);
|
|
450
|
+
if (cached) return cached;
|
|
451
|
+
const pending = fetch(source).then(async (response) => {
|
|
452
|
+
if (!response.ok) {
|
|
453
|
+
throw new Error(`Image could not be loaded for SVG capture: ${source}`);
|
|
454
|
+
}
|
|
455
|
+
return blobToDataUrl(await response.blob(), source);
|
|
456
|
+
});
|
|
457
|
+
cache.set(source, pending);
|
|
458
|
+
return pending;
|
|
459
|
+
}
|
|
460
|
+
function captureImageReference(element) {
|
|
461
|
+
if (element.localName === "img") {
|
|
462
|
+
const source2 = element.getAttribute("src") ?? "";
|
|
463
|
+
return source2 ? {
|
|
464
|
+
source: source2,
|
|
465
|
+
replace: (dataUrl) => element.setAttribute("src", dataUrl)
|
|
466
|
+
} : null;
|
|
467
|
+
}
|
|
468
|
+
const xlinkNamespace = "http://www.w3.org/1999/xlink";
|
|
469
|
+
const source = element.getAttribute("href") ?? element.getAttributeNS(xlinkNamespace, "href") ?? "";
|
|
470
|
+
return source ? {
|
|
471
|
+
source,
|
|
472
|
+
replace: (dataUrl) => {
|
|
473
|
+
if (element.hasAttribute("href")) element.setAttribute("href", dataUrl);
|
|
474
|
+
if (element.hasAttributeNS(xlinkNamespace, "href")) {
|
|
475
|
+
element.setAttributeNS(xlinkNamespace, "href", dataUrl);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} : null;
|
|
479
|
+
}
|
|
480
|
+
async function embedCaptureSvgImages(svg, cache) {
|
|
481
|
+
const references = Array.from(svg.querySelectorAll("image, img")).map(captureImageReference).filter((reference) => reference !== null);
|
|
482
|
+
for (const reference of references) {
|
|
483
|
+
if (/^data:/i.test(reference.source) || reference.source.startsWith("#")) continue;
|
|
484
|
+
const dataUrl = await resolveCaptureImageDataUrl(reference.source, cache);
|
|
485
|
+
reference.replace(dataUrl);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function captureSvgRasterCacheKey(svg, index) {
|
|
489
|
+
const blockId = svg.dataset.blockId;
|
|
490
|
+
return blockId ? `block:${blockId}` : `index:${index}`;
|
|
491
|
+
}
|
|
492
|
+
async function rasterizeCaptureSvgClones(originalRoot, clonedRoot, transientCanvases = [], imageDataUrls = /* @__PURE__ */ new Map(), rasterCache) {
|
|
493
|
+
const cache = rasterCache ?? createCaptureSvgRasterCache();
|
|
494
|
+
const ownsRasterCache = rasterCache === void 0;
|
|
495
|
+
const originalSvgs = Array.from(
|
|
496
|
+
originalRoot.querySelectorAll(CAPTURE_SVG_SELECTOR)
|
|
497
|
+
);
|
|
498
|
+
const clonedSvgs = Array.from(clonedRoot.querySelectorAll(CAPTURE_SVG_SELECTOR));
|
|
499
|
+
const activeCacheKeys = /* @__PURE__ */ new Set();
|
|
500
|
+
try {
|
|
501
|
+
for (const [index, svg] of clonedSvgs.entries()) {
|
|
502
|
+
const originalSvg = originalSvgs[index];
|
|
503
|
+
const { width, height } = captureSvgRasterSize(svg, originalSvg);
|
|
504
|
+
const cacheKey = captureSvgRasterCacheKey(originalSvg ?? svg, index);
|
|
505
|
+
activeCacheKeys.add(cacheKey);
|
|
506
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
507
|
+
svg.setAttribute("width", String(width));
|
|
508
|
+
svg.setAttribute("height", String(height));
|
|
509
|
+
let bitmap = null;
|
|
510
|
+
let replacement = null;
|
|
511
|
+
let image = null;
|
|
512
|
+
let rasterCanvas = null;
|
|
513
|
+
try {
|
|
514
|
+
await embedCaptureSvgImages(svg, imageDataUrls);
|
|
515
|
+
const serializedSvg = new XMLSerializer().serializeToString(svg);
|
|
516
|
+
let entry = cache.get(cacheKey);
|
|
517
|
+
if (!entry || entry.serializedSvg !== serializedSvg || entry.width !== width || entry.height !== height) {
|
|
518
|
+
const containsForeignObject = svg.querySelector("foreignObject") !== null;
|
|
519
|
+
image = svg.ownerDocument.createElement("img");
|
|
520
|
+
image.decoding = "sync";
|
|
521
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serializedSvg)}`;
|
|
522
|
+
await waitForImageDecode(image);
|
|
523
|
+
if (!containsForeignObject && typeof createImageBitmap === "function") {
|
|
524
|
+
try {
|
|
525
|
+
bitmap = await createImageBitmap(image);
|
|
526
|
+
} catch {
|
|
527
|
+
bitmap = null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
rasterCanvas = entry?.canvas ?? originalRoot.ownerDocument.createElement("canvas");
|
|
531
|
+
if (rasterCanvas.width !== width || rasterCanvas.height !== height) {
|
|
532
|
+
rasterCanvas.width = width;
|
|
533
|
+
rasterCanvas.height = height;
|
|
534
|
+
}
|
|
535
|
+
const rasterContext = rasterCanvas.getContext("2d");
|
|
536
|
+
if (!rasterContext) {
|
|
537
|
+
throw new Error("Could not create the cached SVG raster canvas context");
|
|
538
|
+
}
|
|
539
|
+
rasterContext.clearRect(0, 0, width, height);
|
|
540
|
+
rasterContext.drawImage(bitmap ?? image, 0, 0, width, height);
|
|
541
|
+
entry = { serializedSvg, width, height, canvas: rasterCanvas };
|
|
542
|
+
cache.set(cacheKey, entry);
|
|
543
|
+
}
|
|
544
|
+
replacement = svg.ownerDocument.createElement("canvas");
|
|
545
|
+
replacement.width = width;
|
|
546
|
+
replacement.height = height;
|
|
547
|
+
copyCaptureSvgPresentation(svg, replacement);
|
|
548
|
+
const context = replacement.getContext("2d");
|
|
549
|
+
if (!context) {
|
|
550
|
+
throw new Error("Could not create the SVG capture canvas context");
|
|
551
|
+
}
|
|
552
|
+
context.drawImage(entry.canvas, 0, 0, width, height);
|
|
553
|
+
svg.replaceWith(replacement);
|
|
554
|
+
transientCanvases.push(replacement);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (replacement && !replacement.isConnected) {
|
|
557
|
+
replacement.width = 0;
|
|
558
|
+
replacement.height = 0;
|
|
559
|
+
}
|
|
560
|
+
if (rasterCanvas && cache.get(cacheKey)?.canvas !== rasterCanvas) {
|
|
561
|
+
rasterCanvas.width = 0;
|
|
562
|
+
rasterCanvas.height = 0;
|
|
563
|
+
}
|
|
564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
565
|
+
throw new Error(`Could not rasterize a full-slide SVG for frame capture: ${detail}`);
|
|
566
|
+
} finally {
|
|
567
|
+
bitmap?.close();
|
|
568
|
+
image?.removeAttribute("src");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const [cacheKey, entry] of cache) {
|
|
572
|
+
if (activeCacheKeys.has(cacheKey)) continue;
|
|
573
|
+
releaseCaptureSvgRasterEntry(entry);
|
|
574
|
+
cache.delete(cacheKey);
|
|
575
|
+
}
|
|
576
|
+
return transientCanvases;
|
|
577
|
+
} finally {
|
|
578
|
+
if (ownsRasterCache) releaseCaptureSvgRasterCache(cache);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function releaseCaptureCloneCanvases(canvases) {
|
|
582
|
+
canvases.forEach((canvas) => {
|
|
583
|
+
canvas.width = 0;
|
|
584
|
+
canvas.height = 0;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function scheduledVideoIsVisual(video) {
|
|
588
|
+
return video.videoWidth > 0 && video.videoHeight > 0 && video.dataset.active === "true";
|
|
589
|
+
}
|
|
590
|
+
function scheduledVideoPresentation(video) {
|
|
591
|
+
return video.closest(SCHEDULED_MEDIA_SELECTOR)?.dataset.presentation;
|
|
592
|
+
}
|
|
593
|
+
function canCompositeScheduledPipVideos(captureRoot) {
|
|
594
|
+
const activeVisualVideos = Array.from(
|
|
595
|
+
captureRoot.querySelectorAll(SCHEDULED_VIDEO_SELECTOR)
|
|
596
|
+
).filter(scheduledVideoIsVisual);
|
|
597
|
+
return activeVisualVideos.length > 0 && activeVisualVideos.every((video) => scheduledVideoPresentation(video) === "picture-in-picture");
|
|
598
|
+
}
|
|
599
|
+
function cssPixelValue(value) {
|
|
600
|
+
const parsed = Number.parseFloat(value);
|
|
601
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
602
|
+
}
|
|
603
|
+
function cssRadius(value, width, height) {
|
|
604
|
+
if (value.trim().endsWith("%")) {
|
|
605
|
+
return cssPixelValue(value) / 100 * Math.min(width, height);
|
|
606
|
+
}
|
|
607
|
+
return cssPixelValue(value);
|
|
608
|
+
}
|
|
609
|
+
function applyFirstBoxShadow(context, boxShadow, scaleX, scaleY) {
|
|
610
|
+
if (!boxShadow || boxShadow === "none") return;
|
|
611
|
+
const color = boxShadow.match(/rgba?\([^)]*\)|#[0-9a-f]{3,8}\b/i)?.[0];
|
|
612
|
+
if (!color) return;
|
|
613
|
+
const lengths = Array.from(
|
|
614
|
+
boxShadow.replace(color, "").matchAll(/(-?\d+(?:\.\d+)?)px/g),
|
|
615
|
+
(match) => Number.parseFloat(match[1])
|
|
616
|
+
);
|
|
617
|
+
context.shadowColor = color;
|
|
618
|
+
context.shadowOffsetX = (lengths[0] ?? 0) * scaleX;
|
|
619
|
+
context.shadowOffsetY = (lengths[1] ?? 0) * scaleY;
|
|
620
|
+
context.shadowBlur = (lengths[2] ?? 0) * Math.max(scaleX, scaleY);
|
|
621
|
+
}
|
|
622
|
+
function addRoundedRect(context, x, y, width, height, radius) {
|
|
623
|
+
context.beginPath();
|
|
624
|
+
if (typeof context.roundRect === "function") {
|
|
625
|
+
context.roundRect(x, y, width, height, Math.max(0, radius));
|
|
626
|
+
} else {
|
|
627
|
+
context.rect(x, y, width, height);
|
|
628
|
+
}
|
|
278
629
|
}
|
|
279
|
-
function
|
|
630
|
+
function compositeScheduledPipVideos(captureRoot, destination) {
|
|
631
|
+
const context = destination.getContext("2d");
|
|
632
|
+
if (!context) throw new Error("Could not create the PiP compositor canvas context");
|
|
633
|
+
const rootRect = captureRoot.getBoundingClientRect();
|
|
634
|
+
if (rootRect.width <= 0 || rootRect.height <= 0) return 0;
|
|
635
|
+
const scaleX = destination.width / rootRect.width;
|
|
636
|
+
const scaleY = destination.height / rootRect.height;
|
|
637
|
+
const videos = Array.from(
|
|
638
|
+
captureRoot.querySelectorAll(SCHEDULED_VIDEO_SELECTOR)
|
|
639
|
+
).filter(
|
|
640
|
+
(video) => scheduledVideoIsVisual(video) && scheduledVideoPresentation(video) === "picture-in-picture"
|
|
641
|
+
);
|
|
642
|
+
for (const video of videos) {
|
|
643
|
+
const rect = video.getBoundingClientRect();
|
|
644
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
645
|
+
const style = video.ownerDocument.defaultView?.getComputedStyle(video);
|
|
646
|
+
if (!style || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const opacity = Number.parseFloat(style.opacity || "1");
|
|
650
|
+
if (opacity <= 0) continue;
|
|
651
|
+
const borderLeft = cssPixelValue(style.borderLeftWidth);
|
|
652
|
+
const borderRight = cssPixelValue(style.borderRightWidth);
|
|
653
|
+
const borderTop = cssPixelValue(style.borderTopWidth);
|
|
654
|
+
const borderBottom = cssPixelValue(style.borderBottomWidth);
|
|
655
|
+
const contentWidth = Math.max(1, rect.width - borderLeft - borderRight);
|
|
656
|
+
const contentHeight = Math.max(1, rect.height - borderTop - borderBottom);
|
|
657
|
+
const outerX = (rect.left - rootRect.left) * scaleX;
|
|
658
|
+
const outerY = (rect.top - rootRect.top) * scaleY;
|
|
659
|
+
const outerWidth = rect.width * scaleX;
|
|
660
|
+
const outerHeight = rect.height * scaleY;
|
|
661
|
+
const innerX = outerX + borderLeft * scaleX;
|
|
662
|
+
const innerY = outerY + borderTop * scaleY;
|
|
663
|
+
const innerWidth = contentWidth * scaleX;
|
|
664
|
+
const innerHeight = contentHeight * scaleY;
|
|
665
|
+
const outerRadius = cssRadius(style.borderTopLeftRadius, rect.width, rect.height) * Math.max(scaleX, scaleY);
|
|
666
|
+
const innerRadius = Math.max(
|
|
667
|
+
0,
|
|
668
|
+
outerRadius - Math.max(borderLeft * scaleX, borderTop * scaleY)
|
|
669
|
+
);
|
|
670
|
+
const source = videoFrameRect(
|
|
671
|
+
video.videoWidth,
|
|
672
|
+
video.videoHeight,
|
|
673
|
+
contentWidth,
|
|
674
|
+
contentHeight,
|
|
675
|
+
style.objectFit || "fill"
|
|
676
|
+
);
|
|
677
|
+
context.save();
|
|
678
|
+
context.globalAlpha = Number.isFinite(opacity) ? opacity : 1;
|
|
679
|
+
applyFirstBoxShadow(context, style.boxShadow, scaleX, scaleY);
|
|
680
|
+
addRoundedRect(context, outerX, outerY, outerWidth, outerHeight, outerRadius);
|
|
681
|
+
context.fillStyle = style.borderTopStyle === "none" || borderTop <= 0 ? "rgba(0, 0, 0, 0.001)" : style.borderTopColor;
|
|
682
|
+
context.fill();
|
|
683
|
+
context.shadowColor = "rgba(0, 0, 0, 0)";
|
|
684
|
+
context.shadowBlur = 0;
|
|
685
|
+
context.shadowOffsetX = 0;
|
|
686
|
+
context.shadowOffsetY = 0;
|
|
687
|
+
addRoundedRect(context, innerX, innerY, innerWidth, innerHeight, innerRadius);
|
|
688
|
+
context.clip();
|
|
689
|
+
context.drawImage(
|
|
690
|
+
video,
|
|
691
|
+
source.sx,
|
|
692
|
+
source.sy,
|
|
693
|
+
source.sw,
|
|
694
|
+
source.sh,
|
|
695
|
+
innerX,
|
|
696
|
+
innerY,
|
|
697
|
+
innerWidth,
|
|
698
|
+
innerHeight
|
|
699
|
+
);
|
|
700
|
+
context.restore();
|
|
701
|
+
}
|
|
702
|
+
return videos.length;
|
|
703
|
+
}
|
|
704
|
+
function getFrameVisualStateKey(captureRoot, timelineTime, options = {}) {
|
|
280
705
|
const markup = captureRoot.innerHTML;
|
|
281
706
|
let needsTimelineKey = false;
|
|
282
707
|
const animationStates = [];
|
|
@@ -307,7 +732,9 @@ function getFrameVisualStateKey(captureRoot, timelineTime) {
|
|
|
307
732
|
return `${src}:${image.complete}:${image.naturalWidth}x${image.naturalHeight}`;
|
|
308
733
|
});
|
|
309
734
|
if (POTENTIALLY_ANIMATED_IMAGE_URL.test(markup)) needsTimelineKey = true;
|
|
310
|
-
const videoStates = Array.from(captureRoot.querySelectorAll("video")).
|
|
735
|
+
const videoStates = Array.from(captureRoot.querySelectorAll("video")).filter(
|
|
736
|
+
(video) => video.videoWidth > 0 && video.videoHeight > 0 && (!options.ignoreScheduledVideoFrames || !video.closest(SCHEDULED_MEDIA_SELECTOR))
|
|
737
|
+
).map(
|
|
311
738
|
(video) => `${video.currentSrc || video.src}:${finiteMediaTime(video.currentTime)}:${video.readyState}:${video.videoWidth}x${video.videoHeight}`
|
|
312
739
|
);
|
|
313
740
|
if (captureRoot.querySelector(
|
|
@@ -331,25 +758,35 @@ function useFrameCapture() {
|
|
|
331
758
|
const renderAPIRef = useRef(null);
|
|
332
759
|
const mediaProviderRef = useRef(null);
|
|
333
760
|
const captureCanvasRef = useRef(null);
|
|
761
|
+
const captureBaseCanvasRef = useRef(null);
|
|
334
762
|
const lastVisualStateKeyRef = useRef(null);
|
|
335
763
|
const hasCapturedFrameRef = useRef(false);
|
|
336
764
|
const decodedImagesRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
765
|
+
const captureImageDataUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
766
|
+
const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
|
|
767
|
+
const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
337
768
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
338
769
|
const init = useCallback(
|
|
339
770
|
async (doc, renderOptions, captionMode) => {
|
|
340
|
-
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current) {
|
|
771
|
+
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
|
|
341
772
|
const oldRoot = rootRef.current;
|
|
342
773
|
const oldContainer = containerRef.current;
|
|
343
774
|
const oldMediaProvider = mediaProviderRef.current;
|
|
344
775
|
const oldCaptureCanvas = captureCanvasRef.current;
|
|
776
|
+
const oldCaptureBaseCanvas = captureBaseCanvasRef.current;
|
|
345
777
|
rootRef.current = null;
|
|
346
778
|
containerRef.current = null;
|
|
347
779
|
renderAPIRef.current = null;
|
|
348
780
|
mediaProviderRef.current = null;
|
|
349
781
|
captureCanvasRef.current = null;
|
|
782
|
+
captureBaseCanvasRef.current = null;
|
|
350
783
|
lastVisualStateKeyRef.current = null;
|
|
351
784
|
hasCapturedFrameRef.current = false;
|
|
352
785
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
786
|
+
captureImageDataUrlsRef.current.clear();
|
|
787
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
788
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
789
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
353
790
|
await new Promise((resolve) => {
|
|
354
791
|
setTimeout(() => {
|
|
355
792
|
if (oldRoot) oldRoot.unmount();
|
|
@@ -359,6 +796,10 @@ function useFrameCapture() {
|
|
|
359
796
|
oldCaptureCanvas.width = 0;
|
|
360
797
|
oldCaptureCanvas.height = 0;
|
|
361
798
|
}
|
|
799
|
+
if (oldCaptureBaseCanvas) {
|
|
800
|
+
oldCaptureBaseCanvas.width = 0;
|
|
801
|
+
oldCaptureBaseCanvas.height = 0;
|
|
802
|
+
}
|
|
362
803
|
resolve();
|
|
363
804
|
}, 0);
|
|
364
805
|
});
|
|
@@ -373,9 +814,19 @@ function useFrameCapture() {
|
|
|
373
814
|
captureCanvas.style.width = `${width}px`;
|
|
374
815
|
captureCanvas.style.height = `${height}px`;
|
|
375
816
|
captureCanvasRef.current = captureCanvas;
|
|
817
|
+
const captureBaseCanvas = document.createElement("canvas");
|
|
818
|
+
captureBaseCanvas.width = width;
|
|
819
|
+
captureBaseCanvas.height = height;
|
|
820
|
+
captureBaseCanvas.style.width = `${width}px`;
|
|
821
|
+
captureBaseCanvas.style.height = `${height}px`;
|
|
822
|
+
captureBaseCanvasRef.current = captureBaseCanvas;
|
|
376
823
|
lastVisualStateKeyRef.current = null;
|
|
377
824
|
hasCapturedFrameRef.current = false;
|
|
378
825
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
826
|
+
captureImageDataUrlsRef.current.clear();
|
|
827
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
828
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
829
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
379
830
|
const container = document.createElement("div");
|
|
380
831
|
container.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-1;overflow:hidden;`;
|
|
381
832
|
document.body.appendChild(container);
|
|
@@ -442,6 +893,7 @@ function useFrameCapture() {
|
|
|
442
893
|
throw new Error("Capture root element not found after player initialization.");
|
|
443
894
|
}
|
|
444
895
|
await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
|
|
896
|
+
await primeIndeterminateCaptureVideos(captureRoot, primedCaptureVideosRef.current);
|
|
445
897
|
clearTimeout(timeout);
|
|
446
898
|
resolve(api.getDuration());
|
|
447
899
|
} catch (assetError) {
|
|
@@ -465,50 +917,84 @@ function useFrameCapture() {
|
|
|
465
917
|
const container = containerRef.current;
|
|
466
918
|
const api = renderAPIRef.current;
|
|
467
919
|
const captureCanvas = captureCanvasRef.current;
|
|
468
|
-
|
|
920
|
+
const captureBaseCanvas = captureBaseCanvasRef.current;
|
|
921
|
+
if (!container || !api || !captureCanvas || !captureBaseCanvas) {
|
|
469
922
|
throw new Error("Frame capture not initialized \u2014 call init() first");
|
|
470
923
|
}
|
|
471
924
|
const { width, height } = dimensionsRef.current;
|
|
472
925
|
await api.seekTo(time);
|
|
926
|
+
const root = container.querySelector("#squisq-capture-root");
|
|
927
|
+
if (!root) {
|
|
928
|
+
throw new Error("Capture root element not found");
|
|
929
|
+
}
|
|
930
|
+
if (await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current)) {
|
|
931
|
+
await api.seekTo(time);
|
|
932
|
+
}
|
|
473
933
|
const renderedTime = api.getRenderedTime();
|
|
474
934
|
if (Math.abs(renderedTime - time) > RENDER_TIME_EPSILON_SECONDS) {
|
|
475
935
|
throw new Error(
|
|
476
936
|
`Player committed ${renderedTime.toFixed(6)}s while capture requested ${time.toFixed(6)}s.`
|
|
477
937
|
);
|
|
478
938
|
}
|
|
479
|
-
const root = container.querySelector("#squisq-capture-root");
|
|
480
|
-
if (!root) {
|
|
481
|
-
throw new Error("Capture root element not found");
|
|
482
|
-
}
|
|
483
939
|
await waitForCaptureAssets(root, decodedImagesRef.current);
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
const
|
|
940
|
+
const compositePip = canCompositeScheduledPipVideos(root);
|
|
941
|
+
const visualStateKey = options.reuseIfUnchanged ? `${compositePip ? "base" : "full"}:${getFrameVisualStateKey(root, time, {
|
|
942
|
+
ignoreScheduledVideoFrames: compositePip
|
|
943
|
+
})}` : null;
|
|
944
|
+
const shouldRasterize = visualStateKey === null || !hasCapturedFrameRef.current || lastVisualStateKeyRef.current !== visualStateKey;
|
|
945
|
+
if (!shouldRasterize && !compositePip) return captureCanvas;
|
|
946
|
+
const rasterCanvas = compositePip ? captureBaseCanvas : captureCanvas;
|
|
947
|
+
const captureContext = rasterCanvas.getContext("2d");
|
|
489
948
|
if (!captureContext) throw new Error("Could not create the frame capture canvas context");
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
949
|
+
if (shouldRasterize) {
|
|
950
|
+
captureContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
951
|
+
captureContext.clearRect(0, 0, width, height);
|
|
952
|
+
}
|
|
953
|
+
const transientCloneCanvases = [];
|
|
954
|
+
if (shouldRasterize) {
|
|
955
|
+
try {
|
|
956
|
+
await html2canvas(root, {
|
|
957
|
+
canvas: rasterCanvas,
|
|
958
|
+
width,
|
|
959
|
+
height,
|
|
960
|
+
scale: 1,
|
|
961
|
+
useCORS: true,
|
|
962
|
+
allowTaint: true,
|
|
963
|
+
backgroundColor: "#000000",
|
|
964
|
+
logging: false,
|
|
965
|
+
onclone: async (_clonedDocument, clonedRoot) => {
|
|
966
|
+
if (compositePip) {
|
|
967
|
+
clonedRoot.querySelectorAll(SCHEDULED_MEDIA_SELECTOR).forEach((element) => element.remove());
|
|
968
|
+
}
|
|
969
|
+
transientCloneCanvases.push(...prepareScheduledVideoClones(root, clonedRoot));
|
|
970
|
+
await rasterizeCaptureSvgClones(
|
|
971
|
+
root,
|
|
972
|
+
clonedRoot,
|
|
973
|
+
transientCloneCanvases,
|
|
974
|
+
captureImageDataUrlsRef.current,
|
|
975
|
+
captureSvgRasterCacheRef.current
|
|
976
|
+
);
|
|
977
|
+
},
|
|
978
|
+
// html2canvas starts cloning at documentElement. Do not clone the rest
|
|
979
|
+
// of the editor/site UI on every frame; only the capture root, its
|
|
980
|
+
// ancestors, descendants, and document styles can affect this render.
|
|
981
|
+
ignoreElements: (element) => shouldIgnoreCaptureSibling(element, root)
|
|
982
|
+
});
|
|
983
|
+
} finally {
|
|
984
|
+
releaseCaptureCloneCanvases(transientCloneCanvases);
|
|
985
|
+
}
|
|
986
|
+
hasCapturedFrameRef.current = true;
|
|
987
|
+
lastVisualStateKeyRef.current = visualStateKey;
|
|
988
|
+
}
|
|
989
|
+
if (compositePip) {
|
|
990
|
+
const outputContext = captureCanvas.getContext("2d");
|
|
991
|
+
if (!outputContext) throw new Error("Could not create the frame output canvas context");
|
|
992
|
+
outputContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
993
|
+
outputContext.clearRect(0, 0, width, height);
|
|
994
|
+
outputContext.drawImage(captureBaseCanvas, 0, 0);
|
|
995
|
+
compositeScheduledPipVideos(root, captureCanvas);
|
|
996
|
+
}
|
|
997
|
+
return captureCanvas;
|
|
512
998
|
},
|
|
513
999
|
[]
|
|
514
1000
|
);
|
|
@@ -535,9 +1021,18 @@ function useFrameCapture() {
|
|
|
535
1021
|
captureCanvasRef.current.height = 0;
|
|
536
1022
|
captureCanvasRef.current = null;
|
|
537
1023
|
}
|
|
1024
|
+
if (captureBaseCanvasRef.current) {
|
|
1025
|
+
captureBaseCanvasRef.current.width = 0;
|
|
1026
|
+
captureBaseCanvasRef.current.height = 0;
|
|
1027
|
+
captureBaseCanvasRef.current = null;
|
|
1028
|
+
}
|
|
538
1029
|
lastVisualStateKeyRef.current = null;
|
|
539
1030
|
hasCapturedFrameRef.current = false;
|
|
540
1031
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
1032
|
+
captureImageDataUrlsRef.current.clear();
|
|
1033
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
1034
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
1035
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
541
1036
|
renderAPIRef.current = null;
|
|
542
1037
|
}, []);
|
|
543
1038
|
return useMemo(
|
|
@@ -828,13 +1323,41 @@ function calculateRollingFramesPerSecond(frameBoundaryTimes) {
|
|
|
828
1323
|
function releaseEncoderFrame(frame) {
|
|
829
1324
|
if ("close" in frame) frame.close();
|
|
830
1325
|
}
|
|
831
|
-
function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult) {
|
|
1326
|
+
function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult, activityDocument) {
|
|
832
1327
|
return new Promise((resolve, reject) => {
|
|
833
1328
|
let settled = false;
|
|
834
|
-
|
|
1329
|
+
let timeout = null;
|
|
1330
|
+
const isInactive = () => activityDocument !== void 0 && activityDocument.visibilityState !== "visible";
|
|
1331
|
+
const clearDeadline = () => {
|
|
1332
|
+
if (timeout === null) return;
|
|
1333
|
+
globalThis.clearTimeout(timeout);
|
|
1334
|
+
timeout = null;
|
|
1335
|
+
};
|
|
1336
|
+
const cleanup = () => {
|
|
1337
|
+
clearDeadline();
|
|
1338
|
+
activityDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1339
|
+
};
|
|
1340
|
+
const fail = () => {
|
|
1341
|
+
timeout = null;
|
|
1342
|
+
if (settled || isInactive()) return;
|
|
835
1343
|
settled = true;
|
|
1344
|
+
cleanup();
|
|
836
1345
|
reject(new Error(timeoutMessage));
|
|
837
|
-
}
|
|
1346
|
+
};
|
|
1347
|
+
const armDeadline = () => {
|
|
1348
|
+
clearDeadline();
|
|
1349
|
+
if (settled || isInactive()) return;
|
|
1350
|
+
timeout = globalThis.setTimeout(fail, timeoutMs);
|
|
1351
|
+
};
|
|
1352
|
+
function handleVisibilityChange() {
|
|
1353
|
+
if (activityDocument?.visibilityState !== "visible") {
|
|
1354
|
+
clearDeadline();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
armDeadline();
|
|
1358
|
+
}
|
|
1359
|
+
activityDocument?.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1360
|
+
armDeadline();
|
|
838
1361
|
void operation.then(
|
|
839
1362
|
(value) => {
|
|
840
1363
|
if (settled) {
|
|
@@ -842,13 +1365,13 @@ function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult) {
|
|
|
842
1365
|
return;
|
|
843
1366
|
}
|
|
844
1367
|
settled = true;
|
|
845
|
-
|
|
1368
|
+
cleanup();
|
|
846
1369
|
resolve(value);
|
|
847
1370
|
},
|
|
848
1371
|
(caught) => {
|
|
849
1372
|
if (settled) return;
|
|
850
1373
|
settled = true;
|
|
851
|
-
|
|
1374
|
+
cleanup();
|
|
852
1375
|
reject(caught);
|
|
853
1376
|
}
|
|
854
1377
|
);
|
|
@@ -939,6 +1462,7 @@ function useVideoExport(options = {}) {
|
|
|
939
1462
|
const [outputFormat, setOutputFormat] = useState("mp4");
|
|
940
1463
|
const [backend, setBackend] = useState(null);
|
|
941
1464
|
const [downloadUrl, setDownloadUrl] = useState(null);
|
|
1465
|
+
const [outputBlob, setOutputBlob] = useState(null);
|
|
942
1466
|
const [fileSize, setFileSize] = useState(0);
|
|
943
1467
|
const [audioIncluded, setAudioIncluded] = useState(false);
|
|
944
1468
|
const [audioSkippedReason, setAudioSkippedReason] = useState(null);
|
|
@@ -988,6 +1512,7 @@ function useVideoExport(options = {}) {
|
|
|
988
1512
|
setOutputFormat("mp4");
|
|
989
1513
|
setBackend(null);
|
|
990
1514
|
setDownloadUrl(null);
|
|
1515
|
+
setOutputBlob(null);
|
|
991
1516
|
setFileSize(0);
|
|
992
1517
|
setAudioIncluded(false);
|
|
993
1518
|
setAudioSkippedReason(null);
|
|
@@ -1019,6 +1544,7 @@ function useVideoExport(options = {}) {
|
|
|
1019
1544
|
downloadUrlRef.current = null;
|
|
1020
1545
|
}
|
|
1021
1546
|
setDownloadUrl(null);
|
|
1547
|
+
setOutputBlob(null);
|
|
1022
1548
|
setFileSize(0);
|
|
1023
1549
|
setAudioIncluded(false);
|
|
1024
1550
|
setAudioSkippedReason(null);
|
|
@@ -1069,8 +1595,10 @@ function useVideoExport(options = {}) {
|
|
|
1069
1595
|
setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1e3));
|
|
1070
1596
|
}, 1e3);
|
|
1071
1597
|
let images = config.images;
|
|
1598
|
+
let ownsLoadedImages = false;
|
|
1072
1599
|
if (!images && config.mediaProvider) {
|
|
1073
1600
|
images = /* @__PURE__ */ new Map();
|
|
1601
|
+
ownsLoadedImages = true;
|
|
1074
1602
|
const entries = await config.mediaProvider.listMedia();
|
|
1075
1603
|
const references = collectDocumentMediaReferences(doc);
|
|
1076
1604
|
const neededEntries = entries.filter(
|
|
@@ -1122,7 +1650,9 @@ function useVideoExport(options = {}) {
|
|
|
1122
1650
|
const canUseWebCodecs = webCodecsAvailable && await settleWithin(
|
|
1123
1651
|
supportsWebCodecsH264({ width, height, fps, quality }),
|
|
1124
1652
|
ENCODER_PROBE_TIMEOUT_MS,
|
|
1125
|
-
"The browser did not finish checking WebCodecs support."
|
|
1653
|
+
"The browser did not finish checking WebCodecs support.",
|
|
1654
|
+
void 0,
|
|
1655
|
+
document
|
|
1126
1656
|
).catch(() => false);
|
|
1127
1657
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
1128
1658
|
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, coverFrameCount / fps) : [];
|
|
@@ -1148,29 +1678,33 @@ function useVideoExport(options = {}) {
|
|
|
1148
1678
|
mediaProvider: config.mediaProvider,
|
|
1149
1679
|
resourcePolicy: config.resourcePolicy
|
|
1150
1680
|
});
|
|
1151
|
-
|
|
1152
|
-
(
|
|
1153
|
-
|
|
1154
|
-
if (missingSources.length > 0) {
|
|
1155
|
-
audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
|
|
1156
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1157
|
-
}
|
|
1158
|
-
if (buffers.size === 0) {
|
|
1159
|
-
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
1160
|
-
} else {
|
|
1161
|
-
const totalAudioDur = timeline.reduce(
|
|
1162
|
-
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
1163
|
-
exportDuration
|
|
1164
|
-
);
|
|
1165
|
-
renderedAudio = await renderAudioTimeline(
|
|
1166
|
-
timeline,
|
|
1167
|
-
buffers,
|
|
1168
|
-
totalAudioDur,
|
|
1169
|
-
EXPORT_AUDIO_SAMPLE_RATE
|
|
1681
|
+
try {
|
|
1682
|
+
const missingSources = [...new Set(timeline.map((clip) => clip.src))].filter(
|
|
1683
|
+
(src) => !buffers.has(src)
|
|
1170
1684
|
);
|
|
1171
|
-
if (
|
|
1172
|
-
audioReasonLocal =
|
|
1685
|
+
if (missingSources.length > 0) {
|
|
1686
|
+
audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
|
|
1687
|
+
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1688
|
+
}
|
|
1689
|
+
if (buffers.size === 0) {
|
|
1690
|
+
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
1691
|
+
} else {
|
|
1692
|
+
const totalAudioDur = timeline.reduce(
|
|
1693
|
+
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
1694
|
+
exportDuration
|
|
1695
|
+
);
|
|
1696
|
+
renderedAudio = await renderAudioTimeline(
|
|
1697
|
+
timeline,
|
|
1698
|
+
buffers,
|
|
1699
|
+
totalAudioDur,
|
|
1700
|
+
EXPORT_AUDIO_SAMPLE_RATE
|
|
1701
|
+
);
|
|
1702
|
+
if (!renderedAudio) {
|
|
1703
|
+
audioReasonLocal = "No included video source contained a decodable audio track.";
|
|
1704
|
+
}
|
|
1173
1705
|
}
|
|
1706
|
+
} finally {
|
|
1707
|
+
buffers.clear();
|
|
1174
1708
|
}
|
|
1175
1709
|
} catch (audioErr) {
|
|
1176
1710
|
renderedAudio = null;
|
|
@@ -1213,7 +1747,9 @@ function useVideoExport(options = {}) {
|
|
|
1213
1747
|
const selectedBackend = await settleWithin(
|
|
1214
1748
|
workerEncoder.ready,
|
|
1215
1749
|
ENCODER_START_TIMEOUT_MS,
|
|
1216
|
-
"The browser export engine did not start within 60 seconds."
|
|
1750
|
+
"The browser export engine did not start within 60 seconds.",
|
|
1751
|
+
void 0,
|
|
1752
|
+
document
|
|
1217
1753
|
);
|
|
1218
1754
|
setBackend(selectedBackend);
|
|
1219
1755
|
} else {
|
|
@@ -1221,6 +1757,25 @@ function useVideoExport(options = {}) {
|
|
|
1221
1757
|
"WebCodecs H.264 is unavailable in this browser and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
1222
1758
|
);
|
|
1223
1759
|
}
|
|
1760
|
+
if (useInlineAudio && renderedAudio && encoder.addAudioChunk) {
|
|
1761
|
+
setPhase("Encoding audio\u2026");
|
|
1762
|
+
try {
|
|
1763
|
+
await encodeAacTrack(
|
|
1764
|
+
renderedAudio,
|
|
1765
|
+
{ addAudioChunk: encoder.addAudioChunk.bind(encoder) },
|
|
1766
|
+
audioBitrate
|
|
1767
|
+
);
|
|
1768
|
+
audioIncludedLocal = true;
|
|
1769
|
+
} catch (audioErr) {
|
|
1770
|
+
audioIncludedLocal = false;
|
|
1771
|
+
audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1772
|
+
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1773
|
+
} finally {
|
|
1774
|
+
renderedAudio = null;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (ownsLoadedImages) images?.clear();
|
|
1778
|
+
images = void 0;
|
|
1224
1779
|
if (cancelledRef.current) return;
|
|
1225
1780
|
setProgress(CAPTURE_PROGRESS_START);
|
|
1226
1781
|
setPhase(`Capturing frame 1/${totalFrames}`);
|
|
@@ -1241,7 +1796,8 @@ function useVideoExport(options = {}) {
|
|
|
1241
1796
|
captureOperation,
|
|
1242
1797
|
FRAME_CAPTURE_TIMEOUT_MS,
|
|
1243
1798
|
`Frame capture stopped responding at frame ${i + 1}/${totalFrames}.`,
|
|
1244
|
-
releaseEncoderFrame
|
|
1799
|
+
releaseEncoderFrame,
|
|
1800
|
+
document
|
|
1245
1801
|
);
|
|
1246
1802
|
if (cancelledRef.current) {
|
|
1247
1803
|
releaseEncoderFrame(frame);
|
|
@@ -1260,7 +1816,9 @@ function useVideoExport(options = {}) {
|
|
|
1260
1816
|
await settleWithin(
|
|
1261
1817
|
encoder.encodeFrame(frame, i),
|
|
1262
1818
|
FRAME_ENCODE_TIMEOUT_MS,
|
|
1263
|
-
`Video encoding stopped responding at frame ${i + 1}/${totalFrames}
|
|
1819
|
+
`Video encoding stopped responding at frame ${i + 1}/${totalFrames}.`,
|
|
1820
|
+
void 0,
|
|
1821
|
+
document
|
|
1264
1822
|
);
|
|
1265
1823
|
const completedFrames = i + 1;
|
|
1266
1824
|
const completedAt = performance.now();
|
|
@@ -1282,31 +1840,15 @@ function useVideoExport(options = {}) {
|
|
|
1282
1840
|
setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1e3));
|
|
1283
1841
|
}
|
|
1284
1842
|
if (cancelledRef.current) return;
|
|
1285
|
-
if (useInlineAudio && renderedAudio && encoder.addAudioChunk) {
|
|
1286
|
-
setState("encoding");
|
|
1287
|
-
setPhase("Encoding audio\u2026");
|
|
1288
|
-
try {
|
|
1289
|
-
await encodeAacTrack(
|
|
1290
|
-
renderedAudio,
|
|
1291
|
-
{ addAudioChunk: encoder.addAudioChunk.bind(encoder) },
|
|
1292
|
-
audioBitrate
|
|
1293
|
-
);
|
|
1294
|
-
audioIncludedLocal = true;
|
|
1295
|
-
} catch (audioErr) {
|
|
1296
|
-
audioIncludedLocal = false;
|
|
1297
|
-
audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1298
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
1843
|
setState("encoding");
|
|
1302
1844
|
setPhase(effectiveOutputFormat === "gif" ? "Finalizing GIF frames\u2026" : "Finalizing video\u2026");
|
|
1303
1845
|
setProgress(95);
|
|
1304
|
-
let outputBytes = await encoder.finalize();
|
|
1846
|
+
let outputBytes = effectiveOutputFormat === "mp4" && !useFfmpegAudio && encoder.finalizeBlob ? await encoder.finalizeBlob() : await encoder.finalize();
|
|
1305
1847
|
encoderRef.current = null;
|
|
1306
1848
|
if (cancelledRef.current) return;
|
|
1307
1849
|
if (effectiveOutputFormat === "gif") {
|
|
1308
1850
|
setPhase("Generating GIF palette\u2026");
|
|
1309
|
-
const videoOnly = outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1851
|
+
const videoOnly = outputBytes instanceof Blob ? new Uint8Array(await outputBytes.arrayBuffer()) : outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1310
1852
|
const gifAbort = new AbortController();
|
|
1311
1853
|
gifAbortRef.current = gifAbort;
|
|
1312
1854
|
try {
|
|
@@ -1323,7 +1865,7 @@ function useVideoExport(options = {}) {
|
|
|
1323
1865
|
setPhase("Muxing audio\u2026");
|
|
1324
1866
|
try {
|
|
1325
1867
|
const wav = audioBufferToWav(renderedAudio);
|
|
1326
|
-
const videoOnly = outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1868
|
+
const videoOnly = outputBytes instanceof Blob ? new Uint8Array(await outputBytes.arrayBuffer()) : outputBytes instanceof Uint8Array ? outputBytes : new Uint8Array(outputBytes);
|
|
1327
1869
|
outputBytes = await muxAudioWithFfmpegWasm(
|
|
1328
1870
|
videoOnly,
|
|
1329
1871
|
wav,
|
|
@@ -1338,13 +1880,18 @@ function useVideoExport(options = {}) {
|
|
|
1338
1880
|
}
|
|
1339
1881
|
}
|
|
1340
1882
|
if (cancelledRef.current) return;
|
|
1341
|
-
const finalBytes = outputBytes instanceof Uint8Array ? outputBytes.slice() : new Uint8Array(outputBytes);
|
|
1342
1883
|
const mimeType = effectiveOutputFormat === "gif" ? "image/gif" : "video/mp4";
|
|
1343
|
-
const blob =
|
|
1884
|
+
const blob = outputBytes instanceof Blob ? outputBytes : new Blob(
|
|
1885
|
+
[
|
|
1886
|
+
outputBytes instanceof Uint8Array ? outputBytes.slice() : new Uint8Array(outputBytes)
|
|
1887
|
+
],
|
|
1888
|
+
{ type: mimeType }
|
|
1889
|
+
);
|
|
1344
1890
|
const url = URL.createObjectURL(blob);
|
|
1345
1891
|
downloadUrlRef.current = url;
|
|
1346
1892
|
setDownloadUrl(url);
|
|
1347
|
-
|
|
1893
|
+
setOutputBlob(blob);
|
|
1894
|
+
setFileSize(blob.size);
|
|
1348
1895
|
setAudioIncluded(audioIncludedLocal);
|
|
1349
1896
|
setAudioSkippedReason(
|
|
1350
1897
|
effectiveOutputFormat === "gif" || audioIncludedLocal ? null : audioReasonLocal
|
|
@@ -1383,6 +1930,7 @@ function useVideoExport(options = {}) {
|
|
|
1383
1930
|
outputFormat,
|
|
1384
1931
|
backend,
|
|
1385
1932
|
downloadUrl,
|
|
1933
|
+
outputBlob,
|
|
1386
1934
|
fileSize,
|
|
1387
1935
|
audioIncluded,
|
|
1388
1936
|
audioSkippedReason,
|