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