supervision 0.1.5 → 0.1.6

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 (44) hide show
  1. package/dist/index.d.ts +8 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +437 -49
  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/sessions/media-session-detections.d.ts.map +1 -1
  19. package/dist/sessions/media-session.d.ts +2 -2
  20. package/dist/sessions/media-session.d.ts.map +1 -1
  21. package/dist/tracking.worker.js +24 -0
  22. package/dist/tracking.worker.js.map +1 -1
  23. package/dist/types/media-session.d.ts +57 -1
  24. package/dist/types/media-session.d.ts.map +1 -1
  25. package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
  26. package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -1
  27. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts +22 -0
  28. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts.map +1 -0
  29. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +2 -7
  30. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -1
  31. package/node_modules/supervision-js-core/dist/index.d.ts +5 -3
  32. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  33. package/node_modules/supervision-js-core/dist/index.js +678 -63
  34. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  35. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +113 -3
  36. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  37. package/node_modules/supervision-js-core/dist/types/detections.d.ts +22 -0
  38. package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
  39. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +31 -0
  40. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  41. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
  42. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts +20 -0
  43. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts.map +1 -0
  44. package/package.json +1 -1
@@ -88,6 +88,9 @@ function copySortedDetectionFrames(detectionFrames) {
88
88
  : undefined,
89
89
  rect: detection.rect ? { ...detection.rect } : undefined,
90
90
  })),
91
+ coordinateSpace: frame.coordinateSpace
92
+ ? { ...frame.coordinateSpace }
93
+ : undefined,
91
94
  endTime: frame.endTime,
92
95
  frameIndex: frame.frameIndex,
93
96
  mediaTime: frame.mediaTime,
@@ -129,6 +132,10 @@ function validateDetectionFrames(detectionFrames) {
129
132
  min: 0,
130
133
  });
131
134
  }
135
+ if (frame.coordinateSpace !== undefined) {
136
+ validateNumber(frame.coordinateSpace.width, `frames[${frameOffset}].coordinateSpace.width`, { exclusiveMin: 0 });
137
+ validateNumber(frame.coordinateSpace.height, `frames[${frameOffset}].coordinateSpace.height`, { exclusiveMin: 0 });
138
+ }
132
139
  for (const [detectionOffset, detection] of frame.detections.entries()) {
133
140
  const detectionPath = `frames[${frameOffset}].detections[${detectionOffset}]`;
134
141
  if (detection.confidence !== undefined) {
@@ -888,7 +895,7 @@ function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
888
895
  for (const frame of changedFrames) {
889
896
  framesByIdentity.set(getDetectionFrameIdentity(frame), frame);
890
897
  }
891
- return Array.from(framesByIdentity.values()).sort(compareDetectionFrames);
898
+ return Array.from(framesByIdentity.values()).sort(compareDetectionFrames$1);
892
899
  }
893
900
  function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
894
901
  const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
@@ -899,7 +906,7 @@ function getDetectionFrameIdentity(frame) {
899
906
  ? `time:${frame.mediaTime}`
900
907
  : `index:${frame.frameIndex}`;
901
908
  }
902
- function compareDetectionFrames(left, right) {
909
+ function compareDetectionFrames$1(left, right) {
903
910
  if (left.mediaTime !== right.mediaTime) {
904
911
  return left.mediaTime - right.mediaTime;
905
912
  }
@@ -918,21 +925,118 @@ function createColdDetectionFrameSource(options) {
918
925
  };
919
926
  }
920
927
 
928
+ /**
929
+ * Projects one detection frame from its declared source coordinate space into
930
+ * `target`.
931
+ *
932
+ * Rectangles, polygons, polylines, and keypoints are scaled. Masks are left
933
+ * untouched because a `DetectionMask` already carries the mask-pixel
934
+ * dimensions its `counts` are encoded against; scaling them here would apply
935
+ * the ratio twice. Frames without `coordinateSpace`, with a degenerate space,
936
+ * or already in the target space are returned unchanged so mixed producers can
937
+ * share one source.
938
+ */
939
+ function projectDetectionFrame(frame, target) {
940
+ const space = frame.coordinateSpace;
941
+ if (!space ||
942
+ !isUsableCoordinateSpace(space) ||
943
+ !isUsableCoordinateSpace(target) ||
944
+ (space.width === target.width && space.height === target.height)) {
945
+ return frame;
946
+ }
947
+ const scaleX = target.width / space.width;
948
+ const scaleY = target.height / space.height;
949
+ return {
950
+ ...frame,
951
+ coordinateSpace: { height: target.height, width: target.width },
952
+ detections: frame.detections.map((detection) => projectDetection(detection, scaleX, scaleY)),
953
+ };
954
+ }
955
+ /**
956
+ * Projects every frame that declares a source coordinate space into `target`.
957
+ *
958
+ * Frames without coordinate metadata pass through by reference.
959
+ */
960
+ function projectDetectionFrames(frames, target) {
961
+ let didProject = false;
962
+ const projectedFrames = frames.map((frame) => {
963
+ const projectedFrame = projectDetectionFrame(frame, target);
964
+ didProject ||= projectedFrame !== frame;
965
+ return projectedFrame;
966
+ });
967
+ return didProject ? projectedFrames : frames;
968
+ }
969
+ function projectDetection(detection, scaleX, scaleY) {
970
+ return {
971
+ ...detection,
972
+ ...(detection.rect
973
+ ? { rect: scaleRect(detection.rect, scaleX, scaleY) }
974
+ : {}),
975
+ ...(detection.polygon
976
+ ? { polygon: scalePolygon(detection.polygon, scaleX, scaleY) }
977
+ : {}),
978
+ ...(detection.polyline
979
+ ? { polyline: scalePolyline(detection.polyline, scaleX, scaleY) }
980
+ : {}),
981
+ ...(detection.keypoints
982
+ ? { keypoints: scaleKeypoints(detection.keypoints, scaleX, scaleY) }
983
+ : {}),
984
+ };
985
+ }
986
+ function scaleRect(rect, scaleX, scaleY) {
987
+ return {
988
+ height: rect.height * scaleY,
989
+ width: rect.width * scaleX,
990
+ x: rect.x * scaleX,
991
+ y: rect.y * scaleY,
992
+ };
993
+ }
994
+ function scalePolygon(polygon, scaleX, scaleY) {
995
+ return { points: scalePoints(polygon.points, scaleX, scaleY) };
996
+ }
997
+ function scalePolyline(polyline, scaleX, scaleY) {
998
+ return { points: scalePoints(polyline.points, scaleX, scaleY) };
999
+ }
1000
+ function scaleKeypoints(keypoints, scaleX, scaleY) {
1001
+ return {
1002
+ ...keypoints,
1003
+ points: scalePoints(keypoints.points, scaleX, scaleY),
1004
+ };
1005
+ }
1006
+ function scalePoints(points, scaleX, scaleY) {
1007
+ return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
1008
+ }
1009
+ function isUsableCoordinateSpace(space) {
1010
+ return (Number.isFinite(space.width) &&
1011
+ Number.isFinite(space.height) &&
1012
+ space.width > 0 &&
1013
+ space.height > 0);
1014
+ }
1015
+
921
1016
  function createCompositeDetectionFrameSource(options) {
922
1017
  const sources = normalizeCompositeSources(options.sources);
923
1018
  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
- })));
1019
+ async loadFrames(startTime, endTime, loadOptions) {
1020
+ const target = loadOptions?.coordinateSpace;
1021
+ const loadedSources = await Promise.all(sources.map(async (source) => {
1022
+ const frames = copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime, loadOptions));
1023
+ return {
1024
+ ...source,
1025
+ // Composition flattens child detections into one frame, which can
1026
+ // only carry one coordinate space. Each child is projected here,
1027
+ // while its own `coordinateSpace` is still attached to its own
1028
+ // detections, so children inferred at different sizes compose
1029
+ // correctly. Masks keep their intrinsic dimensions.
1030
+ frames: target ? projectDetectionFrames(frames, target) : frames,
1031
+ };
1032
+ }));
929
1033
  if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
930
- const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options);
1034
+ const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options, target);
931
1035
  if (nearestFrames) {
932
1036
  return nearestFrames;
933
1037
  }
934
1038
  }
935
- return composeIntervalFrames(loadedSources, startTime, endTime, options);
1039
+ return composeIntervalFrames(loadedSources, startTime, endTime, options, target);
936
1040
  },
937
1041
  async waitForRange(range) {
938
1042
  await Promise.all(sources
@@ -979,7 +1083,7 @@ function normalizeCompositeSources(entries) {
979
1083
  .sort((left, right) => left.order - right.order ||
980
1084
  left.declarationIndex - right.declarationIndex);
981
1085
  }
982
- function composeIntervalFrames(sources, startTime, endTime, options) {
1086
+ function composeIntervalFrames(sources, startTime, endTime, options, coordinateSpace) {
983
1087
  const boundaryTimes = new Set([startTime]);
984
1088
  for (const source of sources) {
985
1089
  for (const frame of source.frames) {
@@ -1001,17 +1105,14 @@ function composeIntervalFrames(sources, startTime, endTime, options) {
1001
1105
  }
1002
1106
  const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
1003
1107
  const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
1004
- const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, {
1005
- ...options,
1006
- selectionMode: DetectionFrameSelectionMode.Interval,
1007
- });
1108
+ const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval }, undefined, coordinateSpace);
1008
1109
  if (frame) {
1009
1110
  frames.push(frame);
1010
1111
  }
1011
1112
  }
1012
1113
  return frames;
1013
1114
  }
1014
- function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
1115
+ function composeNearestFrameIndexFrames(sources, startTime, endTime, options, coordinateSpace) {
1015
1116
  const frameRate = options.frameRate;
1016
1117
  if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
1017
1118
  return null;
@@ -1037,14 +1138,14 @@ function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
1037
1138
  const frame = composeFrameAtTime(sources, mediaTime, Math.min(mediaTime + 1 / frameRate, endTime), {
1038
1139
  ...options,
1039
1140
  selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
1040
- }, frameIndex);
1141
+ }, frameIndex, coordinateSpace);
1041
1142
  if (frame) {
1042
1143
  frames.push(frame);
1043
1144
  }
1044
1145
  }
1045
1146
  return frames;
1046
1147
  }
1047
- function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
1148
+ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex, coordinateSpace) {
1048
1149
  const detections = [];
1049
1150
  const activeFrameIndexes = [];
1050
1151
  for (const source of sources) {
@@ -1070,6 +1171,9 @@ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
1070
1171
  endTime,
1071
1172
  frameIndex: frameIndex ?? resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
1072
1173
  mediaTime,
1174
+ // Children were projected before composition, so a composed frame is
1175
+ // already in the coordinate space the renderer presents.
1176
+ ...(coordinateSpace ? { coordinateSpace } : {}),
1073
1177
  };
1074
1178
  }
1075
1179
  function copyDetectionWithSource(detection, sourceId, sourceDetectionIndex) {
@@ -1284,6 +1388,16 @@ function createMemoryColdDetectionFrameStore() {
1284
1388
  detectionFrameOverlapsRange(frame, startTime, endTime));
1285
1389
  return copySortedDetectionFrames(frames);
1286
1390
  },
1391
+ async pruneFrames(options) {
1392
+ assertActive();
1393
+ assertRetentionFloor(options.startTime);
1394
+ const dataset = datasets.get(options.datasetId);
1395
+ if (!dataset) {
1396
+ return createWriteSummary(options.datasetId, createDataset(DEFAULT_CHUNK_DURATION_SECONDS));
1397
+ }
1398
+ pruneDatasetBefore(dataset, options.startTime);
1399
+ return createWriteSummary(options.datasetId, dataset);
1400
+ },
1287
1401
  async clearDataset(datasetId) {
1288
1402
  assertActive();
1289
1403
  datasets.delete(datasetId);
@@ -1322,7 +1436,7 @@ function upsertFrames(dataset, frames) {
1322
1436
  dataset.framesByKey.set(frameKey, { chunkIndexes, frame });
1323
1437
  dataset.detectionCount += frame.detections.length;
1324
1438
  dataset.startTime = Math.min(dataset.startTime ?? frame.mediaTime, frame.mediaTime);
1325
- dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
1439
+ dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
1326
1440
  for (const chunkIndex of chunkIndexes) {
1327
1441
  const frameKeys = dataset.frameKeysByChunk.get(chunkIndex) ?? new Set();
1328
1442
  frameKeys.add(frameKey);
@@ -1344,18 +1458,81 @@ function removeStoredFrame(dataset, frameKey, storedFrame) {
1344
1458
  }
1345
1459
  }
1346
1460
  }
1461
+ /**
1462
+ * Drops frames that end before `startTime`.
1463
+ *
1464
+ * Only chunks at or below the retention boundary are visited, and the new
1465
+ * start bound is read from the lowest surviving chunk, so eviction costs work
1466
+ * proportional to the evicted range rather than to retained history.
1467
+ */
1468
+ function pruneDatasetBefore(dataset, startTime) {
1469
+ const boundaryChunkIndex = getChunkIndex(Math.max(0, startTime), dataset.chunkDurationSeconds);
1470
+ let didRemoveFrame = false;
1471
+ for (const chunkIndex of Array.from(dataset.frameKeysByChunk.keys())) {
1472
+ if (chunkIndex > boundaryChunkIndex) {
1473
+ continue;
1474
+ }
1475
+ for (const frameKey of Array.from(dataset.frameKeysByChunk.get(chunkIndex) ?? [])) {
1476
+ const storedFrame = dataset.framesByKey.get(frameKey);
1477
+ if (!storedFrame || isFrameRetainedFrom(storedFrame.frame, startTime)) {
1478
+ continue;
1479
+ }
1480
+ removeStoredFrame(dataset, frameKey, storedFrame);
1481
+ didRemoveFrame = true;
1482
+ }
1483
+ }
1484
+ if (!didRemoveFrame) {
1485
+ return;
1486
+ }
1487
+ if (dataset.framesByKey.size === 0) {
1488
+ dataset.startTime = null;
1489
+ dataset.endTime = null;
1490
+ return;
1491
+ }
1492
+ // Every frame is registered in the chunk its `mediaTime` falls in, so the
1493
+ // earliest retained frame lives in the lowest remaining chunk. Removing
1494
+ // frames can only raise the start bound; `endTime` is unchanged because only
1495
+ // frames ending before `startTime` were dropped.
1496
+ dataset.startTime = getEarliestChunkStartTime(dataset);
1497
+ }
1498
+ /**
1499
+ * Mirrors `detectionFrameOverlapsRange` so pruning keeps exactly the frames a
1500
+ * load from `startTime` would return.
1501
+ */
1502
+ function isFrameRetainedFrom(frame, startTime) {
1503
+ return frame.endTime === undefined
1504
+ ? frame.mediaTime >= startTime
1505
+ : frame.endTime > startTime;
1506
+ }
1507
+ function getEarliestChunkStartTime(dataset) {
1508
+ let earliestChunkIndex = null;
1509
+ for (const chunkIndex of dataset.frameKeysByChunk.keys()) {
1510
+ if (earliestChunkIndex === null || chunkIndex < earliestChunkIndex) {
1511
+ earliestChunkIndex = chunkIndex;
1512
+ }
1513
+ }
1514
+ let startTime = null;
1515
+ for (const frameKey of dataset.frameKeysByChunk.get(earliestChunkIndex ?? 0) ?? []) {
1516
+ const storedFrame = dataset.framesByKey.get(frameKey);
1517
+ if (!storedFrame) {
1518
+ continue;
1519
+ }
1520
+ startTime = Math.min(startTime ?? storedFrame.frame.mediaTime, storedFrame.frame.mediaTime);
1521
+ }
1522
+ return startTime;
1523
+ }
1347
1524
  function replacementCanShrinkBounds(dataset, previousFrame, nextFrame) {
1348
1525
  return ((dataset.startTime === previousFrame.mediaTime &&
1349
1526
  nextFrame.mediaTime > previousFrame.mediaTime) ||
1350
- (dataset.endTime === getFrameEndTime(previousFrame) &&
1351
- getFrameEndTime(nextFrame) < getFrameEndTime(previousFrame)));
1527
+ (dataset.endTime === getFrameEndTime$1(previousFrame) &&
1528
+ getFrameEndTime$1(nextFrame) < getFrameEndTime$1(previousFrame)));
1352
1529
  }
1353
1530
  function recalculateBounds(dataset) {
1354
1531
  let startTime = null;
1355
1532
  let endTime = null;
1356
1533
  for (const { frame } of dataset.framesByKey.values()) {
1357
1534
  startTime = Math.min(startTime ?? frame.mediaTime, frame.mediaTime);
1358
- endTime = Math.max(endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
1535
+ endTime = Math.max(endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
1359
1536
  }
1360
1537
  dataset.startTime = startTime;
1361
1538
  dataset.endTime = endTime;
@@ -1387,10 +1564,10 @@ function createWriteSummary(datasetId, dataset) {
1387
1564
  }
1388
1565
  function getFrameChunkIndexes(frame, chunkDurationSeconds) {
1389
1566
  const startIndex = getChunkIndex(frame.mediaTime, chunkDurationSeconds);
1390
- const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime(frame) / chunkDurationSeconds) - 1);
1567
+ const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime$1(frame) / chunkDurationSeconds) - 1);
1391
1568
  return Array.from({ length: endIndex - startIndex + 1 }, (_, offset) => startIndex + offset);
1392
1569
  }
1393
- function getFrameEndTime(frame) {
1570
+ function getFrameEndTime$1(frame) {
1394
1571
  return frame.endTime ?? frame.mediaTime;
1395
1572
  }
1396
1573
  function getChunkIndex(mediaTime, chunkDurationSeconds) {
@@ -1401,11 +1578,87 @@ function getDetectionFrameDedupeKey(frame) {
1401
1578
  ? `time:${frame.mediaTime}`
1402
1579
  : `index:${frame.frameIndex}`;
1403
1580
  }
1581
+ /**
1582
+ * Rejects a retention floor that could not describe a real media time.
1583
+ *
1584
+ * Pruning mutates retained history in place, and `NaN` compares false against
1585
+ * every retained frame, so an unvalidated floor would silently erase a dataset
1586
+ * instead of evicting a bounded range.
1587
+ */
1588
+ function assertRetentionFloor(startTime) {
1589
+ if (!Number.isFinite(startTime) || startTime < 0) {
1590
+ throw new RangeError("pruneFrames requires a finite, non-negative startTime.");
1591
+ }
1592
+ }
1593
+
1594
+ /**
1595
+ * Wraps a detection source so every loaded frame that declares a source
1596
+ * coordinate space is projected into the target space.
1597
+ *
1598
+ * The target is resolved per load because media dimensions are only known once
1599
+ * the media has opened, while sources are composed before that. Frames without
1600
+ * `coordinateSpace`, and loads made before a target exists, pass through by
1601
+ * reference, so a producer already emitting media-pixel geometry pays nothing.
1602
+ *
1603
+ * The target is also handed to the wrapped source through
1604
+ * `DetectionFrameLoadOptions`. A source that flattens child frames, such as a
1605
+ * composite source, needs it to project each child while that child's own
1606
+ * `coordinateSpace` is still attached; projecting the flattened result again
1607
+ * here is then a no-op.
1608
+ *
1609
+ * Optional source hooks are forwarded only when the wrapped source implements
1610
+ * them, so capability detection keeps working through the wrapper.
1611
+ */
1612
+ function createProjectedDetectionFrameSource(source, resolveTarget) {
1613
+ const { getAvailableRanges, getChangesSince, getVersion, waitForRange } = source;
1614
+ return {
1615
+ async loadFrames(startTime, endTime, options) {
1616
+ const target = resolveTarget();
1617
+ const frames = await source.loadFrames(startTime, endTime, {
1618
+ ...options,
1619
+ ...(target ? { coordinateSpace: target } : {}),
1620
+ });
1621
+ return target ? projectDetectionFrames(frames, target) : frames;
1622
+ },
1623
+ ...(waitForRange
1624
+ ? {
1625
+ waitForRange: (range) => waitForRange.call(source, range),
1626
+ }
1627
+ : {}),
1628
+ ...(getAvailableRanges
1629
+ ? { getAvailableRanges: () => getAvailableRanges.call(source) }
1630
+ : {}),
1631
+ ...(getVersion
1632
+ ? {
1633
+ getVersion: (range) => getVersion.call(source, range),
1634
+ }
1635
+ : {}),
1636
+ ...(getChangesSince
1637
+ ? {
1638
+ getChangesSince: (version, ranges) => getChangesSince.call(source, version, ranges),
1639
+ }
1640
+ : {}),
1641
+ ...(source.destroy ? { destroy: () => source.destroy?.() } : {}),
1642
+ };
1643
+ }
1404
1644
 
1405
1645
  const RANGE_EPSILON_SECONDS = 1e-6;
1406
1646
  const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
1647
+ const DEFAULT_LIVE_HOLD_SECONDS = 60;
1648
+ const NO_COVERAGE_INVALIDATION = { ranges: [] };
1407
1649
  function createWritableDetectionFrameSource(options) {
1650
+ const liveHoldSeconds = resolveLiveHoldSeconds(options.live?.holdSeconds);
1408
1651
  let summary = null;
1652
+ let latestFrame = null;
1653
+ let heldLiveFrame = null;
1654
+ // Newest live `mediaTime` accepted so far. It survives retention so a late
1655
+ // result cannot reopen coverage the source already moved past.
1656
+ let liveFrontierTime = null;
1657
+ // Newest media time actually covered by producer-declared data. Live frames
1658
+ // are written with a synthetic hold end, so the summary end time cannot be
1659
+ // used to place the retention window without pushing it into the future.
1660
+ let coverageFrontierTime = null;
1661
+ let writeQueue = Promise.resolve();
1409
1662
  let version = 0;
1410
1663
  let allRangeVersion = 0;
1411
1664
  let journalFloorVersion = 0;
@@ -1418,13 +1671,25 @@ function createWritableDetectionFrameSource(options) {
1418
1671
  datasetId: options.datasetId,
1419
1672
  frames,
1420
1673
  });
1421
- const recordRangeWrite = (nextSummary, changedSourceRanges) => {
1674
+ const recordRangeWrite = (nextSummary, changedSourceRanges, invalidation, prunedRange) => {
1422
1675
  summary = nextSummary;
1423
1676
  version += 1;
1424
1677
  for (const changedRange of changedSourceRanges) {
1425
1678
  changedRanges.push({ ...changedRange, version });
1426
1679
  recordAvailableRange(changedRange, availableRanges);
1427
1680
  }
1681
+ for (const invalidatedRange of invalidation.ranges) {
1682
+ // Coverage a write took away is journaled but never made available: a
1683
+ // consumer parked there has to redraw, and nothing new can be loaded.
1684
+ changedRanges.push({ ...invalidatedRange, version });
1685
+ }
1686
+ if (prunedRange) {
1687
+ // Evicting old history is a change to that range only. Recording it
1688
+ // alongside the append keeps a buffered timeline patching two small
1689
+ // ranges instead of invalidating and rebuilding everything it retains.
1690
+ changedRanges.push({ ...prunedRange, version });
1691
+ dropAvailableRangesBefore(prunedRange.endTime, availableRanges);
1692
+ }
1428
1693
  compactChangedRangeJournal();
1429
1694
  resolveCoveredWaiters();
1430
1695
  return nextSummary;
@@ -1449,38 +1714,113 @@ function createWritableDetectionFrameSource(options) {
1449
1714
  datasetId: options.datasetId,
1450
1715
  async appendFrames(frames) {
1451
1716
  assertActive();
1452
- const nextSummary = await options.store.appendFrames(writeOptions(frames));
1717
+ return enqueueWrite(async () => {
1718
+ const nextSummary = await options.store.appendFrames(writeOptions(frames));
1719
+ assertActive();
1720
+ const changedSourceRanges = getDetectionFrameRanges(frames);
1721
+ const invalidation = getInvalidatedCoverage(frames, changedSourceRanges);
1722
+ recordLatestFrame(frames);
1723
+ recordCoverageFrontierFromFrames(frames);
1724
+ return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
1725
+ });
1726
+ },
1727
+ async appendLiveFrame(frame) {
1453
1728
  assertActive();
1454
- const changedSourceRanges = getDetectionFrameRanges(frames);
1455
- const retainedSummary = await applyRetention(nextSummary);
1729
+ return enqueueWrite(async () => {
1730
+ if (isStaleLiveFrame(frame)) {
1731
+ // A result older than the live frontier no longer describes what is
1732
+ // on screen. Writing it would reopen coverage the source already
1733
+ // closed, so the newest causal frame simply wins.
1734
+ return summary ? { ...summary } : createEmptyWriteSummary();
1735
+ }
1736
+ const framesToWrite = createLiveFrameWrite(frame);
1737
+ const nextSummary = await options.store.appendFrames(writeOptions(framesToWrite));
1738
+ assertActive();
1739
+ const changedSourceRanges = getDetectionFrameRanges(framesToWrite);
1740
+ const invalidation = getInvalidatedCoverage(framesToWrite, changedSourceRanges);
1741
+ heldLiveFrame = framesToWrite.at(-1) ?? null;
1742
+ liveFrontierTime = Math.max(liveFrontierTime ?? frame.mediaTime, frame.mediaTime);
1743
+ recordLatestFrame(framesToWrite);
1744
+ // The hold end is a placeholder for "still current", not covered data.
1745
+ recordCoverageFrontier(getFrameEndTime(frame));
1746
+ return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
1747
+ });
1748
+ },
1749
+ async finalizeCoverage(endTime) {
1456
1750
  assertActive();
1457
- if (retainedSummary !== nextSummary) {
1458
- return recordAllRangesWrite(retainedSummary);
1751
+ if (!Number.isFinite(endTime)) {
1752
+ throw new RangeError("finalizeCoverage requires a finite endTime.");
1459
1753
  }
1460
- if (changedSourceRanges.length === 0) {
1461
- summary = nextSummary;
1462
- return nextSummary;
1463
- }
1464
- return recordRangeWrite(nextSummary, changedSourceRanges);
1754
+ return enqueueWrite(async () => {
1755
+ const frameToFinalize = latestFrame;
1756
+ if (!frameToFinalize ||
1757
+ // A frame that starts at or after the end of media cannot describe a
1758
+ // non-empty terminal interval.
1759
+ frameToFinalize.mediaTime >= endTime ||
1760
+ getFrameEndTime(frameToFinalize) === endTime) {
1761
+ return summary ? { ...summary } : null;
1762
+ }
1763
+ const previousEndTime = getFrameEndTime(frameToFinalize);
1764
+ const finalizedFrame = { ...frameToFinalize, endTime };
1765
+ const nextSummary = await options.store.appendFrames(writeOptions([finalizedFrame]));
1766
+ assertActive();
1767
+ const changedSourceRanges = [
1768
+ {
1769
+ endTime: Math.max(endTime, previousEndTime),
1770
+ startTime: frameToFinalize.mediaTime,
1771
+ },
1772
+ ];
1773
+ const invalidation = getInvalidatedCoverage([finalizedFrame], changedSourceRanges);
1774
+ latestFrame = finalizedFrame;
1775
+ if (heldLiveFrame &&
1776
+ haveSameDetectionFrameIdentity(heldLiveFrame, finalizedFrame)) {
1777
+ heldLiveFrame = finalizedFrame;
1778
+ }
1779
+ coverageFrontierTime = endTime;
1780
+ const finalSummary = await retainAndRecord(nextSummary, changedSourceRanges,
1781
+ // Finalizing usually closes at the end of media, where the tail an
1782
+ // open-ended frame gives up is unreachable. The caller may finalize
1783
+ // earlier, though, so report it rather than assume.
1784
+ invalidation);
1785
+ // Closing a held live frame shortens coverage. Reported availability
1786
+ // has to shrink with it, or a playback gate would still believe the
1787
+ // source covers time past the end of media.
1788
+ clipAvailableRangesAfter(endTime);
1789
+ return finalSummary;
1790
+ });
1465
1791
  },
1466
1792
  async replaceFrames(frames) {
1467
1793
  assertActive();
1468
- const nextSummary = await options.store.putFrames(writeOptions(frames));
1469
- assertActive();
1470
- const retainedSummary = await applyRetention(nextSummary);
1471
- assertActive();
1472
- return recordAllRangesWrite(retainedSummary);
1794
+ return enqueueWrite(async () => {
1795
+ const nextSummary = await options.store.putFrames(writeOptions(frames));
1796
+ assertActive();
1797
+ latestFrame = null;
1798
+ heldLiveFrame = null;
1799
+ liveFrontierTime = null;
1800
+ coverageFrontierTime = null;
1801
+ recordLatestFrame(frames);
1802
+ recordCoverageFrontierFromFrames(frames);
1803
+ const retention = await applyRetention(nextSummary);
1804
+ assertActive();
1805
+ return recordAllRangesWrite(retention.kind === "unchanged" ? nextSummary : retention.summary);
1806
+ });
1473
1807
  },
1474
1808
  async clear() {
1475
1809
  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;
1810
+ await enqueueWrite(async () => {
1811
+ await options.store.clearDataset(options.datasetId);
1812
+ assertActive();
1813
+ summary = null;
1814
+ latestFrame = null;
1815
+ heldLiveFrame = null;
1816
+ liveFrontierTime = null;
1817
+ coverageFrontierTime = null;
1818
+ version += 1;
1819
+ allRangeVersion = version;
1820
+ journalFloorVersion = version;
1821
+ changedRanges.length = 0;
1822
+ availableRanges.length = 0;
1823
+ });
1484
1824
  },
1485
1825
  async loadFrames(startTime, endTime) {
1486
1826
  assertActive();
@@ -1513,14 +1853,10 @@ function createWritableDetectionFrameSource(options) {
1513
1853
  if (!range) {
1514
1854
  return version;
1515
1855
  }
1516
- return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
1517
- ? Math.max(rangeVersion, changedRange.version)
1518
- : rangeVersion, Math.max(allRangeVersion, journalFloorVersion));
1856
+ return getRangeVersion(range);
1519
1857
  },
1520
1858
  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));
1859
+ const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion, getRangeVersion(range)), getReloadFloorVersion());
1524
1860
  if (relevantVersion <= previousVersion) {
1525
1861
  return {
1526
1862
  ranges: [],
@@ -1528,18 +1864,34 @@ function createWritableDetectionFrameSource(options) {
1528
1864
  version: relevantVersion,
1529
1865
  };
1530
1866
  }
1531
- if (previousVersion < allRangeVersion ||
1532
- previousVersion < journalFloorVersion) {
1867
+ if (previousVersion < getReloadFloorVersion()) {
1533
1868
  return {
1534
1869
  ranges: [],
1535
1870
  requiresReload: true,
1536
1871
  version: relevantVersion,
1537
1872
  };
1538
1873
  }
1874
+ // Clipped to the requested ranges: a consumer gets back exactly the part
1875
+ // of the change it asked about, and an unbounded invalidation stays a
1876
+ // bounded, loadable answer.
1539
1877
  const changedSourceRanges = changedRanges
1540
- .filter((changedRange) => changedRange.version > previousVersion &&
1541
- ranges.some((range) => rangesOverlap(range, changedRange)))
1542
- .map(({ endTime, startTime }) => ({ endTime, startTime }));
1878
+ .filter((changedRange) => changedRange.version > previousVersion)
1879
+ .flatMap((changedRange) => ranges
1880
+ .filter((range) => rangesOverlap(range, changedRange))
1881
+ .map((range) => {
1882
+ const startTime = Math.max(range.startTime, changedRange.startTime);
1883
+ const endTime = Math.min(range.endTime, changedRange.endTime);
1884
+ return {
1885
+ // An unbounded ask against an unbounded invalidation would
1886
+ // otherwise hand back an infinite range, and a consumer would
1887
+ // take it straight to `loadFrames`. Nothing past the coverage
1888
+ // the source knows about can be loaded.
1889
+ endTime: Number.isFinite(endTime)
1890
+ ? endTime
1891
+ : Math.max(startTime, getKnownEndTime()),
1892
+ startTime,
1893
+ };
1894
+ }));
1543
1895
  return {
1544
1896
  ranges: mergeRanges(changedSourceRanges),
1545
1897
  requiresReload: false,
@@ -1563,12 +1915,112 @@ function createWritableDetectionFrameSource(options) {
1563
1915
  throw createDestroyedError();
1564
1916
  }
1565
1917
  }
1918
+ /**
1919
+ * Oldest version the journal can still describe incrementally. Anything
1920
+ * older has to reload, because replacement, clearing, or journal compaction
1921
+ * dropped the intervening changes.
1922
+ */
1923
+ function getReloadFloorVersion() {
1924
+ return Math.max(allRangeVersion, journalFloorVersion);
1925
+ }
1926
+ /**
1927
+ * Latest media time the source can actually serve frames for.
1928
+ */
1929
+ function getKnownEndTime() {
1930
+ return Math.max(coverageFrontierTime ?? 0, summary?.endTime ?? 0);
1931
+ }
1932
+ function getRangeVersion(range) {
1933
+ return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
1934
+ ? Math.max(rangeVersion, changedRange.version)
1935
+ : rangeVersion, getReloadFloorVersion());
1936
+ }
1937
+ /**
1938
+ * Runs mutating writes one at a time.
1939
+ *
1940
+ * Every write reads the held live frame, the latest frame, and the coverage
1941
+ * frontier before it awaits storage. Without serialization two concurrent
1942
+ * live appends would both observe the pre-write state and both stay open,
1943
+ * which is exactly the stale overlay live semantics exist to prevent.
1944
+ */
1945
+ function enqueueWrite(run) {
1946
+ const result = writeQueue.then(() => {
1947
+ assertActive();
1948
+ return run();
1949
+ });
1950
+ writeQueue = result.then(() => undefined, () => undefined);
1951
+ return result;
1952
+ }
1953
+ function isStaleLiveFrame(frame) {
1954
+ if (liveFrontierTime === null || frame.mediaTime > liveFrontierTime) {
1955
+ return false;
1956
+ }
1957
+ // A frame at the frontier that shares the held frame's identity is a
1958
+ // revision of the current result, not a late duplicate of an older one.
1959
+ return !(heldLiveFrame && haveSameDetectionFrameIdentity(heldLiveFrame, frame));
1960
+ }
1961
+ function recordCoverageFrontier(endTime) {
1962
+ coverageFrontierTime =
1963
+ coverageFrontierTime === null
1964
+ ? endTime
1965
+ : Math.max(coverageFrontierTime, endTime);
1966
+ }
1967
+ function recordCoverageFrontierFromFrames(frames) {
1968
+ for (const frame of frames) {
1969
+ recordCoverageFrontier(getFrameEndTime(frame));
1970
+ }
1971
+ }
1972
+ function clipAvailableRangesAfter(endTime) {
1973
+ let writeIndex = 0;
1974
+ for (const range of availableRanges) {
1975
+ if (range.startTime >= endTime) {
1976
+ continue;
1977
+ }
1978
+ availableRanges[writeIndex] = {
1979
+ endTime: Math.min(range.endTime, endTime),
1980
+ startTime: range.startTime,
1981
+ };
1982
+ writeIndex += 1;
1983
+ }
1984
+ availableRanges.length = writeIndex;
1985
+ }
1986
+ function createEmptyWriteSummary() {
1987
+ return {
1988
+ chunkCount: 0,
1989
+ chunkDurationSeconds: options.chunkDurationSeconds ?? 1,
1990
+ datasetId: options.datasetId,
1991
+ detectionCount: 0,
1992
+ endTime: null,
1993
+ frameCount: 0,
1994
+ startTime: null,
1995
+ };
1996
+ }
1997
+ /**
1998
+ * Applies retention and records the result as an incremental change when the
1999
+ * store can prune in place, or as a full rewrite when it cannot.
2000
+ */
2001
+ async function retainAndRecord(nextSummary, changedSourceRanges, invalidation = NO_COVERAGE_INVALIDATION) {
2002
+ const retention = await applyRetention(nextSummary);
2003
+ assertActive();
2004
+ if (retention.kind === "rewritten") {
2005
+ return recordAllRangesWrite(retention.summary);
2006
+ }
2007
+ if (retention.kind === "unchanged") {
2008
+ if (changedSourceRanges.length === 0 &&
2009
+ invalidation.ranges.length === 0) {
2010
+ summary = nextSummary;
2011
+ return nextSummary;
2012
+ }
2013
+ return recordRangeWrite(nextSummary, changedSourceRanges, invalidation);
2014
+ }
2015
+ forgetFramesBefore(retention.prunedRange.endTime);
2016
+ return recordRangeWrite(retention.summary, changedSourceRanges, invalidation, retention.prunedRange);
2017
+ }
1566
2018
  async function applyRetention(nextSummary) {
1567
2019
  const retention = options.retention;
1568
2020
  if (retention === undefined ||
1569
2021
  !shouldApplyWindowRetention(retention) ||
1570
2022
  nextSummary.endTime === null) {
1571
- return nextSummary;
2023
+ return { kind: "unchanged" };
1572
2024
  }
1573
2025
  const retentionWindowSeconds = retention.windowSeconds;
1574
2026
  if (retentionWindowSeconds === undefined ||
@@ -1576,17 +2028,119 @@ function createWritableDetectionFrameSource(options) {
1576
2028
  retentionWindowSeconds <= 0) {
1577
2029
  throw new Error("retention.windowSeconds must be greater than 0.");
1578
2030
  }
1579
- const retentionStartTime = Math.max(0, nextSummary.endTime - retentionWindowSeconds);
2031
+ // Place the window against real producer coverage. A live frame held open
2032
+ // for `holdSeconds` reports a summary end far in the future, and anchoring
2033
+ // eviction there would evict everything the producer just wrote.
2034
+ const retentionStartTime = Math.max(0, Math.min(coverageFrontierTime ?? nextSummary.endTime, nextSummary.endTime) - retentionWindowSeconds);
1580
2035
  if (nextSummary.startTime !== null &&
1581
2036
  nextSummary.startTime + RANGE_EPSILON_SECONDS >= retentionStartTime) {
1582
- return nextSummary;
2037
+ return { kind: "unchanged" };
1583
2038
  }
2039
+ if (options.store.pruneFrames) {
2040
+ return {
2041
+ kind: "pruned",
2042
+ prunedRange: {
2043
+ endTime: retentionStartTime,
2044
+ startTime: nextSummary.startTime ?? 0,
2045
+ },
2046
+ summary: await options.store.pruneFrames({
2047
+ datasetId: options.datasetId,
2048
+ startTime: retentionStartTime,
2049
+ }),
2050
+ };
2051
+ }
2052
+ // Stores without in-place pruning still get a correct retention window,
2053
+ // at the cost of reloading and rewriting everything they keep.
1584
2054
  const retainedFrames = await options.store.loadFrames({
1585
2055
  datasetId: options.datasetId,
1586
2056
  endTime: nextSummary.endTime,
1587
2057
  startTime: retentionStartTime,
1588
2058
  });
1589
- return options.store.putFrames(writeOptions(retainedFrames));
2059
+ return {
2060
+ kind: "rewritten",
2061
+ summary: await options.store.putFrames(writeOptions(retainedFrames)),
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Builds the one or two frames a live append writes: the newest frame held
2066
+ * open, plus the previously held frame closed where the newest begins.
2067
+ */
2068
+ function createLiveFrameWrite(frame) {
2069
+ const openFrame = {
2070
+ ...frame,
2071
+ endTime: Math.max(frame.endTime ?? frame.mediaTime, frame.mediaTime + liveHoldSeconds),
2072
+ };
2073
+ const heldFrame = heldLiveFrame;
2074
+ // Stale results are rejected before this point, so a held frame that is
2075
+ // neither the same frame nor already closed always ends strictly later.
2076
+ if (!heldFrame ||
2077
+ haveSameDetectionFrameIdentity(heldFrame, openFrame) ||
2078
+ getFrameEndTime(heldFrame) <= frame.mediaTime) {
2079
+ return [openFrame];
2080
+ }
2081
+ return [{ ...heldFrame, endTime: frame.mediaTime }, openFrame];
2082
+ }
2083
+ function recordLatestFrame(frames) {
2084
+ for (const frame of frames) {
2085
+ if (!latestFrame ||
2086
+ compareDetectionFrames(frame, latestFrame) > 0 ||
2087
+ // A write that shares the tracked frame's identity replaces its stored
2088
+ // record. Keeping the superseded contents here would let a later
2089
+ // `finalizeCoverage` write them back over the revision.
2090
+ haveSameDetectionFrameIdentity(frame, latestFrame)) {
2091
+ latestFrame = frame;
2092
+ }
2093
+ }
2094
+ }
2095
+ /**
2096
+ * Coverage a write takes away from a frame the source already wrote.
2097
+ *
2098
+ * The journal describes a write by the interval it covers, which is enough
2099
+ * while writes only add. Rewriting a frame with a shorter interval, or at a
2100
+ * different time, instead stops covering times the new interval never
2101
+ * mentions, and a consumer parked in one of them would keep showing a frame
2102
+ * the source no longer selects.
2103
+ *
2104
+ * A frame without an `endTime` was selected with no upper bound, so what it
2105
+ * gives up is genuinely unbounded and is journaled that way. The journal
2106
+ * stays range-scoped either way: a consumer asking about times the write
2107
+ * never touched still hears that nothing changed.
2108
+ */
2109
+ function getInvalidatedCoverage(frames, coveredRanges) {
2110
+ const invalidatedRanges = [];
2111
+ for (const frame of frames) {
2112
+ for (const trackedFrame of [latestFrame, heldLiveFrame]) {
2113
+ if (!trackedFrame ||
2114
+ !haveSameDetectionFrameIdentity(trackedFrame, frame)) {
2115
+ continue;
2116
+ }
2117
+ const previousRange = {
2118
+ endTime: trackedFrame.endTime ?? Number.POSITIVE_INFINITY,
2119
+ startTime: trackedFrame.mediaTime,
2120
+ };
2121
+ const nextEndTime = frame.endTime ?? Number.POSITIVE_INFINITY;
2122
+ if (trackedFrame.mediaTime >= frame.mediaTime &&
2123
+ previousRange.endTime <= nextEndTime) {
2124
+ continue;
2125
+ }
2126
+ // A live append closes the held frame and reopens it further out in the
2127
+ // same write, so the write's own range already describes the tail. Only
2128
+ // coverage nothing else reported is worth a second journal entry.
2129
+ if (isRangeCovered(previousRange, coveredRanges)) {
2130
+ continue;
2131
+ }
2132
+ invalidatedRanges.push(previousRange);
2133
+ }
2134
+ }
2135
+ return { ranges: mergeRanges(invalidatedRanges) };
2136
+ }
2137
+ function forgetFramesBefore(startTime) {
2138
+ if (latestFrame && getFrameEndTime(latestFrame) < startTime) {
2139
+ latestFrame = null;
2140
+ }
2141
+ if (heldLiveFrame && getFrameEndTime(heldLiveFrame) < startTime) {
2142
+ heldLiveFrame = null;
2143
+ }
1590
2144
  }
1591
2145
  function resolveCoveredWaiters() {
1592
2146
  for (let index = waiters.length - 1; index >= 0; index -= 1) {
@@ -1610,6 +2164,43 @@ function createWritableDetectionFrameSource(options) {
1610
2164
  function createDestroyedError() {
1611
2165
  return new Error("Detection frame source has been destroyed.");
1612
2166
  }
2167
+ function resolveLiveHoldSeconds(holdSeconds) {
2168
+ if (holdSeconds === undefined) {
2169
+ return DEFAULT_LIVE_HOLD_SECONDS;
2170
+ }
2171
+ if (!Number.isFinite(holdSeconds) || holdSeconds <= 0) {
2172
+ throw new RangeError("live.holdSeconds must be greater than 0.");
2173
+ }
2174
+ return holdSeconds;
2175
+ }
2176
+ function getFrameEndTime(frame) {
2177
+ return frame.endTime ?? frame.mediaTime;
2178
+ }
2179
+ function haveSameDetectionFrameIdentity(left, right) {
2180
+ return left.frameIndex === undefined || right.frameIndex === undefined
2181
+ ? left.frameIndex === right.frameIndex && left.mediaTime === right.mediaTime
2182
+ : left.frameIndex === right.frameIndex;
2183
+ }
2184
+ function compareDetectionFrames(left, right) {
2185
+ if (left.mediaTime !== right.mediaTime) {
2186
+ return left.mediaTime - right.mediaTime;
2187
+ }
2188
+ return (left.frameIndex ?? 0) - (right.frameIndex ?? 0);
2189
+ }
2190
+ function dropAvailableRangesBefore(startTime, availableRanges) {
2191
+ let writeIndex = 0;
2192
+ for (const range of availableRanges) {
2193
+ if (range.endTime <= startTime) {
2194
+ continue;
2195
+ }
2196
+ availableRanges[writeIndex] = {
2197
+ endTime: range.endTime,
2198
+ startTime: Math.max(range.startTime, startTime),
2199
+ };
2200
+ writeIndex += 1;
2201
+ }
2202
+ availableRanges.length = writeIndex;
2203
+ }
1613
2204
  function shouldApplyWindowRetention(retention) {
1614
2205
  return (retention?.mode === DetectionFrameRetentionMode.PersistWindow ||
1615
2206
  retention?.mode === DetectionFrameRetentionMode.MemoryOnly);
@@ -6337,6 +6928,30 @@ var MediaRendererPlaybackState;
6337
6928
  MediaRendererPlaybackState["Error"] = "error";
6338
6929
  MediaRendererPlaybackState["Destroyed"] = "destroyed";
6339
6930
  })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));
6931
+ /**
6932
+ * Stable classification of a media failure.
6933
+ *
6934
+ * Applications branch on these kinds instead of parsing decoder, demuxer, or
6935
+ * container error text, and still own their localized user-facing copy. New
6936
+ * kinds may be added over time, so treat unrecognized values like `Unknown`.
6937
+ */
6938
+ var MediaErrorKind;
6939
+ (function (MediaErrorKind) {
6940
+ /** The media could not be opened or read at all. */
6941
+ MediaErrorKind["Unreadable"] = "unreadable";
6942
+ /** The container or codec is not supported by this platform. */
6943
+ MediaErrorKind["UnsupportedFormat"] = "unsupportedFormat";
6944
+ /** The media opened but carries no usable video track. */
6945
+ MediaErrorKind["NoVideoTrack"] = "noVideoTrack";
6946
+ /** Decoding a sample failed after the source opened. */
6947
+ MediaErrorKind["Decode"] = "decode";
6948
+ /** The media could not be fetched. */
6949
+ MediaErrorKind["Network"] = "network";
6950
+ /** The host environment lacks the APIs this media source requires. */
6951
+ MediaErrorKind["EnvironmentUnsupported"] = "environmentUnsupported";
6952
+ /** The failure could not be classified. */
6953
+ MediaErrorKind["Unknown"] = "unknown";
6954
+ })(MediaErrorKind || (MediaErrorKind = {}));
6340
6955
  /**
6341
6956
  * Lower-level media source readiness.
6342
6957
  */
@@ -6405,5 +7020,5 @@ var MediaSessionActivityStatus;
6405
7020
  MediaSessionActivityStatus["Waiting"] = "waiting";
6406
7021
  })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
6407
7022
 
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 };
7023
+ 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
7024
  //# sourceMappingURL=index.js.map