@bendyline/squisq-video-react 2.3.1 → 2.3.3

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useVideoExport
3
- } from "./chunk-U3RSDWQL.js";
3
+ } from "./chunk-UXTYD6U5.js";
4
4
 
5
5
  // src/VideoExportModal.tsx
6
6
  import { useState, useCallback, useId, useRef } from "react";
@@ -27,6 +27,8 @@ var CAPTURE_VIDEO_READINESS_POLL_MS = 16;
27
27
  var CAPTURE_VIDEO_END_PROBE_TIME = 1e101;
28
28
  var SCHEDULED_MEDIA_SELECTOR = ".doc-player__media-clips";
29
29
  var SCHEDULED_VIDEO_SELECTOR = `${SCHEDULED_MEDIA_SELECTOR} video[data-clip-id]`;
30
+ var CAPTION_OVERLAY_SELECTOR = ".caption-overlay, .social-caption-overlay";
31
+ var COVER_BLOCK_SELECTOR = ".doc-player__block--cover";
30
32
  async function waitForImageDecode(image) {
31
33
  const src = image.currentSrc || image.src;
32
34
  if (!src) return;
@@ -144,6 +146,7 @@ async function primeIndeterminateCaptureVideos(captureRoot, primedVideos = /* @_
144
146
  return;
145
147
  }
146
148
  if (Number.isFinite(video.duration)) {
149
+ video.dataset.captureSequential = "true";
147
150
  primedVideos.add(video);
148
151
  return;
149
152
  }
@@ -581,7 +584,7 @@ function scheduledVideoIsVisual(video) {
581
584
  function scheduledVideoPresentation(video) {
582
585
  return video.closest(SCHEDULED_MEDIA_SELECTOR)?.dataset.presentation;
583
586
  }
584
- function planScheduledVideoComposite(captureRoot) {
587
+ function planScheduledVideoComposite(captureRoot, options = {}) {
585
588
  const activeVideos = Array.from(
586
589
  captureRoot.querySelectorAll(SCHEDULED_VIDEO_SELECTOR)
587
590
  ).filter(scheduledVideoIsVisual);
@@ -592,21 +595,60 @@ function planScheduledVideoComposite(captureRoot) {
592
595
  const presentation = scheduledVideoPresentation(video);
593
596
  if (presentation === "background") underlays.push(video);
594
597
  else if (presentation === "picture-in-picture") overlays.push(video);
595
- else return null;
598
+ else if (presentation === "full-frame" && options.includeFullFrameOverlays) {
599
+ overlays.push(video);
600
+ } else return null;
596
601
  }
597
602
  return { underlays, overlays };
598
603
  }
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) {
604
+ function resolveScheduledVideoCompositePlan(captureRoot, options = {}) {
605
+ const plan = planScheduledVideoComposite(captureRoot, options);
606
+ if (!plan) return null;
607
+ if (captureRoot.querySelector(COVER_BLOCK_SELECTOR)) {
608
+ return { underlays: [], overlays: [] };
609
+ }
610
+ return plan;
611
+ }
612
+ function clearAncestorBackdropsFor(clonedRoot, targets) {
613
+ for (const target of targets) {
614
+ for (let element = target.parentElement; element; element = element === clonedRoot ? null : element.parentElement) {
605
615
  element.style.backgroundColor = "transparent";
606
616
  element.style.backgroundImage = "none";
607
617
  }
608
618
  }
609
619
  }
620
+ function clearScheduledUnderlayBackdrops(clonedRoot) {
621
+ clearAncestorBackdropsFor(
622
+ clonedRoot,
623
+ Array.from(
624
+ clonedRoot.querySelectorAll(
625
+ `${SCHEDULED_MEDIA_SELECTOR}[data-presentation="background"]`
626
+ )
627
+ )
628
+ );
629
+ }
630
+ function getCaptureCaptionOverlays(captureRoot) {
631
+ return Array.from(captureRoot.querySelectorAll(CAPTION_OVERLAY_SELECTOR));
632
+ }
633
+ function clearCaptionOverlayBackdrops(clonedRoot) {
634
+ clearAncestorBackdropsFor(clonedRoot, getCaptureCaptionOverlays(clonedRoot));
635
+ }
636
+ function shouldIgnoreCaptureCaptionSibling(element, captureRoot, captionOverlays) {
637
+ if (shouldIgnoreCaptureSibling(element, captureRoot)) return true;
638
+ if (element === captureRoot || element.contains(captureRoot)) return false;
639
+ if (!captureRoot.contains(element)) return false;
640
+ return !captionOverlays.some(
641
+ (overlay) => overlay === element || overlay.contains(element) || element.contains(overlay)
642
+ );
643
+ }
644
+ function getCaptionLayerStateKey(captionOverlays, ownerDocument, width, height) {
645
+ return JSON.stringify({
646
+ markup: captionOverlays.map((overlay) => overlay.outerHTML),
647
+ fontStatus: ownerDocument.fonts?.status ?? "unsupported",
648
+ width,
649
+ height
650
+ });
651
+ }
610
652
  function cssPixelValue(value) {
611
653
  const parsed = Number.parseFloat(value);
612
654
  return Number.isFinite(parsed) ? parsed : 0;
@@ -773,6 +815,9 @@ function useFrameCapture() {
773
815
  const captureSvgRasterCacheRef = useRef(createCaptureSvgRasterCache());
774
816
  const primedCaptureVideosRef = useRef(/* @__PURE__ */ new WeakSet());
775
817
  const dimensionsRef = useRef({ width: 1920, height: 1080 });
818
+ const captionLayerCanvasRef = useRef(null);
819
+ const captionLayerKeyRef = useRef(null);
820
+ const captionLayerHasContentRef = useRef(false);
776
821
  const init = useCallback(
777
822
  async (doc, renderOptions, captionMode) => {
778
823
  if (rootRef.current || containerRef.current || mediaProviderRef.current || captureCanvasRef.current || captureBaseCanvasRef.current) {
@@ -782,6 +827,7 @@ function useFrameCapture() {
782
827
  const oldOwnsMediaProvider = ownsMediaProviderRef.current;
783
828
  const oldCaptureCanvas = captureCanvasRef.current;
784
829
  const oldCaptureBaseCanvas = captureBaseCanvasRef.current;
830
+ const oldCaptionLayerCanvas = captionLayerCanvasRef.current;
785
831
  rootRef.current = null;
786
832
  containerRef.current = null;
787
833
  renderAPIRef.current = null;
@@ -789,6 +835,9 @@ function useFrameCapture() {
789
835
  ownsMediaProviderRef.current = false;
790
836
  captureCanvasRef.current = null;
791
837
  captureBaseCanvasRef.current = null;
838
+ captionLayerCanvasRef.current = null;
839
+ captionLayerKeyRef.current = null;
840
+ captionLayerHasContentRef.current = false;
792
841
  lastVisualStateKeyRef.current = null;
793
842
  hasCapturedFrameRef.current = false;
794
843
  decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
@@ -809,6 +858,10 @@ function useFrameCapture() {
809
858
  oldCaptureBaseCanvas.width = 0;
810
859
  oldCaptureBaseCanvas.height = 0;
811
860
  }
861
+ if (oldCaptionLayerCanvas) {
862
+ oldCaptionLayerCanvas.width = 0;
863
+ oldCaptionLayerCanvas.height = 0;
864
+ }
812
865
  resolve();
813
866
  }, 0);
814
867
  });
@@ -829,6 +882,8 @@ function useFrameCapture() {
829
882
  captureBaseCanvas.style.width = `${width}px`;
830
883
  captureBaseCanvas.style.height = `${height}px`;
831
884
  captureBaseCanvasRef.current = captureBaseCanvas;
885
+ captionLayerKeyRef.current = null;
886
+ captionLayerHasContentRef.current = false;
832
887
  lastVisualStateKeyRef.current = null;
833
888
  hasCapturedFrameRef.current = false;
834
889
  decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
@@ -956,7 +1011,9 @@ function useFrameCapture() {
956
1011
  );
957
1012
  }
958
1013
  await waitForCaptureAssets(root, decodedImagesRef.current);
959
- const compositePlan = planScheduledVideoComposite(root);
1014
+ const compositePlan = resolveScheduledVideoCompositePlan(root, {
1015
+ includeFullFrameOverlays: true
1016
+ });
960
1017
  const hasUnderlays = compositePlan !== null && compositePlan.underlays.length > 0;
961
1018
  const rasterMode = compositePlan ? hasUnderlays ? "base-underlay" : "base" : "full";
962
1019
  const visualStateKey = options.reuseIfUnchanged ? `${rasterMode}:${getFrameVisualStateKey(root, time, {
@@ -990,6 +1047,7 @@ function useFrameCapture() {
990
1047
  if (compositePlan) {
991
1048
  if (hasUnderlays) clearScheduledUnderlayBackdrops(clonedRoot);
992
1049
  clonedRoot.querySelectorAll(SCHEDULED_MEDIA_SELECTOR).forEach((element) => element.remove());
1050
+ clonedRoot.querySelectorAll(CAPTION_OVERLAY_SELECTOR).forEach((element) => element.remove());
993
1051
  }
994
1052
  transientCloneCanvases.push(...prepareScheduledVideoClones(root, clonedRoot));
995
1053
  await rasterizeCaptureSvgClones(
@@ -1021,6 +1079,64 @@ function useFrameCapture() {
1021
1079
  drawScheduledVideosOnto(captureCanvas, root, compositePlan.underlays);
1022
1080
  outputContext.drawImage(captureBaseCanvas, 0, 0);
1023
1081
  drawScheduledVideosOnto(captureCanvas, root, compositePlan.overlays);
1082
+ const captionOverlays = getCaptureCaptionOverlays(root);
1083
+ if (captionOverlays.length > 0) {
1084
+ const hasCaptionText = captionOverlays.some(
1085
+ (overlay) => (overlay.textContent ?? "").trim().length > 0
1086
+ );
1087
+ const captionKey = getCaptionLayerStateKey(
1088
+ captionOverlays,
1089
+ root.ownerDocument,
1090
+ width,
1091
+ height
1092
+ );
1093
+ if (captionLayerKeyRef.current !== captionKey) {
1094
+ if (hasCaptionText) {
1095
+ let captionCanvas = captionLayerCanvasRef.current;
1096
+ if (!captionCanvas) {
1097
+ captionCanvas = document.createElement("canvas");
1098
+ captionLayerCanvasRef.current = captionCanvas;
1099
+ }
1100
+ if (captionCanvas.width !== width || captionCanvas.height !== height) {
1101
+ captionCanvas.width = width;
1102
+ captionCanvas.height = height;
1103
+ }
1104
+ const captionContext = captionCanvas.getContext("2d");
1105
+ if (!captionContext) {
1106
+ throw new Error("Could not create the caption layer canvas context");
1107
+ }
1108
+ captionContext.setTransform(1, 0, 0, 1, 0, 0);
1109
+ captionContext.clearRect(0, 0, width, height);
1110
+ await html2canvas(root, {
1111
+ canvas: captionCanvas,
1112
+ width,
1113
+ height,
1114
+ scale: 1,
1115
+ useCORS: true,
1116
+ allowTaint: true,
1117
+ backgroundColor: null,
1118
+ logging: false,
1119
+ onclone: (_clonedDocument, clonedRoot) => {
1120
+ clearCaptionOverlayBackdrops(clonedRoot);
1121
+ },
1122
+ ignoreElements: (element) => shouldIgnoreCaptureCaptionSibling(element, root, captionOverlays)
1123
+ });
1124
+ }
1125
+ captionLayerKeyRef.current = captionKey;
1126
+ captionLayerHasContentRef.current = hasCaptionText;
1127
+ }
1128
+ if (captionLayerHasContentRef.current && captionLayerCanvasRef.current) {
1129
+ const overlayOpacity = Number.parseFloat(
1130
+ root.ownerDocument.defaultView?.getComputedStyle(captionOverlays[0]).opacity ?? "1"
1131
+ );
1132
+ if (!Number.isFinite(overlayOpacity) || overlayOpacity > 0) {
1133
+ outputContext.save();
1134
+ outputContext.globalAlpha = Number.isFinite(overlayOpacity) ? overlayOpacity : 1;
1135
+ outputContext.drawImage(captionLayerCanvasRef.current, 0, 0);
1136
+ outputContext.restore();
1137
+ }
1138
+ }
1139
+ }
1024
1140
  }
1025
1141
  return captureCanvas;
1026
1142
  },
@@ -1055,6 +1171,13 @@ function useFrameCapture() {
1055
1171
  captureBaseCanvasRef.current.height = 0;
1056
1172
  captureBaseCanvasRef.current = null;
1057
1173
  }
1174
+ if (captionLayerCanvasRef.current) {
1175
+ captionLayerCanvasRef.current.width = 0;
1176
+ captionLayerCanvasRef.current.height = 0;
1177
+ captionLayerCanvasRef.current = null;
1178
+ }
1179
+ captionLayerKeyRef.current = null;
1180
+ captionLayerHasContentRef.current = false;
1058
1181
  lastVisualStateKeyRef.current = null;
1059
1182
  hasCapturedFrameRef.current = false;
1060
1183
  decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useFrameCapture
3
- } from "./chunk-YTEDBL6F.js";
3
+ } from "./chunk-I5PNMCSZ.js";
4
4
  import {
5
5
  EXPORT_AUDIO_CHANNELS,
6
6
  EXPORT_AUDIO_SAMPLE_RATE,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useFrameCapture
3
- } from "./chunk-YTEDBL6F.js";
3
+ } from "./chunk-I5PNMCSZ.js";
4
4
 
5
5
  // src/CoverImageExportModal.tsx
6
6
  import { useCallback, useId, useRef, useState } from "react";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "../chunk-CRXJOZSH.js";
4
+ } from "../chunk-FX5CHFGY.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "../chunk-NTRCMTCF.js";
10
- import "../chunk-U3RSDWQL.js";
11
- import "../chunk-YTEDBL6F.js";
9
+ } from "../chunk-YZN7URUB.js";
10
+ import "../chunk-UXTYD6U5.js";
11
+ import "../chunk-I5PNMCSZ.js";
12
12
  import "../chunk-32QCPFXE.js";
13
13
  import "../chunk-5MFQMJ5Z.js";
14
14
  export {
@@ -2,8 +2,8 @@ import {
2
2
  CoverImageExportModal,
3
3
  coverImageFilename,
4
4
  validateCoverImageDimensions
5
- } from "../chunk-NTRCMTCF.js";
6
- import "../chunk-YTEDBL6F.js";
5
+ } from "../chunk-YZN7URUB.js";
6
+ import "../chunk-I5PNMCSZ.js";
7
7
  export {
8
8
  CoverImageExportModal,
9
9
  coverImageFilename,
@@ -3,10 +3,10 @@ import {
3
3
  resolveVideoCoverFramePlan,
4
4
  resolveVideoExportCover,
5
5
  useVideoExport
6
- } from "../chunk-U3RSDWQL.js";
6
+ } from "../chunk-UXTYD6U5.js";
7
7
  import {
8
8
  useFrameCapture
9
- } from "../chunk-YTEDBL6F.js";
9
+ } from "../chunk-I5PNMCSZ.js";
10
10
  import "../chunk-32QCPFXE.js";
11
11
  import "../chunk-5MFQMJ5Z.js";
12
12
  export {
package/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "./chunk-CRXJOZSH.js";
4
+ } from "./chunk-FX5CHFGY.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "./chunk-NTRCMTCF.js";
9
+ } from "./chunk-YZN7URUB.js";
10
10
  import {
11
11
  DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
12
12
  resolveVideoCoverFramePlan,
13
13
  resolveVideoExportCover,
14
14
  useVideoExport
15
- } from "./chunk-U3RSDWQL.js";
15
+ } from "./chunk-UXTYD6U5.js";
16
16
  import {
17
17
  useFrameCapture
18
- } from "./chunk-YTEDBL6F.js";
18
+ } from "./chunk-I5PNMCSZ.js";
19
19
  import {
20
20
  createEncoder,
21
21
  supportsWebCodecs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video-react",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -69,9 +69,9 @@
69
69
  "react-dom": "^18.0.0 || ^19.0.0"
70
70
  },
71
71
  "dependencies": {
72
- "@bendyline/squisq": "2.6.0",
73
- "@bendyline/squisq-video": "2.2.10",
74
- "@bendyline/squisq-react": "2.6.0",
72
+ "@bendyline/squisq": "2.7.1",
73
+ "@bendyline/squisq-video": "2.2.12",
74
+ "@bendyline/squisq-react": "2.7.1",
75
75
  "@ffmpeg/core": "0.12.9",
76
76
  "@ffmpeg/ffmpeg": "0.12.15",
77
77
  "@ffmpeg/util": "0.12.2",