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.
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +437 -49
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +24 -0
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/media-errors.d.ts +32 -0
- package/dist/media/media-errors.d.ts.map +1 -0
- package/dist/media/media-source-state.d.ts.map +1 -1
- package/dist/media/media-stream-media-source.d.ts +31 -0
- package/dist/media/media-stream-media-source.d.ts.map +1 -1
- package/dist/media/mediabunny-media-source.d.ts.map +1 -1
- package/dist/media/presentation-timeline-media-source.d.ts +17 -0
- package/dist/media/presentation-timeline-media-source.d.ts.map +1 -0
- package/dist/media/static-image-media-source.d.ts.map +1 -1
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-state.d.ts.map +1 -1
- package/dist/sessions/media-session-detections.d.ts.map +1 -1
- package/dist/sessions/media-session.d.ts +2 -2
- package/dist/sessions/media-session.d.ts.map +1 -1
- package/dist/tracking.worker.js +24 -0
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-session.d.ts +57 -1
- package/dist/types/media-session.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts +22 -0
- package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +2 -7
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.d.ts +5 -3
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +678 -63
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +113 -3
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detections.d.ts +22 -0
- package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +31 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts +20 -0
- package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts.map +1 -0
- 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
|
|
926
|
-
|
|
927
|
-
frames
|
|
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
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
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 (
|
|
1458
|
-
|
|
1751
|
+
if (!Number.isFinite(endTime)) {
|
|
1752
|
+
throw new RangeError("finalizeCoverage requires a finite endTime.");
|
|
1459
1753
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
|
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,
|
|
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 <
|
|
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
|
-
|
|
1542
|
-
.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|