@bendyline/squisq-video-react 2.2.10 → 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 +1 -1
- package/dist/{chunk-GLOLS2CQ.js → chunk-F2XUI32B.js} +520 -136
- package/dist/{chunk-MEPETH5V.js → chunk-I4SXMCDF.js} +78 -3
- package/dist/{chunk-2XACUF6E.js → chunk-KJ5RKG67.js} +186 -84
- package/dist/{chunk-ERG6OLLO.js → chunk-KW5BBYKP.js} +3 -3
- package/dist/components/index.d.ts +2 -2
- 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-raCbwbwb.d.ts → useVideoExport-CM5XiM6Z.d.ts} +1 -1
- package/dist/workers/encode.worker.js +1 -1
- package/package.json +2 -2
|
@@ -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";
|
|
@@ -37,6 +37,10 @@ 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
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]`;
|
|
40
44
|
async function waitForImageDecode(image) {
|
|
41
45
|
const src = image.currentSrc || image.src;
|
|
42
46
|
if (!src) return;
|
|
@@ -83,14 +87,111 @@ async function waitForCaptureAssets(captureRoot, decodedImages = /* @__PURE__ */
|
|
|
83
87
|
})
|
|
84
88
|
);
|
|
85
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
|
+
}
|
|
86
185
|
function createInlineProvider(images) {
|
|
87
186
|
const blobUrls = /* @__PURE__ */ new Map();
|
|
88
187
|
const mimeTypes = /* @__PURE__ */ new Map();
|
|
188
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
89
189
|
for (const [path, buffer] of images) {
|
|
90
190
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
91
191
|
const mime = MIME_MAP[ext] ?? "application/octet-stream";
|
|
92
192
|
blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
|
|
93
193
|
mimeTypes.set(path, mime);
|
|
194
|
+
sizes.set(path, buffer.byteLength);
|
|
94
195
|
}
|
|
95
196
|
return {
|
|
96
197
|
async resolveUrl(relativePath) {
|
|
@@ -100,7 +201,7 @@ function createInlineProvider(images) {
|
|
|
100
201
|
return [...blobUrls.keys()].map((name) => ({
|
|
101
202
|
name,
|
|
102
203
|
mimeType: mimeTypes.get(name) ?? "application/octet-stream",
|
|
103
|
-
size:
|
|
204
|
+
size: sizes.get(name) ?? 0
|
|
104
205
|
}));
|
|
105
206
|
},
|
|
106
207
|
async addMedia() {
|
|
@@ -112,6 +213,7 @@ function createInlineProvider(images) {
|
|
|
112
213
|
dispose() {
|
|
113
214
|
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
114
215
|
blobUrls.clear();
|
|
216
|
+
sizes.clear();
|
|
115
217
|
}
|
|
116
218
|
};
|
|
117
219
|
}
|
|
@@ -274,6 +376,17 @@ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
|
274
376
|
});
|
|
275
377
|
return preparedCanvases;
|
|
276
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
|
+
}
|
|
277
390
|
function parseAbsoluteSvgLength(value) {
|
|
278
391
|
if (!value) return 0;
|
|
279
392
|
const match = /^\s*(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
|
|
@@ -336,9 +449,11 @@ function resolveCaptureImageDataUrl(source, cache) {
|
|
|
336
449
|
const cached = cache.get(source);
|
|
337
450
|
if (cached) return cached;
|
|
338
451
|
const pending = fetch(source).then(async (response) => {
|
|
339
|
-
if (!response.ok)
|
|
452
|
+
if (!response.ok) {
|
|
453
|
+
throw new Error(`Image could not be loaded for SVG capture: ${source}`);
|
|
454
|
+
}
|
|
340
455
|
return blobToDataUrl(await response.blob(), source);
|
|
341
|
-
})
|
|
456
|
+
});
|
|
342
457
|
cache.set(source, pending);
|
|
343
458
|
return pending;
|
|
344
459
|
}
|
|
@@ -367,51 +482,101 @@ async function embedCaptureSvgImages(svg, cache) {
|
|
|
367
482
|
for (const reference of references) {
|
|
368
483
|
if (/^data:/i.test(reference.source) || reference.source.startsWith("#")) continue;
|
|
369
484
|
const dataUrl = await resolveCaptureImageDataUrl(reference.source, cache);
|
|
370
|
-
if (!dataUrl) return false;
|
|
371
485
|
reference.replace(dataUrl);
|
|
372
486
|
}
|
|
373
|
-
return true;
|
|
374
487
|
}
|
|
375
|
-
|
|
376
|
-
|
|
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;
|
|
377
495
|
const originalSvgs = Array.from(
|
|
378
496
|
originalRoot.querySelectorAll(CAPTURE_SVG_SELECTOR)
|
|
379
497
|
);
|
|
380
498
|
const clonedSvgs = Array.from(clonedRoot.querySelectorAll(CAPTURE_SVG_SELECTOR));
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
bitmap =
|
|
392
|
-
replacement =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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");
|
|
409
569
|
}
|
|
410
|
-
} finally {
|
|
411
|
-
bitmap?.close();
|
|
412
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);
|
|
413
579
|
}
|
|
414
|
-
return transientCanvases;
|
|
415
580
|
}
|
|
416
581
|
function releaseCaptureCloneCanvases(canvases) {
|
|
417
582
|
canvases.forEach((canvas) => {
|
|
@@ -419,7 +584,124 @@ function releaseCaptureCloneCanvases(canvases) {
|
|
|
419
584
|
canvas.height = 0;
|
|
420
585
|
});
|
|
421
586
|
}
|
|
422
|
-
function
|
|
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
|
+
}
|
|
629
|
+
}
|
|
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 = {}) {
|
|
423
705
|
const markup = captureRoot.innerHTML;
|
|
424
706
|
let needsTimelineKey = false;
|
|
425
707
|
const animationStates = [];
|
|
@@ -450,7 +732,9 @@ function getFrameVisualStateKey(captureRoot, timelineTime) {
|
|
|
450
732
|
return `${src}:${image.complete}:${image.naturalWidth}x${image.naturalHeight}`;
|
|
451
733
|
});
|
|
452
734
|
if (POTENTIALLY_ANIMATED_IMAGE_URL.test(markup)) needsTimelineKey = true;
|
|
453
|
-
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(
|
|
454
738
|
(video) => `${video.currentSrc || video.src}:${finiteMediaTime(video.currentTime)}:${video.readyState}:${video.videoWidth}x${video.videoHeight}`
|
|
455
739
|
);
|
|
456
740
|
if (captureRoot.querySelector(
|
|
@@ -474,27 +758,35 @@ function useFrameCapture() {
|
|
|
474
758
|
const renderAPIRef = useRef(null);
|
|
475
759
|
const mediaProviderRef = useRef(null);
|
|
476
760
|
const captureCanvasRef = useRef(null);
|
|
761
|
+
const captureBaseCanvasRef = useRef(null);
|
|
477
762
|
const lastVisualStateKeyRef = useRef(null);
|
|
478
763
|
const hasCapturedFrameRef = useRef(false);
|
|
479
764
|
const decodedImagesRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
480
765
|
const captureImageDataUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
766
|
+
const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
|
|
767
|
+
const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
481
768
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
482
769
|
const init = useCallback(
|
|
483
770
|
async (doc, renderOptions, captionMode) => {
|
|
484
|
-
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current) {
|
|
771
|
+
if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
|
|
485
772
|
const oldRoot = rootRef.current;
|
|
486
773
|
const oldContainer = containerRef.current;
|
|
487
774
|
const oldMediaProvider = mediaProviderRef.current;
|
|
488
775
|
const oldCaptureCanvas = captureCanvasRef.current;
|
|
776
|
+
const oldCaptureBaseCanvas = captureBaseCanvasRef.current;
|
|
489
777
|
rootRef.current = null;
|
|
490
778
|
containerRef.current = null;
|
|
491
779
|
renderAPIRef.current = null;
|
|
492
780
|
mediaProviderRef.current = null;
|
|
493
781
|
captureCanvasRef.current = null;
|
|
782
|
+
captureBaseCanvasRef.current = null;
|
|
494
783
|
lastVisualStateKeyRef.current = null;
|
|
495
784
|
hasCapturedFrameRef.current = false;
|
|
496
785
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
497
786
|
captureImageDataUrlsRef.current.clear();
|
|
787
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
788
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
789
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
498
790
|
await new Promise((resolve) => {
|
|
499
791
|
setTimeout(() => {
|
|
500
792
|
if (oldRoot) oldRoot.unmount();
|
|
@@ -504,6 +796,10 @@ function useFrameCapture() {
|
|
|
504
796
|
oldCaptureCanvas.width = 0;
|
|
505
797
|
oldCaptureCanvas.height = 0;
|
|
506
798
|
}
|
|
799
|
+
if (oldCaptureBaseCanvas) {
|
|
800
|
+
oldCaptureBaseCanvas.width = 0;
|
|
801
|
+
oldCaptureBaseCanvas.height = 0;
|
|
802
|
+
}
|
|
507
803
|
resolve();
|
|
508
804
|
}, 0);
|
|
509
805
|
});
|
|
@@ -518,10 +814,19 @@ function useFrameCapture() {
|
|
|
518
814
|
captureCanvas.style.width = `${width}px`;
|
|
519
815
|
captureCanvas.style.height = `${height}px`;
|
|
520
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;
|
|
521
823
|
lastVisualStateKeyRef.current = null;
|
|
522
824
|
hasCapturedFrameRef.current = false;
|
|
523
825
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
524
826
|
captureImageDataUrlsRef.current.clear();
|
|
827
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
828
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
829
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
525
830
|
const container = document.createElement("div");
|
|
526
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;`;
|
|
527
832
|
document.body.appendChild(container);
|
|
@@ -588,6 +893,7 @@ function useFrameCapture() {
|
|
|
588
893
|
throw new Error("Capture root element not found after player initialization.");
|
|
589
894
|
}
|
|
590
895
|
await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
|
|
896
|
+
await primeIndeterminateCaptureVideos(captureRoot, primedCaptureVideosRef.current);
|
|
591
897
|
clearTimeout(timeout);
|
|
592
898
|
resolve(api.getDuration());
|
|
593
899
|
} catch (assetError) {
|
|
@@ -611,62 +917,84 @@ function useFrameCapture() {
|
|
|
611
917
|
const container = containerRef.current;
|
|
612
918
|
const api = renderAPIRef.current;
|
|
613
919
|
const captureCanvas = captureCanvasRef.current;
|
|
614
|
-
|
|
920
|
+
const captureBaseCanvas = captureBaseCanvasRef.current;
|
|
921
|
+
if (!container || !api || !captureCanvas || !captureBaseCanvas) {
|
|
615
922
|
throw new Error("Frame capture not initialized \u2014 call init() first");
|
|
616
923
|
}
|
|
617
924
|
const { width, height } = dimensionsRef.current;
|
|
618
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
|
+
}
|
|
619
933
|
const renderedTime = api.getRenderedTime();
|
|
620
934
|
if (Math.abs(renderedTime - time) > RENDER_TIME_EPSILON_SECONDS) {
|
|
621
935
|
throw new Error(
|
|
622
936
|
`Player committed ${renderedTime.toFixed(6)}s while capture requested ${time.toFixed(6)}s.`
|
|
623
937
|
);
|
|
624
938
|
}
|
|
625
|
-
const root = container.querySelector("#squisq-capture-root");
|
|
626
|
-
if (!root) {
|
|
627
|
-
throw new Error("Capture root element not found");
|
|
628
|
-
}
|
|
629
939
|
await waitForCaptureAssets(root, decodedImagesRef.current);
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
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");
|
|
635
948
|
if (!captureContext) throw new Error("Could not create the frame capture canvas context");
|
|
636
|
-
|
|
637
|
-
|
|
949
|
+
if (shouldRasterize) {
|
|
950
|
+
captureContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
951
|
+
captureContext.clearRect(0, 0, width, height);
|
|
952
|
+
}
|
|
638
953
|
const transientCloneCanvases = [];
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
clonedRoot
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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);
|
|
666
996
|
}
|
|
667
|
-
|
|
668
|
-
lastVisualStateKeyRef.current = visualStateKey;
|
|
669
|
-
return canvas;
|
|
997
|
+
return captureCanvas;
|
|
670
998
|
},
|
|
671
999
|
[]
|
|
672
1000
|
);
|
|
@@ -693,10 +1021,18 @@ function useFrameCapture() {
|
|
|
693
1021
|
captureCanvasRef.current.height = 0;
|
|
694
1022
|
captureCanvasRef.current = null;
|
|
695
1023
|
}
|
|
1024
|
+
if (captureBaseCanvasRef.current) {
|
|
1025
|
+
captureBaseCanvasRef.current.width = 0;
|
|
1026
|
+
captureBaseCanvasRef.current.height = 0;
|
|
1027
|
+
captureBaseCanvasRef.current = null;
|
|
1028
|
+
}
|
|
696
1029
|
lastVisualStateKeyRef.current = null;
|
|
697
1030
|
hasCapturedFrameRef.current = false;
|
|
698
1031
|
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
699
1032
|
captureImageDataUrlsRef.current.clear();
|
|
1033
|
+
releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
|
|
1034
|
+
captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
|
|
1035
|
+
primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
|
|
700
1036
|
renderAPIRef.current = null;
|
|
701
1037
|
}, []);
|
|
702
1038
|
return useMemo(
|
|
@@ -987,13 +1323,41 @@ function calculateRollingFramesPerSecond(frameBoundaryTimes) {
|
|
|
987
1323
|
function releaseEncoderFrame(frame) {
|
|
988
1324
|
if ("close" in frame) frame.close();
|
|
989
1325
|
}
|
|
990
|
-
function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult) {
|
|
1326
|
+
function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult, activityDocument) {
|
|
991
1327
|
return new Promise((resolve, reject) => {
|
|
992
1328
|
let settled = false;
|
|
993
|
-
|
|
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;
|
|
994
1343
|
settled = true;
|
|
1344
|
+
cleanup();
|
|
995
1345
|
reject(new Error(timeoutMessage));
|
|
996
|
-
}
|
|
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();
|
|
997
1361
|
void operation.then(
|
|
998
1362
|
(value) => {
|
|
999
1363
|
if (settled) {
|
|
@@ -1001,13 +1365,13 @@ function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult) {
|
|
|
1001
1365
|
return;
|
|
1002
1366
|
}
|
|
1003
1367
|
settled = true;
|
|
1004
|
-
|
|
1368
|
+
cleanup();
|
|
1005
1369
|
resolve(value);
|
|
1006
1370
|
},
|
|
1007
1371
|
(caught) => {
|
|
1008
1372
|
if (settled) return;
|
|
1009
1373
|
settled = true;
|
|
1010
|
-
|
|
1374
|
+
cleanup();
|
|
1011
1375
|
reject(caught);
|
|
1012
1376
|
}
|
|
1013
1377
|
);
|
|
@@ -1231,8 +1595,10 @@ function useVideoExport(options = {}) {
|
|
|
1231
1595
|
setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1e3));
|
|
1232
1596
|
}, 1e3);
|
|
1233
1597
|
let images = config.images;
|
|
1598
|
+
let ownsLoadedImages = false;
|
|
1234
1599
|
if (!images && config.mediaProvider) {
|
|
1235
1600
|
images = /* @__PURE__ */ new Map();
|
|
1601
|
+
ownsLoadedImages = true;
|
|
1236
1602
|
const entries = await config.mediaProvider.listMedia();
|
|
1237
1603
|
const references = collectDocumentMediaReferences(doc);
|
|
1238
1604
|
const neededEntries = entries.filter(
|
|
@@ -1284,7 +1650,9 @@ function useVideoExport(options = {}) {
|
|
|
1284
1650
|
const canUseWebCodecs = webCodecsAvailable && await settleWithin(
|
|
1285
1651
|
supportsWebCodecsH264({ width, height, fps, quality }),
|
|
1286
1652
|
ENCODER_PROBE_TIMEOUT_MS,
|
|
1287
|
-
"The browser did not finish checking WebCodecs support."
|
|
1653
|
+
"The browser did not finish checking WebCodecs support.",
|
|
1654
|
+
void 0,
|
|
1655
|
+
document
|
|
1288
1656
|
).catch(() => false);
|
|
1289
1657
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
1290
1658
|
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, coverFrameCount / fps) : [];
|
|
@@ -1310,29 +1678,33 @@ function useVideoExport(options = {}) {
|
|
|
1310
1678
|
mediaProvider: config.mediaProvider,
|
|
1311
1679
|
resourcePolicy: config.resourcePolicy
|
|
1312
1680
|
});
|
|
1313
|
-
|
|
1314
|
-
(
|
|
1315
|
-
|
|
1316
|
-
if (missingSources.length > 0) {
|
|
1317
|
-
audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
|
|
1318
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1319
|
-
}
|
|
1320
|
-
if (buffers.size === 0) {
|
|
1321
|
-
audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
|
|
1322
|
-
} else {
|
|
1323
|
-
const totalAudioDur = timeline.reduce(
|
|
1324
|
-
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
1325
|
-
exportDuration
|
|
1326
|
-
);
|
|
1327
|
-
renderedAudio = await renderAudioTimeline(
|
|
1328
|
-
timeline,
|
|
1329
|
-
buffers,
|
|
1330
|
-
totalAudioDur,
|
|
1331
|
-
EXPORT_AUDIO_SAMPLE_RATE
|
|
1681
|
+
try {
|
|
1682
|
+
const missingSources = [...new Set(timeline.map((clip) => clip.src))].filter(
|
|
1683
|
+
(src) => !buffers.has(src)
|
|
1332
1684
|
);
|
|
1333
|
-
if (
|
|
1334
|
-
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
|
+
}
|
|
1335
1705
|
}
|
|
1706
|
+
} finally {
|
|
1707
|
+
buffers.clear();
|
|
1336
1708
|
}
|
|
1337
1709
|
} catch (audioErr) {
|
|
1338
1710
|
renderedAudio = null;
|
|
@@ -1375,7 +1747,9 @@ function useVideoExport(options = {}) {
|
|
|
1375
1747
|
const selectedBackend = await settleWithin(
|
|
1376
1748
|
workerEncoder.ready,
|
|
1377
1749
|
ENCODER_START_TIMEOUT_MS,
|
|
1378
|
-
"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
|
|
1379
1753
|
);
|
|
1380
1754
|
setBackend(selectedBackend);
|
|
1381
1755
|
} else {
|
|
@@ -1383,6 +1757,25 @@ function useVideoExport(options = {}) {
|
|
|
1383
1757
|
"WebCodecs H.264 is unavailable in this browser and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
1384
1758
|
);
|
|
1385
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;
|
|
1386
1779
|
if (cancelledRef.current) return;
|
|
1387
1780
|
setProgress(CAPTURE_PROGRESS_START);
|
|
1388
1781
|
setPhase(`Capturing frame 1/${totalFrames}`);
|
|
@@ -1403,7 +1796,8 @@ function useVideoExport(options = {}) {
|
|
|
1403
1796
|
captureOperation,
|
|
1404
1797
|
FRAME_CAPTURE_TIMEOUT_MS,
|
|
1405
1798
|
`Frame capture stopped responding at frame ${i + 1}/${totalFrames}.`,
|
|
1406
|
-
releaseEncoderFrame
|
|
1799
|
+
releaseEncoderFrame,
|
|
1800
|
+
document
|
|
1407
1801
|
);
|
|
1408
1802
|
if (cancelledRef.current) {
|
|
1409
1803
|
releaseEncoderFrame(frame);
|
|
@@ -1422,7 +1816,9 @@ function useVideoExport(options = {}) {
|
|
|
1422
1816
|
await settleWithin(
|
|
1423
1817
|
encoder.encodeFrame(frame, i),
|
|
1424
1818
|
FRAME_ENCODE_TIMEOUT_MS,
|
|
1425
|
-
`Video encoding stopped responding at frame ${i + 1}/${totalFrames}
|
|
1819
|
+
`Video encoding stopped responding at frame ${i + 1}/${totalFrames}.`,
|
|
1820
|
+
void 0,
|
|
1821
|
+
document
|
|
1426
1822
|
);
|
|
1427
1823
|
const completedFrames = i + 1;
|
|
1428
1824
|
const completedAt = performance.now();
|
|
@@ -1444,31 +1840,15 @@ function useVideoExport(options = {}) {
|
|
|
1444
1840
|
setElapsed(Math.floor((performance.now() - startTimeRef.current) / 1e3));
|
|
1445
1841
|
}
|
|
1446
1842
|
if (cancelledRef.current) return;
|
|
1447
|
-
if (useInlineAudio && renderedAudio && encoder.addAudioChunk) {
|
|
1448
|
-
setState("encoding");
|
|
1449
|
-
setPhase("Encoding audio\u2026");
|
|
1450
|
-
try {
|
|
1451
|
-
await encodeAacTrack(
|
|
1452
|
-
renderedAudio,
|
|
1453
|
-
{ addAudioChunk: encoder.addAudioChunk.bind(encoder) },
|
|
1454
|
-
audioBitrate
|
|
1455
|
-
);
|
|
1456
|
-
audioIncludedLocal = true;
|
|
1457
|
-
} catch (audioErr) {
|
|
1458
|
-
audioIncludedLocal = false;
|
|
1459
|
-
audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
|
|
1460
|
-
if (audioPolicy === "require") throw new Error(audioReasonLocal);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
1843
|
setState("encoding");
|
|
1464
1844
|
setPhase(effectiveOutputFormat === "gif" ? "Finalizing GIF frames\u2026" : "Finalizing video\u2026");
|
|
1465
1845
|
setProgress(95);
|
|
1466
|
-
let outputBytes = await encoder.finalize();
|
|
1846
|
+
let outputBytes = effectiveOutputFormat === "mp4" && !useFfmpegAudio && encoder.finalizeBlob ? await encoder.finalizeBlob() : await encoder.finalize();
|
|
1467
1847
|
encoderRef.current = null;
|
|
1468
1848
|
if (cancelledRef.current) return;
|
|
1469
1849
|
if (effectiveOutputFormat === "gif") {
|
|
1470
1850
|
setPhase("Generating GIF palette\u2026");
|
|
1471
|
-
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);
|
|
1472
1852
|
const gifAbort = new AbortController();
|
|
1473
1853
|
gifAbortRef.current = gifAbort;
|
|
1474
1854
|
try {
|
|
@@ -1485,7 +1865,7 @@ function useVideoExport(options = {}) {
|
|
|
1485
1865
|
setPhase("Muxing audio\u2026");
|
|
1486
1866
|
try {
|
|
1487
1867
|
const wav = audioBufferToWav(renderedAudio);
|
|
1488
|
-
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);
|
|
1489
1869
|
outputBytes = await muxAudioWithFfmpegWasm(
|
|
1490
1870
|
videoOnly,
|
|
1491
1871
|
wav,
|
|
@@ -1500,14 +1880,18 @@ function useVideoExport(options = {}) {
|
|
|
1500
1880
|
}
|
|
1501
1881
|
}
|
|
1502
1882
|
if (cancelledRef.current) return;
|
|
1503
|
-
const finalBytes = outputBytes instanceof Uint8Array ? outputBytes.slice() : new Uint8Array(outputBytes);
|
|
1504
1883
|
const mimeType = effectiveOutputFormat === "gif" ? "image/gif" : "video/mp4";
|
|
1505
|
-
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
|
+
);
|
|
1506
1890
|
const url = URL.createObjectURL(blob);
|
|
1507
1891
|
downloadUrlRef.current = url;
|
|
1508
1892
|
setDownloadUrl(url);
|
|
1509
1893
|
setOutputBlob(blob);
|
|
1510
|
-
setFileSize(
|
|
1894
|
+
setFileSize(blob.size);
|
|
1511
1895
|
setAudioIncluded(audioIncludedLocal);
|
|
1512
1896
|
setAudioSkippedReason(
|
|
1513
1897
|
effectiveOutputFormat === "gif" || audioIncludedLocal ? null : audioReasonLocal
|