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