@skippr/live-agent-sdk 0.97.0 → 0.99.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.
@@ -3146,6 +3146,7 @@ function useSessionHold({
3146
3146
  const [countdown, setCountdown] = useState9(timeoutSeconds);
3147
3147
  const screenWasSharedBeforeHoldRef = useRef11(false);
3148
3148
  const holdTransitionInFlightRef = useRef11(false);
3149
+ const clearPausedCaptureRef = useRef11(null);
3149
3150
  const publish = useCallback10(async (type) => {
3150
3151
  await localParticipant.publishData(textEncoder2.encode(JSON.stringify({ type })), {
3151
3152
  reliable: true,
@@ -3192,6 +3193,7 @@ function useSessionHold({
3192
3193
  try {
3193
3194
  await localParticipant.setMicrophoneEnabled(true);
3194
3195
  await resumeScreenShareOnResume();
3196
+ clearPausedCaptureRef.current?.();
3195
3197
  await publish("resume");
3196
3198
  setIsHeld(false);
3197
3199
  } catch (error) {
@@ -3223,7 +3225,7 @@ function useSessionHold({
3223
3225
  holdTransitionInFlightRef.current = false;
3224
3226
  });
3225
3227
  }, [isHeld, hold, release]);
3226
- return { isHeld, countdown, toggle };
3228
+ return { isHeld, countdown, toggle, clearPausedCaptureRef };
3227
3229
  }
3228
3230
 
3229
3231
  // src/context/SessionHoldContext.tsx
@@ -3231,7 +3233,8 @@ import { jsx as jsx13 } from "react/jsx-runtime";
3231
3233
  var SessionHoldContext = createContext3({
3232
3234
  isHeld: false,
3233
3235
  countdown: 0,
3234
- toggle: () => {}
3236
+ toggle: () => {},
3237
+ clearPausedCaptureRef: { current: null }
3235
3238
  });
3236
3239
  function SessionHoldProvider({ children }) {
3237
3240
  const { pauseSession } = useLiveAgent();
@@ -4976,6 +4979,7 @@ function accessibleName(element, options = {}) {
4976
4979
  var REF_PREFIX = "r-";
4977
4980
  var VALUE_MAX_CHARS = 80;
4978
4981
  var HREF_MAX_CHARS = 60;
4982
+ var TABLE_ROW_MAX_CHARS = 160;
4979
4983
  var PAGE_CONTENT_MAX_BYTES = 14000;
4980
4984
  var NODE_LIMIT = 1500;
4981
4985
  var SENSITIVE_IFRAME_HOSTS = [
@@ -5321,8 +5325,9 @@ function ensureStableRef(element) {
5321
5325
  function formatNodeAsLine(entry) {
5322
5326
  const indent = " ".repeat(entry.depth);
5323
5327
  const namePart = entry.name ? ` "${escapeAttributeValue(entry.name)}"` : "";
5328
+ const refPart = entry.ref ? ` ${entry.ref}` : "";
5324
5329
  const attrsPart = entry.attrs.length > 0 ? ` ${entry.attrs.join(" ")}` : "";
5325
- return `${indent}${entry.role}${namePart} ${entry.ref}${attrsPart}`;
5330
+ return `${indent}${entry.role}${namePart}${refPart}${attrsPart}`;
5326
5331
  }
5327
5332
  function shouldSkipSubtree(element) {
5328
5333
  if (element.id === WIDGET_ROOT_ID)
@@ -5350,13 +5355,66 @@ function getSameOriginIframeBody(element) {
5350
5355
  return null;
5351
5356
  }
5352
5357
  }
5353
- function walkAndCollect(element, depth, entries) {
5358
+ function normalizeText(text) {
5359
+ return (text ?? "").replace(/\s+/g, " ").trim();
5360
+ }
5361
+ function formatTableRowText(row) {
5362
+ const cellValues = Array.from(row.children).filter((cell) => cell.tagName === "TD" || cell.tagName === "TH").map((cell) => normalizeText(cell.textContent).slice(0, VALUE_MAX_CHARS));
5363
+ return cellValues.join(" | ").slice(0, TABLE_ROW_MAX_CHARS).trim();
5364
+ }
5365
+ function isTableHeaderRow(row) {
5366
+ return Array.from(row.children).some((cell) => cell.tagName === "TH");
5367
+ }
5368
+ function directTextOfElement(element) {
5369
+ let combinedText = "";
5370
+ for (const childNode of Array.from(element.childNodes)) {
5371
+ if (childNode.nodeType === Node.TEXT_NODE)
5372
+ combinedText += `${childNode.textContent} `;
5373
+ }
5374
+ return normalizeText(combinedText);
5375
+ }
5376
+ var REFLESS_TEXT_ENTRY_REF = "";
5377
+ function collectRefLessTextLeaf(text, depth, rect, entries) {
5378
+ entries.push({
5379
+ depth,
5380
+ role: "text",
5381
+ name: text.slice(0, VALUE_MAX_CHARS),
5382
+ ref: REFLESS_TEXT_ENTRY_REF,
5383
+ attrs: [],
5384
+ area: rect.width * rect.height
5385
+ });
5386
+ }
5387
+ function collectTableRowAsSingleEntry(row, depth, rect, entries) {
5388
+ const rowText = formatTableRowText(row);
5389
+ if (!rowText)
5390
+ return false;
5391
+ entries.push({
5392
+ depth,
5393
+ role: "row",
5394
+ name: rowText,
5395
+ ref: ensureStableRef(row),
5396
+ attrs: isTableHeaderRow(row) ? ["header"] : [],
5397
+ area: rect.width * rect.height
5398
+ });
5399
+ for (const child of Array.from(row.children)) {
5400
+ walkAndCollect(child, depth + 1, entries, true);
5401
+ }
5402
+ return true;
5403
+ }
5404
+ function walkAndCollect(element, depth, entries, suppressTextLeaves = false) {
5354
5405
  if (shouldSkipSubtree(element))
5355
5406
  return;
5356
5407
  const rect = element.getBoundingClientRect();
5408
+ if (element.tagName === "TR") {
5409
+ if (!isVisible(element, rect))
5410
+ return;
5411
+ if (collectTableRowAsSingleEntry(element, depth, rect, entries))
5412
+ return;
5413
+ }
5357
5414
  const role = inferRole(element) ?? (isCursorClickable(element, rect) ? "button" : null);
5358
5415
  const isVisibleAndEmittable = role !== null && isVisible(element, rect) && shouldEmitElement(element, role);
5359
5416
  let childDepth = depth;
5417
+ let childSuppressTextLeaves = suppressTextLeaves;
5360
5418
  if (isVisibleAndEmittable && role) {
5361
5419
  const ref = ensureStableRef(element);
5362
5420
  const name = accessibleName(element);
@@ -5369,12 +5427,20 @@ function walkAndCollect(element, depth, entries) {
5369
5427
  area: rect.width * rect.height
5370
5428
  });
5371
5429
  childDepth = depth + 1;
5430
+ const nameAlreadyCoversSubtreeText = name !== "";
5431
+ if (nameAlreadyCoversSubtreeText)
5432
+ childSuppressTextLeaves = true;
5433
+ } else if (!suppressTextLeaves) {
5434
+ const ownText = directTextOfElement(element);
5435
+ if (ownText && isVisible(element, rect)) {
5436
+ collectRefLessTextLeaf(ownText, depth, rect, entries);
5437
+ }
5372
5438
  }
5373
5439
  if (role === "iframe") {
5374
5440
  const iframeBody = getSameOriginIframeBody(element);
5375
5441
  if (iframeBody) {
5376
5442
  for (const child of Array.from(iframeBody.children)) {
5377
- walkAndCollect(child, childDepth, entries);
5443
+ walkAndCollect(child, childDepth, entries, childSuppressTextLeaves);
5378
5444
  }
5379
5445
  }
5380
5446
  return;
@@ -5382,15 +5448,19 @@ function walkAndCollect(element, depth, entries) {
5382
5448
  if (role && isBlindRegion(role))
5383
5449
  return;
5384
5450
  for (const child of Array.from(element.children)) {
5385
- walkAndCollect(child, childDepth, entries);
5451
+ walkAndCollect(child, childDepth, entries, childSuppressTextLeaves);
5386
5452
  }
5387
5453
  }
5388
- function dropSmallestAreaEntriesUntilUnderLimit(entries) {
5454
+ var TRUNCATION_DROP_ORDER_BY_ROLE = { text: 0, row: 1 };
5455
+ function truncationDropOrder(entry) {
5456
+ return TRUNCATION_DROP_ORDER_BY_ROLE[entry.role] ?? 2;
5457
+ }
5458
+ function dropInformationalEntriesFirstUntilUnderLimit(entries) {
5389
5459
  if (entries.length <= NODE_LIMIT)
5390
5460
  return entries;
5391
5461
  const indexedEntries = entries.map((entry, index2) => ({ entry, index: index2 }));
5392
- const entriesByAreaAscending = indexedEntries.slice(1).sort((a, b) => a.entry.area - b.entry.area);
5393
- const indicesToDrop = new Set(entriesByAreaAscending.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
5462
+ const entriesMostDroppableFirst = indexedEntries.slice(1).sort((a, b) => truncationDropOrder(a.entry) - truncationDropOrder(b.entry) || a.entry.area - b.entry.area);
5463
+ const indicesToDrop = new Set(entriesMostDroppableFirst.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
5394
5464
  const keptEntries = [];
5395
5465
  for (let i = 0;i < entries.length; i++) {
5396
5466
  if (!indicesToDrop.has(i))
@@ -5415,7 +5485,7 @@ function buildAccessibilityTree() {
5415
5485
  }
5416
5486
  }
5417
5487
  const exceededNodeLimit = collectedEntries.length > NODE_LIMIT;
5418
- const finalEntries = exceededNodeLimit ? dropSmallestAreaEntriesUntilUnderLimit(collectedEntries) : collectedEntries;
5488
+ const finalEntries = exceededNodeLimit ? dropInformationalEntriesFirstUntilUnderLimit(collectedEntries) : collectedEntries;
5419
5489
  const treeLines = finalEntries.map(formatNodeAsLine);
5420
5490
  let pageContent = treeLines.join(`
5421
5491
  `);
@@ -5790,21 +5860,46 @@ function paintCursorOverlay(ctx, transform, brand = DEFAULT_BRAND_PALETTE) {
5790
5860
  }
5791
5861
 
5792
5862
  // src/capture/snapdom.ts
5863
+ var UNLOADABLE_IMAGE_PLACEHOLDER_FILL = "#e2e8f0";
5864
+ function placeholderForUnloadableImage({
5865
+ width = 100,
5866
+ height = 100
5867
+ }) {
5868
+ const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="100%" height="100%" fill="${UNLOADABLE_IMAGE_PLACEHOLDER_FILL}"/></svg>`;
5869
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(placeholderSvg)}`;
5870
+ }
5793
5871
  var cachedSnapdomModule = null;
5794
5872
  function loadSnapdom() {
5795
- if (!cachedSnapdomModule)
5873
+ if (!cachedSnapdomModule) {
5796
5874
  cachedSnapdomModule = import("@zumer/snapdom");
5875
+ }
5797
5876
  return cachedSnapdomModule;
5798
5877
  }
5878
+ function preCacheSnapshotResources() {
5879
+ loadSnapdom().then(({ preCache }) => preCache(document, { embedFonts: true })).catch(() => {
5880
+ return;
5881
+ });
5882
+ }
5799
5883
  async function snapToCanvas(element, options = {}) {
5800
5884
  const snapdomModule = await loadSnapdom();
5801
- return snapdomModule.snapdom.toCanvas(element, options);
5885
+ const optionsWithEngineDefaults = {
5886
+ embedFonts: true,
5887
+ fallbackURL: placeholderForUnloadableImage,
5888
+ ...options,
5889
+ snap: snapdomModule.snapdom
5890
+ };
5891
+ return snapdomModule.snapdom.toCanvas(element, optionsWithEngineDefaults);
5802
5892
  }
5803
5893
 
5804
5894
  // src/components/DomCapture.tsx
5805
5895
  var SNAPSHOT_INTERVAL_MS = 3000;
5806
5896
  var A11Y_PUBLISH_INTERVAL_MS = 2000;
5807
5897
  var FIRST_SNAPSHOT_DELAY_MS = 400;
5898
+ var RASTERIZE_TIMEOUT_MS = 1e4;
5899
+ var FULL_QUALITY_TIMEOUTS_BEFORE_COOLDOWN = 1;
5900
+ var FAST_CAPTURES_BEFORE_FULL_QUALITY_RETRY = 10;
5901
+ var RASTERIZE_TIMED_OUT = Symbol("rasterize-timed-out");
5902
+ var SCROLL_SETTLE_MS = 300;
5808
5903
  var CAPTURE_PRESET = ScreenSharePresets2.h1080fps30;
5809
5904
  var CAPTURE_FPS = CAPTURE_PRESET.encoding.maxFramerate ?? 30;
5810
5905
  var CAPTURE_BITRATE = 4000000;
@@ -5812,15 +5907,20 @@ var DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES = 14000;
5812
5907
  var MAX_CANVAS_DPR = 1.5;
5813
5908
  var CONSECUTIVE_FAILURES_BEFORE_REPORT = 3;
5814
5909
  var textEncoder4 = new TextEncoder;
5910
+ var SNAPSHOT_EXCLUDE_SELECTORS = [`#${WIDGET_ROOT_ID}`, `[${PRIVATE_ATTR}]`];
5911
+ function isCapturableIframe(element) {
5912
+ return isSameOriginIframe(element) && !isSensitiveIframe(element);
5913
+ }
5815
5914
  function shouldIncludeInSnapshot(element) {
5816
- if (!(element instanceof Element))
5817
- return true;
5818
- if (element.id === WIDGET_ROOT_ID)
5819
- return false;
5820
- if (element.hasAttribute?.(PRIVATE_ATTR))
5915
+ if (element instanceof HTMLIFrameElement && !isCapturableIframe(element))
5821
5916
  return false;
5822
5917
  return true;
5823
5918
  }
5919
+ function shouldIncludeInFirstFrame(element) {
5920
+ if (element.tagName === "IMG")
5921
+ return false;
5922
+ return shouldIncludeInSnapshot(element);
5923
+ }
5824
5924
  function createPublishingCanvas() {
5825
5925
  const dprBoost = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
5826
5926
  const canvas = document.createElement("canvas");
@@ -5841,22 +5941,23 @@ function getCanvasCaptureStream(canvas, fps) {
5841
5941
  }
5842
5942
  return captureStreamFn.call(canvas, fps);
5843
5943
  }
5844
- async function rasterizeViewportToPageLayer(width, height) {
5845
- const dpr = window.devicePixelRatio || 1;
5944
+ async function rasterizeViewportToPageLayer(width, height, { skipCrossOriginImageSettling, includeImages }) {
5945
+ const requestedDpr = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
5946
+ const viewportWidthAtCaptureStart = window.innerWidth;
5846
5947
  const snapshotCanvas = await snapToCanvas(document.documentElement, {
5847
- filter: shouldIncludeInSnapshot,
5848
- filterMode: "remove",
5948
+ exclude: SNAPSHOT_EXCLUDE_SELECTORS,
5949
+ excludeMode: "remove",
5950
+ filter: includeImages ? shouldIncludeInSnapshot : shouldIncludeInFirstFrame,
5951
+ filterMode: "hide",
5849
5952
  backgroundColor: "#ffffff",
5850
- fast: false,
5851
- dpr
5953
+ fast: skipCrossOriginImageSettling,
5954
+ dpr: requestedDpr,
5955
+ clip: "viewport"
5852
5956
  });
5853
- const sourceX = window.scrollX * dpr;
5854
- const sourceY = window.scrollY * dpr;
5855
- const sourceWidth = window.innerWidth * dpr;
5856
- const sourceHeight = window.innerHeight * dpr;
5857
- const fitScale = Math.min(width / sourceWidth, height / sourceHeight);
5858
- const destWidth = sourceWidth * fitScale;
5859
- const destHeight = sourceHeight * fitScale;
5957
+ const canvasPixelsPerCssPixel = viewportWidthAtCaptureStart > 0 ? snapshotCanvas.width / viewportWidthAtCaptureStart : requestedDpr;
5958
+ const fitScale = Math.min(width / snapshotCanvas.width, height / snapshotCanvas.height);
5959
+ const destWidth = snapshotCanvas.width * fitScale;
5960
+ const destHeight = snapshotCanvas.height * fitScale;
5860
5961
  const destX = (width - destWidth) / 2;
5861
5962
  const destY = (height - destHeight) / 2;
5862
5963
  const layer = document.createElement("canvas");
@@ -5867,10 +5968,14 @@ async function rasterizeViewportToPageLayer(width, height) {
5867
5968
  throw new Error("Failed to get 2D context for page layer");
5868
5969
  layerCtx.fillStyle = "#ffffff";
5869
5970
  layerCtx.fillRect(0, 0, width, height);
5870
- layerCtx.drawImage(snapshotCanvas, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
5971
+ layerCtx.drawImage(snapshotCanvas, destX, destY, destWidth, destHeight);
5871
5972
  return {
5872
5973
  canvas: layer,
5873
- viewportToCanvas: { scale: dpr * fitScale, offsetX: destX, offsetY: destY }
5974
+ viewportToCanvas: {
5975
+ scale: canvasPixelsPerCssPixel * fitScale,
5976
+ offsetX: destX,
5977
+ offsetY: destY
5978
+ }
5874
5979
  };
5875
5980
  }
5876
5981
  function compositePageAndCursorOverlay(ctx, canvas, cachedPage, brand) {
@@ -5894,6 +5999,12 @@ async function gzipIfBeneficial(snapshotBytes) {
5894
5999
  return { bytes: snapshotBytes, compressionFlag: 0 };
5895
6000
  }
5896
6001
  }
6002
+ function frameWithCompressionFlag(compressionFlag, payload) {
6003
+ const frame = new Uint8Array(payload.byteLength + 1);
6004
+ frame[0] = compressionFlag;
6005
+ frame.set(payload, 1);
6006
+ return frame;
6007
+ }
5897
6008
  async function buildDomSnapshotFrame() {
5898
6009
  const a11yTree = buildAccessibilityTree();
5899
6010
  const snapshotJson = JSON.stringify({
@@ -5906,14 +6017,12 @@ async function buildDomSnapshotFrame() {
5906
6017
  });
5907
6018
  const snapshotBytes = textEncoder4.encode(snapshotJson);
5908
6019
  const { bytes, compressionFlag } = await gzipIfBeneficial(snapshotBytes);
5909
- const frame = new Uint8Array(bytes.byteLength + 1);
5910
- frame[0] = compressionFlag;
5911
- frame.set(bytes, 1);
5912
- return frame;
6020
+ return frameWithCompressionFlag(compressionFlag, bytes);
5913
6021
  }
5914
6022
  function reportCaptureError(localParticipant) {
5915
6023
  try {
5916
- localParticipant.publishData(textEncoder4.encode(JSON.stringify({ type: "capture_error" })), {
6024
+ const payload = textEncoder4.encode(JSON.stringify({ type: "capture_error" }));
6025
+ localParticipant.publishData(frameWithCompressionFlag(0, payload), {
5917
6026
  reliable: true,
5918
6027
  topic: DOM_SNAPSHOT_TOPIC
5919
6028
  }).catch(() => {
@@ -5930,7 +6039,7 @@ async function unpublishAndStopTrack(localParticipant, videoTrack) {
5930
6039
  function DomCapture({ pushOrTapMicMode = false }) {
5931
6040
  const { localParticipant } = useLocalParticipant5();
5932
6041
  const connectionState = useConnectionState2();
5933
- const { isHeld } = useSessionHoldContext();
6042
+ const { isHeld, clearPausedCaptureRef } = useSessionHoldContext();
5934
6043
  const accentColor = useContext9(LiveAgentContext)?.appearance?.accentColor ?? null;
5935
6044
  const brand = useMemo4(() => resolveBrandPalette(accentColor), [accentColor]);
5936
6045
  const brandRef = useRef17(brand);
@@ -5956,12 +6065,18 @@ function DomCapture({ pushOrTapMicMode = false }) {
5956
6065
  return;
5957
6066
  videoTrack.contentHint = "text";
5958
6067
  didStartRef.current = true;
6068
+ preCacheSnapshotResources();
5959
6069
  let cancelled = false;
5960
6070
  let snapshotInFlight = false;
5961
6071
  let a11yPublishInFlight = false;
5962
6072
  let a11yPublishPending = false;
5963
6073
  let consecutiveCaptureFailures = 0;
5964
6074
  let cachedPage = null;
6075
+ let rasterizeSequence = 0;
6076
+ let lastCommittedSequence = 0;
6077
+ let firstFrameCommitted = false;
6078
+ let capturesUntilFullQualityRetry = 0;
6079
+ let consecutiveFullQualityTimeouts = 0;
5965
6080
  if (!pushOrTapMicMode && !localParticipant.isMicrophoneEnabled) {
5966
6081
  localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
5967
6082
  }
@@ -5977,16 +6092,61 @@ function DomCapture({ pushOrTapMicMode = false }) {
5977
6092
  for (const track of canvasStream.getTracks())
5978
6093
  track.stop();
5979
6094
  });
5980
- const refreshPageAndComposite = async () => {
5981
- if (cancelled || snapshotInFlight || pausedRef.current)
6095
+ const commitPageLayerIfNewest = (sequence, page) => {
6096
+ if (cancelled || pausedRef.current || sequence <= lastCommittedSequence)
6097
+ return false;
6098
+ lastCommittedSequence = sequence;
6099
+ cachedPage = page;
6100
+ compositePageAndCursorOverlay(ctx, canvas, page, brandRef.current);
6101
+ firstFrameCommitted = true;
6102
+ return true;
6103
+ };
6104
+ const rasterizeAndCommitWaitingOutTimeout = async (options) => {
6105
+ const sequence = ++rasterizeSequence;
6106
+ const captureInProgress = rasterizeViewportToPageLayer(canvas.width, canvas.height, options);
6107
+ const captureOrTimeout = await Promise.race([
6108
+ captureInProgress,
6109
+ new Promise((resolve) => setTimeout(() => resolve(RASTERIZE_TIMED_OUT), RASTERIZE_TIMEOUT_MS))
6110
+ ]);
6111
+ if (captureOrTimeout === RASTERIZE_TIMED_OUT) {
6112
+ captureInProgress.then((lateCapture) => commitPageLayerIfNewest(sequence, lateCapture)).catch(() => {
6113
+ return;
6114
+ });
6115
+ return { timedOut: true };
6116
+ }
6117
+ commitPageLayerIfNewest(sequence, captureOrTimeout);
6118
+ return { timedOut: false };
6119
+ };
6120
+ let followUpRefreshQueued = false;
6121
+ const refreshPageAndComposite = async ({ queueWhenBusy = false } = {}) => {
6122
+ if (cancelled || snapshotInFlight || pausedRef.current) {
6123
+ if (snapshotInFlight && !cancelled && queueWhenBusy)
6124
+ followUpRefreshQueued = true;
5982
6125
  return;
6126
+ }
5983
6127
  snapshotInFlight = true;
5984
6128
  try {
5985
- cachedPage = await rasterizeViewportToPageLayer(canvas.width, canvas.height);
5986
- if (cancelled || pausedRef.current)
5987
- return;
5988
- compositePageAndCursorOverlay(ctx, canvas, cachedPage, brandRef.current);
6129
+ const isImagelessFirstFrame = !firstFrameCommitted;
6130
+ const useFullQuality = firstFrameCommitted && capturesUntilFullQualityRetry === 0;
6131
+ const { timedOut } = await rasterizeAndCommitWaitingOutTimeout({
6132
+ skipCrossOriginImageSettling: !useFullQuality,
6133
+ includeImages: !isImagelessFirstFrame
6134
+ });
6135
+ if (isImagelessFirstFrame && firstFrameCommitted)
6136
+ followUpRefreshQueued = true;
5989
6137
  consecutiveCaptureFailures = 0;
6138
+ if (useFullQuality) {
6139
+ if (timedOut) {
6140
+ consecutiveFullQualityTimeouts += 1;
6141
+ if (consecutiveFullQualityTimeouts >= FULL_QUALITY_TIMEOUTS_BEFORE_COOLDOWN) {
6142
+ capturesUntilFullQualityRetry = FAST_CAPTURES_BEFORE_FULL_QUALITY_RETRY;
6143
+ }
6144
+ } else {
6145
+ consecutiveFullQualityTimeouts = 0;
6146
+ }
6147
+ } else if (capturesUntilFullQualityRetry > 0) {
6148
+ capturesUntilFullQualityRetry -= 1;
6149
+ }
5990
6150
  } catch {
5991
6151
  if (cancelled)
5992
6152
  return;
@@ -5996,6 +6156,10 @@ function DomCapture({ pushOrTapMicMode = false }) {
5996
6156
  }
5997
6157
  } finally {
5998
6158
  snapshotInFlight = false;
6159
+ if (followUpRefreshQueued && !cancelled) {
6160
+ followUpRefreshQueued = false;
6161
+ refreshPageAndComposite();
6162
+ }
5999
6163
  }
6000
6164
  };
6001
6165
  let overlayAnimationFrame = null;
@@ -6061,36 +6225,55 @@ function DomCapture({ pushOrTapMicMode = false }) {
6061
6225
  });
6062
6226
  const cleanupSnapshotRequest = onSnapshotRequest(() => {
6063
6227
  tickA11yPublish();
6064
- refreshPageAndComposite();
6228
+ refreshPageAndComposite({ queueWhenBusy: true });
6065
6229
  });
6230
+ let scrollSettleTimer = null;
6231
+ const refreshOnceScrollSettles = () => {
6232
+ if (scrollSettleTimer)
6233
+ clearTimeout(scrollSettleTimer);
6234
+ scrollSettleTimer = setTimeout(() => {
6235
+ tickA11yPublish();
6236
+ refreshPageAndComposite({ queueWhenBusy: true });
6237
+ }, SCROLL_SETTLE_MS);
6238
+ };
6239
+ window.addEventListener("scroll", refreshOnceScrollSettles, { passive: true, capture: true });
6066
6240
  let snapshotTimer = null;
6067
6241
  const scheduleNextSnapshot = (delay) => {
6068
6242
  if (cancelled)
6069
6243
  return;
6070
- snapshotTimer = setTimeout(async () => {
6071
- await refreshPageAndComposite();
6244
+ snapshotTimer = setTimeout(() => {
6245
+ refreshPageAndComposite();
6072
6246
  scheduleNextSnapshot(SNAPSHOT_INTERVAL_MS);
6073
6247
  }, delay);
6074
6248
  };
6075
6249
  scheduleNextSnapshot(FIRST_SNAPSHOT_DELAY_MS);
6076
6250
  tickA11yPublish();
6077
- const a11yPublishTimer = setInterval(tickA11yPublish, A11Y_PUBLISH_INTERVAL_MS);
6251
+ const a11yPublishTimer = setInterval(() => void tickA11yPublish(), A11Y_PUBLISH_INTERVAL_MS);
6078
6252
  freezeCaptureRef.current = () => {
6079
6253
  pausedRef.current = true;
6080
6254
  drawPausedOverlay(canvas, ctx);
6081
6255
  };
6082
- resumeCaptureRef.current = () => {
6256
+ const repaintCleanFrameFromCachedPage = () => {
6083
6257
  pausedRef.current = false;
6258
+ compositePageAndCursorOverlay(ctx, canvas, cachedPage, brandRef.current);
6259
+ };
6260
+ resumeCaptureRef.current = () => {
6261
+ repaintCleanFrameFromCachedPage();
6084
6262
  tickA11yPublish();
6085
6263
  refreshPageAndComposite();
6086
6264
  };
6265
+ clearPausedCaptureRef.current = repaintCleanFrameFromCachedPage;
6087
6266
  return () => {
6088
6267
  cancelled = true;
6089
6268
  didStartRef.current = false;
6090
6269
  freezeCaptureRef.current = null;
6091
6270
  resumeCaptureRef.current = null;
6271
+ clearPausedCaptureRef.current = null;
6092
6272
  if (snapshotTimer)
6093
6273
  clearTimeout(snapshotTimer);
6274
+ if (scrollSettleTimer)
6275
+ clearTimeout(scrollSettleTimer);
6276
+ window.removeEventListener("scroll", refreshOnceScrollSettles, { capture: true });
6094
6277
  clearInterval(a11yPublishTimer);
6095
6278
  if (overlayAnimationFrame !== null)
6096
6279
  cancelAnimationFrame(overlayAnimationFrame);
@@ -6101,7 +6284,7 @@ function DomCapture({ pushOrTapMicMode = false }) {
6101
6284
  for (const track of canvasStream.getTracks())
6102
6285
  track.stop();
6103
6286
  };
6104
- }, [connectionState, localParticipant, pushOrTapMicMode]);
6287
+ }, [connectionState, localParticipant, pushOrTapMicMode, clearPausedCaptureRef]);
6105
6288
  useEffect22(() => {
6106
6289
  if (isHeld) {
6107
6290
  freezeCaptureRef.current?.();