supervision 0.1.5 → 0.1.7

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.
Files changed (61) hide show
  1. package/dist/index.d.ts +8 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +641 -131
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js +24 -0
  6. package/dist/mask-preparation.worker.js.map +1 -1
  7. package/dist/media/media-errors.d.ts +32 -0
  8. package/dist/media/media-errors.d.ts.map +1 -0
  9. package/dist/media/media-source-state.d.ts.map +1 -1
  10. package/dist/media/media-stream-media-source.d.ts +31 -0
  11. package/dist/media/media-stream-media-source.d.ts.map +1 -1
  12. package/dist/media/mediabunny-media-source.d.ts.map +1 -1
  13. package/dist/media/presentation-timeline-media-source.d.ts +17 -0
  14. package/dist/media/presentation-timeline-media-source.d.ts.map +1 -0
  15. package/dist/media/static-image-media-source.d.ts.map +1 -1
  16. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  17. package/dist/renderers/media-renderer-state.d.ts.map +1 -1
  18. package/dist/renderers/pixi-annotation-overlay-layer.d.ts +3 -2
  19. package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -1
  20. package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -1
  21. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  22. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  23. package/dist/renderers/pixi-vector-layer.d.ts +2 -1
  24. package/dist/renderers/pixi-vector-layer.d.ts.map +1 -1
  25. package/dist/sessions/media-session-detections.d.ts.map +1 -1
  26. package/dist/sessions/media-session.d.ts +2 -2
  27. package/dist/sessions/media-session.d.ts.map +1 -1
  28. package/dist/tracking.worker.js +24 -0
  29. package/dist/tracking.worker.js.map +1 -1
  30. package/dist/types/media-session.d.ts +57 -1
  31. package/dist/types/media-session.d.ts.map +1 -1
  32. package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
  33. package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -1
  34. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts +22 -0
  35. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts.map +1 -0
  36. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +2 -7
  37. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -1
  38. package/node_modules/supervision-js-core/dist/index.d.ts +5 -3
  39. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  40. package/node_modules/supervision-js-core/dist/index.js +802 -91
  41. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  42. package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts.map +1 -1
  43. package/node_modules/supervision-js-core/dist/interactions/annotation-handles.d.ts +1 -1
  44. package/node_modules/supervision-js-core/dist/interactions/annotation-handles.d.ts.map +1 -1
  45. package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts.map +1 -1
  46. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +113 -3
  47. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  48. package/node_modules/supervision-js-core/dist/types/detections.d.ts +29 -0
  49. package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
  50. package/node_modules/supervision-js-core/dist/types/editing.d.ts +6 -0
  51. package/node_modules/supervision-js-core/dist/types/editing.d.ts.map +1 -1
  52. package/node_modules/supervision-js-core/dist/types/interaction-style.d.ts +2 -0
  53. package/node_modules/supervision-js-core/dist/types/interaction-style.d.ts.map +1 -1
  54. package/node_modules/supervision-js-core/dist/types/interaction.d.ts +9 -1
  55. package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -1
  56. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +31 -0
  57. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  58. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
  59. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts +20 -0
  60. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts.map +1 -0
  61. package/package.json +1 -1
@@ -65,6 +65,9 @@ function copySortedDetectionFrames(detectionFrames) {
65
65
  : undefined,
66
66
  keypoints: detection.keypoints
67
67
  ? {
68
+ boxRelative: detection.keypoints.boxRelative
69
+ ? [...detection.keypoints.boxRelative]
70
+ : undefined,
68
71
  edges: detection.keypoints.edges.map((edge) => [edge[0], edge[1]]),
69
72
  points: detection.keypoints.points.map((point) => ({ ...point })),
70
73
  visibility: detection.keypoints.visibility
@@ -88,6 +91,9 @@ function copySortedDetectionFrames(detectionFrames) {
88
91
  : undefined,
89
92
  rect: detection.rect ? { ...detection.rect } : undefined,
90
93
  })),
94
+ coordinateSpace: frame.coordinateSpace
95
+ ? { ...frame.coordinateSpace }
96
+ : undefined,
91
97
  endTime: frame.endTime,
92
98
  frameIndex: frame.frameIndex,
93
99
  mediaTime: frame.mediaTime,
@@ -129,6 +135,10 @@ function validateDetectionFrames(detectionFrames) {
129
135
  min: 0,
130
136
  });
131
137
  }
138
+ if (frame.coordinateSpace !== undefined) {
139
+ validateNumber(frame.coordinateSpace.width, `frames[${frameOffset}].coordinateSpace.width`, { exclusiveMin: 0 });
140
+ validateNumber(frame.coordinateSpace.height, `frames[${frameOffset}].coordinateSpace.height`, { exclusiveMin: 0 });
141
+ }
132
142
  for (const [detectionOffset, detection] of frame.detections.entries()) {
133
143
  const detectionPath = `frames[${frameOffset}].detections[${detectionOffset}]`;
134
144
  if (detection.confidence !== undefined) {
@@ -888,7 +898,7 @@ function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
888
898
  for (const frame of changedFrames) {
889
899
  framesByIdentity.set(getDetectionFrameIdentity(frame), frame);
890
900
  }
891
- return Array.from(framesByIdentity.values()).sort(compareDetectionFrames);
901
+ return Array.from(framesByIdentity.values()).sort(compareDetectionFrames$1);
892
902
  }
893
903
  function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
894
904
  const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
@@ -899,7 +909,7 @@ function getDetectionFrameIdentity(frame) {
899
909
  ? `time:${frame.mediaTime}`
900
910
  : `index:${frame.frameIndex}`;
901
911
  }
902
- function compareDetectionFrames(left, right) {
912
+ function compareDetectionFrames$1(left, right) {
903
913
  if (left.mediaTime !== right.mediaTime) {
904
914
  return left.mediaTime - right.mediaTime;
905
915
  }
@@ -918,21 +928,118 @@ function createColdDetectionFrameSource(options) {
918
928
  };
919
929
  }
920
930
 
931
+ /**
932
+ * Projects one detection frame from its declared source coordinate space into
933
+ * `target`.
934
+ *
935
+ * Rectangles, polygons, polylines, and keypoints are scaled. Masks are left
936
+ * untouched because a `DetectionMask` already carries the mask-pixel
937
+ * dimensions its `counts` are encoded against; scaling them here would apply
938
+ * the ratio twice. Frames without `coordinateSpace`, with a degenerate space,
939
+ * or already in the target space are returned unchanged so mixed producers can
940
+ * share one source.
941
+ */
942
+ function projectDetectionFrame(frame, target) {
943
+ const space = frame.coordinateSpace;
944
+ if (!space ||
945
+ !isUsableCoordinateSpace(space) ||
946
+ !isUsableCoordinateSpace(target) ||
947
+ (space.width === target.width && space.height === target.height)) {
948
+ return frame;
949
+ }
950
+ const scaleX = target.width / space.width;
951
+ const scaleY = target.height / space.height;
952
+ return {
953
+ ...frame,
954
+ coordinateSpace: { height: target.height, width: target.width },
955
+ detections: frame.detections.map((detection) => projectDetection(detection, scaleX, scaleY)),
956
+ };
957
+ }
958
+ /**
959
+ * Projects every frame that declares a source coordinate space into `target`.
960
+ *
961
+ * Frames without coordinate metadata pass through by reference.
962
+ */
963
+ function projectDetectionFrames(frames, target) {
964
+ let didProject = false;
965
+ const projectedFrames = frames.map((frame) => {
966
+ const projectedFrame = projectDetectionFrame(frame, target);
967
+ didProject ||= projectedFrame !== frame;
968
+ return projectedFrame;
969
+ });
970
+ return didProject ? projectedFrames : frames;
971
+ }
972
+ function projectDetection(detection, scaleX, scaleY) {
973
+ return {
974
+ ...detection,
975
+ ...(detection.rect
976
+ ? { rect: scaleRect(detection.rect, scaleX, scaleY) }
977
+ : {}),
978
+ ...(detection.polygon
979
+ ? { polygon: scalePolygon(detection.polygon, scaleX, scaleY) }
980
+ : {}),
981
+ ...(detection.polyline
982
+ ? { polyline: scalePolyline(detection.polyline, scaleX, scaleY) }
983
+ : {}),
984
+ ...(detection.keypoints
985
+ ? { keypoints: scaleKeypoints(detection.keypoints, scaleX, scaleY) }
986
+ : {}),
987
+ };
988
+ }
989
+ function scaleRect(rect, scaleX, scaleY) {
990
+ return {
991
+ height: rect.height * scaleY,
992
+ width: rect.width * scaleX,
993
+ x: rect.x * scaleX,
994
+ y: rect.y * scaleY,
995
+ };
996
+ }
997
+ function scalePolygon(polygon, scaleX, scaleY) {
998
+ return { points: scalePoints(polygon.points, scaleX, scaleY) };
999
+ }
1000
+ function scalePolyline(polyline, scaleX, scaleY) {
1001
+ return { points: scalePoints(polyline.points, scaleX, scaleY) };
1002
+ }
1003
+ function scaleKeypoints(keypoints, scaleX, scaleY) {
1004
+ return {
1005
+ ...keypoints,
1006
+ points: scalePoints(keypoints.points, scaleX, scaleY),
1007
+ };
1008
+ }
1009
+ function scalePoints(points, scaleX, scaleY) {
1010
+ return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
1011
+ }
1012
+ function isUsableCoordinateSpace(space) {
1013
+ return (Number.isFinite(space.width) &&
1014
+ Number.isFinite(space.height) &&
1015
+ space.width > 0 &&
1016
+ space.height > 0);
1017
+ }
1018
+
921
1019
  function createCompositeDetectionFrameSource(options) {
922
1020
  const sources = normalizeCompositeSources(options.sources);
923
1021
  return {
924
- async loadFrames(startTime, endTime) {
925
- const loadedSources = await Promise.all(sources.map(async (source) => ({
926
- ...source,
927
- frames: copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime)),
928
- })));
1022
+ async loadFrames(startTime, endTime, loadOptions) {
1023
+ const target = loadOptions?.coordinateSpace;
1024
+ const loadedSources = await Promise.all(sources.map(async (source) => {
1025
+ const frames = copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime, loadOptions));
1026
+ return {
1027
+ ...source,
1028
+ // Composition flattens child detections into one frame, which can
1029
+ // only carry one coordinate space. Each child is projected here,
1030
+ // while its own `coordinateSpace` is still attached to its own
1031
+ // detections, so children inferred at different sizes compose
1032
+ // correctly. Masks keep their intrinsic dimensions.
1033
+ frames: target ? projectDetectionFrames(frames, target) : frames,
1034
+ };
1035
+ }));
929
1036
  if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
930
- const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options);
1037
+ const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options, target);
931
1038
  if (nearestFrames) {
932
1039
  return nearestFrames;
933
1040
  }
934
1041
  }
935
- return composeIntervalFrames(loadedSources, startTime, endTime, options);
1042
+ return composeIntervalFrames(loadedSources, startTime, endTime, options, target);
936
1043
  },
937
1044
  async waitForRange(range) {
938
1045
  await Promise.all(sources
@@ -979,7 +1086,7 @@ function normalizeCompositeSources(entries) {
979
1086
  .sort((left, right) => left.order - right.order ||
980
1087
  left.declarationIndex - right.declarationIndex);
981
1088
  }
982
- function composeIntervalFrames(sources, startTime, endTime, options) {
1089
+ function composeIntervalFrames(sources, startTime, endTime, options, coordinateSpace) {
983
1090
  const boundaryTimes = new Set([startTime]);
984
1091
  for (const source of sources) {
985
1092
  for (const frame of source.frames) {
@@ -1001,17 +1108,14 @@ function composeIntervalFrames(sources, startTime, endTime, options) {
1001
1108
  }
1002
1109
  const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
1003
1110
  const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
1004
- const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, {
1005
- ...options,
1006
- selectionMode: DetectionFrameSelectionMode.Interval,
1007
- });
1111
+ const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval }, undefined, coordinateSpace);
1008
1112
  if (frame) {
1009
1113
  frames.push(frame);
1010
1114
  }
1011
1115
  }
1012
1116
  return frames;
1013
1117
  }
1014
- function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
1118
+ function composeNearestFrameIndexFrames(sources, startTime, endTime, options, coordinateSpace) {
1015
1119
  const frameRate = options.frameRate;
1016
1120
  if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
1017
1121
  return null;
@@ -1037,14 +1141,14 @@ function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
1037
1141
  const frame = composeFrameAtTime(sources, mediaTime, Math.min(mediaTime + 1 / frameRate, endTime), {
1038
1142
  ...options,
1039
1143
  selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
1040
- }, frameIndex);
1144
+ }, frameIndex, coordinateSpace);
1041
1145
  if (frame) {
1042
1146
  frames.push(frame);
1043
1147
  }
1044
1148
  }
1045
1149
  return frames;
1046
1150
  }
1047
- function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
1151
+ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex, coordinateSpace) {
1048
1152
  const detections = [];
1049
1153
  const activeFrameIndexes = [];
1050
1154
  for (const source of sources) {
@@ -1070,6 +1174,9 @@ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
1070
1174
  endTime,
1071
1175
  frameIndex: frameIndex ?? resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
1072
1176
  mediaTime,
1177
+ // Children were projected before composition, so a composed frame is
1178
+ // already in the coordinate space the renderer presents.
1179
+ ...(coordinateSpace ? { coordinateSpace } : {}),
1073
1180
  };
1074
1181
  }
1075
1182
  function copyDetectionWithSource(detection, sourceId, sourceDetectionIndex) {
@@ -1284,6 +1391,16 @@ function createMemoryColdDetectionFrameStore() {
1284
1391
  detectionFrameOverlapsRange(frame, startTime, endTime));
1285
1392
  return copySortedDetectionFrames(frames);
1286
1393
  },
1394
+ async pruneFrames(options) {
1395
+ assertActive();
1396
+ assertRetentionFloor(options.startTime);
1397
+ const dataset = datasets.get(options.datasetId);
1398
+ if (!dataset) {
1399
+ return createWriteSummary(options.datasetId, createDataset(DEFAULT_CHUNK_DURATION_SECONDS));
1400
+ }
1401
+ pruneDatasetBefore(dataset, options.startTime);
1402
+ return createWriteSummary(options.datasetId, dataset);
1403
+ },
1287
1404
  async clearDataset(datasetId) {
1288
1405
  assertActive();
1289
1406
  datasets.delete(datasetId);
@@ -1322,7 +1439,7 @@ function upsertFrames(dataset, frames) {
1322
1439
  dataset.framesByKey.set(frameKey, { chunkIndexes, frame });
1323
1440
  dataset.detectionCount += frame.detections.length;
1324
1441
  dataset.startTime = Math.min(dataset.startTime ?? frame.mediaTime, frame.mediaTime);
1325
- dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
1442
+ dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
1326
1443
  for (const chunkIndex of chunkIndexes) {
1327
1444
  const frameKeys = dataset.frameKeysByChunk.get(chunkIndex) ?? new Set();
1328
1445
  frameKeys.add(frameKey);
@@ -1344,18 +1461,81 @@ function removeStoredFrame(dataset, frameKey, storedFrame) {
1344
1461
  }
1345
1462
  }
1346
1463
  }
1464
+ /**
1465
+ * Drops frames that end before `startTime`.
1466
+ *
1467
+ * Only chunks at or below the retention boundary are visited, and the new
1468
+ * start bound is read from the lowest surviving chunk, so eviction costs work
1469
+ * proportional to the evicted range rather than to retained history.
1470
+ */
1471
+ function pruneDatasetBefore(dataset, startTime) {
1472
+ const boundaryChunkIndex = getChunkIndex(Math.max(0, startTime), dataset.chunkDurationSeconds);
1473
+ let didRemoveFrame = false;
1474
+ for (const chunkIndex of Array.from(dataset.frameKeysByChunk.keys())) {
1475
+ if (chunkIndex > boundaryChunkIndex) {
1476
+ continue;
1477
+ }
1478
+ for (const frameKey of Array.from(dataset.frameKeysByChunk.get(chunkIndex) ?? [])) {
1479
+ const storedFrame = dataset.framesByKey.get(frameKey);
1480
+ if (!storedFrame || isFrameRetainedFrom(storedFrame.frame, startTime)) {
1481
+ continue;
1482
+ }
1483
+ removeStoredFrame(dataset, frameKey, storedFrame);
1484
+ didRemoveFrame = true;
1485
+ }
1486
+ }
1487
+ if (!didRemoveFrame) {
1488
+ return;
1489
+ }
1490
+ if (dataset.framesByKey.size === 0) {
1491
+ dataset.startTime = null;
1492
+ dataset.endTime = null;
1493
+ return;
1494
+ }
1495
+ // Every frame is registered in the chunk its `mediaTime` falls in, so the
1496
+ // earliest retained frame lives in the lowest remaining chunk. Removing
1497
+ // frames can only raise the start bound; `endTime` is unchanged because only
1498
+ // frames ending before `startTime` were dropped.
1499
+ dataset.startTime = getEarliestChunkStartTime(dataset);
1500
+ }
1501
+ /**
1502
+ * Mirrors `detectionFrameOverlapsRange` so pruning keeps exactly the frames a
1503
+ * load from `startTime` would return.
1504
+ */
1505
+ function isFrameRetainedFrom(frame, startTime) {
1506
+ return frame.endTime === undefined
1507
+ ? frame.mediaTime >= startTime
1508
+ : frame.endTime > startTime;
1509
+ }
1510
+ function getEarliestChunkStartTime(dataset) {
1511
+ let earliestChunkIndex = null;
1512
+ for (const chunkIndex of dataset.frameKeysByChunk.keys()) {
1513
+ if (earliestChunkIndex === null || chunkIndex < earliestChunkIndex) {
1514
+ earliestChunkIndex = chunkIndex;
1515
+ }
1516
+ }
1517
+ let startTime = null;
1518
+ for (const frameKey of dataset.frameKeysByChunk.get(earliestChunkIndex ?? 0) ?? []) {
1519
+ const storedFrame = dataset.framesByKey.get(frameKey);
1520
+ if (!storedFrame) {
1521
+ continue;
1522
+ }
1523
+ startTime = Math.min(startTime ?? storedFrame.frame.mediaTime, storedFrame.frame.mediaTime);
1524
+ }
1525
+ return startTime;
1526
+ }
1347
1527
  function replacementCanShrinkBounds(dataset, previousFrame, nextFrame) {
1348
1528
  return ((dataset.startTime === previousFrame.mediaTime &&
1349
1529
  nextFrame.mediaTime > previousFrame.mediaTime) ||
1350
- (dataset.endTime === getFrameEndTime(previousFrame) &&
1351
- getFrameEndTime(nextFrame) < getFrameEndTime(previousFrame)));
1530
+ (dataset.endTime === getFrameEndTime$1(previousFrame) &&
1531
+ getFrameEndTime$1(nextFrame) < getFrameEndTime$1(previousFrame)));
1352
1532
  }
1353
1533
  function recalculateBounds(dataset) {
1354
1534
  let startTime = null;
1355
1535
  let endTime = null;
1356
1536
  for (const { frame } of dataset.framesByKey.values()) {
1357
1537
  startTime = Math.min(startTime ?? frame.mediaTime, frame.mediaTime);
1358
- endTime = Math.max(endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
1538
+ endTime = Math.max(endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
1359
1539
  }
1360
1540
  dataset.startTime = startTime;
1361
1541
  dataset.endTime = endTime;
@@ -1387,10 +1567,10 @@ function createWriteSummary(datasetId, dataset) {
1387
1567
  }
1388
1568
  function getFrameChunkIndexes(frame, chunkDurationSeconds) {
1389
1569
  const startIndex = getChunkIndex(frame.mediaTime, chunkDurationSeconds);
1390
- const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime(frame) / chunkDurationSeconds) - 1);
1570
+ const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime$1(frame) / chunkDurationSeconds) - 1);
1391
1571
  return Array.from({ length: endIndex - startIndex + 1 }, (_, offset) => startIndex + offset);
1392
1572
  }
1393
- function getFrameEndTime(frame) {
1573
+ function getFrameEndTime$1(frame) {
1394
1574
  return frame.endTime ?? frame.mediaTime;
1395
1575
  }
1396
1576
  function getChunkIndex(mediaTime, chunkDurationSeconds) {
@@ -1401,11 +1581,87 @@ function getDetectionFrameDedupeKey(frame) {
1401
1581
  ? `time:${frame.mediaTime}`
1402
1582
  : `index:${frame.frameIndex}`;
1403
1583
  }
1584
+ /**
1585
+ * Rejects a retention floor that could not describe a real media time.
1586
+ *
1587
+ * Pruning mutates retained history in place, and `NaN` compares false against
1588
+ * every retained frame, so an unvalidated floor would silently erase a dataset
1589
+ * instead of evicting a bounded range.
1590
+ */
1591
+ function assertRetentionFloor(startTime) {
1592
+ if (!Number.isFinite(startTime) || startTime < 0) {
1593
+ throw new RangeError("pruneFrames requires a finite, non-negative startTime.");
1594
+ }
1595
+ }
1596
+
1597
+ /**
1598
+ * Wraps a detection source so every loaded frame that declares a source
1599
+ * coordinate space is projected into the target space.
1600
+ *
1601
+ * The target is resolved per load because media dimensions are only known once
1602
+ * the media has opened, while sources are composed before that. Frames without
1603
+ * `coordinateSpace`, and loads made before a target exists, pass through by
1604
+ * reference, so a producer already emitting media-pixel geometry pays nothing.
1605
+ *
1606
+ * The target is also handed to the wrapped source through
1607
+ * `DetectionFrameLoadOptions`. A source that flattens child frames, such as a
1608
+ * composite source, needs it to project each child while that child's own
1609
+ * `coordinateSpace` is still attached; projecting the flattened result again
1610
+ * here is then a no-op.
1611
+ *
1612
+ * Optional source hooks are forwarded only when the wrapped source implements
1613
+ * them, so capability detection keeps working through the wrapper.
1614
+ */
1615
+ function createProjectedDetectionFrameSource(source, resolveTarget) {
1616
+ const { getAvailableRanges, getChangesSince, getVersion, waitForRange } = source;
1617
+ return {
1618
+ async loadFrames(startTime, endTime, options) {
1619
+ const target = resolveTarget();
1620
+ const frames = await source.loadFrames(startTime, endTime, {
1621
+ ...options,
1622
+ ...(target ? { coordinateSpace: target } : {}),
1623
+ });
1624
+ return target ? projectDetectionFrames(frames, target) : frames;
1625
+ },
1626
+ ...(waitForRange
1627
+ ? {
1628
+ waitForRange: (range) => waitForRange.call(source, range),
1629
+ }
1630
+ : {}),
1631
+ ...(getAvailableRanges
1632
+ ? { getAvailableRanges: () => getAvailableRanges.call(source) }
1633
+ : {}),
1634
+ ...(getVersion
1635
+ ? {
1636
+ getVersion: (range) => getVersion.call(source, range),
1637
+ }
1638
+ : {}),
1639
+ ...(getChangesSince
1640
+ ? {
1641
+ getChangesSince: (version, ranges) => getChangesSince.call(source, version, ranges),
1642
+ }
1643
+ : {}),
1644
+ ...(source.destroy ? { destroy: () => source.destroy?.() } : {}),
1645
+ };
1646
+ }
1404
1647
 
1405
1648
  const RANGE_EPSILON_SECONDS = 1e-6;
1406
1649
  const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
1650
+ const DEFAULT_LIVE_HOLD_SECONDS = 60;
1651
+ const NO_COVERAGE_INVALIDATION = { ranges: [] };
1407
1652
  function createWritableDetectionFrameSource(options) {
1653
+ const liveHoldSeconds = resolveLiveHoldSeconds(options.live?.holdSeconds);
1408
1654
  let summary = null;
1655
+ let latestFrame = null;
1656
+ let heldLiveFrame = null;
1657
+ // Newest live `mediaTime` accepted so far. It survives retention so a late
1658
+ // result cannot reopen coverage the source already moved past.
1659
+ let liveFrontierTime = null;
1660
+ // Newest media time actually covered by producer-declared data. Live frames
1661
+ // are written with a synthetic hold end, so the summary end time cannot be
1662
+ // used to place the retention window without pushing it into the future.
1663
+ let coverageFrontierTime = null;
1664
+ let writeQueue = Promise.resolve();
1409
1665
  let version = 0;
1410
1666
  let allRangeVersion = 0;
1411
1667
  let journalFloorVersion = 0;
@@ -1418,13 +1674,25 @@ function createWritableDetectionFrameSource(options) {
1418
1674
  datasetId: options.datasetId,
1419
1675
  frames,
1420
1676
  });
1421
- const recordRangeWrite = (nextSummary, changedSourceRanges) => {
1677
+ const recordRangeWrite = (nextSummary, changedSourceRanges, invalidation, prunedRange) => {
1422
1678
  summary = nextSummary;
1423
1679
  version += 1;
1424
1680
  for (const changedRange of changedSourceRanges) {
1425
1681
  changedRanges.push({ ...changedRange, version });
1426
1682
  recordAvailableRange(changedRange, availableRanges);
1427
1683
  }
1684
+ for (const invalidatedRange of invalidation.ranges) {
1685
+ // Coverage a write took away is journaled but never made available: a
1686
+ // consumer parked there has to redraw, and nothing new can be loaded.
1687
+ changedRanges.push({ ...invalidatedRange, version });
1688
+ }
1689
+ if (prunedRange) {
1690
+ // Evicting old history is a change to that range only. Recording it
1691
+ // alongside the append keeps a buffered timeline patching two small
1692
+ // ranges instead of invalidating and rebuilding everything it retains.
1693
+ changedRanges.push({ ...prunedRange, version });
1694
+ dropAvailableRangesBefore(prunedRange.endTime, availableRanges);
1695
+ }
1428
1696
  compactChangedRangeJournal();
1429
1697
  resolveCoveredWaiters();
1430
1698
  return nextSummary;
@@ -1449,38 +1717,113 @@ function createWritableDetectionFrameSource(options) {
1449
1717
  datasetId: options.datasetId,
1450
1718
  async appendFrames(frames) {
1451
1719
  assertActive();
1452
- const nextSummary = await options.store.appendFrames(writeOptions(frames));
1720
+ return enqueueWrite(async () => {
1721
+ const nextSummary = await options.store.appendFrames(writeOptions(frames));
1722
+ assertActive();
1723
+ const changedSourceRanges = getDetectionFrameRanges(frames);
1724
+ const invalidation = getInvalidatedCoverage(frames, changedSourceRanges);
1725
+ recordLatestFrame(frames);
1726
+ recordCoverageFrontierFromFrames(frames);
1727
+ return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
1728
+ });
1729
+ },
1730
+ async appendLiveFrame(frame) {
1453
1731
  assertActive();
1454
- const changedSourceRanges = getDetectionFrameRanges(frames);
1455
- const retainedSummary = await applyRetention(nextSummary);
1732
+ return enqueueWrite(async () => {
1733
+ if (isStaleLiveFrame(frame)) {
1734
+ // A result older than the live frontier no longer describes what is
1735
+ // on screen. Writing it would reopen coverage the source already
1736
+ // closed, so the newest causal frame simply wins.
1737
+ return summary ? { ...summary } : createEmptyWriteSummary();
1738
+ }
1739
+ const framesToWrite = createLiveFrameWrite(frame);
1740
+ const nextSummary = await options.store.appendFrames(writeOptions(framesToWrite));
1741
+ assertActive();
1742
+ const changedSourceRanges = getDetectionFrameRanges(framesToWrite);
1743
+ const invalidation = getInvalidatedCoverage(framesToWrite, changedSourceRanges);
1744
+ heldLiveFrame = framesToWrite.at(-1) ?? null;
1745
+ liveFrontierTime = Math.max(liveFrontierTime ?? frame.mediaTime, frame.mediaTime);
1746
+ recordLatestFrame(framesToWrite);
1747
+ // The hold end is a placeholder for "still current", not covered data.
1748
+ recordCoverageFrontier(getFrameEndTime(frame));
1749
+ return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
1750
+ });
1751
+ },
1752
+ async finalizeCoverage(endTime) {
1456
1753
  assertActive();
1457
- if (retainedSummary !== nextSummary) {
1458
- return recordAllRangesWrite(retainedSummary);
1754
+ if (!Number.isFinite(endTime)) {
1755
+ throw new RangeError("finalizeCoverage requires a finite endTime.");
1459
1756
  }
1460
- if (changedSourceRanges.length === 0) {
1461
- summary = nextSummary;
1462
- return nextSummary;
1463
- }
1464
- return recordRangeWrite(nextSummary, changedSourceRanges);
1757
+ return enqueueWrite(async () => {
1758
+ const frameToFinalize = latestFrame;
1759
+ if (!frameToFinalize ||
1760
+ // A frame that starts at or after the end of media cannot describe a
1761
+ // non-empty terminal interval.
1762
+ frameToFinalize.mediaTime >= endTime ||
1763
+ getFrameEndTime(frameToFinalize) === endTime) {
1764
+ return summary ? { ...summary } : null;
1765
+ }
1766
+ const previousEndTime = getFrameEndTime(frameToFinalize);
1767
+ const finalizedFrame = { ...frameToFinalize, endTime };
1768
+ const nextSummary = await options.store.appendFrames(writeOptions([finalizedFrame]));
1769
+ assertActive();
1770
+ const changedSourceRanges = [
1771
+ {
1772
+ endTime: Math.max(endTime, previousEndTime),
1773
+ startTime: frameToFinalize.mediaTime,
1774
+ },
1775
+ ];
1776
+ const invalidation = getInvalidatedCoverage([finalizedFrame], changedSourceRanges);
1777
+ latestFrame = finalizedFrame;
1778
+ if (heldLiveFrame &&
1779
+ haveSameDetectionFrameIdentity(heldLiveFrame, finalizedFrame)) {
1780
+ heldLiveFrame = finalizedFrame;
1781
+ }
1782
+ coverageFrontierTime = endTime;
1783
+ const finalSummary = await retainAndRecord(nextSummary, changedSourceRanges,
1784
+ // Finalizing usually closes at the end of media, where the tail an
1785
+ // open-ended frame gives up is unreachable. The caller may finalize
1786
+ // earlier, though, so report it rather than assume.
1787
+ invalidation);
1788
+ // Closing a held live frame shortens coverage. Reported availability
1789
+ // has to shrink with it, or a playback gate would still believe the
1790
+ // source covers time past the end of media.
1791
+ clipAvailableRangesAfter(endTime);
1792
+ return finalSummary;
1793
+ });
1465
1794
  },
1466
1795
  async replaceFrames(frames) {
1467
1796
  assertActive();
1468
- const nextSummary = await options.store.putFrames(writeOptions(frames));
1469
- assertActive();
1470
- const retainedSummary = await applyRetention(nextSummary);
1471
- assertActive();
1472
- return recordAllRangesWrite(retainedSummary);
1797
+ return enqueueWrite(async () => {
1798
+ const nextSummary = await options.store.putFrames(writeOptions(frames));
1799
+ assertActive();
1800
+ latestFrame = null;
1801
+ heldLiveFrame = null;
1802
+ liveFrontierTime = null;
1803
+ coverageFrontierTime = null;
1804
+ recordLatestFrame(frames);
1805
+ recordCoverageFrontierFromFrames(frames);
1806
+ const retention = await applyRetention(nextSummary);
1807
+ assertActive();
1808
+ return recordAllRangesWrite(retention.kind === "unchanged" ? nextSummary : retention.summary);
1809
+ });
1473
1810
  },
1474
1811
  async clear() {
1475
1812
  assertActive();
1476
- await options.store.clearDataset(options.datasetId);
1477
- assertActive();
1478
- summary = null;
1479
- version += 1;
1480
- allRangeVersion = version;
1481
- journalFloorVersion = version;
1482
- changedRanges.length = 0;
1483
- availableRanges.length = 0;
1813
+ await enqueueWrite(async () => {
1814
+ await options.store.clearDataset(options.datasetId);
1815
+ assertActive();
1816
+ summary = null;
1817
+ latestFrame = null;
1818
+ heldLiveFrame = null;
1819
+ liveFrontierTime = null;
1820
+ coverageFrontierTime = null;
1821
+ version += 1;
1822
+ allRangeVersion = version;
1823
+ journalFloorVersion = version;
1824
+ changedRanges.length = 0;
1825
+ availableRanges.length = 0;
1826
+ });
1484
1827
  },
1485
1828
  async loadFrames(startTime, endTime) {
1486
1829
  assertActive();
@@ -1513,14 +1856,10 @@ function createWritableDetectionFrameSource(options) {
1513
1856
  if (!range) {
1514
1857
  return version;
1515
1858
  }
1516
- return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
1517
- ? Math.max(rangeVersion, changedRange.version)
1518
- : rangeVersion, Math.max(allRangeVersion, journalFloorVersion));
1859
+ return getRangeVersion(range);
1519
1860
  },
1520
1861
  getChangesSince(previousVersion, ranges) {
1521
- const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion, changedRanges.reduce((changedVersion, changedRange) => rangesOverlap(range, changedRange)
1522
- ? Math.max(changedVersion, changedRange.version)
1523
- : changedVersion, Math.max(allRangeVersion, journalFloorVersion))), Math.max(allRangeVersion, journalFloorVersion));
1862
+ const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion, getRangeVersion(range)), getReloadFloorVersion());
1524
1863
  if (relevantVersion <= previousVersion) {
1525
1864
  return {
1526
1865
  ranges: [],
@@ -1528,18 +1867,34 @@ function createWritableDetectionFrameSource(options) {
1528
1867
  version: relevantVersion,
1529
1868
  };
1530
1869
  }
1531
- if (previousVersion < allRangeVersion ||
1532
- previousVersion < journalFloorVersion) {
1870
+ if (previousVersion < getReloadFloorVersion()) {
1533
1871
  return {
1534
1872
  ranges: [],
1535
1873
  requiresReload: true,
1536
1874
  version: relevantVersion,
1537
1875
  };
1538
1876
  }
1877
+ // Clipped to the requested ranges: a consumer gets back exactly the part
1878
+ // of the change it asked about, and an unbounded invalidation stays a
1879
+ // bounded, loadable answer.
1539
1880
  const changedSourceRanges = changedRanges
1540
- .filter((changedRange) => changedRange.version > previousVersion &&
1541
- ranges.some((range) => rangesOverlap(range, changedRange)))
1542
- .map(({ endTime, startTime }) => ({ endTime, startTime }));
1881
+ .filter((changedRange) => changedRange.version > previousVersion)
1882
+ .flatMap((changedRange) => ranges
1883
+ .filter((range) => rangesOverlap(range, changedRange))
1884
+ .map((range) => {
1885
+ const startTime = Math.max(range.startTime, changedRange.startTime);
1886
+ const endTime = Math.min(range.endTime, changedRange.endTime);
1887
+ return {
1888
+ // An unbounded ask against an unbounded invalidation would
1889
+ // otherwise hand back an infinite range, and a consumer would
1890
+ // take it straight to `loadFrames`. Nothing past the coverage
1891
+ // the source knows about can be loaded.
1892
+ endTime: Number.isFinite(endTime)
1893
+ ? endTime
1894
+ : Math.max(startTime, getKnownEndTime()),
1895
+ startTime,
1896
+ };
1897
+ }));
1543
1898
  return {
1544
1899
  ranges: mergeRanges(changedSourceRanges),
1545
1900
  requiresReload: false,
@@ -1563,12 +1918,112 @@ function createWritableDetectionFrameSource(options) {
1563
1918
  throw createDestroyedError();
1564
1919
  }
1565
1920
  }
1921
+ /**
1922
+ * Oldest version the journal can still describe incrementally. Anything
1923
+ * older has to reload, because replacement, clearing, or journal compaction
1924
+ * dropped the intervening changes.
1925
+ */
1926
+ function getReloadFloorVersion() {
1927
+ return Math.max(allRangeVersion, journalFloorVersion);
1928
+ }
1929
+ /**
1930
+ * Latest media time the source can actually serve frames for.
1931
+ */
1932
+ function getKnownEndTime() {
1933
+ return Math.max(coverageFrontierTime ?? 0, summary?.endTime ?? 0);
1934
+ }
1935
+ function getRangeVersion(range) {
1936
+ return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
1937
+ ? Math.max(rangeVersion, changedRange.version)
1938
+ : rangeVersion, getReloadFloorVersion());
1939
+ }
1940
+ /**
1941
+ * Runs mutating writes one at a time.
1942
+ *
1943
+ * Every write reads the held live frame, the latest frame, and the coverage
1944
+ * frontier before it awaits storage. Without serialization two concurrent
1945
+ * live appends would both observe the pre-write state and both stay open,
1946
+ * which is exactly the stale overlay live semantics exist to prevent.
1947
+ */
1948
+ function enqueueWrite(run) {
1949
+ const result = writeQueue.then(() => {
1950
+ assertActive();
1951
+ return run();
1952
+ });
1953
+ writeQueue = result.then(() => undefined, () => undefined);
1954
+ return result;
1955
+ }
1956
+ function isStaleLiveFrame(frame) {
1957
+ if (liveFrontierTime === null || frame.mediaTime > liveFrontierTime) {
1958
+ return false;
1959
+ }
1960
+ // A frame at the frontier that shares the held frame's identity is a
1961
+ // revision of the current result, not a late duplicate of an older one.
1962
+ return !(heldLiveFrame && haveSameDetectionFrameIdentity(heldLiveFrame, frame));
1963
+ }
1964
+ function recordCoverageFrontier(endTime) {
1965
+ coverageFrontierTime =
1966
+ coverageFrontierTime === null
1967
+ ? endTime
1968
+ : Math.max(coverageFrontierTime, endTime);
1969
+ }
1970
+ function recordCoverageFrontierFromFrames(frames) {
1971
+ for (const frame of frames) {
1972
+ recordCoverageFrontier(getFrameEndTime(frame));
1973
+ }
1974
+ }
1975
+ function clipAvailableRangesAfter(endTime) {
1976
+ let writeIndex = 0;
1977
+ for (const range of availableRanges) {
1978
+ if (range.startTime >= endTime) {
1979
+ continue;
1980
+ }
1981
+ availableRanges[writeIndex] = {
1982
+ endTime: Math.min(range.endTime, endTime),
1983
+ startTime: range.startTime,
1984
+ };
1985
+ writeIndex += 1;
1986
+ }
1987
+ availableRanges.length = writeIndex;
1988
+ }
1989
+ function createEmptyWriteSummary() {
1990
+ return {
1991
+ chunkCount: 0,
1992
+ chunkDurationSeconds: options.chunkDurationSeconds ?? 1,
1993
+ datasetId: options.datasetId,
1994
+ detectionCount: 0,
1995
+ endTime: null,
1996
+ frameCount: 0,
1997
+ startTime: null,
1998
+ };
1999
+ }
2000
+ /**
2001
+ * Applies retention and records the result as an incremental change when the
2002
+ * store can prune in place, or as a full rewrite when it cannot.
2003
+ */
2004
+ async function retainAndRecord(nextSummary, changedSourceRanges, invalidation = NO_COVERAGE_INVALIDATION) {
2005
+ const retention = await applyRetention(nextSummary);
2006
+ assertActive();
2007
+ if (retention.kind === "rewritten") {
2008
+ return recordAllRangesWrite(retention.summary);
2009
+ }
2010
+ if (retention.kind === "unchanged") {
2011
+ if (changedSourceRanges.length === 0 &&
2012
+ invalidation.ranges.length === 0) {
2013
+ summary = nextSummary;
2014
+ return nextSummary;
2015
+ }
2016
+ return recordRangeWrite(nextSummary, changedSourceRanges, invalidation);
2017
+ }
2018
+ forgetFramesBefore(retention.prunedRange.endTime);
2019
+ return recordRangeWrite(retention.summary, changedSourceRanges, invalidation, retention.prunedRange);
2020
+ }
1566
2021
  async function applyRetention(nextSummary) {
1567
2022
  const retention = options.retention;
1568
2023
  if (retention === undefined ||
1569
2024
  !shouldApplyWindowRetention(retention) ||
1570
2025
  nextSummary.endTime === null) {
1571
- return nextSummary;
2026
+ return { kind: "unchanged" };
1572
2027
  }
1573
2028
  const retentionWindowSeconds = retention.windowSeconds;
1574
2029
  if (retentionWindowSeconds === undefined ||
@@ -1576,17 +2031,119 @@ function createWritableDetectionFrameSource(options) {
1576
2031
  retentionWindowSeconds <= 0) {
1577
2032
  throw new Error("retention.windowSeconds must be greater than 0.");
1578
2033
  }
1579
- const retentionStartTime = Math.max(0, nextSummary.endTime - retentionWindowSeconds);
2034
+ // Place the window against real producer coverage. A live frame held open
2035
+ // for `holdSeconds` reports a summary end far in the future, and anchoring
2036
+ // eviction there would evict everything the producer just wrote.
2037
+ const retentionStartTime = Math.max(0, Math.min(coverageFrontierTime ?? nextSummary.endTime, nextSummary.endTime) - retentionWindowSeconds);
1580
2038
  if (nextSummary.startTime !== null &&
1581
2039
  nextSummary.startTime + RANGE_EPSILON_SECONDS >= retentionStartTime) {
1582
- return nextSummary;
2040
+ return { kind: "unchanged" };
2041
+ }
2042
+ if (options.store.pruneFrames) {
2043
+ return {
2044
+ kind: "pruned",
2045
+ prunedRange: {
2046
+ endTime: retentionStartTime,
2047
+ startTime: nextSummary.startTime ?? 0,
2048
+ },
2049
+ summary: await options.store.pruneFrames({
2050
+ datasetId: options.datasetId,
2051
+ startTime: retentionStartTime,
2052
+ }),
2053
+ };
1583
2054
  }
2055
+ // Stores without in-place pruning still get a correct retention window,
2056
+ // at the cost of reloading and rewriting everything they keep.
1584
2057
  const retainedFrames = await options.store.loadFrames({
1585
2058
  datasetId: options.datasetId,
1586
2059
  endTime: nextSummary.endTime,
1587
2060
  startTime: retentionStartTime,
1588
2061
  });
1589
- return options.store.putFrames(writeOptions(retainedFrames));
2062
+ return {
2063
+ kind: "rewritten",
2064
+ summary: await options.store.putFrames(writeOptions(retainedFrames)),
2065
+ };
2066
+ }
2067
+ /**
2068
+ * Builds the one or two frames a live append writes: the newest frame held
2069
+ * open, plus the previously held frame closed where the newest begins.
2070
+ */
2071
+ function createLiveFrameWrite(frame) {
2072
+ const openFrame = {
2073
+ ...frame,
2074
+ endTime: Math.max(frame.endTime ?? frame.mediaTime, frame.mediaTime + liveHoldSeconds),
2075
+ };
2076
+ const heldFrame = heldLiveFrame;
2077
+ // Stale results are rejected before this point, so a held frame that is
2078
+ // neither the same frame nor already closed always ends strictly later.
2079
+ if (!heldFrame ||
2080
+ haveSameDetectionFrameIdentity(heldFrame, openFrame) ||
2081
+ getFrameEndTime(heldFrame) <= frame.mediaTime) {
2082
+ return [openFrame];
2083
+ }
2084
+ return [{ ...heldFrame, endTime: frame.mediaTime }, openFrame];
2085
+ }
2086
+ function recordLatestFrame(frames) {
2087
+ for (const frame of frames) {
2088
+ if (!latestFrame ||
2089
+ compareDetectionFrames(frame, latestFrame) > 0 ||
2090
+ // A write that shares the tracked frame's identity replaces its stored
2091
+ // record. Keeping the superseded contents here would let a later
2092
+ // `finalizeCoverage` write them back over the revision.
2093
+ haveSameDetectionFrameIdentity(frame, latestFrame)) {
2094
+ latestFrame = frame;
2095
+ }
2096
+ }
2097
+ }
2098
+ /**
2099
+ * Coverage a write takes away from a frame the source already wrote.
2100
+ *
2101
+ * The journal describes a write by the interval it covers, which is enough
2102
+ * while writes only add. Rewriting a frame with a shorter interval, or at a
2103
+ * different time, instead stops covering times the new interval never
2104
+ * mentions, and a consumer parked in one of them would keep showing a frame
2105
+ * the source no longer selects.
2106
+ *
2107
+ * A frame without an `endTime` was selected with no upper bound, so what it
2108
+ * gives up is genuinely unbounded and is journaled that way. The journal
2109
+ * stays range-scoped either way: a consumer asking about times the write
2110
+ * never touched still hears that nothing changed.
2111
+ */
2112
+ function getInvalidatedCoverage(frames, coveredRanges) {
2113
+ const invalidatedRanges = [];
2114
+ for (const frame of frames) {
2115
+ for (const trackedFrame of [latestFrame, heldLiveFrame]) {
2116
+ if (!trackedFrame ||
2117
+ !haveSameDetectionFrameIdentity(trackedFrame, frame)) {
2118
+ continue;
2119
+ }
2120
+ const previousRange = {
2121
+ endTime: trackedFrame.endTime ?? Number.POSITIVE_INFINITY,
2122
+ startTime: trackedFrame.mediaTime,
2123
+ };
2124
+ const nextEndTime = frame.endTime ?? Number.POSITIVE_INFINITY;
2125
+ if (trackedFrame.mediaTime >= frame.mediaTime &&
2126
+ previousRange.endTime <= nextEndTime) {
2127
+ continue;
2128
+ }
2129
+ // A live append closes the held frame and reopens it further out in the
2130
+ // same write, so the write's own range already describes the tail. Only
2131
+ // coverage nothing else reported is worth a second journal entry.
2132
+ if (isRangeCovered(previousRange, coveredRanges)) {
2133
+ continue;
2134
+ }
2135
+ invalidatedRanges.push(previousRange);
2136
+ }
2137
+ }
2138
+ return { ranges: mergeRanges(invalidatedRanges) };
2139
+ }
2140
+ function forgetFramesBefore(startTime) {
2141
+ if (latestFrame && getFrameEndTime(latestFrame) < startTime) {
2142
+ latestFrame = null;
2143
+ }
2144
+ if (heldLiveFrame && getFrameEndTime(heldLiveFrame) < startTime) {
2145
+ heldLiveFrame = null;
2146
+ }
1590
2147
  }
1591
2148
  function resolveCoveredWaiters() {
1592
2149
  for (let index = waiters.length - 1; index >= 0; index -= 1) {
@@ -1610,6 +2167,43 @@ function createWritableDetectionFrameSource(options) {
1610
2167
  function createDestroyedError() {
1611
2168
  return new Error("Detection frame source has been destroyed.");
1612
2169
  }
2170
+ function resolveLiveHoldSeconds(holdSeconds) {
2171
+ if (holdSeconds === undefined) {
2172
+ return DEFAULT_LIVE_HOLD_SECONDS;
2173
+ }
2174
+ if (!Number.isFinite(holdSeconds) || holdSeconds <= 0) {
2175
+ throw new RangeError("live.holdSeconds must be greater than 0.");
2176
+ }
2177
+ return holdSeconds;
2178
+ }
2179
+ function getFrameEndTime(frame) {
2180
+ return frame.endTime ?? frame.mediaTime;
2181
+ }
2182
+ function haveSameDetectionFrameIdentity(left, right) {
2183
+ return left.frameIndex === undefined || right.frameIndex === undefined
2184
+ ? left.frameIndex === right.frameIndex && left.mediaTime === right.mediaTime
2185
+ : left.frameIndex === right.frameIndex;
2186
+ }
2187
+ function compareDetectionFrames(left, right) {
2188
+ if (left.mediaTime !== right.mediaTime) {
2189
+ return left.mediaTime - right.mediaTime;
2190
+ }
2191
+ return (left.frameIndex ?? 0) - (right.frameIndex ?? 0);
2192
+ }
2193
+ function dropAvailableRangesBefore(startTime, availableRanges) {
2194
+ let writeIndex = 0;
2195
+ for (const range of availableRanges) {
2196
+ if (range.endTime <= startTime) {
2197
+ continue;
2198
+ }
2199
+ availableRanges[writeIndex] = {
2200
+ endTime: range.endTime,
2201
+ startTime: Math.max(range.startTime, startTime),
2202
+ };
2203
+ writeIndex += 1;
2204
+ }
2205
+ availableRanges.length = writeIndex;
2206
+ }
1613
2207
  function shouldApplyWindowRetention(retention) {
1614
2208
  return (retention?.mode === DetectionFrameRetentionMode.PersistWindow ||
1615
2209
  retention?.mode === DetectionFrameRetentionMode.MemoryOnly);
@@ -3359,10 +3953,18 @@ function pickDetectionAtPoint(frame, point, options = {}) {
3359
3953
  if (!frame) {
3360
3954
  return null;
3361
3955
  }
3362
- const padding = Math.max(0, options.padding ?? 0);
3363
- const polylinePadding = Math.max(0, options.polylinePadding ?? 6);
3364
- const keypointPadding = Math.max(0, options.keypointPadding ?? 10);
3365
- const edgePadding = Math.max(0, options.edgePadding ?? 8);
3956
+ // Tolerances are screen-space sizes; a zoomed-out viewport maps more media
3957
+ // units per screen pixel, so a fixed media-space padding would shrink the
3958
+ // hit target below the drawn marker.
3959
+ const viewportScale = options.viewportScale !== undefined &&
3960
+ Number.isFinite(options.viewportScale) &&
3961
+ options.viewportScale > 0
3962
+ ? options.viewportScale
3963
+ : 1;
3964
+ const padding = Math.max(0, options.padding ?? 0) / viewportScale;
3965
+ const polylinePadding = Math.max(0, options.polylinePadding ?? 6) / viewportScale;
3966
+ const keypointPadding = Math.max(0, options.keypointPadding ?? 10) / viewportScale;
3967
+ const edgePadding = Math.max(0, options.edgePadding ?? 8) / viewportScale;
3366
3968
  const candidates = [];
3367
3969
  for (let detectionIndex = 0; detectionIndex < frame.detections.length; detectionIndex += 1) {
3368
3970
  const detection = frame.detections[detectionIndex];
@@ -3401,10 +4003,22 @@ function pickDetectionAtPoint(frame, point, options = {}) {
3401
4003
  }
3402
4004
  }
3403
4005
  if (detection.keypoints) {
3404
- const keypointIndex = detection.keypoints.points.findIndex((keypoint, index) => detection.keypoints?.visibility?.[index] !==
3405
- KeypointVisibility.NotLabeled &&
3406
- Math.hypot(point.x - keypoint.x, point.y - keypoint.y) <=
3407
- keypointPadding);
4006
+ // Clustered keypoints share the pick tolerance; take the nearest so the
4007
+ // hover label, the menu, and the handle drag all name the same point.
4008
+ // Later points draw on top, so they win exact ties.
4009
+ let keypointIndex = -1;
4010
+ let keypointDistance = Number.POSITIVE_INFINITY;
4011
+ detection.keypoints.points.forEach((keypoint, index) => {
4012
+ if (detection.keypoints?.visibility?.[index] ===
4013
+ KeypointVisibility.NotLabeled) {
4014
+ return;
4015
+ }
4016
+ const distance = Math.hypot(point.x - keypoint.x, point.y - keypoint.y);
4017
+ if (distance <= keypointPadding && distance <= keypointDistance) {
4018
+ keypointIndex = index;
4019
+ keypointDistance = distance;
4020
+ }
4021
+ });
3408
4022
  if (keypointIndex !== -1) {
3409
4023
  pushCandidate(DetectionPickTarget.Keypoint, Math.PI * keypointPadding * keypointPadding, keypointIndex);
3410
4024
  }
@@ -3777,29 +4391,77 @@ function getAnnotationHandles(detection, viewportScale = 1) {
3777
4391
  if (detection.polyline)
3778
4392
  return getPathHandles(detection.polyline.points, false, radius, hitSize);
3779
4393
  if (detection.keypoints) {
3780
- return detection.keypoints.points.map((point, geometryIndex) => ({
3781
- cursor: "move",
3782
- geometryIndex,
3783
- hitSize,
3784
- id: `kp-${geometryIndex}`,
3785
- kind: AnnotationHandleKind.Keypoint,
3786
- point,
3787
- radius,
3788
- }));
4394
+ const keypointHandles = detection.keypoints.points.flatMap((point, geometryIndex) => detection.keypoints?.visibility?.[geometryIndex] ===
4395
+ KeypointVisibility.NotLabeled
4396
+ ? []
4397
+ : [
4398
+ {
4399
+ cursor: "pointer",
4400
+ geometryIndex,
4401
+ hitSize,
4402
+ id: `kp-${geometryIndex}`,
4403
+ kind: AnnotationHandleKind.Keypoint,
4404
+ point,
4405
+ radius,
4406
+ },
4407
+ ]);
4408
+ if (!detection.rect)
4409
+ return keypointHandles;
4410
+ // The skeleton's box resizes like any box; keypoints come last so a point
4411
+ // sitting on the box edge still wins the hit test.
4412
+ return [
4413
+ ...getBoxHandles(detection.rect, radius, hitSize),
4414
+ ...keypointHandles,
4415
+ ];
3789
4416
  }
3790
4417
  if (detection.rect)
3791
4418
  return getBoxHandles(detection.rect, radius, hitSize);
3792
4419
  return [];
3793
4420
  }
3794
4421
  function pickAnnotationHandle(handles, point) {
3795
- return [...handles]
3796
- .reverse()
3797
- .find((handle) => Math.abs(point.x - handle.point.x) <= handle.hitSize / 2 &&
3798
- Math.abs(point.y - handle.point.y) <= handle.hitSize / 2);
4422
+ let picked;
4423
+ let pickedDistance = Number.POSITIVE_INFINITY;
4424
+ for (const handle of handles) {
4425
+ const dx = Math.abs(point.x - handle.point.x);
4426
+ const dy = Math.abs(point.y - handle.point.y);
4427
+ if (dx > handle.hitSize / 2 || dy > handle.hitSize / 2)
4428
+ continue;
4429
+ // Clustered handles (face keypoints, a tiny box's corners) share hit
4430
+ // areas; the nearest one is what the pointer is on. Later handles draw on
4431
+ // top, so they win exact ties.
4432
+ const distance = Math.hypot(dx, dy);
4433
+ if (distance <= pickedDistance) {
4434
+ picked = handle;
4435
+ pickedDistance = distance;
4436
+ }
4437
+ }
4438
+ return picked;
3799
4439
  }
3800
4440
  function applyAnnotationHandleDrag(detection, handle, point) {
3801
4441
  if (detection.rect && handle.kind === AnnotationHandleKind.Resize) {
3802
- return { ...detection, rect: resizeRect(detection.rect, handle.id, point) };
4442
+ const rect = resizeRect(detection.rect, handle.id, point);
4443
+ const boxRelative = detection.keypoints?.boxRelative;
4444
+ if (!detection.keypoints || !boxRelative?.some(Boolean)) {
4445
+ return { ...detection, rect };
4446
+ }
4447
+ // Box-relative keypoints (unplaced template points) follow the rect;
4448
+ // points the user positioned keep their coordinates.
4449
+ const from = detection.rect;
4450
+ const scaleX = from.width > 0 ? rect.width / from.width : 1;
4451
+ const scaleY = from.height > 0 ? rect.height / from.height : 1;
4452
+ return {
4453
+ ...detection,
4454
+ keypoints: {
4455
+ ...detection.keypoints,
4456
+ points: detection.keypoints.points.map((keypoint, index) => boxRelative[index]
4457
+ ? {
4458
+ x: rect.x + (keypoint.x - from.x) * scaleX,
4459
+ y: rect.y + (keypoint.y - from.y) * scaleY,
4460
+ }
4461
+ : keypoint),
4462
+ },
4463
+ rect,
4464
+ };
3803
4465
  }
3804
4466
  if (handle.kind === AnnotationHandleKind.AddVertex &&
3805
4467
  handle.edgeIndex !== undefined) {
@@ -3820,7 +4482,16 @@ function applyAnnotationHandleDrag(detection, handle, point) {
3820
4482
  if (detection.keypoints) {
3821
4483
  const points = [...detection.keypoints.points];
3822
4484
  points[handle.geometryIndex] = point;
3823
- return { ...detection, keypoints: { ...detection.keypoints, points } };
4485
+ // A positioned point no longer follows the box.
4486
+ const boxRelative = detection.keypoints.boxRelative?.map((relative, index) => relative && index !== handle.geometryIndex);
4487
+ return {
4488
+ ...detection,
4489
+ keypoints: {
4490
+ ...detection.keypoints,
4491
+ points,
4492
+ ...(boxRelative ? { boxRelative } : {}),
4493
+ },
4494
+ };
3824
4495
  }
3825
4496
  return detection;
3826
4497
  }
@@ -3989,7 +4660,7 @@ function createAnnotationEditingEngine(options = {}) {
3989
4660
  case "move": {
3990
4661
  const dx = input.point.x - gesture.start.point.x;
3991
4662
  const dy = input.point.y - gesture.start.point.y;
3992
- const moved = gesture.moved || Math.hypot(dx, dy) >= MOVE_THRESHOLD / scale();
4663
+ const moved = gesture.moved || exceedsMoveThreshold(gesture.start, input);
3993
4664
  gesture = { ...gesture, current: input, moved };
3994
4665
  if (moved) {
3995
4666
  const preview = offsetDetection(gesture.detection, dx, dy);
@@ -4003,10 +4674,17 @@ function createAnnotationEditingEngine(options = {}) {
4003
4674
  }
4004
4675
  break;
4005
4676
  }
4006
- case "resize":
4007
- gesture = { ...gesture, current: input };
4008
- setPreview(applyAnnotationHandleDrag(gesture.detection, gesture.handle, input.point));
4677
+ case "resize": {
4678
+ // A click on a handle is not a drag: a keypoint tapped to open its
4679
+ // menu must not snap to the pointer, so nothing previews or commits
4680
+ // until the pointer has travelled like a move gesture.
4681
+ const moved = gesture.moved || exceedsMoveThreshold(gesture.start, input);
4682
+ gesture = { ...gesture, current: input, moved };
4683
+ if (moved) {
4684
+ setPreview(applyAnnotationHandleDrag(gesture.detection, gesture.handle, input.point));
4685
+ }
4009
4686
  break;
4687
+ }
4010
4688
  case "path":
4011
4689
  setPreview(resolvePathPreview([...gesture.points, input.point]));
4012
4690
  break;
@@ -4068,6 +4746,10 @@ function createAnnotationEditingEngine(options = {}) {
4068
4746
  commit(preview, active.detection);
4069
4747
  return;
4070
4748
  }
4749
+ if (!active.moved) {
4750
+ setState(idleState());
4751
+ return;
4752
+ }
4071
4753
  const preview = applyAnnotationHandleDrag(active.detection, active.handle, input.point);
4072
4754
  commit(preview, active.detection);
4073
4755
  },
@@ -4088,6 +4770,7 @@ function createAnnotationEditingEngine(options = {}) {
4088
4770
  detection,
4089
4771
  handle,
4090
4772
  kind: "resize",
4773
+ moved: false,
4091
4774
  start: input,
4092
4775
  };
4093
4776
  setState({
@@ -4095,7 +4778,7 @@ function createAnnotationEditingEngine(options = {}) {
4095
4778
  activeHandleId: handle.id,
4096
4779
  kind: AnnotationGestureStateKind.Resizing,
4097
4780
  pointerId: input.pointerId ?? null,
4098
- preview: detection,
4781
+ preview: null,
4099
4782
  });
4100
4783
  },
4101
4784
  deleteVertex(detection, vertexIndex) {
@@ -4273,6 +4956,10 @@ function createAnnotationEditingEngine(options = {}) {
4273
4956
  function scale() {
4274
4957
  return Math.max(options.viewportScale?.() ?? 1, Number.EPSILON);
4275
4958
  }
4959
+ function exceedsMoveThreshold(start, input) {
4960
+ return (Math.hypot(input.point.x - start.point.x, input.point.y - start.point.y) >=
4961
+ MOVE_THRESHOLD / scale());
4962
+ }
4276
4963
  function capture(pointerId) {
4277
4964
  if (pointerId !== undefined)
4278
4965
  options.capturePointer?.(pointerId);
@@ -6337,6 +7024,30 @@ var MediaRendererPlaybackState;
6337
7024
  MediaRendererPlaybackState["Error"] = "error";
6338
7025
  MediaRendererPlaybackState["Destroyed"] = "destroyed";
6339
7026
  })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));
7027
+ /**
7028
+ * Stable classification of a media failure.
7029
+ *
7030
+ * Applications branch on these kinds instead of parsing decoder, demuxer, or
7031
+ * container error text, and still own their localized user-facing copy. New
7032
+ * kinds may be added over time, so treat unrecognized values like `Unknown`.
7033
+ */
7034
+ var MediaErrorKind;
7035
+ (function (MediaErrorKind) {
7036
+ /** The media could not be opened or read at all. */
7037
+ MediaErrorKind["Unreadable"] = "unreadable";
7038
+ /** The container or codec is not supported by this platform. */
7039
+ MediaErrorKind["UnsupportedFormat"] = "unsupportedFormat";
7040
+ /** The media opened but carries no usable video track. */
7041
+ MediaErrorKind["NoVideoTrack"] = "noVideoTrack";
7042
+ /** Decoding a sample failed after the source opened. */
7043
+ MediaErrorKind["Decode"] = "decode";
7044
+ /** The media could not be fetched. */
7045
+ MediaErrorKind["Network"] = "network";
7046
+ /** The host environment lacks the APIs this media source requires. */
7047
+ MediaErrorKind["EnvironmentUnsupported"] = "environmentUnsupported";
7048
+ /** The failure could not be classified. */
7049
+ MediaErrorKind["Unknown"] = "unknown";
7050
+ })(MediaErrorKind || (MediaErrorKind = {}));
6340
7051
  /**
6341
7052
  * Lower-level media source readiness.
6342
7053
  */
@@ -6405,5 +7116,5 @@ var MediaSessionActivityStatus;
6405
7116
  MediaSessionActivityStatus["Waiting"] = "waiting";
6406
7117
  })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
6407
7118
 
6408
- export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, StrokeAlignment as BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionMaskPayloadFormat, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MAX_ID_MASK_PALETTE_ENTRIES, MAX_ID_MASK_STROKE_WIDTH, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, ShapeInstructionKind, StrokeAlignment, TrackingGeometry, annotationRendererKinds, annotationRenderers, applyAnnotationHandleDrag, canReuseMaskStyleArtifacts, centerRectToTopLeftRect, computeDetectionMaskRect, computeMaskBounds, containsPoint, convertDetectionBoxToMask, convertDetectionBoxToPolygon, convertDetectionMaskToBox, convertDetectionMaskToPolygon, convertDetectionPolygonToBox, convertDetectionPolygonToMask, copySortedDetectionFrames, createAnnotationEditingEngine, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createDetectionPickKey, createEditableAnnotationFrameSession, createIdMaskFrame, createIdleDetectionBufferState, createMemoryColdDetectionFrameStore, createOCSortTracker, createSortTracker, createSourceAwarePresentation, createViewportController, createWritableDetectionFrameSource, decodeCompressedRleCounts, decodeCompressedRleMask, decodeDetectionMaskPayload, deleteAnnotationVertex, detectMaskBorders, detectionFrameOverlapsRange, detectionPostProcessors, distanceToSegment, encodeBinaryMask, encodeBinaryMaskWithBounds, encodeCompressedRleCounts, encodeDetectionMaskPayload, extractMaskContour, extractMaskRectRuns, filterDetectionFramesForRange, findClosestAnnotationSegment, followDetectionPickAcrossFrames, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, haveSameDetectionPickIdentities, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, projectDetectionFrameForTracking, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationRendererPresentation, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
7119
+ export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, StrokeAlignment as BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionMaskPayloadFormat, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MAX_ID_MASK_PALETTE_ENTRIES, MAX_ID_MASK_STROKE_WIDTH, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, ShapeInstructionKind, StrokeAlignment, TrackingGeometry, annotationRendererKinds, annotationRenderers, applyAnnotationHandleDrag, canReuseMaskStyleArtifacts, centerRectToTopLeftRect, computeDetectionMaskRect, computeMaskBounds, containsPoint, convertDetectionBoxToMask, convertDetectionBoxToPolygon, convertDetectionMaskToBox, convertDetectionMaskToPolygon, convertDetectionPolygonToBox, convertDetectionPolygonToMask, copySortedDetectionFrames, createAnnotationEditingEngine, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createDetectionPickKey, createEditableAnnotationFrameSession, createIdMaskFrame, createIdleDetectionBufferState, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createSourceAwarePresentation, createViewportController, createWritableDetectionFrameSource, decodeCompressedRleCounts, decodeCompressedRleMask, decodeDetectionMaskPayload, deleteAnnotationVertex, detectMaskBorders, detectionFrameOverlapsRange, detectionPostProcessors, distanceToSegment, encodeBinaryMask, encodeBinaryMaskWithBounds, encodeCompressedRleCounts, encodeDetectionMaskPayload, extractMaskContour, extractMaskRectRuns, filterDetectionFramesForRange, findClosestAnnotationSegment, followDetectionPickAcrossFrames, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, haveSameDetectionPickIdentities, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationRendererPresentation, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
6409
7120
  //# sourceMappingURL=index.js.map