@bendyline/squisq-video-react 2.2.10 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1075 @@
1
+ // src/hooks/useFrameCapture.ts
2
+ import { createElement } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { useRef, useCallback, useMemo } from "react";
5
+ import { DocPlayer, MediaContext } from "@bendyline/squisq-react";
6
+ import html2canvas from "html2canvas";
7
+ var MIME_MAP = {
8
+ jpg: "image/jpeg",
9
+ jpeg: "image/jpeg",
10
+ png: "image/png",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ svg: "image/svg+xml",
14
+ bmp: "image/bmp",
15
+ avif: "image/avif",
16
+ mp3: "audio/mpeg",
17
+ wav: "audio/wav",
18
+ ogg: "audio/ogg",
19
+ mp4: "video/mp4",
20
+ webm: "video/webm"
21
+ };
22
+ var CAPTURE_ASSET_TIMEOUT_MS = 15e3;
23
+ var RENDER_TIME_EPSILON_SECONDS = 1e-6;
24
+ var POTENTIALLY_ANIMATED_IMAGE_URL = /(?:^data:image\/(?:gif|webp|avif)[;,]|\.(?:gif|webp|avif)(?:[?#]|$))/i;
25
+ var CAPTURE_SVG_SELECTOR = "svg.block-svg";
26
+ var CAPTURE_VIDEO_READINESS_POLL_MS = 16;
27
+ var CAPTURE_VIDEO_END_PROBE_TIME = 1e101;
28
+ var SCHEDULED_MEDIA_SELECTOR = ".doc-player__media-clips";
29
+ var SCHEDULED_VIDEO_SELECTOR = `${SCHEDULED_MEDIA_SELECTOR} video[data-clip-id]`;
30
+ async function waitForImageDecode(image) {
31
+ const src = image.currentSrc || image.src;
32
+ if (!src) return;
33
+ const decoded = typeof image.decode === "function" ? image.decode() : new Promise((resolve, reject) => {
34
+ if (image.complete) {
35
+ if (image.naturalWidth > 0) resolve();
36
+ else reject(new Error(`Image could not be decoded: ${src}`));
37
+ return;
38
+ }
39
+ image.addEventListener("load", () => resolve(), { once: true });
40
+ image.addEventListener(
41
+ "error",
42
+ () => reject(new Error(`Image could not be loaded: ${src}`)),
43
+ {
44
+ once: true
45
+ }
46
+ );
47
+ });
48
+ let timeout;
49
+ try {
50
+ await Promise.race([
51
+ decoded,
52
+ new Promise((_resolve, reject) => {
53
+ timeout = setTimeout(
54
+ () => reject(new Error(`Image did not become ready within 15s: ${src}`)),
55
+ CAPTURE_ASSET_TIMEOUT_MS
56
+ );
57
+ })
58
+ ]);
59
+ } finally {
60
+ if (timeout !== void 0) clearTimeout(timeout);
61
+ }
62
+ }
63
+ async function waitForCaptureAssets(captureRoot, decodedImages = /* @__PURE__ */ new WeakSet()) {
64
+ const fonts = captureRoot.ownerDocument.fonts;
65
+ if (fonts) await fonts.ready;
66
+ const pendingImages = Array.from(captureRoot.querySelectorAll("img")).filter(
67
+ (image) => !decodedImages.has(image)
68
+ );
69
+ await Promise.all(
70
+ pendingImages.map(async (image) => {
71
+ await waitForImageDecode(image);
72
+ decodedImages.add(image);
73
+ })
74
+ );
75
+ }
76
+ function waitForCaptureVideoState(video, description, isReady, update) {
77
+ if (isReady()) return Promise.resolve();
78
+ return new Promise((resolve, reject) => {
79
+ let settled = false;
80
+ const events = ["loadedmetadata", "durationchange", "loadeddata", "canplay", "seeked"];
81
+ const cleanup = () => {
82
+ clearTimeout(timeout);
83
+ clearInterval(poll);
84
+ events.forEach((eventName) => video.removeEventListener(eventName, check));
85
+ video.removeEventListener("error", fail);
86
+ };
87
+ const finish = () => {
88
+ if (settled) return;
89
+ settled = true;
90
+ cleanup();
91
+ resolve();
92
+ };
93
+ const fail = () => {
94
+ if (settled) return;
95
+ settled = true;
96
+ cleanup();
97
+ reject(
98
+ new Error(
99
+ `Video did not become ready while ${description} within 15s: ${video.currentSrc || video.src}`
100
+ )
101
+ );
102
+ };
103
+ function check() {
104
+ if (isReady()) finish();
105
+ }
106
+ const timeout = setTimeout(fail, CAPTURE_ASSET_TIMEOUT_MS);
107
+ const poll = setInterval(check, CAPTURE_VIDEO_READINESS_POLL_MS);
108
+ events.forEach((eventName) => video.addEventListener(eventName, check));
109
+ video.addEventListener("error", fail, { once: true });
110
+ try {
111
+ update?.();
112
+ queueMicrotask(check);
113
+ } catch (error) {
114
+ settled = true;
115
+ cleanup();
116
+ reject(error);
117
+ }
118
+ });
119
+ }
120
+ function captureVideoNeedsPriming(video, primedVideos) {
121
+ if (video.readyState >= HTMLMediaElement.HAVE_METADATA && video.videoWidth <= 0 && video.videoHeight <= 0) {
122
+ return false;
123
+ }
124
+ return !primedVideos.has(video) || !Number.isFinite(video.duration);
125
+ }
126
+ async function primeIndeterminateCaptureVideos(captureRoot, primedVideos = /* @__PURE__ */ new WeakSet()) {
127
+ const videos = Array.from(captureRoot.querySelectorAll("video")).filter(
128
+ (video) => captureVideoNeedsPriming(video, primedVideos)
129
+ );
130
+ let primedCount = 0;
131
+ await Promise.all(
132
+ videos.map(async (video) => {
133
+ const source = video.currentSrc || video.src;
134
+ if (!source) {
135
+ return;
136
+ }
137
+ await waitForCaptureVideoState(
138
+ video,
139
+ "loading capture metadata",
140
+ () => video.readyState >= HTMLMediaElement.HAVE_METADATA
141
+ );
142
+ if (video.videoWidth <= 0 && video.videoHeight <= 0) {
143
+ primedVideos.add(video);
144
+ return;
145
+ }
146
+ if (Number.isFinite(video.duration)) {
147
+ primedVideos.add(video);
148
+ return;
149
+ }
150
+ const restoreTime = Number.isFinite(video.currentTime) ? Math.max(0, video.currentTime) : 0;
151
+ video.pause();
152
+ await waitForCaptureVideoState(
153
+ video,
154
+ "indexing an indeterminate-duration capture source",
155
+ () => Number.isFinite(video.duration) && video.duration > 0 && !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA,
156
+ () => {
157
+ video.currentTime = CAPTURE_VIDEO_END_PROBE_TIME;
158
+ }
159
+ );
160
+ const reachableRestoreTime = Math.min(restoreTime, video.duration);
161
+ await waitForCaptureVideoState(
162
+ video,
163
+ "restoring the capture source after indexing",
164
+ () => !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && Math.abs(video.currentTime - reachableRestoreTime) <= 0.01,
165
+ () => {
166
+ video.currentTime = reachableRestoreTime;
167
+ }
168
+ );
169
+ video.dataset.captureSequential = "true";
170
+ primedVideos.add(video);
171
+ primedCount += 1;
172
+ })
173
+ );
174
+ return primedCount;
175
+ }
176
+ function createInlineProvider(images) {
177
+ const blobUrls = /* @__PURE__ */ new Map();
178
+ const mimeTypes = /* @__PURE__ */ new Map();
179
+ const sizes = /* @__PURE__ */ new Map();
180
+ for (const [path, buffer] of images) {
181
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
182
+ const mime = MIME_MAP[ext] ?? "application/octet-stream";
183
+ blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
184
+ mimeTypes.set(path, mime);
185
+ sizes.set(path, buffer.byteLength);
186
+ }
187
+ return {
188
+ async resolveUrl(relativePath) {
189
+ return blobUrls.get(relativePath) ?? relativePath;
190
+ },
191
+ async listMedia() {
192
+ return [...blobUrls.keys()].map((name) => ({
193
+ name,
194
+ mimeType: mimeTypes.get(name) ?? "application/octet-stream",
195
+ size: sizes.get(name) ?? 0
196
+ }));
197
+ },
198
+ async addMedia() {
199
+ throw new Error("Read-only");
200
+ },
201
+ async removeMedia() {
202
+ throw new Error("Read-only");
203
+ },
204
+ dispose() {
205
+ blobUrls.forEach((url) => URL.revokeObjectURL(url));
206
+ blobUrls.clear();
207
+ sizes.clear();
208
+ }
209
+ };
210
+ }
211
+ function shouldIgnoreCaptureSibling(element, captureRoot) {
212
+ const { head } = captureRoot.ownerDocument;
213
+ const isInDocumentHead = element === head || head.contains(element);
214
+ const isInCaptureBranch = element === captureRoot || element.contains(captureRoot) || captureRoot.contains(element);
215
+ return !isInDocumentHead && !isInCaptureBranch;
216
+ }
217
+ function finiteMediaTime(value) {
218
+ return Number.isFinite(value) ? value.toFixed(6) : "unknown";
219
+ }
220
+ function coverSourceRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight) {
221
+ const sourceRatio = sourceWidth / sourceHeight;
222
+ const destinationRatio = destinationWidth / destinationHeight;
223
+ if (sourceRatio > destinationRatio) {
224
+ const sw = sourceHeight * destinationRatio;
225
+ return { sx: (sourceWidth - sw) / 2, sy: 0, sw, sh: sourceHeight };
226
+ }
227
+ const sh = sourceWidth / destinationRatio;
228
+ return { sx: 0, sy: (sourceHeight - sh) / 2, sw: sourceWidth, sh };
229
+ }
230
+ function videoFrameRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight, objectFit) {
231
+ if (objectFit === "cover") {
232
+ return {
233
+ ...coverSourceRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight),
234
+ dx: 0,
235
+ dy: 0,
236
+ dw: destinationWidth,
237
+ dh: destinationHeight
238
+ };
239
+ }
240
+ if (objectFit === "contain" || objectFit === "scale-down") {
241
+ const containScale = Math.min(destinationWidth / sourceWidth, destinationHeight / sourceHeight);
242
+ const scale = objectFit === "scale-down" ? Math.min(1, containScale) : containScale;
243
+ const dw = sourceWidth * scale;
244
+ const dh = sourceHeight * scale;
245
+ return {
246
+ sx: 0,
247
+ sy: 0,
248
+ sw: sourceWidth,
249
+ sh: sourceHeight,
250
+ dx: (destinationWidth - dw) / 2,
251
+ dy: (destinationHeight - dh) / 2,
252
+ dw,
253
+ dh
254
+ };
255
+ }
256
+ if (objectFit === "none") {
257
+ return {
258
+ sx: 0,
259
+ sy: 0,
260
+ sw: sourceWidth,
261
+ sh: sourceHeight,
262
+ dx: (destinationWidth - sourceWidth) / 2,
263
+ dy: (destinationHeight - sourceHeight) / 2,
264
+ dw: sourceWidth,
265
+ dh: sourceHeight
266
+ };
267
+ }
268
+ return {
269
+ sx: 0,
270
+ sy: 0,
271
+ sw: sourceWidth,
272
+ sh: sourceHeight,
273
+ dx: 0,
274
+ dy: 0,
275
+ dw: destinationWidth,
276
+ dh: destinationHeight
277
+ };
278
+ }
279
+ function prepareScheduledVideoClones(originalRoot, clonedRoot) {
280
+ const captureFamilies = [
281
+ {
282
+ original: ".doc-player__media-clips video[data-clip-id]",
283
+ clone: ".doc-player__media-clips canvas"
284
+ },
285
+ {
286
+ original: ".block-layer--video video[data-clip-start]",
287
+ clone: ".block-layer--video canvas"
288
+ }
289
+ ];
290
+ const pairs = captureFamilies.flatMap(({ original, clone }) => {
291
+ const videos = Array.from(originalRoot.querySelectorAll(original));
292
+ const canvases = Array.from(clonedRoot.querySelectorAll(clone));
293
+ return videos.flatMap((video, index) => {
294
+ const canvas = canvases[index];
295
+ return canvas ? [{ video, canvas }] : [];
296
+ });
297
+ });
298
+ const preparedCanvases = pairs.map(({ canvas }) => canvas);
299
+ pairs.forEach(({ video, canvas }) => {
300
+ canvas.className = video.className;
301
+ canvas.style.cssText = video.style.cssText;
302
+ for (const attribute of Array.from(video.attributes)) {
303
+ if (attribute.name.startsWith("data-")) {
304
+ canvas.setAttribute(attribute.name, attribute.value);
305
+ }
306
+ }
307
+ canvas.dataset.videoCaptureClone = "true";
308
+ const destinationWidth = Math.round(video.clientWidth || video.offsetWidth);
309
+ const destinationHeight = Math.round(video.clientHeight || video.offsetHeight);
310
+ if (video.videoWidth <= 0 || video.videoHeight <= 0 || destinationWidth <= 0 || destinationHeight <= 0) {
311
+ return;
312
+ }
313
+ try {
314
+ const view = video.ownerDocument.defaultView;
315
+ const objectFit = video.style.objectFit || view?.getComputedStyle(video).objectFit || "fill";
316
+ const frame = videoFrameRect(
317
+ video.videoWidth,
318
+ video.videoHeight,
319
+ destinationWidth,
320
+ destinationHeight,
321
+ objectFit
322
+ );
323
+ const context = canvas.getContext("2d");
324
+ if (!context) return;
325
+ canvas.width = destinationWidth;
326
+ canvas.height = destinationHeight;
327
+ context.drawImage(
328
+ video,
329
+ frame.sx,
330
+ frame.sy,
331
+ frame.sw,
332
+ frame.sh,
333
+ frame.dx,
334
+ frame.dy,
335
+ frame.dw,
336
+ frame.dh
337
+ );
338
+ const foreignObject = canvas.closest("foreignObject");
339
+ const svg = canvas.closest("svg");
340
+ if (foreignObject && svg) {
341
+ const originalHost = video.closest(".doc-player__block") ?? originalRoot;
342
+ const clonedHost = svg.closest(".doc-player__block") ?? clonedRoot;
343
+ const videoRect = video.getBoundingClientRect();
344
+ const hostRect = originalHost.getBoundingClientRect();
345
+ const renderedWidth = videoRect.width || destinationWidth;
346
+ const renderedHeight = videoRect.height || destinationHeight;
347
+ const fallbackX = Number.parseFloat(foreignObject.getAttribute("x") ?? "0") || 0;
348
+ const fallbackY = Number.parseFloat(foreignObject.getAttribute("y") ?? "0") || 0;
349
+ const left = videoRect.width ? videoRect.left - hostRect.left : fallbackX;
350
+ const top = videoRect.height ? videoRect.top - hostRect.top : fallbackY;
351
+ foreignObject.remove();
352
+ if (clonedHost === clonedRoot && !clonedHost.style.position) {
353
+ clonedHost.style.position = "relative";
354
+ }
355
+ canvas.style.position = "absolute";
356
+ canvas.style.left = `${left}px`;
357
+ canvas.style.top = `${top}px`;
358
+ canvas.style.width = `${renderedWidth}px`;
359
+ canvas.style.height = `${renderedHeight}px`;
360
+ canvas.style.zIndex = "3";
361
+ canvas.style.margin = "0";
362
+ canvas.style.transform = "none";
363
+ clonedHost.appendChild(canvas);
364
+ }
365
+ } catch {
366
+ }
367
+ });
368
+ return preparedCanvases;
369
+ }
370
+ function createCaptureSvgRasterCache() {
371
+ return /* @__PURE__ */ new Map();
372
+ }
373
+ function releaseCaptureSvgRasterEntry(entry) {
374
+ entry.canvas.width = 0;
375
+ entry.canvas.height = 0;
376
+ }
377
+ function releaseCaptureSvgRasterCache(cache) {
378
+ cache.forEach(releaseCaptureSvgRasterEntry);
379
+ cache.clear();
380
+ }
381
+ function parseAbsoluteSvgLength(value) {
382
+ if (!value) return 0;
383
+ const match = /^\s*(\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/i.exec(value);
384
+ return match ? Number.parseFloat(match[1]) : 0;
385
+ }
386
+ function svgViewBoxSize(svg) {
387
+ const values = (svg.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/).map(Number);
388
+ if (values.length === 4 && values.every(Number.isFinite)) {
389
+ return { width: Math.max(0, values[2]), height: Math.max(0, values[3]) };
390
+ }
391
+ return { width: 0, height: 0 };
392
+ }
393
+ function captureSvgRasterSize(clonedSvg, originalSvg) {
394
+ const clonedRect = clonedSvg.getBoundingClientRect();
395
+ const originalRect = originalSvg?.getBoundingClientRect();
396
+ const clonedViewBox = svgViewBoxSize(clonedSvg);
397
+ const originalViewBox = originalSvg ? svgViewBoxSize(originalSvg) : { width: 0, height: 0 };
398
+ const width = clonedRect.width || originalRect?.width || parseAbsoluteSvgLength(clonedSvg.getAttribute("width")) || (originalSvg ? parseAbsoluteSvgLength(originalSvg.getAttribute("width")) : 0) || clonedViewBox.width || originalViewBox.width;
399
+ const height = clonedRect.height || originalRect?.height || parseAbsoluteSvgLength(clonedSvg.getAttribute("height")) || (originalSvg ? parseAbsoluteSvgLength(originalSvg.getAttribute("height")) : 0) || clonedViewBox.height || originalViewBox.height;
400
+ return {
401
+ width: Math.max(1, Math.round(width)),
402
+ height: Math.max(1, Math.round(height))
403
+ };
404
+ }
405
+ function copyCaptureSvgPresentation(svg, canvas) {
406
+ for (const attribute of Array.from(svg.attributes)) {
407
+ if (attribute.name === "id" || attribute.name === "class" || attribute.name === "style" || attribute.name.startsWith("data-") || attribute.name.startsWith("aria-")) {
408
+ canvas.setAttribute(attribute.name, attribute.value);
409
+ }
410
+ }
411
+ canvas.dataset.svgCaptureClone = "true";
412
+ }
413
+ function captureImageMimeType(source, blob) {
414
+ if (blob.type) return blob.type;
415
+ const path = source.split(/[?#]/, 1)[0];
416
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
417
+ return MIME_MAP[ext] ?? "application/octet-stream";
418
+ }
419
+ function blobToDataUrl(blob, source) {
420
+ const typedBlob = blob.type ? blob : blob.slice(0, blob.size, captureImageMimeType(source, blob));
421
+ return new Promise((resolve, reject) => {
422
+ const reader = new FileReader();
423
+ reader.addEventListener(
424
+ "load",
425
+ () => {
426
+ if (typeof reader.result === "string") resolve(reader.result);
427
+ else reject(new Error(`Image could not be embedded for SVG capture: ${source}`));
428
+ },
429
+ { once: true }
430
+ );
431
+ reader.addEventListener(
432
+ "error",
433
+ () => reject(reader.error ?? new Error(`Image could not be read for SVG capture: ${source}`)),
434
+ { once: true }
435
+ );
436
+ reader.readAsDataURL(typedBlob);
437
+ });
438
+ }
439
+ function resolveCaptureImageDataUrl(source, cache) {
440
+ const cached = cache.get(source);
441
+ if (cached) return cached;
442
+ const pending = fetch(source).then(async (response) => {
443
+ if (!response.ok) {
444
+ throw new Error(`Image could not be loaded for SVG capture: ${source}`);
445
+ }
446
+ return blobToDataUrl(await response.blob(), source);
447
+ });
448
+ cache.set(source, pending);
449
+ return pending;
450
+ }
451
+ function captureImageReference(element) {
452
+ if (element.localName === "img") {
453
+ const source2 = element.getAttribute("src") ?? "";
454
+ return source2 ? {
455
+ source: source2,
456
+ replace: (dataUrl) => element.setAttribute("src", dataUrl)
457
+ } : null;
458
+ }
459
+ const xlinkNamespace = "http://www.w3.org/1999/xlink";
460
+ const source = element.getAttribute("href") ?? element.getAttributeNS(xlinkNamespace, "href") ?? "";
461
+ return source ? {
462
+ source,
463
+ replace: (dataUrl) => {
464
+ if (element.hasAttribute("href")) element.setAttribute("href", dataUrl);
465
+ if (element.hasAttributeNS(xlinkNamespace, "href")) {
466
+ element.setAttributeNS(xlinkNamespace, "href", dataUrl);
467
+ }
468
+ }
469
+ } : null;
470
+ }
471
+ async function embedCaptureSvgImages(svg, cache) {
472
+ const references = Array.from(svg.querySelectorAll("image, img")).map(captureImageReference).filter((reference) => reference !== null);
473
+ for (const reference of references) {
474
+ if (/^data:/i.test(reference.source) || reference.source.startsWith("#")) continue;
475
+ const dataUrl = await resolveCaptureImageDataUrl(reference.source, cache);
476
+ reference.replace(dataUrl);
477
+ }
478
+ }
479
+ function captureSvgRasterCacheKey(svg, index) {
480
+ const blockId = svg.dataset.blockId;
481
+ return blockId ? `block:${blockId}` : `index:${index}`;
482
+ }
483
+ async function rasterizeCaptureSvgClones(originalRoot, clonedRoot, transientCanvases = [], imageDataUrls = /* @__PURE__ */ new Map(), rasterCache) {
484
+ const cache = rasterCache ?? createCaptureSvgRasterCache();
485
+ const ownsRasterCache = rasterCache === void 0;
486
+ const originalSvgs = Array.from(
487
+ originalRoot.querySelectorAll(CAPTURE_SVG_SELECTOR)
488
+ );
489
+ const clonedSvgs = Array.from(clonedRoot.querySelectorAll(CAPTURE_SVG_SELECTOR));
490
+ const activeCacheKeys = /* @__PURE__ */ new Set();
491
+ try {
492
+ for (const [index, svg] of clonedSvgs.entries()) {
493
+ const originalSvg = originalSvgs[index];
494
+ const { width, height } = captureSvgRasterSize(svg, originalSvg);
495
+ const cacheKey = captureSvgRasterCacheKey(originalSvg ?? svg, index);
496
+ activeCacheKeys.add(cacheKey);
497
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
498
+ svg.setAttribute("width", String(width));
499
+ svg.setAttribute("height", String(height));
500
+ let bitmap = null;
501
+ let replacement = null;
502
+ let image = null;
503
+ let rasterCanvas = null;
504
+ try {
505
+ await embedCaptureSvgImages(svg, imageDataUrls);
506
+ const serializedSvg = new XMLSerializer().serializeToString(svg);
507
+ let entry = cache.get(cacheKey);
508
+ if (!entry || entry.serializedSvg !== serializedSvg || entry.width !== width || entry.height !== height) {
509
+ const containsForeignObject = svg.querySelector("foreignObject") !== null;
510
+ image = svg.ownerDocument.createElement("img");
511
+ image.decoding = "sync";
512
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serializedSvg)}`;
513
+ await waitForImageDecode(image);
514
+ if (!containsForeignObject && typeof createImageBitmap === "function") {
515
+ try {
516
+ bitmap = await createImageBitmap(image);
517
+ } catch {
518
+ bitmap = null;
519
+ }
520
+ }
521
+ rasterCanvas = entry?.canvas ?? originalRoot.ownerDocument.createElement("canvas");
522
+ if (rasterCanvas.width !== width || rasterCanvas.height !== height) {
523
+ rasterCanvas.width = width;
524
+ rasterCanvas.height = height;
525
+ }
526
+ const rasterContext = rasterCanvas.getContext("2d");
527
+ if (!rasterContext) {
528
+ throw new Error("Could not create the cached SVG raster canvas context");
529
+ }
530
+ rasterContext.clearRect(0, 0, width, height);
531
+ rasterContext.drawImage(bitmap ?? image, 0, 0, width, height);
532
+ entry = { serializedSvg, width, height, canvas: rasterCanvas };
533
+ cache.set(cacheKey, entry);
534
+ }
535
+ replacement = svg.ownerDocument.createElement("canvas");
536
+ replacement.width = width;
537
+ replacement.height = height;
538
+ copyCaptureSvgPresentation(svg, replacement);
539
+ const context = replacement.getContext("2d");
540
+ if (!context) {
541
+ throw new Error("Could not create the SVG capture canvas context");
542
+ }
543
+ context.drawImage(entry.canvas, 0, 0, width, height);
544
+ svg.replaceWith(replacement);
545
+ transientCanvases.push(replacement);
546
+ } catch (error) {
547
+ if (replacement && !replacement.isConnected) {
548
+ replacement.width = 0;
549
+ replacement.height = 0;
550
+ }
551
+ if (rasterCanvas && cache.get(cacheKey)?.canvas !== rasterCanvas) {
552
+ rasterCanvas.width = 0;
553
+ rasterCanvas.height = 0;
554
+ }
555
+ const detail = error instanceof Error ? error.message : String(error);
556
+ throw new Error(`Could not rasterize a full-slide SVG for frame capture: ${detail}`);
557
+ } finally {
558
+ bitmap?.close();
559
+ image?.removeAttribute("src");
560
+ }
561
+ }
562
+ for (const [cacheKey, entry] of cache) {
563
+ if (activeCacheKeys.has(cacheKey)) continue;
564
+ releaseCaptureSvgRasterEntry(entry);
565
+ cache.delete(cacheKey);
566
+ }
567
+ return transientCanvases;
568
+ } finally {
569
+ if (ownsRasterCache) releaseCaptureSvgRasterCache(cache);
570
+ }
571
+ }
572
+ function releaseCaptureCloneCanvases(canvases) {
573
+ canvases.forEach((canvas) => {
574
+ canvas.width = 0;
575
+ canvas.height = 0;
576
+ });
577
+ }
578
+ function scheduledVideoIsVisual(video) {
579
+ return video.videoWidth > 0 && video.videoHeight > 0 && video.dataset.active === "true";
580
+ }
581
+ function scheduledVideoPresentation(video) {
582
+ return video.closest(SCHEDULED_MEDIA_SELECTOR)?.dataset.presentation;
583
+ }
584
+ function planScheduledVideoComposite(captureRoot) {
585
+ const activeVideos = Array.from(
586
+ captureRoot.querySelectorAll(SCHEDULED_VIDEO_SELECTOR)
587
+ ).filter(scheduledVideoIsVisual);
588
+ if (activeVideos.length === 0) return null;
589
+ const underlays = [];
590
+ const overlays = [];
591
+ for (const video of activeVideos) {
592
+ const presentation = scheduledVideoPresentation(video);
593
+ if (presentation === "background") underlays.push(video);
594
+ else if (presentation === "picture-in-picture") overlays.push(video);
595
+ else return null;
596
+ }
597
+ return { underlays, overlays };
598
+ }
599
+ function clearScheduledUnderlayBackdrops(clonedRoot) {
600
+ const groups = clonedRoot.querySelectorAll(
601
+ `${SCHEDULED_MEDIA_SELECTOR}[data-presentation="background"]`
602
+ );
603
+ for (const group of Array.from(groups)) {
604
+ for (let element = group.parentElement; element; element = element === clonedRoot ? null : element.parentElement) {
605
+ element.style.backgroundColor = "transparent";
606
+ element.style.backgroundImage = "none";
607
+ }
608
+ }
609
+ }
610
+ function cssPixelValue(value) {
611
+ const parsed = Number.parseFloat(value);
612
+ return Number.isFinite(parsed) ? parsed : 0;
613
+ }
614
+ function cssRadius(value, width, height) {
615
+ if (value.trim().endsWith("%")) {
616
+ return cssPixelValue(value) / 100 * Math.min(width, height);
617
+ }
618
+ return cssPixelValue(value);
619
+ }
620
+ function applyFirstBoxShadow(context, boxShadow, scaleX, scaleY) {
621
+ if (!boxShadow || boxShadow === "none") return;
622
+ const color = boxShadow.match(/rgba?\([^)]*\)|#[0-9a-f]{3,8}\b/i)?.[0];
623
+ if (!color) return;
624
+ const lengths = Array.from(
625
+ boxShadow.replace(color, "").matchAll(/(-?\d+(?:\.\d+)?)px/g),
626
+ (match) => Number.parseFloat(match[1])
627
+ );
628
+ context.shadowColor = color;
629
+ context.shadowOffsetX = (lengths[0] ?? 0) * scaleX;
630
+ context.shadowOffsetY = (lengths[1] ?? 0) * scaleY;
631
+ context.shadowBlur = (lengths[2] ?? 0) * Math.max(scaleX, scaleY);
632
+ }
633
+ function addRoundedRect(context, x, y, width, height, radius) {
634
+ context.beginPath();
635
+ if (typeof context.roundRect === "function") {
636
+ context.roundRect(x, y, width, height, Math.max(0, radius));
637
+ } else {
638
+ context.rect(x, y, width, height);
639
+ }
640
+ }
641
+ function drawScheduledVideosOnto(destination, captureRoot, videos) {
642
+ const context = destination.getContext("2d");
643
+ if (!context) throw new Error("Could not create the scheduled-video compositor canvas context");
644
+ const rootRect = captureRoot.getBoundingClientRect();
645
+ if (rootRect.width <= 0 || rootRect.height <= 0) return 0;
646
+ const scaleX = destination.width / rootRect.width;
647
+ const scaleY = destination.height / rootRect.height;
648
+ for (const video of videos) {
649
+ const rect = video.getBoundingClientRect();
650
+ if (rect.width <= 0 || rect.height <= 0) continue;
651
+ const style = video.ownerDocument.defaultView?.getComputedStyle(video);
652
+ if (!style || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
653
+ continue;
654
+ }
655
+ const opacity = Number.parseFloat(style.opacity || "1");
656
+ if (opacity <= 0) continue;
657
+ const borderLeft = cssPixelValue(style.borderLeftWidth);
658
+ const borderRight = cssPixelValue(style.borderRightWidth);
659
+ const borderTop = cssPixelValue(style.borderTopWidth);
660
+ const borderBottom = cssPixelValue(style.borderBottomWidth);
661
+ const contentWidth = Math.max(1, rect.width - borderLeft - borderRight);
662
+ const contentHeight = Math.max(1, rect.height - borderTop - borderBottom);
663
+ const outerX = (rect.left - rootRect.left) * scaleX;
664
+ const outerY = (rect.top - rootRect.top) * scaleY;
665
+ const outerWidth = rect.width * scaleX;
666
+ const outerHeight = rect.height * scaleY;
667
+ const innerX = outerX + borderLeft * scaleX;
668
+ const innerY = outerY + borderTop * scaleY;
669
+ const innerWidth = contentWidth * scaleX;
670
+ const innerHeight = contentHeight * scaleY;
671
+ const outerRadius = cssRadius(style.borderTopLeftRadius, rect.width, rect.height) * Math.max(scaleX, scaleY);
672
+ const innerRadius = Math.max(
673
+ 0,
674
+ outerRadius - Math.max(borderLeft * scaleX, borderTop * scaleY)
675
+ );
676
+ const source = videoFrameRect(
677
+ video.videoWidth,
678
+ video.videoHeight,
679
+ contentWidth,
680
+ contentHeight,
681
+ style.objectFit || "fill"
682
+ );
683
+ context.save();
684
+ context.globalAlpha = Number.isFinite(opacity) ? opacity : 1;
685
+ applyFirstBoxShadow(context, style.boxShadow, scaleX, scaleY);
686
+ addRoundedRect(context, outerX, outerY, outerWidth, outerHeight, outerRadius);
687
+ context.fillStyle = style.borderTopStyle === "none" || borderTop <= 0 ? "rgba(0, 0, 0, 0.001)" : style.borderTopColor;
688
+ context.fill();
689
+ context.shadowColor = "rgba(0, 0, 0, 0)";
690
+ context.shadowBlur = 0;
691
+ context.shadowOffsetX = 0;
692
+ context.shadowOffsetY = 0;
693
+ addRoundedRect(context, innerX, innerY, innerWidth, innerHeight, innerRadius);
694
+ context.clip();
695
+ context.drawImage(
696
+ video,
697
+ source.sx,
698
+ source.sy,
699
+ source.sw,
700
+ source.sh,
701
+ innerX,
702
+ innerY,
703
+ innerWidth,
704
+ innerHeight
705
+ );
706
+ context.restore();
707
+ }
708
+ return videos.length;
709
+ }
710
+ function getFrameVisualStateKey(captureRoot, timelineTime, options = {}) {
711
+ const markup = captureRoot.innerHTML;
712
+ let needsTimelineKey = false;
713
+ const animationStates = [];
714
+ if (typeof captureRoot.getAnimations === "function") {
715
+ const animations = captureRoot.getAnimations({ subtree: true });
716
+ animations.forEach((animation, index) => {
717
+ try {
718
+ const timing = animation.effect?.getComputedTiming();
719
+ if (!timing) {
720
+ needsTimelineKey = true;
721
+ return;
722
+ }
723
+ animationStates.push(
724
+ `${index}:${animation.playState}:${String(timing.progress)}:${String(
725
+ timing.currentIteration
726
+ )}`
727
+ );
728
+ } catch {
729
+ needsTimelineKey = true;
730
+ }
731
+ });
732
+ } else if (/\b(?:anim-|transition-)|animation(?:-name)?\s*:/i.test(markup)) {
733
+ needsTimelineKey = true;
734
+ }
735
+ const imageStates = Array.from(captureRoot.querySelectorAll("img")).map((image) => {
736
+ const src = image.currentSrc || image.src;
737
+ if (POTENTIALLY_ANIMATED_IMAGE_URL.test(src)) needsTimelineKey = true;
738
+ return `${src}:${image.complete}:${image.naturalWidth}x${image.naturalHeight}`;
739
+ });
740
+ if (POTENTIALLY_ANIMATED_IMAGE_URL.test(markup)) needsTimelineKey = true;
741
+ const videoStates = Array.from(captureRoot.querySelectorAll("video")).filter(
742
+ (video) => video.videoWidth > 0 && video.videoHeight > 0 && (!options.ignoreScheduledVideoFrames || !video.closest(SCHEDULED_MEDIA_SELECTOR))
743
+ ).map(
744
+ (video) => `${video.currentSrc || video.src}:${finiteMediaTime(video.currentTime)}:${video.readyState}:${video.videoWidth}x${video.videoHeight}`
745
+ );
746
+ if (captureRoot.querySelector(
747
+ "canvas, iframe, object, embed, animate, animateMotion, animateTransform, set"
748
+ )) {
749
+ needsTimelineKey = true;
750
+ }
751
+ const fontStatus = captureRoot.ownerDocument.fonts?.status ?? "unsupported";
752
+ return JSON.stringify({
753
+ markup,
754
+ animationStates,
755
+ imageStates,
756
+ videoStates,
757
+ fontStatus,
758
+ timelineTime: needsTimelineKey ? timelineTime.toFixed(6) : null
759
+ });
760
+ }
761
+ function useFrameCapture() {
762
+ const containerRef = useRef(null);
763
+ const rootRef = useRef(null);
764
+ const renderAPIRef = useRef(null);
765
+ const mediaProviderRef = useRef(null);
766
+ const ownsMediaProviderRef = useRef(false);
767
+ const captureCanvasRef = useRef(null);
768
+ const captureBaseCanvasRef = useRef(null);
769
+ const lastVisualStateKeyRef = useRef(null);
770
+ const hasCapturedFrameRef = useRef(false);
771
+ const decodedImagesRef = useRef(/* @__PURE__ */ new WeakSet());
772
+ const captureImageDataUrlsRef = useRef(/* @__PURE__ */ new Map());
773
+ const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
774
+ const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
775
+ const dimensionsRef = useRef({ width: 1920, height: 1080 });
776
+ const init = useCallback(
777
+ async (doc, renderOptions, captionMode) => {
778
+ if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
779
+ const oldRoot = rootRef.current;
780
+ const oldContainer = containerRef.current;
781
+ const oldMediaProvider = mediaProviderRef.current;
782
+ const oldOwnsMediaProvider = ownsMediaProviderRef.current;
783
+ const oldCaptureCanvas = captureCanvasRef.current;
784
+ const oldCaptureBaseCanvas = captureBaseCanvasRef.current;
785
+ rootRef.current = null;
786
+ containerRef.current = null;
787
+ renderAPIRef.current = null;
788
+ mediaProviderRef.current = null;
789
+ ownsMediaProviderRef.current = false;
790
+ captureCanvasRef.current = null;
791
+ captureBaseCanvasRef.current = null;
792
+ lastVisualStateKeyRef.current = null;
793
+ hasCapturedFrameRef.current = false;
794
+ decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
795
+ captureImageDataUrlsRef.current.clear();
796
+ releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
797
+ captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
798
+ primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
799
+ await new Promise((resolve) => {
800
+ setTimeout(() => {
801
+ if (oldRoot) oldRoot.unmount();
802
+ if (oldContainer) oldContainer.remove();
803
+ if (oldOwnsMediaProvider) oldMediaProvider?.dispose();
804
+ if (oldCaptureCanvas) {
805
+ oldCaptureCanvas.width = 0;
806
+ oldCaptureCanvas.height = 0;
807
+ }
808
+ if (oldCaptureBaseCanvas) {
809
+ oldCaptureBaseCanvas.width = 0;
810
+ oldCaptureBaseCanvas.height = 0;
811
+ }
812
+ resolve();
813
+ }, 0);
814
+ });
815
+ }
816
+ const width = renderOptions.width ?? 1920;
817
+ const height = renderOptions.height ?? 1080;
818
+ const animationsEnabled = renderOptions.animationsEnabled ?? true;
819
+ dimensionsRef.current = { width, height };
820
+ const captureCanvas = document.createElement("canvas");
821
+ captureCanvas.width = width;
822
+ captureCanvas.height = height;
823
+ captureCanvas.style.width = `${width}px`;
824
+ captureCanvas.style.height = `${height}px`;
825
+ captureCanvasRef.current = captureCanvas;
826
+ const captureBaseCanvas = document.createElement("canvas");
827
+ captureBaseCanvas.width = width;
828
+ captureBaseCanvas.height = height;
829
+ captureBaseCanvas.style.width = `${width}px`;
830
+ captureBaseCanvas.style.height = `${height}px`;
831
+ captureBaseCanvasRef.current = captureBaseCanvas;
832
+ lastVisualStateKeyRef.current = null;
833
+ hasCapturedFrameRef.current = false;
834
+ decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
835
+ captureImageDataUrlsRef.current.clear();
836
+ releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
837
+ captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
838
+ primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
839
+ const container = document.createElement("div");
840
+ 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;`;
841
+ document.body.appendChild(container);
842
+ containerRef.current = container;
843
+ const renderRoot = document.createElement("div");
844
+ renderRoot.id = "squisq-capture-root";
845
+ renderRoot.style.cssText = `width:${width}px;height:${height}px;`;
846
+ container.appendChild(renderRoot);
847
+ const mediaProvider = renderOptions.images ? createInlineProvider(renderOptions.images) : renderOptions.mediaProvider ?? null;
848
+ mediaProviderRef.current = mediaProvider;
849
+ ownsMediaProviderRef.current = !!renderOptions.images;
850
+ const root = createRoot(renderRoot);
851
+ rootRef.current = root;
852
+ const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
853
+ const captionStyle = captionMode === "social" ? "social" : "standard";
854
+ let resolveRenderAPI;
855
+ const renderAPIReady = new Promise((resolve) => {
856
+ resolveRenderAPI = resolve;
857
+ });
858
+ const playerElement = createElement(DocPlayer, {
859
+ doc,
860
+ basePath: ".",
861
+ renderMode: true,
862
+ animationsEnabled,
863
+ showControls: false,
864
+ autoPlay: false,
865
+ forceViewport: { width, height, name: "export" },
866
+ theme: renderOptions.theme,
867
+ videoPresentation: renderOptions.videoPresentation,
868
+ pipSize: renderOptions.pipSize,
869
+ pipShape: renderOptions.pipShape,
870
+ pipPosition: renderOptions.pipPosition,
871
+ showCoverSlide: renderOptions.showCoverSlide,
872
+ coverSlideTemplate: renderOptions.coverSlideTemplate,
873
+ captionsEnabled,
874
+ captionStyle,
875
+ onRenderAPIReady: (api) => {
876
+ if (containerRef.current !== container) return;
877
+ renderAPIRef.current = api;
878
+ if (api) resolveRenderAPI(api);
879
+ }
880
+ });
881
+ await new Promise((resolve) => setTimeout(resolve, 0));
882
+ if (mediaProvider) {
883
+ root.render(createElement(MediaContext.Provider, { value: mediaProvider }, playerElement));
884
+ } else {
885
+ root.render(playerElement);
886
+ }
887
+ return new Promise((resolve, reject) => {
888
+ const timeout = setTimeout(() => {
889
+ const api = renderAPIRef.current;
890
+ const hasSeek = typeof api?.seekTo === "function";
891
+ const hasDur = typeof api?.getDuration === "function";
892
+ const rootEl = containerRef.current?.querySelector("#squisq-capture-root");
893
+ const hasPlayer = rootEl ? rootEl.querySelector(".doc-player") !== null : false;
894
+ reject(
895
+ new Error(
896
+ `Render API did not initialize within 15s. seekTo=${hasSeek}, getDuration=${hasDur}, player=${hasPlayer}, root=${!!rootEl}`
897
+ )
898
+ );
899
+ }, 15e3);
900
+ void renderAPIReady.then(async (api) => {
901
+ try {
902
+ const captureRoot = container.querySelector("#squisq-capture-root");
903
+ if (!(captureRoot instanceof HTMLElement)) {
904
+ throw new Error("Capture root element not found after player initialization.");
905
+ }
906
+ await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
907
+ await primeIndeterminateCaptureVideos(captureRoot, primedCaptureVideosRef.current);
908
+ clearTimeout(timeout);
909
+ resolve(api.getDuration());
910
+ } catch (assetError) {
911
+ clearTimeout(timeout);
912
+ reject(assetError);
913
+ }
914
+ });
915
+ });
916
+ },
917
+ []
918
+ );
919
+ const setCoverVisible = useCallback(async (visible) => {
920
+ const api = renderAPIRef.current;
921
+ if (!api) throw new Error("Frame capture not initialized \xE2\u20AC\u201D call init() first");
922
+ if (visible) await api.showCover();
923
+ else await api.hideCover();
924
+ lastVisualStateKeyRef.current = null;
925
+ }, []);
926
+ const captureCanvasFrame = useCallback(
927
+ async (time, options = {}) => {
928
+ const container = containerRef.current;
929
+ const api = renderAPIRef.current;
930
+ const captureCanvas = captureCanvasRef.current;
931
+ const captureBaseCanvas = captureBaseCanvasRef.current;
932
+ if (!container || !api || !captureCanvas || !captureBaseCanvas) {
933
+ throw new Error("Frame capture not initialized \u2014 call init() first");
934
+ }
935
+ const { width, height } = dimensionsRef.current;
936
+ const root = container.querySelector("#squisq-capture-root");
937
+ if (!root) {
938
+ throw new Error("Capture root element not found");
939
+ }
940
+ await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current);
941
+ try {
942
+ await api.seekTo(time);
943
+ } catch (seekError) {
944
+ if (!await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current)) {
945
+ throw seekError;
946
+ }
947
+ await api.seekTo(time);
948
+ }
949
+ if (await primeIndeterminateCaptureVideos(root, primedCaptureVideosRef.current)) {
950
+ await api.seekTo(time);
951
+ }
952
+ const renderedTime = api.getRenderedTime();
953
+ if (Math.abs(renderedTime - time) > RENDER_TIME_EPSILON_SECONDS) {
954
+ throw new Error(
955
+ `Player committed ${renderedTime.toFixed(6)}s while capture requested ${time.toFixed(6)}s.`
956
+ );
957
+ }
958
+ await waitForCaptureAssets(root, decodedImagesRef.current);
959
+ const compositePlan = planScheduledVideoComposite(root);
960
+ const hasUnderlays = compositePlan !== null && compositePlan.underlays.length > 0;
961
+ const rasterMode = compositePlan ? hasUnderlays ? "base-underlay" : "base" : "full";
962
+ const visualStateKey = options.reuseIfUnchanged ? `${rasterMode}:${getFrameVisualStateKey(root, time, {
963
+ ignoreScheduledVideoFrames: compositePlan !== null
964
+ })}` : null;
965
+ const shouldRasterize = visualStateKey === null || !hasCapturedFrameRef.current || lastVisualStateKeyRef.current !== visualStateKey;
966
+ if (!shouldRasterize && !compositePlan) return captureCanvas;
967
+ const rasterCanvas = compositePlan ? captureBaseCanvas : captureCanvas;
968
+ const captureContext = rasterCanvas.getContext("2d");
969
+ if (!captureContext) throw new Error("Could not create the frame capture canvas context");
970
+ if (shouldRasterize) {
971
+ captureContext.setTransform(1, 0, 0, 1, 0, 0);
972
+ captureContext.clearRect(0, 0, width, height);
973
+ }
974
+ const transientCloneCanvases = [];
975
+ if (shouldRasterize) {
976
+ try {
977
+ await html2canvas(root, {
978
+ canvas: rasterCanvas,
979
+ width,
980
+ height,
981
+ scale: 1,
982
+ useCORS: true,
983
+ allowTaint: true,
984
+ // An underlay base must stay transparent so the background video
985
+ // composited beneath it shows through everything the player does
986
+ // not paint. The compositor restores the opaque black backdrop.
987
+ backgroundColor: hasUnderlays ? null : "#000000",
988
+ logging: false,
989
+ onclone: async (_clonedDocument, clonedRoot) => {
990
+ if (compositePlan) {
991
+ if (hasUnderlays) clearScheduledUnderlayBackdrops(clonedRoot);
992
+ clonedRoot.querySelectorAll(SCHEDULED_MEDIA_SELECTOR).forEach((element) => element.remove());
993
+ }
994
+ transientCloneCanvases.push(...prepareScheduledVideoClones(root, clonedRoot));
995
+ await rasterizeCaptureSvgClones(
996
+ root,
997
+ clonedRoot,
998
+ transientCloneCanvases,
999
+ captureImageDataUrlsRef.current,
1000
+ captureSvgRasterCacheRef.current
1001
+ );
1002
+ },
1003
+ // html2canvas starts cloning at documentElement. Do not clone the rest
1004
+ // of the editor/site UI on every frame; only the capture root, its
1005
+ // ancestors, descendants, and document styles can affect this render.
1006
+ ignoreElements: (element) => shouldIgnoreCaptureSibling(element, root)
1007
+ });
1008
+ } finally {
1009
+ releaseCaptureCloneCanvases(transientCloneCanvases);
1010
+ }
1011
+ hasCapturedFrameRef.current = true;
1012
+ lastVisualStateKeyRef.current = visualStateKey;
1013
+ }
1014
+ if (compositePlan) {
1015
+ const outputContext = captureCanvas.getContext("2d");
1016
+ if (!outputContext) throw new Error("Could not create the frame output canvas context");
1017
+ outputContext.setTransform(1, 0, 0, 1, 0, 0);
1018
+ outputContext.clearRect(0, 0, width, height);
1019
+ outputContext.fillStyle = "#000000";
1020
+ outputContext.fillRect(0, 0, width, height);
1021
+ drawScheduledVideosOnto(captureCanvas, root, compositePlan.underlays);
1022
+ outputContext.drawImage(captureBaseCanvas, 0, 0);
1023
+ drawScheduledVideosOnto(captureCanvas, root, compositePlan.overlays);
1024
+ }
1025
+ return captureCanvas;
1026
+ },
1027
+ []
1028
+ );
1029
+ const captureFrame = useCallback(
1030
+ async (time, options = {}) => {
1031
+ const canvas = await captureCanvasFrame(time, options);
1032
+ return createImageBitmap(canvas);
1033
+ },
1034
+ [captureCanvasFrame]
1035
+ );
1036
+ const destroy = useCallback(() => {
1037
+ if (rootRef.current) {
1038
+ rootRef.current.unmount();
1039
+ rootRef.current = null;
1040
+ }
1041
+ if (containerRef.current) {
1042
+ containerRef.current.remove();
1043
+ containerRef.current = null;
1044
+ }
1045
+ if (ownsMediaProviderRef.current) mediaProviderRef.current?.dispose();
1046
+ mediaProviderRef.current = null;
1047
+ ownsMediaProviderRef.current = false;
1048
+ if (captureCanvasRef.current) {
1049
+ captureCanvasRef.current.width = 0;
1050
+ captureCanvasRef.current.height = 0;
1051
+ captureCanvasRef.current = null;
1052
+ }
1053
+ if (captureBaseCanvasRef.current) {
1054
+ captureBaseCanvasRef.current.width = 0;
1055
+ captureBaseCanvasRef.current.height = 0;
1056
+ captureBaseCanvasRef.current = null;
1057
+ }
1058
+ lastVisualStateKeyRef.current = null;
1059
+ hasCapturedFrameRef.current = false;
1060
+ decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
1061
+ captureImageDataUrlsRef.current.clear();
1062
+ releaseCaptureSvgRasterCache(captureSvgRasterCacheRef.current);
1063
+ captureSvgRasterCacheRef.current = createCaptureSvgRasterCache();
1064
+ primedCaptureVideosRef.current = /* @__PURE__ */ new WeakSet();
1065
+ renderAPIRef.current = null;
1066
+ }, []);
1067
+ return useMemo(
1068
+ () => ({ init, setCoverVisible, captureFrame, captureCanvasFrame, destroy }),
1069
+ [init, setCoverVisible, captureFrame, captureCanvasFrame, destroy]
1070
+ );
1071
+ }
1072
+
1073
+ export {
1074
+ useFrameCapture
1075
+ };