supervision 0.1.6 → 0.2.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detections/chunked-detection-frame-source.d.ts.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8969 -4550
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +310 -164
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/display-pixel-ratio.d.ts +15 -0
- package/dist/media/display-pixel-ratio.d.ts.map +1 -0
- package/dist/media/engine-import-failure.d.ts +16 -0
- package/dist/media/engine-import-failure.d.ts.map +1 -0
- package/dist/media/media-condition-probe.d.ts +51 -0
- package/dist/media/media-condition-probe.d.ts.map +1 -0
- package/dist/media/media-conditions.d.ts +28 -0
- package/dist/media/media-conditions.d.ts.map +1 -0
- package/dist/media/media-errors.d.ts +6 -0
- package/dist/media/media-errors.d.ts.map +1 -1
- package/dist/media/media-normalization.d.ts.map +1 -1
- package/dist/media/media-probe.d.ts.map +1 -1
- package/dist/media/media-source.d.ts +17 -0
- package/dist/media/media-source.d.ts.map +1 -1
- package/dist/media/mediabunny-media-source.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.d.ts +44 -0
- package/dist/media/video-engine-media-source.d.ts.map +1 -0
- package/dist/render-preparation/mask-frame-artifact.d.ts +46 -12
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts +5 -5
- package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -1
- package/dist/render-preparation/mask-preparation-worker-count.d.ts +4 -4
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +12 -3
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
- package/dist/render-preparation/prepared-render-window.d.ts +23 -0
- package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
- package/dist/render-preparation/prepared-window-timeline.d.ts +2 -6
- package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
- package/dist/renderers/mask-palette.d.ts +14 -0
- package/dist/renderers/mask-palette.d.ts.map +1 -0
- package/dist/renderers/mask-vertex.d.ts +14 -0
- package/dist/renderers/mask-vertex.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +37 -0
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/media-renderer-state.d.ts +11 -1
- package/dist/renderers/media-renderer-state.d.ts.map +1 -1
- package/dist/renderers/media-renderer-transport.d.ts +59 -0
- package/dist/renderers/media-renderer-transport.d.ts.map +1 -0
- 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-box-layer.d.ts +5 -0
- package/dist/renderers/pixi-box-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-focus-layer.d.ts +18 -2
- package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-frame-present.d.ts +77 -0
- package/dist/renderers/pixi-frame-present.d.ts.map +1 -0
- package/dist/renderers/pixi-id-mask-shader.d.ts +12 -2
- package/dist/renderers/pixi-id-mask-shader.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 +13 -2
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-halo.d.ts +24 -0
- package/dist/renderers/pixi-mask-halo.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-layer.d.ts +71 -8
- package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-media-scene.d.ts +67 -0
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-polygon-layer.d.ts +7 -2
- package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-region-coverage-mask.d.ts +91 -0
- package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -0
- package/dist/renderers/pixi-region-effect.d.ts +42 -0
- package/dist/renderers/pixi-region-effect.d.ts.map +1 -0
- package/dist/renderers/pixi-region-layer.d.ts +68 -2
- package/dist/renderers/pixi-region-layer.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/renderers/prepared-annotation-window.d.ts +47 -0
- package/dist/renderers/prepared-annotation-window.d.ts.map +1 -0
- package/dist/renderers/presented-frame-channel.d.ts +107 -0
- package/dist/renderers/presented-frame-channel.d.ts.map +1 -0
- package/dist/renderers/scene-render-scheduler.d.ts +20 -0
- package/dist/renderers/scene-render-scheduler.d.ts.map +1 -0
- package/dist/sessions/media-session-defaults.d.ts +10 -6
- package/dist/sessions/media-session-defaults.d.ts.map +1 -1
- package/dist/sessions/media-session-media.d.ts.map +1 -1
- package/dist/sessions/media-session-state.d.ts.map +1 -1
- package/dist/sessions/media-session.d.ts.map +1 -1
- package/dist/tracking.worker.js +58 -3
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-conditions.d.ts +149 -0
- package/dist/types/media-conditions.d.ts.map +1 -0
- package/dist/types/media-normalization.d.ts +34 -0
- package/dist/types/media-normalization.d.ts.map +1 -1
- package/dist/types/media-renderer.d.ts +31 -1
- package/dist/types/media-renderer.d.ts.map +1 -1
- package/dist/types/media-session.d.ts +89 -8
- package/dist/types/media-session.d.ts.map +1 -1
- package/dist/types/render-preparation.d.ts +101 -4
- package/dist/types/render-preparation.d.ts.map +1 -1
- package/dist/web-video-engine/analysis-session.d.ts +76 -0
- package/dist/web-video-engine/analysis-session.d.ts.map +1 -0
- package/dist/web-video-engine/analysis.d.ts +7 -0
- package/dist/web-video-engine/analysis.d.ts.map +1 -0
- package/dist/web-video-engine/analysis.js +1678 -0
- package/dist/web-video-engine/analysis.js.map +1 -0
- package/dist/web-video-engine/cache-budget.d.ts +19 -0
- package/dist/web-video-engine/cache-budget.d.ts.map +1 -0
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +90 -0
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts.map +1 -0
- package/dist/web-video-engine/clock.d.ts +83 -0
- package/dist/web-video-engine/clock.d.ts.map +1 -0
- package/dist/web-video-engine/constants.d.ts +253 -0
- package/dist/web-video-engine/constants.d.ts.map +1 -0
- package/dist/web-video-engine/create-scrub-cursor.d.ts +62 -0
- package/dist/web-video-engine/create-scrub-cursor.d.ts.map +1 -0
- package/dist/web-video-engine/decode-resolution.d.ts +115 -0
- package/dist/web-video-engine/decode-resolution.d.ts.map +1 -0
- package/dist/web-video-engine/decode-scheduler.d.ts +356 -0
- package/dist/web-video-engine/decode-scheduler.d.ts.map +1 -0
- package/dist/web-video-engine/decode-session.d.ts +276 -0
- package/dist/web-video-engine/decode-session.d.ts.map +1 -0
- package/dist/web-video-engine/decode-source.d.ts +250 -0
- package/dist/web-video-engine/decode-source.d.ts.map +1 -0
- package/dist/web-video-engine/diagnostics-store.d.ts +18 -0
- package/dist/web-video-engine/diagnostics-store.d.ts.map +1 -0
- package/dist/web-video-engine/diagnostics.d.ts +316 -0
- package/dist/web-video-engine/diagnostics.d.ts.map +1 -0
- package/dist/web-video-engine/embedded-engine-worker.d.ts +2 -0
- package/dist/web-video-engine/embedded-engine-worker.d.ts.map +1 -0
- package/dist/web-video-engine/engine-core.d.ts +258 -0
- package/dist/web-video-engine/engine-core.d.ts.map +1 -0
- package/dist/web-video-engine/engine.d.ts +26 -0
- package/dist/web-video-engine/engine.d.ts.map +1 -0
- package/dist/web-video-engine/engine.js +873 -0
- package/dist/web-video-engine/engine.js.map +1 -0
- package/dist/web-video-engine/engine.worker.d.ts +2 -0
- package/dist/web-video-engine/engine.worker.d.ts.map +1 -0
- package/dist/web-video-engine/engine.worker.js +31320 -0
- package/dist/web-video-engine/engine.worker.js.map +1 -0
- package/dist/web-video-engine/frame-cache.d.ts +129 -0
- package/dist/web-video-engine/frame-cache.d.ts.map +1 -0
- package/dist/web-video-engine/frame-extractor.d.ts +28 -0
- package/dist/web-video-engine/frame-extractor.d.ts.map +1 -0
- package/dist/web-video-engine/frame-timeline-GINI8Gum.js +560 -0
- package/dist/web-video-engine/frame-timeline-GINI8Gum.js.map +1 -0
- package/dist/web-video-engine/frame-timeline.d.ts +71 -0
- package/dist/web-video-engine/frame-timeline.d.ts.map +1 -0
- package/dist/web-video-engine/frame-walker.d.ts +74 -0
- package/dist/web-video-engine/frame-walker.d.ts.map +1 -0
- package/dist/web-video-engine/index.d.ts +3 -0
- package/dist/web-video-engine/index.d.ts.map +1 -0
- package/dist/web-video-engine/index.js +3 -0
- package/dist/web-video-engine/index.js.map +1 -0
- package/dist/web-video-engine/key-packet.d.ts +65 -0
- package/dist/web-video-engine/key-packet.d.ts.map +1 -0
- package/dist/web-video-engine/keyframe-index.d.ts +91 -0
- package/dist/web-video-engine/keyframe-index.d.ts.map +1 -0
- package/dist/web-video-engine/mirror-store.d.ts +56 -0
- package/dist/web-video-engine/mirror-store.d.ts.map +1 -0
- package/dist/web-video-engine/renderer.d.ts +94 -0
- package/dist/web-video-engine/renderer.d.ts.map +1 -0
- package/dist/web-video-engine/rotation.d.ts +35 -0
- package/dist/web-video-engine/rotation.d.ts.map +1 -0
- package/dist/web-video-engine/scrub-controller.d.ts +376 -0
- package/dist/web-video-engine/scrub-controller.d.ts.map +1 -0
- package/dist/web-video-engine/scrub-cursor.d.ts +342 -0
- package/dist/web-video-engine/scrub-cursor.d.ts.map +1 -0
- package/dist/web-video-engine/scrub-trajectory.d.ts +30 -0
- package/dist/web-video-engine/scrub-trajectory.d.ts.map +1 -0
- package/dist/web-video-engine/source-residency.d.ts +57 -0
- package/dist/web-video-engine/source-residency.d.ts.map +1 -0
- package/dist/web-video-engine/trace-recorder.d.ts +164 -0
- package/dist/web-video-engine/trace-recorder.d.ts.map +1 -0
- package/dist/web-video-engine/types.d.ts +204 -0
- package/dist/web-video-engine/types.d.ts.map +1 -0
- package/dist/web-video-engine/video-engine.d.ts +426 -0
- package/dist/web-video-engine/video-engine.d.ts.map +1 -0
- package/dist/web-video-engine/webgpu-renderer.d.ts +62 -0
- package/dist/web-video-engine/webgpu-renderer.d.ts.map +1 -0
- package/dist/web-video-engine/worker-bridge.d.ts +12 -0
- package/dist/web-video-engine/worker-bridge.d.ts.map +1 -0
- package/dist/web-video-engine/worker-dispatch.d.ts +17 -0
- package/dist/web-video-engine/worker-dispatch.d.ts.map +1 -0
- package/dist/web-video-engine/worker-protocol.d.ts +267 -0
- package/dist/web-video-engine/worker-protocol.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.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/index.d.ts +4 -3
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +1199 -294
- 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/styles/default-annotation-presentation.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts +4 -36
- package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts +5 -0
- package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts +92 -7
- package/node_modules/supervision-js-core/dist/types/annotation-renderer.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +179 -17
- 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 +7 -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 +78 -2
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts +2 -0
- package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts +16 -0
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts +5 -0
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts +9 -0
- package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-ranges.d.ts +18 -0
- package/node_modules/supervision-js-core/dist/utils/detection-ranges.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +15 -2
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/wait-bound.d.ts +7 -0
- package/node_modules/supervision-js-core/dist/utils/wait-bound.d.ts.map +1 -0
- package/package.json +22 -1
|
@@ -14,6 +14,12 @@ var DetectionBufferStatus;
|
|
|
14
14
|
(function (DetectionBufferStatus) {
|
|
15
15
|
DetectionBufferStatus["Idle"] = "idle";
|
|
16
16
|
DetectionBufferStatus["Loading"] = "loading";
|
|
17
|
+
/**
|
|
18
|
+
* The playback gate is holding playback until the source produces detections
|
|
19
|
+
* for the requested range. {@link DetectionBufferStatus.Loading} fetches
|
|
20
|
+
* detections the source already has; this one waits on a producer.
|
|
21
|
+
*/
|
|
22
|
+
DetectionBufferStatus["AwaitingCoverage"] = "awaitingCoverage";
|
|
17
23
|
DetectionBufferStatus["Ready"] = "ready";
|
|
18
24
|
DetectionBufferStatus["Error"] = "error";
|
|
19
25
|
DetectionBufferStatus["Destroyed"] = "destroyed";
|
|
@@ -26,12 +32,30 @@ var DetectionFrameSelectionMode;
|
|
|
26
32
|
/**
|
|
27
33
|
* Select the frame whose `[mediaTime, endTime)` interval contains the media
|
|
28
34
|
* time. This is the default for interval annotations and timestamped sources.
|
|
35
|
+
*
|
|
36
|
+
* Frame times must come from the media itself. Times reconstructed from a
|
|
37
|
+
* nominal frame rate drift against a clip whose real rate differs, and once
|
|
38
|
+
* that drift passes the half-millisecond selection tolerance a playhead
|
|
39
|
+
* landing on a frame boundary selects the previous frame's detections.
|
|
40
|
+
*
|
|
41
|
+
* A frame that carries no `endTime` stays active until the next frame starts,
|
|
42
|
+
* which bridges an index the source never produced.
|
|
43
|
+
* {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to
|
|
44
|
+
* one grid step instead, so a missing index reads as no detections.
|
|
29
45
|
*/
|
|
30
46
|
DetectionFrameSelectionMode["Interval"] = "interval";
|
|
31
47
|
/**
|
|
32
|
-
* Select from a known inference frame grid using `frameIndex
|
|
33
|
-
*
|
|
34
|
-
*
|
|
48
|
+
* Select from a known inference frame grid using `frameIndex` and
|
|
49
|
+
* `frameRate`. This is useful when inference was run on normalized frames and
|
|
50
|
+
* playback should snap detections to that grid.
|
|
51
|
+
*
|
|
52
|
+
* A frame speaks for one grid step starting at its own media time, and an
|
|
53
|
+
* index the source never produced selects nothing rather than a neighbour.
|
|
54
|
+
*
|
|
55
|
+
* A frame carrying no `frameIndex` speaks for its step on the same terms,
|
|
56
|
+
* reached by the time it starts at rather than by an index that names it. Its
|
|
57
|
+
* `endTime` does not widen it past that step, so a source that labels only
|
|
58
|
+
* part of what it writes cannot bridge the indexes it never produced.
|
|
35
59
|
*/
|
|
36
60
|
DetectionFrameSelectionMode["NearestFrameIndex"] = "nearestFrameIndex";
|
|
37
61
|
})(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));
|
|
@@ -54,17 +78,37 @@ var DetectionFrameRetentionMode;
|
|
|
54
78
|
DetectionFrameRetentionMode["PersistWindow"] = "persistWindow";
|
|
55
79
|
})(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));
|
|
56
80
|
|
|
81
|
+
/**
|
|
82
|
+
* A playhead compared by time can arrive perturbed by whatever plane it crossed
|
|
83
|
+
* to get here: a producer that publishes whole milliseconds reports a source
|
|
84
|
+
* frame at 5.033333s as 5.033s, and even an exact producer loses a bit to a
|
|
85
|
+
* seconds-to-milliseconds-and-back trip. A frame starting within that reach may
|
|
86
|
+
* already be on screen while one starting past it cannot.
|
|
87
|
+
*
|
|
88
|
+
* Selection by identity carries none of this, which is why it lives in its own
|
|
89
|
+
* function and not in a smaller number here.
|
|
90
|
+
*/
|
|
91
|
+
const PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS = 0.0005;
|
|
57
92
|
function copySortedDetectionFrames(detectionFrames) {
|
|
58
93
|
validateDetectionFrames(detectionFrames ?? []);
|
|
59
94
|
return (detectionFrames ?? [])
|
|
60
|
-
.map(
|
|
95
|
+
.map(copyDetectionFrame)
|
|
96
|
+
.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One frame's deep copy, for a caller that already knows which frames it is
|
|
100
|
+
* keeping and only needs the ones it does not hold.
|
|
101
|
+
*/
|
|
102
|
+
function copyDetectionFrame(frame) {
|
|
103
|
+
return {
|
|
61
104
|
detections: frame.detections.map((detection) => ({
|
|
62
105
|
...detection,
|
|
63
|
-
attributes: detection.attributes
|
|
64
|
-
? [...detection.attributes]
|
|
65
|
-
: undefined,
|
|
106
|
+
attributes: detection.attributes ? [...detection.attributes] : undefined,
|
|
66
107
|
keypoints: detection.keypoints
|
|
67
108
|
? {
|
|
109
|
+
boxRelative: detection.keypoints.boxRelative
|
|
110
|
+
? [...detection.keypoints.boxRelative]
|
|
111
|
+
: undefined,
|
|
68
112
|
edges: detection.keypoints.edges.map((edge) => [edge[0], edge[1]]),
|
|
69
113
|
points: detection.keypoints.points.map((point) => ({ ...point })),
|
|
70
114
|
visibility: detection.keypoints.visibility
|
|
@@ -94,16 +138,56 @@ function copySortedDetectionFrames(detectionFrames) {
|
|
|
94
138
|
endTime: frame.endTime,
|
|
95
139
|
frameIndex: frame.frameIndex,
|
|
96
140
|
mediaTime: frame.mediaTime,
|
|
97
|
-
}
|
|
98
|
-
.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
141
|
+
};
|
|
99
142
|
}
|
|
143
|
+
/** Returned by the plain copier when it meets a value only structuredClone can
|
|
144
|
+
* carry, so the caller pays for that only when the metadata needs it. */
|
|
145
|
+
const NOT_PLAIN = Symbol("not-plain-metadata");
|
|
100
146
|
function copyDetectionMetadata(metadata) {
|
|
147
|
+
// Detection metadata is almost always a handful of strings and numbers, and a
|
|
148
|
+
// window rebuild copies it once per detection, tens of thousands of times a
|
|
149
|
+
// second while scrubbing. structuredClone serialises; walking the object does
|
|
150
|
+
// not, so it is the fallback rather than the default.
|
|
151
|
+
const plain = copyPlainMetadata(metadata);
|
|
152
|
+
if (plain !== NOT_PLAIN) {
|
|
153
|
+
return plain;
|
|
154
|
+
}
|
|
101
155
|
const clone = globalThis.structuredClone;
|
|
102
156
|
if (clone) {
|
|
103
157
|
return clone(metadata);
|
|
104
158
|
}
|
|
105
159
|
return copyMetadataValue(metadata);
|
|
106
160
|
}
|
|
161
|
+
function copyPlainMetadata(value) {
|
|
162
|
+
if (value === null || typeof value !== "object") {
|
|
163
|
+
return typeof value === "function" || typeof value === "symbol"
|
|
164
|
+
? NOT_PLAIN
|
|
165
|
+
: value;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
const copied = [];
|
|
169
|
+
for (const entry of value) {
|
|
170
|
+
const child = copyPlainMetadata(entry);
|
|
171
|
+
if (child === NOT_PLAIN) {
|
|
172
|
+
return NOT_PLAIN;
|
|
173
|
+
}
|
|
174
|
+
copied.push(child);
|
|
175
|
+
}
|
|
176
|
+
return copied;
|
|
177
|
+
}
|
|
178
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
179
|
+
return NOT_PLAIN;
|
|
180
|
+
}
|
|
181
|
+
const copied = {};
|
|
182
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
183
|
+
const child = copyPlainMetadata(entry);
|
|
184
|
+
if (child === NOT_PLAIN) {
|
|
185
|
+
return NOT_PLAIN;
|
|
186
|
+
}
|
|
187
|
+
copied[key] = child;
|
|
188
|
+
}
|
|
189
|
+
return copied;
|
|
190
|
+
}
|
|
107
191
|
function copyMetadataValue(value) {
|
|
108
192
|
if (Array.isArray(value)) {
|
|
109
193
|
return value.map(copyMetadataValue);
|
|
@@ -117,95 +201,85 @@ function copyMetadataValue(value) {
|
|
|
117
201
|
return value;
|
|
118
202
|
}
|
|
119
203
|
function validateDetectionFrames(detectionFrames) {
|
|
120
|
-
for (
|
|
121
|
-
|
|
204
|
+
for (let frameOffset = 0; frameOffset < detectionFrames.length; frameOffset += 1) {
|
|
205
|
+
const frame = detectionFrames[frameOffset];
|
|
206
|
+
const framePath = () => `frames[${frameOffset}]`;
|
|
207
|
+
validateNumber(frame.mediaTime, () => `${framePath()}.mediaTime`, {
|
|
122
208
|
min: 0,
|
|
123
209
|
});
|
|
124
210
|
if (frame.endTime !== undefined) {
|
|
125
|
-
validateNumber(frame.endTime,
|
|
211
|
+
validateNumber(frame.endTime, () => `${framePath()}.endTime`, {
|
|
126
212
|
exclusiveMin: frame.mediaTime,
|
|
127
213
|
});
|
|
128
214
|
}
|
|
129
215
|
if (frame.frameIndex !== undefined) {
|
|
130
|
-
validateNumber(frame.frameIndex,
|
|
216
|
+
validateNumber(frame.frameIndex, () => `${framePath()}.frameIndex`, {
|
|
131
217
|
integer: true,
|
|
132
218
|
min: 0,
|
|
133
219
|
});
|
|
134
220
|
}
|
|
135
221
|
if (frame.coordinateSpace !== undefined) {
|
|
136
|
-
validateNumber(frame.coordinateSpace.width,
|
|
137
|
-
validateNumber(frame.coordinateSpace.height,
|
|
222
|
+
validateNumber(frame.coordinateSpace.width, () => `${framePath()}.coordinateSpace.width`, { exclusiveMin: 0 });
|
|
223
|
+
validateNumber(frame.coordinateSpace.height, () => `${framePath()}.coordinateSpace.height`, { exclusiveMin: 0 });
|
|
138
224
|
}
|
|
139
|
-
for (
|
|
140
|
-
const
|
|
225
|
+
for (let detectionOffset = 0; detectionOffset < frame.detections.length; detectionOffset += 1) {
|
|
226
|
+
const detection = frame.detections[detectionOffset];
|
|
227
|
+
const detectionPath = () => `${framePath()}.detections[${detectionOffset}]`;
|
|
141
228
|
if (detection.confidence !== undefined) {
|
|
142
|
-
validateNumber(detection.confidence, `${detectionPath}.confidence`, {
|
|
143
|
-
max: 1,
|
|
144
|
-
min: 0,
|
|
145
|
-
});
|
|
229
|
+
validateNumber(detection.confidence, () => `${detectionPath()}.confidence`, { max: 1, min: 0 });
|
|
146
230
|
}
|
|
147
231
|
if (detection.zIndex !== undefined) {
|
|
148
|
-
validateNumber(detection.zIndex, `${detectionPath}.zIndex`);
|
|
232
|
+
validateNumber(detection.zIndex, () => `${detectionPath()}.zIndex`);
|
|
149
233
|
}
|
|
150
234
|
if (detection.trackerId !== undefined) {
|
|
151
|
-
validateNumber(detection.trackerId, `${detectionPath}.trackerId`, {
|
|
152
|
-
integer: true,
|
|
153
|
-
min: 0,
|
|
154
|
-
});
|
|
235
|
+
validateNumber(detection.trackerId, () => `${detectionPath()}.trackerId`, { integer: true, min: 0 });
|
|
155
236
|
}
|
|
156
237
|
if (detection.sourceId !== undefined &&
|
|
157
238
|
typeof detection.sourceId !== "string") {
|
|
158
|
-
throw new Error(`${detectionPath}.sourceId must be a string.`);
|
|
239
|
+
throw new Error(`${detectionPath()}.sourceId must be a string.`);
|
|
159
240
|
}
|
|
160
241
|
if (detection.sourceDetectionIndex !== undefined) {
|
|
161
|
-
validateNumber(detection.sourceDetectionIndex, `${detectionPath}.sourceDetectionIndex`, {
|
|
242
|
+
validateNumber(detection.sourceDetectionIndex, () => `${detectionPath()}.sourceDetectionIndex`, {
|
|
162
243
|
integer: true,
|
|
163
244
|
min: 0,
|
|
164
245
|
});
|
|
165
246
|
}
|
|
166
247
|
if (detection.rect) {
|
|
167
|
-
validateNumber(detection.rect.x, `${detectionPath}.rect.x`);
|
|
168
|
-
validateNumber(detection.rect.y, `${detectionPath}.rect.y`);
|
|
169
|
-
validateNumber(detection.rect.width, `${detectionPath}.rect.width`, {
|
|
170
|
-
|
|
171
|
-
});
|
|
172
|
-
validateNumber(detection.rect.height, `${detectionPath}.rect.height`, {
|
|
173
|
-
exclusiveMin: 0,
|
|
174
|
-
});
|
|
248
|
+
validateNumber(detection.rect.x, () => `${detectionPath()}.rect.x`);
|
|
249
|
+
validateNumber(detection.rect.y, () => `${detectionPath()}.rect.y`);
|
|
250
|
+
validateNumber(detection.rect.width, () => `${detectionPath()}.rect.width`, { exclusiveMin: 0 });
|
|
251
|
+
validateNumber(detection.rect.height, () => `${detectionPath()}.rect.height`, { exclusiveMin: 0 });
|
|
175
252
|
}
|
|
176
|
-
validatePoints(detection.polygon?.points, `${detectionPath}.polygon`, 3);
|
|
177
|
-
validatePoints(detection.polyline?.points, `${detectionPath}.polyline`, 2);
|
|
253
|
+
validatePoints(detection.polygon?.points, () => `${detectionPath()}.polygon`, 3);
|
|
254
|
+
validatePoints(detection.polyline?.points, () => `${detectionPath()}.polyline`, 2);
|
|
178
255
|
if (detection.keypoints) {
|
|
179
|
-
validatePoints(detection.keypoints.points, `${detectionPath}.keypoints`, 1);
|
|
256
|
+
validatePoints(detection.keypoints.points, () => `${detectionPath()}.keypoints`, 1);
|
|
180
257
|
if (detection.keypoints.visibility !== undefined &&
|
|
181
258
|
detection.keypoints.visibility.length !==
|
|
182
259
|
detection.keypoints.points.length) {
|
|
183
|
-
throw new Error(`${detectionPath}.keypoints.visibility must match points length.`);
|
|
260
|
+
throw new Error(`${detectionPath()}.keypoints.visibility must match points length.`);
|
|
184
261
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
262
|
+
const edges = detection.keypoints.edges;
|
|
263
|
+
for (let edgeOffset = 0; edgeOffset < edges.length; edgeOffset += 1) {
|
|
264
|
+
const edge = edges[edgeOffset];
|
|
265
|
+
const edgePath = () => `${detectionPath()}.keypoints.edges[${edgeOffset}]`;
|
|
266
|
+
for (let endpointOffset = 0; endpointOffset < edge.length; endpointOffset += 1) {
|
|
267
|
+
const endpoint = edge[endpointOffset];
|
|
268
|
+
validateNumber(endpoint, () => `${edgePath()}[${endpointOffset}]`, {
|
|
189
269
|
integer: true,
|
|
190
270
|
min: 0,
|
|
191
271
|
});
|
|
192
272
|
if (endpoint >= detection.keypoints.points.length) {
|
|
193
|
-
throw new Error(`${edgePath}[${endpointOffset}] is out of range.`);
|
|
273
|
+
throw new Error(`${edgePath()}[${endpointOffset}] is out of range.`);
|
|
194
274
|
}
|
|
195
275
|
}
|
|
196
276
|
}
|
|
197
277
|
}
|
|
198
278
|
if (detection.mask) {
|
|
199
|
-
validateNumber(detection.mask.width, `${detectionPath}.mask.width`, {
|
|
200
|
-
|
|
201
|
-
exclusiveMin: 0,
|
|
202
|
-
});
|
|
203
|
-
validateNumber(detection.mask.height, `${detectionPath}.mask.height`, {
|
|
204
|
-
integer: true,
|
|
205
|
-
exclusiveMin: 0,
|
|
206
|
-
});
|
|
279
|
+
validateNumber(detection.mask.width, () => `${detectionPath()}.mask.width`, { integer: true, exclusiveMin: 0 });
|
|
280
|
+
validateNumber(detection.mask.height, () => `${detectionPath()}.mask.height`, { integer: true, exclusiveMin: 0 });
|
|
207
281
|
if (detection.mask.counts.length === 0) {
|
|
208
|
-
throw new Error(`${detectionPath}.mask.counts must not be empty.`);
|
|
282
|
+
throw new Error(`${detectionPath()}.mask.counts must not be empty.`);
|
|
209
283
|
}
|
|
210
284
|
}
|
|
211
285
|
}
|
|
@@ -216,28 +290,30 @@ function validatePoints(points, path, minimumLength) {
|
|
|
216
290
|
return;
|
|
217
291
|
}
|
|
218
292
|
if (points.length < minimumLength) {
|
|
219
|
-
throw new Error(`${path}.points must contain at least ${minimumLength} points.`);
|
|
293
|
+
throw new Error(`${path()}.points must contain at least ${minimumLength} points.`);
|
|
220
294
|
}
|
|
221
|
-
for (
|
|
222
|
-
|
|
223
|
-
validateNumber(point.
|
|
295
|
+
for (let pointOffset = 0; pointOffset < points.length; pointOffset += 1) {
|
|
296
|
+
const point = points[pointOffset];
|
|
297
|
+
validateNumber(point.x, () => `${path()}.points[${pointOffset}].x`);
|
|
298
|
+
validateNumber(point.y, () => `${path()}.points[${pointOffset}].y`);
|
|
224
299
|
}
|
|
225
300
|
}
|
|
226
|
-
|
|
301
|
+
const NO_BOUNDS = {};
|
|
302
|
+
function validateNumber(value, path, options = NO_BOUNDS) {
|
|
227
303
|
if (!Number.isFinite(value)) {
|
|
228
|
-
throw new Error(`${path} must be a finite number.`);
|
|
304
|
+
throw new Error(`${path()} must be a finite number.`);
|
|
229
305
|
}
|
|
230
306
|
if (options.integer && !Number.isInteger(value)) {
|
|
231
|
-
throw new Error(`${path} must be an integer.`);
|
|
307
|
+
throw new Error(`${path()} must be an integer.`);
|
|
232
308
|
}
|
|
233
309
|
if (options.min !== undefined && value < options.min) {
|
|
234
|
-
throw new Error(`${path} must be greater than or equal to ${options.min}.`);
|
|
310
|
+
throw new Error(`${path()} must be greater than or equal to ${options.min}.`);
|
|
235
311
|
}
|
|
236
312
|
if (options.exclusiveMin !== undefined && value <= options.exclusiveMin) {
|
|
237
|
-
throw new Error(`${path} must be greater than ${options.exclusiveMin}.`);
|
|
313
|
+
throw new Error(`${path()} must be greater than ${options.exclusiveMin}.`);
|
|
238
314
|
}
|
|
239
315
|
if (options.max !== undefined && value > options.max) {
|
|
240
|
-
throw new Error(`${path} must be less than or equal to ${options.max}.`);
|
|
316
|
+
throw new Error(`${path()} must be less than or equal to ${options.max}.`);
|
|
241
317
|
}
|
|
242
318
|
}
|
|
243
319
|
function filterDetectionFramesForRange(detectionFrames, startTime, endTime) {
|
|
@@ -245,7 +321,7 @@ function filterDetectionFramesForRange(detectionFrames, startTime, endTime) {
|
|
|
245
321
|
}
|
|
246
322
|
function selectDetectionFrame(detectionFrames, mediaTime, options = {}) {
|
|
247
323
|
if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
248
|
-
const selection = selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, options.frameRate
|
|
324
|
+
const selection = selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, options.frameRate);
|
|
249
325
|
if (selection.isApplicable) {
|
|
250
326
|
return selection.frame;
|
|
251
327
|
}
|
|
@@ -282,13 +358,77 @@ function decodeCompressedRleMask(mask) {
|
|
|
282
358
|
};
|
|
283
359
|
}
|
|
284
360
|
function selectIntervalDetectionFrame(detectionFrames, mediaTime) {
|
|
361
|
+
const selectedFrame = findLastStartedDetectionFrame(detectionFrames, mediaTime);
|
|
362
|
+
return selectedFrame && isDetectionFrameActive(selectedFrame, mediaTime)
|
|
363
|
+
? selectedFrame
|
|
364
|
+
: undefined;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The grid frame a playhead is standing on, or nothing.
|
|
368
|
+
*
|
|
369
|
+
* A frame at index `n` speaks for one step of the inference grid and no more,
|
|
370
|
+
* so it is on screen from its own media time until the next index is due. An
|
|
371
|
+
* index the source never produced leaves that step blank: the frames on either
|
|
372
|
+
* side describe media the playhead is not on, and a source still being written
|
|
373
|
+
* has a playback gate to hold the picture rather than a neighbour to borrow.
|
|
374
|
+
*
|
|
375
|
+
* A grid the source labelled only in part is still that source describing this
|
|
376
|
+
* media, so the frame the playhead stands on is the last one to have started,
|
|
377
|
+
* whether or not an index names it. The step it is held to is measured from the
|
|
378
|
+
* labelled frames alone.
|
|
379
|
+
*/
|
|
380
|
+
function selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, frameRate) {
|
|
381
|
+
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
382
|
+
return { frame: undefined, isApplicable: false };
|
|
383
|
+
}
|
|
384
|
+
const firstIndexedFrame = findIndexedDetectionFrame(detectionFrames, 1);
|
|
385
|
+
const lastIndexedFrame = findIndexedDetectionFrame(detectionFrames, -1);
|
|
386
|
+
if (!firstIndexedFrame || !lastIndexedFrame) {
|
|
387
|
+
return { frame: undefined, isApplicable: false };
|
|
388
|
+
}
|
|
389
|
+
const gridStep = measureGridStep$1(firstIndexedFrame, lastIndexedFrame) ?? 1 / frameRate;
|
|
390
|
+
const startedFrame = findLastStartedDetectionFrame(detectionFrames, mediaTime);
|
|
391
|
+
return {
|
|
392
|
+
frame: startedFrame &&
|
|
393
|
+
mediaTime + PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS <
|
|
394
|
+
startedFrame.mediaTime + gridStep
|
|
395
|
+
? startedFrame
|
|
396
|
+
: undefined,
|
|
397
|
+
isApplicable: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* A grid step read off the frames instead of taken from `frameRate`.
|
|
402
|
+
*
|
|
403
|
+
* `frameRate` is what a manifest or an upload target says the grid is, and a
|
|
404
|
+
* clip whose real rate differs walks a step further out of position for every
|
|
405
|
+
* second that plays. The buffered frames carry the times the media itself
|
|
406
|
+
* reported, and measuring across the widest index span present divides the
|
|
407
|
+
* producer's millisecond rounding by that span.
|
|
408
|
+
*/
|
|
409
|
+
function measureGridStep$1(firstIndexedFrame, lastIndexedFrame) {
|
|
410
|
+
const indexSpan = lastIndexedFrame.frameIndex - firstIndexedFrame.frameIndex;
|
|
411
|
+
const step = (lastIndexedFrame.mediaTime - firstIndexedFrame.mediaTime) / indexSpan;
|
|
412
|
+
return indexSpan > 0 && Number.isFinite(step) && step > 0 ? step : undefined;
|
|
413
|
+
}
|
|
414
|
+
function findIndexedDetectionFrame(detectionFrames, direction) {
|
|
415
|
+
const startOffset = direction === 1 ? 0 : detectionFrames.length - 1;
|
|
416
|
+
for (let offset = startOffset; offset >= 0 && offset < detectionFrames.length; offset += direction) {
|
|
417
|
+
const frame = detectionFrames[offset];
|
|
418
|
+
if (isIndexedDetectionFrame(frame)) {
|
|
419
|
+
return frame;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
function findLastStartedDetectionFrame(detectionFrames, mediaTime) {
|
|
285
425
|
let selectedFrame;
|
|
286
426
|
let low = 0;
|
|
287
427
|
let high = detectionFrames.length - 1;
|
|
288
428
|
while (low <= high) {
|
|
289
429
|
const middle = Math.floor((low + high) / 2);
|
|
290
430
|
const frame = detectionFrames[middle];
|
|
291
|
-
if (frame
|
|
431
|
+
if (hasDetectionFrameStarted(frame, mediaTime)) {
|
|
292
432
|
selectedFrame = frame;
|
|
293
433
|
low = middle + 1;
|
|
294
434
|
}
|
|
@@ -296,44 +436,16 @@ function selectIntervalDetectionFrame(detectionFrames, mediaTime) {
|
|
|
296
436
|
high = middle - 1;
|
|
297
437
|
}
|
|
298
438
|
}
|
|
299
|
-
return selectedFrame
|
|
300
|
-
? selectedFrame
|
|
301
|
-
: undefined;
|
|
439
|
+
return selectedFrame;
|
|
302
440
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (!firstIndexedFrame || firstIndexedFrame.frameIndex === undefined) {
|
|
309
|
-
return { frame: undefined, isApplicable: false };
|
|
310
|
-
}
|
|
311
|
-
const originTime = frameIndexOriginTime !== undefined
|
|
312
|
-
? frameIndexOriginTime
|
|
313
|
-
: firstIndexedFrame.mediaTime - firstIndexedFrame.frameIndex / frameRate;
|
|
314
|
-
const targetFrameIndex = Math.round((mediaTime - originTime) * frameRate);
|
|
315
|
-
let nearestFrame;
|
|
316
|
-
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
317
|
-
for (const frame of detectionFrames) {
|
|
318
|
-
if (frame.frameIndex === undefined) {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
const distance = Math.abs(frame.frameIndex - targetFrameIndex);
|
|
322
|
-
if (distance < nearestDistance ||
|
|
323
|
-
(distance === nearestDistance &&
|
|
324
|
-
nearestFrame?.frameIndex !== undefined &&
|
|
325
|
-
frame.frameIndex > nearestFrame.frameIndex)) {
|
|
326
|
-
nearestFrame = frame;
|
|
327
|
-
nearestDistance = distance;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
frame: nearestDistance <= 1 ? nearestFrame : undefined,
|
|
332
|
-
isApplicable: true,
|
|
333
|
-
};
|
|
441
|
+
function isIndexedDetectionFrame(frame) {
|
|
442
|
+
return frame.frameIndex !== undefined;
|
|
443
|
+
}
|
|
444
|
+
function hasDetectionFrameStarted(frame, mediaTime) {
|
|
445
|
+
return frame.mediaTime - PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS <= mediaTime;
|
|
334
446
|
}
|
|
335
447
|
function isDetectionFrameActive(frame, mediaTime) {
|
|
336
|
-
return (frame
|
|
448
|
+
return (hasDetectionFrameStarted(frame, mediaTime) &&
|
|
337
449
|
(frame.endTime === undefined || mediaTime < frame.endTime));
|
|
338
450
|
}
|
|
339
451
|
function detectionFrameOverlapsRange(frame, startTime, endTime) {
|
|
@@ -395,8 +507,84 @@ function createArrayDetectionFrameSource(frames) {
|
|
|
395
507
|
};
|
|
396
508
|
}
|
|
397
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Slack allowed when comparing two range ends.
|
|
512
|
+
*
|
|
513
|
+
* Range bounds are computed from frame times and durations, so an end that
|
|
514
|
+
* should meet another exactly can miss it by a float's last bits.
|
|
515
|
+
*/
|
|
516
|
+
const RANGE_EPSILON_SECONDS = 1e-6;
|
|
517
|
+
/**
|
|
518
|
+
* Whether the available ranges together span the whole of `range`.
|
|
519
|
+
*
|
|
520
|
+
* Detections arrive a batch at a time, and a source that has not coalesced its
|
|
521
|
+
* ranges holds several that abut. Asking each range on its own whether it spans
|
|
522
|
+
* the window reports a window that is entirely covered as uncovered, so the
|
|
523
|
+
* walk below spends ranges in order and asks how far their union reaches.
|
|
524
|
+
*/
|
|
525
|
+
function isRangeCovered(range, availableRanges) {
|
|
526
|
+
let reach = null;
|
|
527
|
+
for (const availableRange of inStartOrder(availableRanges)) {
|
|
528
|
+
if (reach === null) {
|
|
529
|
+
if (availableRange.startTime > range.startTime + RANGE_EPSILON_SECONDS) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
if (availableRange.endTime + RANGE_EPSILON_SECONDS < range.startTime) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
reach = availableRange.endTime;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
if (availableRange.startTime > reach + RANGE_EPSILON_SECONDS) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
reach = Math.max(reach, availableRange.endTime);
|
|
542
|
+
}
|
|
543
|
+
if (reach + RANGE_EPSILON_SECONDS >= range.endTime) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
function inStartOrder(ranges) {
|
|
550
|
+
for (let index = 1; index < ranges.length; index += 1) {
|
|
551
|
+
if (ranges[index].startTime < ranges[index - 1].startTime) {
|
|
552
|
+
return [...ranges].sort((left, right) => left.startTime - right.startTime);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return ranges;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function startWaitBound(timeoutMs) {
|
|
559
|
+
let handle;
|
|
560
|
+
const expired = new Promise((resolve) => {
|
|
561
|
+
handle = setTimeout(() => resolve(false), timeoutMs);
|
|
562
|
+
});
|
|
563
|
+
return { cancel: () => clearTimeout(handle), expired };
|
|
564
|
+
}
|
|
565
|
+
|
|
398
566
|
const DEFAULT_BUFFER_AHEAD_SECONDS = 5;
|
|
399
567
|
const DEFAULT_BUFFER_BEHIND_SECONDS = 0.5;
|
|
568
|
+
/**
|
|
569
|
+
* Share of the ahead window that must still lead the playhead when the next
|
|
570
|
+
* window is fetched.
|
|
571
|
+
*
|
|
572
|
+
* A window fetched only once the playhead reaches its end leaves the playhead
|
|
573
|
+
* uncovered for exactly as long as the fetch takes, every time, so annotations
|
|
574
|
+
* blink out once per window at a steady rate. Fetching while the current window
|
|
575
|
+
* still answers means the load lands behind a picture that never lost them.
|
|
576
|
+
*/
|
|
577
|
+
const REFILL_LEAD_FRACTION = 0.5;
|
|
578
|
+
const MIN_REFILL_LEAD_SECONDS = 1;
|
|
579
|
+
/**
|
|
580
|
+
* How long a playback gate holds before it gives up on coverage.
|
|
581
|
+
*
|
|
582
|
+
* The gate waits on a producer, and a producer that has failed, stalled, or
|
|
583
|
+
* fallen far behind is indistinguishable from one that is about to answer. Past
|
|
584
|
+
* this a frozen picture is the worse of the two outcomes, so the frame is
|
|
585
|
+
* presented with whatever detections exist.
|
|
586
|
+
*/
|
|
587
|
+
const DEFAULT_PLAYBACK_GATE_MAX_WAIT_SECONDS = 10;
|
|
400
588
|
const bufferedFrameSnapshots = new WeakMap();
|
|
401
589
|
/**
|
|
402
590
|
* Returns the timeline-owned hot-buffer snapshot without copying it.
|
|
@@ -414,6 +602,9 @@ function createBufferedDetectionTimeline(options) {
|
|
|
414
602
|
? null
|
|
415
603
|
: Math.max(0, options.refreshIntervalSeconds);
|
|
416
604
|
const playbackGate = options.playbackGate;
|
|
605
|
+
const refillLeadSeconds = bufferAheadSeconds <= 0
|
|
606
|
+
? 0
|
|
607
|
+
: Math.min(bufferAheadSeconds, Math.max(MIN_REFILL_LEAD_SECONDS, bufferAheadSeconds * REFILL_LEAD_FRACTION));
|
|
417
608
|
let buffer = [];
|
|
418
609
|
let state = createIdleDetectionBufferState();
|
|
419
610
|
let destroyed = false;
|
|
@@ -426,30 +617,53 @@ function createBufferedDetectionTimeline(options) {
|
|
|
426
617
|
};
|
|
427
618
|
let inFlight;
|
|
428
619
|
let incrementalRefresh;
|
|
620
|
+
/** Source version the gate last gave up on, or null while it still waits. */
|
|
621
|
+
let abandonedGateSourceVersion = null;
|
|
429
622
|
let pendingPrefetch;
|
|
430
623
|
let prefetchPump;
|
|
624
|
+
const listeners = new Set();
|
|
625
|
+
const notifyBufferChanged = () => {
|
|
626
|
+
for (const listener of listeners) {
|
|
627
|
+
listener();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
431
630
|
const getSourceVersion = (ranges) => {
|
|
432
631
|
if (!ranges) {
|
|
433
632
|
return options.source.getVersion?.() ?? 0;
|
|
434
633
|
}
|
|
435
634
|
return ranges.reduce((version, range) => Math.max(version, options.source.getVersion?.(range) ?? 0), 0);
|
|
436
635
|
};
|
|
636
|
+
const isLoadingEnabled = () => typeof options.enabled === "function"
|
|
637
|
+
? options.enabled()
|
|
638
|
+
: options.enabled !== false;
|
|
437
639
|
const isBufferFresh = () => bufferedVersionRange !== null &&
|
|
438
640
|
bufferedSourceVersion === getSourceVersion(getBufferedSourceRanges());
|
|
439
|
-
const getLoadRange = (mediaTime) => {
|
|
641
|
+
const getLoadRange = (mediaTime, lead = "full") => {
|
|
440
642
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
441
643
|
const startTime = comparableMediaTime - bufferBehindSeconds;
|
|
442
|
-
const endTime = comparableMediaTime + bufferAheadSeconds;
|
|
644
|
+
const endTime = comparableMediaTime + (lead === "full" ? bufferAheadSeconds : 0);
|
|
443
645
|
return createLoadPlan(startTime, endTime);
|
|
444
646
|
};
|
|
445
|
-
const loadWindow = (mediaTime) => {
|
|
446
|
-
const
|
|
647
|
+
const loadWindow = (mediaTime, lead = "full") => {
|
|
648
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
649
|
+
const { endTime, sourceRanges, startTime } = getLoadRange(mediaTime, lead);
|
|
447
650
|
const versionRange = { endTime, startTime };
|
|
448
651
|
const sourceVersion = getSourceVersion(sourceRanges);
|
|
449
|
-
if (inFlight &&
|
|
450
|
-
inFlight.
|
|
451
|
-
|
|
452
|
-
|
|
652
|
+
if (inFlight && inFlight.sourceVersion === sourceVersion) {
|
|
653
|
+
if (rangeContains(inFlight.startTime, inFlight.endTime, startTime, endTime)) {
|
|
654
|
+
return inFlight.promise;
|
|
655
|
+
}
|
|
656
|
+
// A load already in flight that still covers where the playhead is going
|
|
657
|
+
// answers the same question a fresh one would. Playback moves the anchor
|
|
658
|
+
// every frame, so a window superseded on anchor equality alone is
|
|
659
|
+
// superseded on every frame of its own flight: the fetch is thrown away
|
|
660
|
+
// and the wait restarts, which is how a gap grows instead of closing.
|
|
661
|
+
if (refillLeadSeconds > 0 &&
|
|
662
|
+
comparableMediaTime >= inFlight.mediaTime &&
|
|
663
|
+
comparableMediaTime + refillLeadSeconds <=
|
|
664
|
+
inFlight.mediaTime + bufferAheadSeconds) {
|
|
665
|
+
return inFlight.promise;
|
|
666
|
+
}
|
|
453
667
|
}
|
|
454
668
|
const currentLoadId = loadId + 1;
|
|
455
669
|
loadId = currentLoadId;
|
|
@@ -466,12 +680,12 @@ function createBufferedDetectionTimeline(options) {
|
|
|
466
680
|
return;
|
|
467
681
|
}
|
|
468
682
|
const committedSourceVersion = getSourceVersion(sourceRanges);
|
|
469
|
-
const
|
|
683
|
+
const loadedFrames = frameRanges.flat();
|
|
470
684
|
buffer =
|
|
471
685
|
bufferedSourceVersion !== null &&
|
|
472
686
|
bufferedSourceVersion === committedSourceVersion
|
|
473
|
-
? reuseBufferedFrameSnapshots(buffer,
|
|
474
|
-
:
|
|
687
|
+
? reuseBufferedFrameSnapshots(buffer, loadedFrames)
|
|
688
|
+
: copySortedDetectionFrames(loadedFrames);
|
|
475
689
|
bufferedVersionRange = versionRange;
|
|
476
690
|
bufferedSourceVersion = committedSourceVersion;
|
|
477
691
|
state = {
|
|
@@ -484,6 +698,7 @@ function createBufferedDetectionTimeline(options) {
|
|
|
484
698
|
requestedStartTime: startTime,
|
|
485
699
|
status: DetectionBufferStatus.Ready,
|
|
486
700
|
};
|
|
701
|
+
notifyBufferChanged();
|
|
487
702
|
})
|
|
488
703
|
.catch((error) => {
|
|
489
704
|
if (!destroyed && currentLoadId === loadId) {
|
|
@@ -500,15 +715,56 @@ function createBufferedDetectionTimeline(options) {
|
|
|
500
715
|
inFlight = undefined;
|
|
501
716
|
}
|
|
502
717
|
});
|
|
718
|
+
if (lead === "covering") {
|
|
719
|
+
// Chained past the clearing of `inFlight`, which would otherwise be
|
|
720
|
+
// handed back to the widening load as a window that already answers.
|
|
721
|
+
void promise
|
|
722
|
+
.then(() => {
|
|
723
|
+
if (!destroyed && currentLoadId === loadId) {
|
|
724
|
+
void loadWindow(mediaTime).catch(() => undefined);
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
.catch(() => undefined);
|
|
728
|
+
}
|
|
503
729
|
inFlight = {
|
|
504
730
|
endTime,
|
|
505
731
|
id: currentLoadId,
|
|
732
|
+
mediaTime: comparableMediaTime,
|
|
506
733
|
promise,
|
|
507
734
|
sourceVersion,
|
|
508
735
|
startTime,
|
|
509
736
|
};
|
|
510
737
|
return promise;
|
|
511
738
|
};
|
|
739
|
+
/**
|
|
740
|
+
* Restates the window in the lap the playhead is on.
|
|
741
|
+
*
|
|
742
|
+
* A window planned near the end of a looping source runs past the loop
|
|
743
|
+
* point, so once playback wraps, a host comparing the window against the
|
|
744
|
+
* media clock reads a range that starts after the time it is holding. The
|
|
745
|
+
* offset removed here is a whole number of laps, which is what
|
|
746
|
+
* `getComparableMediaTime` already treats as the same position, so
|
|
747
|
+
* membership and the frames on screen are untouched.
|
|
748
|
+
*/
|
|
749
|
+
const anchorWindowToPlayhead = (mediaTime) => {
|
|
750
|
+
const duration = timelineContext.duration;
|
|
751
|
+
if (!isLoopingTimeline() ||
|
|
752
|
+
duration === null ||
|
|
753
|
+
state.bufferStartTime === null ||
|
|
754
|
+
state.bufferEndTime === null) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const laps = Math.round((getComparableMediaTime(mediaTime) - mediaTime) / duration);
|
|
758
|
+
if (laps === 0) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
state = {
|
|
762
|
+
...state,
|
|
763
|
+
bufferEndTime: state.bufferEndTime - laps * duration,
|
|
764
|
+
bufferStartTime: state.bufferStartTime - laps * duration,
|
|
765
|
+
};
|
|
766
|
+
notifyBufferChanged();
|
|
767
|
+
};
|
|
512
768
|
const isBuffered = (mediaTime) => {
|
|
513
769
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
514
770
|
return (isBufferFresh() &&
|
|
@@ -525,7 +781,7 @@ function createBufferedDetectionTimeline(options) {
|
|
|
525
781
|
comparableMediaTime >= state.bufferStartTime &&
|
|
526
782
|
comparableMediaTime <= state.bufferEndTime);
|
|
527
783
|
};
|
|
528
|
-
const refreshBuffer = async (mediaTime) => {
|
|
784
|
+
const refreshBuffer = async (mediaTime, lead = "full") => {
|
|
529
785
|
if (isBuffered(mediaTime)) {
|
|
530
786
|
return;
|
|
531
787
|
}
|
|
@@ -541,11 +797,18 @@ function createBufferedDetectionTimeline(options) {
|
|
|
541
797
|
if (isBuffered(mediaTime)) {
|
|
542
798
|
return;
|
|
543
799
|
}
|
|
544
|
-
return refreshBuffer(mediaTime);
|
|
800
|
+
return refreshBuffer(mediaTime, lead);
|
|
545
801
|
}
|
|
546
|
-
await loadWindow(mediaTime);
|
|
802
|
+
await loadWindow(mediaTime, lead);
|
|
547
803
|
};
|
|
804
|
+
/**
|
|
805
|
+
* Whether the next window is worth fetching while the current one still
|
|
806
|
+
* answers, so the load lands behind annotations that never went away.
|
|
807
|
+
*/
|
|
548
808
|
const shouldPrefetch = (mediaTime) => {
|
|
809
|
+
if (!isLoadingEnabled()) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
549
812
|
if (!isBuffered(mediaTime)) {
|
|
550
813
|
return true;
|
|
551
814
|
}
|
|
@@ -555,12 +818,19 @@ function createBufferedDetectionTimeline(options) {
|
|
|
555
818
|
if (state.bufferEndTime === null || bufferAheadSeconds <= 0) {
|
|
556
819
|
return false;
|
|
557
820
|
}
|
|
558
|
-
|
|
559
|
-
state.bufferEndTime)
|
|
821
|
+
if (getComparableMediaTime(mediaTime) + bufferAheadSeconds / 2 <
|
|
822
|
+
state.bufferEndTime) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const { endTime, startTime } = getLoadRange(mediaTime);
|
|
826
|
+
// A window already spanning everything the source can offer has nowhere to
|
|
827
|
+
// advance to, so its lead only shrinks from here. Refetching it would
|
|
828
|
+
// repeat for every remaining frame of playback and buy no coverage.
|
|
829
|
+
return (startTime !== state.bufferStartTime || endTime !== state.bufferEndTime);
|
|
560
830
|
};
|
|
561
831
|
const timeline = {
|
|
562
832
|
async prepare(mediaTime, prepareOptions) {
|
|
563
|
-
if (destroyed) {
|
|
833
|
+
if (destroyed || !isLoadingEnabled()) {
|
|
564
834
|
return;
|
|
565
835
|
}
|
|
566
836
|
if (shouldWaitForPlaybackGate(prepareOptions)) {
|
|
@@ -570,9 +840,26 @@ function createBufferedDetectionTimeline(options) {
|
|
|
570
840
|
}
|
|
571
841
|
}
|
|
572
842
|
await refreshBuffer(mediaTime);
|
|
843
|
+
anchorWindowToPlayhead(mediaTime);
|
|
844
|
+
},
|
|
845
|
+
needsPlaybackGateWait(mediaTime, prepareOptions) {
|
|
846
|
+
if (destroyed ||
|
|
847
|
+
!isLoadingEnabled() ||
|
|
848
|
+
!playbackGate?.enabled ||
|
|
849
|
+
!options.source.waitForRange ||
|
|
850
|
+
!options.source.getAvailableRanges ||
|
|
851
|
+
hasAbandonedGate()) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
const availableRanges = options.source.getAvailableRanges();
|
|
855
|
+
return !createPlaybackGateCoveragePlan(mediaTime, prepareOptions).sourceRanges.every((range) => isRangeCovered(range, availableRanges));
|
|
573
856
|
},
|
|
574
857
|
prefetch(mediaTime) {
|
|
575
|
-
if (destroyed || !
|
|
858
|
+
if (destroyed || !isLoadingEnabled()) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
anchorWindowToPlayhead(mediaTime);
|
|
862
|
+
if (!shouldPrefetch(mediaTime)) {
|
|
576
863
|
return;
|
|
577
864
|
}
|
|
578
865
|
pendingPrefetch = { loadId, mediaTime };
|
|
@@ -595,11 +882,18 @@ function createBufferedDetectionTimeline(options) {
|
|
|
595
882
|
getState() {
|
|
596
883
|
return { ...state };
|
|
597
884
|
},
|
|
885
|
+
subscribe(listener) {
|
|
886
|
+
listeners.add(listener);
|
|
887
|
+
return () => {
|
|
888
|
+
listeners.delete(listener);
|
|
889
|
+
};
|
|
890
|
+
},
|
|
598
891
|
destroy() {
|
|
599
892
|
if (destroyed) {
|
|
600
893
|
return;
|
|
601
894
|
}
|
|
602
895
|
destroyed = true;
|
|
896
|
+
listeners.clear();
|
|
603
897
|
pendingPrefetch = undefined;
|
|
604
898
|
buffer = [];
|
|
605
899
|
bufferedSourceVersion = null;
|
|
@@ -643,9 +937,16 @@ function createBufferedDetectionTimeline(options) {
|
|
|
643
937
|
if (!shouldPrefetch(mediaTime)) {
|
|
644
938
|
continue;
|
|
645
939
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
940
|
+
// A playhead the window does not reach is a jump rather than the window
|
|
941
|
+
// rolling forward, and the frame under it is the one thing the picture
|
|
942
|
+
// cannot draw without. A window that still spans it, however stale, is
|
|
943
|
+
// missing no such frame.
|
|
944
|
+
const lead = isInsideBufferedRange(mediaTime)
|
|
945
|
+
? "full"
|
|
946
|
+
: "covering";
|
|
947
|
+
await (isBuffered(mediaTime) || shouldRefreshRollingWindow(mediaTime)
|
|
948
|
+
? loadWindow(mediaTime, lead)
|
|
949
|
+
: refreshBuffer(mediaTime, lead)).catch(() => undefined);
|
|
649
950
|
}
|
|
650
951
|
}
|
|
651
952
|
async function applyIncrementalChanges() {
|
|
@@ -687,6 +988,7 @@ function createBufferedDetectionTimeline(options) {
|
|
|
687
988
|
frameCount: buffer.length,
|
|
688
989
|
status: DetectionBufferStatus.Ready,
|
|
689
990
|
};
|
|
991
|
+
notifyBufferChanged();
|
|
690
992
|
}
|
|
691
993
|
catch (error) {
|
|
692
994
|
if (!destroyed) {
|
|
@@ -704,31 +1006,44 @@ function createBufferedDetectionTimeline(options) {
|
|
|
704
1006
|
playbackGate?.enabled === true &&
|
|
705
1007
|
Boolean(options.source.waitForRange));
|
|
706
1008
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
const requiredAheadSeconds = Math.max(0, playbackGate.requiredAheadSeconds ?? 0);
|
|
1009
|
+
function createPlaybackGateCoveragePlan(mediaTime, prepareOptions) {
|
|
1010
|
+
const requiredAheadSeconds = Math.max(0, playbackGate?.requiredAheadSeconds ?? 0);
|
|
712
1011
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
713
1012
|
const endTime = getRequiredCoverageEndTime({
|
|
1013
|
+
// A looping window counts past the end of media and wraps into the
|
|
1014
|
+
// replay, so clamping it to duration would ask for less than it needs.
|
|
714
1015
|
duration: isLoopingTimeline() ? null : prepareOptions?.duration,
|
|
715
1016
|
firstTimestamp: prepareOptions?.firstTimestamp,
|
|
716
1017
|
mediaTime: comparableMediaTime,
|
|
717
1018
|
requiredAheadSeconds,
|
|
718
1019
|
});
|
|
719
|
-
|
|
1020
|
+
// A lead clamped away at the end of media, or asked for as zero, still
|
|
1021
|
+
// leaves the frame under the playhead to wait for.
|
|
1022
|
+
return createLoadPlan(comparableMediaTime, Math.max(comparableMediaTime, endTime));
|
|
1023
|
+
}
|
|
1024
|
+
function hasAbandonedGate() {
|
|
1025
|
+
return (abandonedGateSourceVersion !== null &&
|
|
1026
|
+
getSourceVersion() <= abandonedGateSourceVersion);
|
|
1027
|
+
}
|
|
1028
|
+
async function waitForPlaybackGate(mediaTime, prepareOptions) {
|
|
1029
|
+
if (!playbackGate?.enabled || !options.source.waitForRange) {
|
|
720
1030
|
return;
|
|
721
1031
|
}
|
|
722
|
-
|
|
1032
|
+
if (hasAbandonedGate()) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const coveragePlan = createPlaybackGateCoveragePlan(mediaTime, prepareOptions);
|
|
723
1036
|
state = {
|
|
724
1037
|
...state,
|
|
725
1038
|
errorMessage: null,
|
|
726
1039
|
requestedEndTime: coveragePlan.endTime,
|
|
727
1040
|
requestedStartTime: coveragePlan.startTime,
|
|
728
|
-
status: DetectionBufferStatus.
|
|
1041
|
+
status: DetectionBufferStatus.AwaitingCoverage,
|
|
729
1042
|
};
|
|
730
1043
|
try {
|
|
731
|
-
await
|
|
1044
|
+
const covered = await waitForSourceCoverage(coveragePlan.sourceRanges, Math.max(0, playbackGate.maxWaitSeconds ?? DEFAULT_PLAYBACK_GATE_MAX_WAIT_SECONDS));
|
|
1045
|
+
abandonedGateSourceVersion =
|
|
1046
|
+
covered || !options.source.getVersion ? null : getSourceVersion();
|
|
732
1047
|
}
|
|
733
1048
|
catch (error) {
|
|
734
1049
|
if (!destroyed) {
|
|
@@ -741,6 +1056,33 @@ function createBufferedDetectionTimeline(options) {
|
|
|
741
1056
|
throw error;
|
|
742
1057
|
}
|
|
743
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Resolves true once the source covers every range, false once the wait has
|
|
1061
|
+
* run longer than `maxWaitSeconds`.
|
|
1062
|
+
*/
|
|
1063
|
+
async function waitForSourceCoverage(sourceRanges, maxWaitSeconds) {
|
|
1064
|
+
const covered = whenRangesCovered(sourceRanges);
|
|
1065
|
+
if (!Number.isFinite(maxWaitSeconds)) {
|
|
1066
|
+
return covered;
|
|
1067
|
+
}
|
|
1068
|
+
const bound = startWaitBound(maxWaitSeconds * 1000);
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await Promise.race([covered, bound.expired]);
|
|
1071
|
+
if (!result) {
|
|
1072
|
+
// The abandoned wait outlives this call, and a rejection it reaches
|
|
1073
|
+
// afterwards has nobody left to hand it to.
|
|
1074
|
+
void covered.catch(() => undefined);
|
|
1075
|
+
}
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
finally {
|
|
1079
|
+
bound.cancel();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async function whenRangesCovered(sourceRanges) {
|
|
1083
|
+
await Promise.all(sourceRanges.map((range) => options.source.waitForRange?.(range)));
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
744
1086
|
function shouldRefreshRollingWindow(mediaTime) {
|
|
745
1087
|
if (refreshIntervalSeconds === null ||
|
|
746
1088
|
refreshIntervalSeconds <= 0 ||
|
|
@@ -749,8 +1091,24 @@ function createBufferedDetectionTimeline(options) {
|
|
|
749
1091
|
return false;
|
|
750
1092
|
}
|
|
751
1093
|
const { endTime, startTime } = getLoadRange(mediaTime);
|
|
752
|
-
return (
|
|
753
|
-
|
|
1094
|
+
return (getWindowDrift(startTime, state.bufferStartTime) >=
|
|
1095
|
+
refreshIntervalSeconds ||
|
|
1096
|
+
getWindowDrift(endTime, state.bufferEndTime) >= refreshIntervalSeconds);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* How far a planned edge sits from the window's, counted the short way around
|
|
1100
|
+
* a looping timeline. A plan is stated on the first lap and the window on the
|
|
1101
|
+
* playhead's, so across the loop point the two state the same edge a whole lap
|
|
1102
|
+
* apart; measured straight, that reads as a window a full lap stale and
|
|
1103
|
+
* rebuilds it on every frame until the plan stops reaching behind zero.
|
|
1104
|
+
*/
|
|
1105
|
+
function getWindowDrift(planTime, bufferTime) {
|
|
1106
|
+
const drift = Math.abs(planTime - bufferTime);
|
|
1107
|
+
if (!isLoopingTimeline() || timelineContext.duration === null) {
|
|
1108
|
+
return drift;
|
|
1109
|
+
}
|
|
1110
|
+
const wrappedDrift = modulo(drift, timelineContext.duration);
|
|
1111
|
+
return Math.min(wrappedDrift, timelineContext.duration - wrappedDrift);
|
|
754
1112
|
}
|
|
755
1113
|
function createLoadPlan(requestedStartTime, requestedEndTime) {
|
|
756
1114
|
const startTime = Math.min(requestedStartTime, requestedEndTime);
|
|
@@ -777,10 +1135,15 @@ function createBufferedDetectionTimeline(options) {
|
|
|
777
1135
|
startTime: 0,
|
|
778
1136
|
};
|
|
779
1137
|
}
|
|
1138
|
+
// Both ends move by the same whole number of laps, so the window keeps its
|
|
1139
|
+
// span and its source ranges while its start reads on the media clock: a
|
|
1140
|
+
// window counted in the laps playback accumulated is one no host can hold
|
|
1141
|
+
// against a current time.
|
|
1142
|
+
const laps = Math.floor(startTime / duration);
|
|
780
1143
|
return {
|
|
781
|
-
endTime,
|
|
1144
|
+
endTime: endTime - laps * duration,
|
|
782
1145
|
sourceRanges: getLoopingSourceRanges(startTime, endTime, duration),
|
|
783
|
-
startTime,
|
|
1146
|
+
startTime: startTime - laps * duration,
|
|
784
1147
|
};
|
|
785
1148
|
}
|
|
786
1149
|
function getBufferedSourceRanges() {
|
|
@@ -802,14 +1165,13 @@ function createBufferedDetectionTimeline(options) {
|
|
|
802
1165
|
return mediaTime;
|
|
803
1166
|
}
|
|
804
1167
|
const duration = timelineContext.duration;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
return comparableMediaTime;
|
|
1168
|
+
// The representative of mediaTime (mod duration) anchored at the window
|
|
1169
|
+
// start. The mapping must depend only on where the window sits, never on
|
|
1170
|
+
// how many laps playback has accumulated, or membership drifts away from
|
|
1171
|
+
// what the buffer actually holds. A time less than one lap past the
|
|
1172
|
+
// anchor never wraps, so ordinary forward playback cannot ratchet.
|
|
1173
|
+
return (state.bufferStartTime +
|
|
1174
|
+
modulo(mediaTime - state.bufferStartTime, duration));
|
|
813
1175
|
}
|
|
814
1176
|
function getSourceMediaTime(mediaTime) {
|
|
815
1177
|
if (!isLoopingTimeline() || timelineContext.duration === null) {
|
|
@@ -897,9 +1259,19 @@ function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
|
|
|
897
1259
|
}
|
|
898
1260
|
return Array.from(framesByIdentity.values()).sort(compareDetectionFrames$1);
|
|
899
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Keeps the frame already held wherever the source returned one this buffer
|
|
1264
|
+
* knows, and copies only what is new. Copying everything first and then
|
|
1265
|
+
* discarding it is the same result for a great deal more work: a window rebuilt
|
|
1266
|
+
* while a gesture moves inside it re-derives hundreds of frames it already has.
|
|
1267
|
+
*/
|
|
900
1268
|
function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
|
|
1269
|
+
validateDetectionFrames(loadedFrames);
|
|
901
1270
|
const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
|
|
902
|
-
return loadedFrames
|
|
1271
|
+
return loadedFrames
|
|
1272
|
+
.map((frame) => currentFramesByIdentity.get(getDetectionFrameIdentity(frame)) ??
|
|
1273
|
+
copyDetectionFrame(frame))
|
|
1274
|
+
.sort(compareDetectionFrames$1);
|
|
903
1275
|
}
|
|
904
1276
|
function getDetectionFrameIdentity(frame) {
|
|
905
1277
|
return frame.frameIndex === undefined
|
|
@@ -1040,7 +1412,7 @@ function createCompositeDetectionFrameSource(options) {
|
|
|
1040
1412
|
},
|
|
1041
1413
|
async waitForRange(range) {
|
|
1042
1414
|
await Promise.all(sources
|
|
1043
|
-
.filter((source) => source.
|
|
1415
|
+
.filter((source) => source.requiredForCoverage)
|
|
1044
1416
|
.map((source) => source.source.waitForRange?.(range)));
|
|
1045
1417
|
},
|
|
1046
1418
|
getAvailableRanges() {
|
|
@@ -1075,7 +1447,7 @@ function normalizeCompositeSources(entries) {
|
|
|
1075
1447
|
declarationIndex,
|
|
1076
1448
|
id: entry.id,
|
|
1077
1449
|
order: entry.order ?? 0,
|
|
1078
|
-
|
|
1450
|
+
requiredForCoverage: entry.requiredForCoverage !== false,
|
|
1079
1451
|
source: entry.source ?? createArrayDetectionFrameSource(entry.frames ?? []),
|
|
1080
1452
|
sync: entry.sync,
|
|
1081
1453
|
};
|
|
@@ -1105,47 +1477,181 @@ function composeIntervalFrames(sources, startTime, endTime, options, coordinateS
|
|
|
1105
1477
|
}
|
|
1106
1478
|
const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
|
|
1107
1479
|
const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
|
|
1108
|
-
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval },
|
|
1480
|
+
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval }, coordinateSpace);
|
|
1109
1481
|
if (frame) {
|
|
1110
1482
|
frames.push(frame);
|
|
1111
1483
|
}
|
|
1112
1484
|
}
|
|
1113
1485
|
return frames;
|
|
1114
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* One composed frame per inference grid index the children wrote, and one per
|
|
1489
|
+
* child frame that carries no index.
|
|
1490
|
+
*
|
|
1491
|
+
* Children are paired on the index their producer labelled, and the composed
|
|
1492
|
+
* frame keeps the media time they reported for it. `frameRate` states the grid
|
|
1493
|
+
* a producer was asked for, not the one the clip plays at, and a slower clip
|
|
1494
|
+
* answers consecutive indexes with the same decoded sample: a timeline placed
|
|
1495
|
+
* by that rate then asks for a time two indexes share, which can only ever
|
|
1496
|
+
* reach one of them and leaves the other's detections stranded. Pairing on the
|
|
1497
|
+
* index reaches every one and keeps each frame's detections on the frame they
|
|
1498
|
+
* were computed for.
|
|
1499
|
+
*
|
|
1500
|
+
* A frame carrying no index has no grid position to be paired on, so it
|
|
1501
|
+
* composes at its own media time, and every child answers there with whatever
|
|
1502
|
+
* it has standing. That is how a child sitting on a grid index keeps its
|
|
1503
|
+
* detections on screen while an unindexed frame from another child overlaps it.
|
|
1504
|
+
* The composed frame carries no index of its own, because only part of what it
|
|
1505
|
+
* holds sits on the grid.
|
|
1506
|
+
*/
|
|
1115
1507
|
function composeNearestFrameIndexFrames(sources, startTime, endTime, options, coordinateSpace) {
|
|
1116
1508
|
const frameRate = options.frameRate;
|
|
1117
1509
|
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
1118
1510
|
return null;
|
|
1119
1511
|
}
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1512
|
+
const gridSources = sources.map((source) => ({
|
|
1513
|
+
...source,
|
|
1514
|
+
framesByIndex: indexFramesByFrameIndex(source, options),
|
|
1515
|
+
}));
|
|
1516
|
+
const indexedSlots = mergeGridSlots(gridSources);
|
|
1517
|
+
if (indexedSlots.length === 0) {
|
|
1123
1518
|
return null;
|
|
1124
1519
|
}
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
...
|
|
1129
|
-
|
|
1130
|
-
.filter((frameIndex) => frameIndex !== undefined)),
|
|
1131
|
-
].sort((left, right) => left - right);
|
|
1520
|
+
const gridStep = measureGridStep(indexedSlots) ?? 1 / frameRate;
|
|
1521
|
+
const slots = [
|
|
1522
|
+
...indexedSlots,
|
|
1523
|
+
...mergeUnindexedGridSlots(gridSources, indexedSlots),
|
|
1524
|
+
];
|
|
1132
1525
|
const frames = [];
|
|
1133
|
-
for (const
|
|
1134
|
-
|
|
1135
|
-
if (mediaTime < startTime || mediaTime >= endTime) {
|
|
1526
|
+
for (const slot of slots) {
|
|
1527
|
+
if (slot.mediaTime < startTime || slot.mediaTime >= endTime) {
|
|
1136
1528
|
continue;
|
|
1137
1529
|
}
|
|
1138
|
-
const frame =
|
|
1530
|
+
const frame = composeFrameAtGridSlot(gridSources, slot, Math.min(slot.mediaTime + gridStep, endTime), {
|
|
1139
1531
|
...options,
|
|
1140
1532
|
selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
|
|
1141
|
-
},
|
|
1533
|
+
}, coordinateSpace);
|
|
1142
1534
|
if (frame) {
|
|
1143
1535
|
frames.push(frame);
|
|
1144
1536
|
}
|
|
1145
1537
|
}
|
|
1146
|
-
return frames;
|
|
1538
|
+
return frames.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
1539
|
+
}
|
|
1540
|
+
function indexFramesByFrameIndex(source, options) {
|
|
1541
|
+
const framesByIndex = new Map();
|
|
1542
|
+
const selectionMode = source.sync?.selectionMode ?? options.selectionMode;
|
|
1543
|
+
if (selectionMode !== DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
1544
|
+
return framesByIndex;
|
|
1545
|
+
}
|
|
1546
|
+
for (const frame of source.frames) {
|
|
1547
|
+
if (frame.frameIndex !== undefined) {
|
|
1548
|
+
framesByIndex.set(frame.frameIndex, frame);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return framesByIndex;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Where each grid index sits in the media, taken as the earliest time any child
|
|
1555
|
+
* reported for it. Children on one grid watched the same source frame, and the
|
|
1556
|
+
* earliest of their answers is where that frame starts.
|
|
1557
|
+
*/
|
|
1558
|
+
function mergeGridSlots(sources) {
|
|
1559
|
+
const mediaTimes = new Map();
|
|
1560
|
+
for (const source of sources) {
|
|
1561
|
+
for (const [frameIndex, frame] of source.framesByIndex) {
|
|
1562
|
+
const mediaTime = mediaTimes.get(frameIndex);
|
|
1563
|
+
if (mediaTime === undefined || frame.mediaTime < mediaTime) {
|
|
1564
|
+
mediaTimes.set(frameIndex, frame.mediaTime);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return [...mediaTimes]
|
|
1569
|
+
.map(([frameIndex, mediaTime]) => ({ frameIndex, mediaTime }))
|
|
1570
|
+
.sort((left, right) => left.frameIndex - right.frameIndex);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Where a frame no producer labelled sits: at the media time it reported, which
|
|
1574
|
+
* is the only handle it has.
|
|
1575
|
+
*/
|
|
1576
|
+
function mergeUnindexedGridSlots(sources, indexedSlots) {
|
|
1577
|
+
const indexedSlotTimes = new Set(indexedSlots.map((slot) => slot.mediaTime));
|
|
1578
|
+
const mediaTimes = new Set();
|
|
1579
|
+
for (const source of sources) {
|
|
1580
|
+
for (const frame of source.frames) {
|
|
1581
|
+
if (frame.frameIndex === undefined &&
|
|
1582
|
+
!indexedSlotTimes.has(frame.mediaTime)) {
|
|
1583
|
+
mediaTimes.add(frame.mediaTime);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return [...mediaTimes]
|
|
1588
|
+
.sort((left, right) => left - right)
|
|
1589
|
+
.map((mediaTime) => ({ mediaTime }));
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* How long one grid index stands, read off the slots across the widest index
|
|
1593
|
+
* span they cover. A clip whose real rate differs from `frameRate` would end
|
|
1594
|
+
* every composed frame a little short or a little long of where the next index
|
|
1595
|
+
* is due, and that error accumulates for as long as the clip plays.
|
|
1596
|
+
*/
|
|
1597
|
+
function measureGridStep(gridSlots) {
|
|
1598
|
+
const firstSlot = gridSlots[0];
|
|
1599
|
+
const lastSlot = gridSlots[gridSlots.length - 1];
|
|
1600
|
+
const indexSpan = lastSlot.frameIndex - firstSlot.frameIndex;
|
|
1601
|
+
const step = (lastSlot.mediaTime - firstSlot.mediaTime) / indexSpan;
|
|
1602
|
+
return indexSpan > 0 && Number.isFinite(step) && step > 0 ? step : undefined;
|
|
1603
|
+
}
|
|
1604
|
+
function composeFrameAtGridSlot(sources, slot, endTime, options, coordinateSpace) {
|
|
1605
|
+
const detections = [];
|
|
1606
|
+
for (const source of sources) {
|
|
1607
|
+
const activeFrame = selectGridSlotFrame(source, slot, options);
|
|
1608
|
+
if (!activeFrame) {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
activeFrame.detections.forEach((detection, sourceDetectionIndex) => {
|
|
1612
|
+
detections.push(copyDetectionWithSource(detection, source.id, sourceDetectionIndex));
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
if (detections.length === 0) {
|
|
1616
|
+
return undefined;
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
detections,
|
|
1620
|
+
endTime,
|
|
1621
|
+
frameIndex: slot.frameIndex,
|
|
1622
|
+
mediaTime: slot.mediaTime,
|
|
1623
|
+
// Children were projected before composition, so a composed frame is
|
|
1624
|
+
// already in the coordinate space the renderer presents.
|
|
1625
|
+
...(coordinateSpace ? { coordinateSpace } : {}),
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* What one child has to say at a slot.
|
|
1630
|
+
*
|
|
1631
|
+
* A child that wrote this index answers with the frame it wrote for it, however
|
|
1632
|
+
* far its own media time sits from the slot's. A child on the grid that never
|
|
1633
|
+
* wrote this index answers only with a frame of its own that carries no index
|
|
1634
|
+
* and is standing here: its neighbouring indexes describe media the slot is not
|
|
1635
|
+
* on, and lending one of those would put those detections on a frame they were
|
|
1636
|
+
* not computed for.
|
|
1637
|
+
*/
|
|
1638
|
+
function selectGridSlotFrame(source, slot, options) {
|
|
1639
|
+
const pairedFrame = slot.frameIndex === undefined
|
|
1640
|
+
? undefined
|
|
1641
|
+
: source.framesByIndex.get(slot.frameIndex);
|
|
1642
|
+
if (pairedFrame) {
|
|
1643
|
+
return pairedFrame;
|
|
1644
|
+
}
|
|
1645
|
+
const selectedFrame = selectDetectionFrame(source.frames, slot.mediaTime, {
|
|
1646
|
+
...options,
|
|
1647
|
+
...source.sync,
|
|
1648
|
+
});
|
|
1649
|
+
const isGridChildAtIndexedSlot = source.framesByIndex.size > 0 && slot.frameIndex !== undefined;
|
|
1650
|
+
return isGridChildAtIndexedSlot && selectedFrame?.frameIndex !== undefined
|
|
1651
|
+
? undefined
|
|
1652
|
+
: selectedFrame;
|
|
1147
1653
|
}
|
|
1148
|
-
function composeFrameAtTime(sources, mediaTime, endTime, options,
|
|
1654
|
+
function composeFrameAtTime(sources, mediaTime, endTime, options, coordinateSpace) {
|
|
1149
1655
|
const detections = [];
|
|
1150
1656
|
const activeFrameIndexes = [];
|
|
1151
1657
|
for (const source of sources) {
|
|
@@ -1169,7 +1675,7 @@ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex, co
|
|
|
1169
1675
|
return {
|
|
1170
1676
|
detections,
|
|
1171
1677
|
endTime,
|
|
1172
|
-
frameIndex:
|
|
1678
|
+
frameIndex: resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
|
|
1173
1679
|
mediaTime,
|
|
1174
1680
|
// Children were projected before composition, so a composed frame is
|
|
1175
1681
|
// already in the coordinate space the renderer presents.
|
|
@@ -1642,7 +2148,6 @@ function createProjectedDetectionFrameSource(source, resolveTarget) {
|
|
|
1642
2148
|
};
|
|
1643
2149
|
}
|
|
1644
2150
|
|
|
1645
|
-
const RANGE_EPSILON_SECONDS = 1e-6;
|
|
1646
2151
|
const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
|
|
1647
2152
|
const DEFAULT_LIVE_HOLD_SECONDS = 60;
|
|
1648
2153
|
const NO_COVERAGE_INVALIDATION = { ranges: [] };
|
|
@@ -1783,8 +2288,8 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1783
2288
|
// earlier, though, so report it rather than assume.
|
|
1784
2289
|
invalidation);
|
|
1785
2290
|
// Closing a held live frame shortens coverage. Reported availability
|
|
1786
|
-
// has to shrink with it, or
|
|
1787
|
-
// source covers time past the end of media.
|
|
2291
|
+
// has to shrink with it, or `waitForRange` and the buffered window
|
|
2292
|
+
// would still believe the source covers time past the end of media.
|
|
1788
2293
|
clipAvailableRangesAfter(endTime);
|
|
1789
2294
|
return finalSummary;
|
|
1790
2295
|
});
|
|
@@ -2249,10 +2754,6 @@ function recordAvailableRange(range, availableRanges) {
|
|
|
2249
2754
|
}
|
|
2250
2755
|
availableRanges.length = writeIndex;
|
|
2251
2756
|
}
|
|
2252
|
-
function isRangeCovered(range, availableRanges) {
|
|
2253
|
-
return availableRanges.some((availableRange) => availableRange.startTime <= range.startTime + RANGE_EPSILON_SECONDS &&
|
|
2254
|
-
availableRange.endTime + RANGE_EPSILON_SECONDS >= range.endTime);
|
|
2255
|
-
}
|
|
2256
2757
|
|
|
2257
2758
|
/**
|
|
2258
2759
|
* Creates one stateful SORT tracker for a single ordered media sequence.
|
|
@@ -3584,53 +4085,230 @@ function extractMaskContour(data, width, height) {
|
|
|
3584
4085
|
}
|
|
3585
4086
|
function extractMaskRectRuns(data, width, height) {
|
|
3586
4087
|
assertMaskDimensions(data, width, height);
|
|
4088
|
+
const rects = mergeRowSpansIntoRects(data, width, height, 0, 0);
|
|
4089
|
+
return rects.length > 0 ? rects : undefined;
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
4092
|
+
* The rect runs of a compressed-RLE mask, without holding the mask's raster.
|
|
4093
|
+
*
|
|
4094
|
+
* A mask is the size of the whole frame, so reading a silhouette that covers a
|
|
4095
|
+
* fraction of one percent of it otherwise costs a megabytes-wide allocation and
|
|
4096
|
+
* a pass over every pixel. The counts carry the occupied box, so the raster and
|
|
4097
|
+
* the pass are that box.
|
|
4098
|
+
*/
|
|
4099
|
+
function extractDetectionMaskRectRuns(mask) {
|
|
4100
|
+
if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {
|
|
4101
|
+
throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);
|
|
4102
|
+
}
|
|
4103
|
+
const { height, width } = mask;
|
|
4104
|
+
assertMaskSize(width, height);
|
|
4105
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
4106
|
+
const bounds = computeCompressedRleBounds(counts, width, height);
|
|
4107
|
+
if (!bounds) {
|
|
4108
|
+
return undefined;
|
|
4109
|
+
}
|
|
4110
|
+
const { left, top } = bounds;
|
|
4111
|
+
const boundsWidth = bounds.width;
|
|
4112
|
+
const boundsHeight = bounds.height;
|
|
4113
|
+
const boundsBuffer = new Uint8Array(boundsWidth * boundsHeight);
|
|
4114
|
+
const pixels = width * height;
|
|
4115
|
+
let offset = 0;
|
|
4116
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
4117
|
+
const start = offset;
|
|
4118
|
+
offset += counts[index] ?? 0;
|
|
4119
|
+
if (!isForegroundRun(index) || start >= pixels) {
|
|
4120
|
+
continue;
|
|
4121
|
+
}
|
|
4122
|
+
const end = Math.min(offset, pixels);
|
|
4123
|
+
let cursor = start;
|
|
4124
|
+
while (cursor < end) {
|
|
4125
|
+
const column = Math.floor(cursor / height);
|
|
4126
|
+
const columnEnd = Math.min(end, (column + 1) * height);
|
|
4127
|
+
let target = (cursor - column * height - top) * boundsWidth + (column - left);
|
|
4128
|
+
for (let step = columnEnd - cursor; step > 0; step -= 1) {
|
|
4129
|
+
boundsBuffer[target] = 1;
|
|
4130
|
+
target += boundsWidth;
|
|
4131
|
+
}
|
|
4132
|
+
cursor = columnEnd;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
const rects = mergeRowSpansIntoRects(boundsBuffer, boundsWidth, boundsHeight, left, top);
|
|
4136
|
+
return rects.length > 0 ? rects : undefined;
|
|
4137
|
+
}
|
|
4138
|
+
/** Counts alternate background, foreground, starting on background. */
|
|
4139
|
+
function isForegroundRun(index) {
|
|
4140
|
+
return index % 2 === 1;
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* The occupied box of a compressed-RLE mask, read from the counts alone.
|
|
4144
|
+
*
|
|
4145
|
+
* COCO runs are column-major, so a run confined to one column bounds its own
|
|
4146
|
+
* rows, and a run that reaches the next column has already spanned a full
|
|
4147
|
+
* column top to bottom.
|
|
4148
|
+
*/
|
|
4149
|
+
function computeCompressedRleBounds(counts, width, height) {
|
|
4150
|
+
const pixels = width * height;
|
|
4151
|
+
let left = width;
|
|
4152
|
+
let top = height;
|
|
4153
|
+
let right = -1;
|
|
4154
|
+
let bottom = -1;
|
|
4155
|
+
let offset = 0;
|
|
4156
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
4157
|
+
const start = offset;
|
|
4158
|
+
offset += counts[index] ?? 0;
|
|
4159
|
+
if (!isForegroundRun(index) || start >= pixels) {
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
const end = Math.min(offset, pixels) - 1;
|
|
4163
|
+
const startColumn = Math.floor(start / height);
|
|
4164
|
+
const endColumn = Math.floor(end / height);
|
|
4165
|
+
if (startColumn < left) {
|
|
4166
|
+
left = startColumn;
|
|
4167
|
+
}
|
|
4168
|
+
if (endColumn > right) {
|
|
4169
|
+
right = endColumn;
|
|
4170
|
+
}
|
|
4171
|
+
if (startColumn !== endColumn) {
|
|
4172
|
+
top = 0;
|
|
4173
|
+
bottom = height - 1;
|
|
4174
|
+
continue;
|
|
4175
|
+
}
|
|
4176
|
+
const startRow = start - startColumn * height;
|
|
4177
|
+
const endRow = end - endColumn * height;
|
|
4178
|
+
if (startRow < top) {
|
|
4179
|
+
top = startRow;
|
|
4180
|
+
}
|
|
4181
|
+
if (endRow > bottom) {
|
|
4182
|
+
bottom = endRow;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return right < 0
|
|
4186
|
+
? null
|
|
4187
|
+
: {
|
|
4188
|
+
height: bottom - top + 1,
|
|
4189
|
+
left,
|
|
4190
|
+
top,
|
|
4191
|
+
width: right - left + 1,
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
function createOpenMaskSpans(capacity) {
|
|
4195
|
+
return {
|
|
4196
|
+
openedAt: new Int32Array(capacity),
|
|
4197
|
+
runWidth: new Int32Array(capacity),
|
|
4198
|
+
startX: new Int32Array(capacity),
|
|
4199
|
+
topY: new Int32Array(capacity),
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Emits rects in the coordinates of the raster the given window was cut from,
|
|
4204
|
+
* not the window's own.
|
|
4205
|
+
*
|
|
4206
|
+
* A span continues the rect above it when its start and width both match, and
|
|
4207
|
+
* the row above holds exactly one open rect per span, so walking both in
|
|
4208
|
+
* ascending x pairs them without a lookup.
|
|
4209
|
+
*/
|
|
4210
|
+
function mergeRowSpansIntoRects(data, width, height, offsetX, offsetY) {
|
|
3587
4211
|
const rects = [];
|
|
3588
|
-
|
|
4212
|
+
// Spans need a gap between them, so a row holds at most half its width.
|
|
4213
|
+
const capacity = (width >> 1) + 2;
|
|
4214
|
+
const closed = new Int32Array(capacity);
|
|
4215
|
+
let open = createOpenMaskSpans(capacity);
|
|
4216
|
+
let next = createOpenMaskSpans(capacity);
|
|
4217
|
+
let openCount = 0;
|
|
4218
|
+
let opened = 0;
|
|
3589
4219
|
for (let y = 0; y < height; y += 1) {
|
|
3590
|
-
const
|
|
4220
|
+
const row = y * width;
|
|
4221
|
+
let nextCount = 0;
|
|
4222
|
+
let closedCount = 0;
|
|
4223
|
+
let cursor = 0;
|
|
3591
4224
|
let x = 0;
|
|
3592
4225
|
while (x < width) {
|
|
3593
|
-
while (x < width &&
|
|
4226
|
+
while (x < width && data[row + x] === 0) {
|
|
3594
4227
|
x += 1;
|
|
3595
4228
|
}
|
|
3596
4229
|
if (x >= width) {
|
|
3597
4230
|
break;
|
|
3598
4231
|
}
|
|
3599
4232
|
const startX = x;
|
|
3600
|
-
while (x < width && data[
|
|
4233
|
+
while (x < width && data[row + x] !== 0) {
|
|
3601
4234
|
x += 1;
|
|
3602
4235
|
}
|
|
3603
4236
|
const runWidth = x - startX;
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
4237
|
+
while (cursor < openCount && open.startX[cursor] < startX) {
|
|
4238
|
+
closed[closedCount] = cursor;
|
|
4239
|
+
closedCount += 1;
|
|
4240
|
+
cursor += 1;
|
|
4241
|
+
}
|
|
4242
|
+
if (cursor < openCount &&
|
|
4243
|
+
open.startX[cursor] === startX &&
|
|
4244
|
+
open.runWidth[cursor] === runWidth) {
|
|
4245
|
+
next.openedAt[nextCount] = open.openedAt[cursor];
|
|
4246
|
+
next.topY[nextCount] = open.topY[cursor];
|
|
4247
|
+
cursor += 1;
|
|
3609
4248
|
}
|
|
3610
4249
|
else {
|
|
3611
|
-
if (
|
|
3612
|
-
|
|
4250
|
+
if (cursor < openCount && open.startX[cursor] === startX) {
|
|
4251
|
+
closed[closedCount] = cursor;
|
|
4252
|
+
closedCount += 1;
|
|
4253
|
+
cursor += 1;
|
|
3613
4254
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
for (const [key, openRect] of openRects) {
|
|
3618
|
-
if (!activeSpans.has(key)) {
|
|
3619
|
-
rects.push(openRect);
|
|
3620
|
-
openRects.delete(key);
|
|
4255
|
+
next.openedAt[nextCount] = opened;
|
|
4256
|
+
next.topY[nextCount] = y;
|
|
4257
|
+
opened += 1;
|
|
3621
4258
|
}
|
|
3622
|
-
|
|
4259
|
+
next.runWidth[nextCount] = runWidth;
|
|
4260
|
+
next.startX[nextCount] = startX;
|
|
4261
|
+
nextCount += 1;
|
|
4262
|
+
}
|
|
4263
|
+
while (cursor < openCount) {
|
|
4264
|
+
closed[closedCount] = cursor;
|
|
4265
|
+
closedCount += 1;
|
|
4266
|
+
cursor += 1;
|
|
4267
|
+
}
|
|
4268
|
+
pushClosedRects(rects, open, closed, closedCount, y, offsetX, offsetY);
|
|
4269
|
+
const carried = open;
|
|
4270
|
+
open = next;
|
|
4271
|
+
next = carried;
|
|
4272
|
+
openCount = nextCount;
|
|
4273
|
+
}
|
|
4274
|
+
for (let index = 0; index < openCount; index += 1) {
|
|
4275
|
+
closed[index] = index;
|
|
4276
|
+
}
|
|
4277
|
+
pushClosedRects(rects, open, closed, openCount, height, offsetX, offsetY);
|
|
4278
|
+
return rects;
|
|
4279
|
+
}
|
|
4280
|
+
/** Rects leave in the order they opened, which is not their order across a row. */
|
|
4281
|
+
function pushClosedRects(rects, open, closed, closedCount, bottomY, offsetX, offsetY) {
|
|
4282
|
+
for (let index = 1; index < closedCount; index += 1) {
|
|
4283
|
+
const span = closed[index];
|
|
4284
|
+
const openedAt = open.openedAt[span];
|
|
4285
|
+
let sorted = index - 1;
|
|
4286
|
+
while (sorted >= 0 && open.openedAt[closed[sorted]] > openedAt) {
|
|
4287
|
+
closed[sorted + 1] = closed[sorted];
|
|
4288
|
+
sorted -= 1;
|
|
4289
|
+
}
|
|
4290
|
+
closed[sorted + 1] = span;
|
|
4291
|
+
}
|
|
4292
|
+
for (let index = 0; index < closedCount; index += 1) {
|
|
4293
|
+
const span = closed[index];
|
|
4294
|
+
rects.push({
|
|
4295
|
+
height: bottomY - open.topY[span],
|
|
4296
|
+
width: open.runWidth[span],
|
|
4297
|
+
x: open.startX[span] + offsetX,
|
|
4298
|
+
y: open.topY[span] + offsetY,
|
|
4299
|
+
});
|
|
3623
4300
|
}
|
|
3624
|
-
rects.push(...openRects.values());
|
|
3625
|
-
return rects.length > 0 ? rects : undefined;
|
|
3626
4301
|
}
|
|
3627
|
-
function
|
|
4302
|
+
function assertMaskSize(width, height) {
|
|
3628
4303
|
if (!Number.isInteger(width) || width <= 0) {
|
|
3629
4304
|
throw new Error("Mask width must be a positive integer.");
|
|
3630
4305
|
}
|
|
3631
4306
|
if (!Number.isInteger(height) || height <= 0) {
|
|
3632
4307
|
throw new Error("Mask height must be a positive integer.");
|
|
3633
4308
|
}
|
|
4309
|
+
}
|
|
4310
|
+
function assertMaskDimensions(data, width, height) {
|
|
4311
|
+
assertMaskSize(width, height);
|
|
3634
4312
|
if (data.length !== width * height) {
|
|
3635
4313
|
throw new Error("Mask data length must equal width * height.");
|
|
3636
4314
|
}
|
|
@@ -3950,10 +4628,18 @@ function pickDetectionAtPoint(frame, point, options = {}) {
|
|
|
3950
4628
|
if (!frame) {
|
|
3951
4629
|
return null;
|
|
3952
4630
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
const
|
|
4631
|
+
// Tolerances are screen-space sizes; a zoomed-out viewport maps more media
|
|
4632
|
+
// units per screen pixel, so a fixed media-space padding would shrink the
|
|
4633
|
+
// hit target below the drawn marker.
|
|
4634
|
+
const viewportScale = options.viewportScale !== undefined &&
|
|
4635
|
+
Number.isFinite(options.viewportScale) &&
|
|
4636
|
+
options.viewportScale > 0
|
|
4637
|
+
? options.viewportScale
|
|
4638
|
+
: 1;
|
|
4639
|
+
const padding = Math.max(0, options.padding ?? 0) / viewportScale;
|
|
4640
|
+
const polylinePadding = Math.max(0, options.polylinePadding ?? 6) / viewportScale;
|
|
4641
|
+
const keypointPadding = Math.max(0, options.keypointPadding ?? 10) / viewportScale;
|
|
4642
|
+
const edgePadding = Math.max(0, options.edgePadding ?? 8) / viewportScale;
|
|
3957
4643
|
const candidates = [];
|
|
3958
4644
|
for (let detectionIndex = 0; detectionIndex < frame.detections.length; detectionIndex += 1) {
|
|
3959
4645
|
const detection = frame.detections[detectionIndex];
|
|
@@ -3992,10 +4678,22 @@ function pickDetectionAtPoint(frame, point, options = {}) {
|
|
|
3992
4678
|
}
|
|
3993
4679
|
}
|
|
3994
4680
|
if (detection.keypoints) {
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4681
|
+
// Clustered keypoints share the pick tolerance; take the nearest so the
|
|
4682
|
+
// hover label, the menu, and the handle drag all name the same point.
|
|
4683
|
+
// Later points draw on top, so they win exact ties.
|
|
4684
|
+
let keypointIndex = -1;
|
|
4685
|
+
let keypointDistance = Number.POSITIVE_INFINITY;
|
|
4686
|
+
detection.keypoints.points.forEach((keypoint, index) => {
|
|
4687
|
+
if (detection.keypoints?.visibility?.[index] ===
|
|
4688
|
+
KeypointVisibility.NotLabeled) {
|
|
4689
|
+
return;
|
|
4690
|
+
}
|
|
4691
|
+
const distance = Math.hypot(point.x - keypoint.x, point.y - keypoint.y);
|
|
4692
|
+
if (distance <= keypointPadding && distance <= keypointDistance) {
|
|
4693
|
+
keypointIndex = index;
|
|
4694
|
+
keypointDistance = distance;
|
|
4695
|
+
}
|
|
4696
|
+
});
|
|
3999
4697
|
if (keypointIndex !== -1) {
|
|
4000
4698
|
pushCandidate(DetectionPickTarget.Keypoint, Math.PI * keypointPadding * keypointPadding, keypointIndex);
|
|
4001
4699
|
}
|
|
@@ -4368,29 +5066,77 @@ function getAnnotationHandles(detection, viewportScale = 1) {
|
|
|
4368
5066
|
if (detection.polyline)
|
|
4369
5067
|
return getPathHandles(detection.polyline.points, false, radius, hitSize);
|
|
4370
5068
|
if (detection.keypoints) {
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
5069
|
+
const keypointHandles = detection.keypoints.points.flatMap((point, geometryIndex) => detection.keypoints?.visibility?.[geometryIndex] ===
|
|
5070
|
+
KeypointVisibility.NotLabeled
|
|
5071
|
+
? []
|
|
5072
|
+
: [
|
|
5073
|
+
{
|
|
5074
|
+
cursor: "pointer",
|
|
5075
|
+
geometryIndex,
|
|
5076
|
+
hitSize,
|
|
5077
|
+
id: `kp-${geometryIndex}`,
|
|
5078
|
+
kind: AnnotationHandleKind.Keypoint,
|
|
5079
|
+
point,
|
|
5080
|
+
radius,
|
|
5081
|
+
},
|
|
5082
|
+
]);
|
|
5083
|
+
if (!detection.rect)
|
|
5084
|
+
return keypointHandles;
|
|
5085
|
+
// The skeleton's box resizes like any box; keypoints come last so a point
|
|
5086
|
+
// sitting on the box edge still wins the hit test.
|
|
5087
|
+
return [
|
|
5088
|
+
...getBoxHandles(detection.rect, radius, hitSize),
|
|
5089
|
+
...keypointHandles,
|
|
5090
|
+
];
|
|
4380
5091
|
}
|
|
4381
5092
|
if (detection.rect)
|
|
4382
5093
|
return getBoxHandles(detection.rect, radius, hitSize);
|
|
4383
5094
|
return [];
|
|
4384
5095
|
}
|
|
4385
5096
|
function pickAnnotationHandle(handles, point) {
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
Math.abs(point.
|
|
5097
|
+
let picked;
|
|
5098
|
+
let pickedDistance = Number.POSITIVE_INFINITY;
|
|
5099
|
+
for (const handle of handles) {
|
|
5100
|
+
const dx = Math.abs(point.x - handle.point.x);
|
|
5101
|
+
const dy = Math.abs(point.y - handle.point.y);
|
|
5102
|
+
if (dx > handle.hitSize / 2 || dy > handle.hitSize / 2)
|
|
5103
|
+
continue;
|
|
5104
|
+
// Clustered handles (face keypoints, a tiny box's corners) share hit
|
|
5105
|
+
// areas; the nearest one is what the pointer is on. Later handles draw on
|
|
5106
|
+
// top, so they win exact ties.
|
|
5107
|
+
const distance = Math.hypot(dx, dy);
|
|
5108
|
+
if (distance <= pickedDistance) {
|
|
5109
|
+
picked = handle;
|
|
5110
|
+
pickedDistance = distance;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return picked;
|
|
4390
5114
|
}
|
|
4391
5115
|
function applyAnnotationHandleDrag(detection, handle, point) {
|
|
4392
5116
|
if (detection.rect && handle.kind === AnnotationHandleKind.Resize) {
|
|
4393
|
-
|
|
5117
|
+
const rect = resizeRect(detection.rect, handle.id, point);
|
|
5118
|
+
const boxRelative = detection.keypoints?.boxRelative;
|
|
5119
|
+
if (!detection.keypoints || !boxRelative?.some(Boolean)) {
|
|
5120
|
+
return { ...detection, rect };
|
|
5121
|
+
}
|
|
5122
|
+
// Box-relative keypoints (unplaced template points) follow the rect;
|
|
5123
|
+
// points the user positioned keep their coordinates.
|
|
5124
|
+
const from = detection.rect;
|
|
5125
|
+
const scaleX = from.width > 0 ? rect.width / from.width : 1;
|
|
5126
|
+
const scaleY = from.height > 0 ? rect.height / from.height : 1;
|
|
5127
|
+
return {
|
|
5128
|
+
...detection,
|
|
5129
|
+
keypoints: {
|
|
5130
|
+
...detection.keypoints,
|
|
5131
|
+
points: detection.keypoints.points.map((keypoint, index) => boxRelative[index]
|
|
5132
|
+
? {
|
|
5133
|
+
x: rect.x + (keypoint.x - from.x) * scaleX,
|
|
5134
|
+
y: rect.y + (keypoint.y - from.y) * scaleY,
|
|
5135
|
+
}
|
|
5136
|
+
: keypoint),
|
|
5137
|
+
},
|
|
5138
|
+
rect,
|
|
5139
|
+
};
|
|
4394
5140
|
}
|
|
4395
5141
|
if (handle.kind === AnnotationHandleKind.AddVertex &&
|
|
4396
5142
|
handle.edgeIndex !== undefined) {
|
|
@@ -4411,7 +5157,16 @@ function applyAnnotationHandleDrag(detection, handle, point) {
|
|
|
4411
5157
|
if (detection.keypoints) {
|
|
4412
5158
|
const points = [...detection.keypoints.points];
|
|
4413
5159
|
points[handle.geometryIndex] = point;
|
|
4414
|
-
|
|
5160
|
+
// A positioned point no longer follows the box.
|
|
5161
|
+
const boxRelative = detection.keypoints.boxRelative?.map((relative, index) => relative && index !== handle.geometryIndex);
|
|
5162
|
+
return {
|
|
5163
|
+
...detection,
|
|
5164
|
+
keypoints: {
|
|
5165
|
+
...detection.keypoints,
|
|
5166
|
+
points,
|
|
5167
|
+
...(boxRelative ? { boxRelative } : {}),
|
|
5168
|
+
},
|
|
5169
|
+
};
|
|
4415
5170
|
}
|
|
4416
5171
|
return detection;
|
|
4417
5172
|
}
|
|
@@ -4580,7 +5335,7 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4580
5335
|
case "move": {
|
|
4581
5336
|
const dx = input.point.x - gesture.start.point.x;
|
|
4582
5337
|
const dy = input.point.y - gesture.start.point.y;
|
|
4583
|
-
const moved = gesture.moved ||
|
|
5338
|
+
const moved = gesture.moved || exceedsMoveThreshold(gesture.start, input);
|
|
4584
5339
|
gesture = { ...gesture, current: input, moved };
|
|
4585
5340
|
if (moved) {
|
|
4586
5341
|
const preview = offsetDetection(gesture.detection, dx, dy);
|
|
@@ -4594,10 +5349,17 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4594
5349
|
}
|
|
4595
5350
|
break;
|
|
4596
5351
|
}
|
|
4597
|
-
case "resize":
|
|
4598
|
-
|
|
4599
|
-
|
|
5352
|
+
case "resize": {
|
|
5353
|
+
// A click on a handle is not a drag: a keypoint tapped to open its
|
|
5354
|
+
// menu must not snap to the pointer, so nothing previews or commits
|
|
5355
|
+
// until the pointer has travelled like a move gesture.
|
|
5356
|
+
const moved = gesture.moved || exceedsMoveThreshold(gesture.start, input);
|
|
5357
|
+
gesture = { ...gesture, current: input, moved };
|
|
5358
|
+
if (moved) {
|
|
5359
|
+
setPreview(applyAnnotationHandleDrag(gesture.detection, gesture.handle, input.point));
|
|
5360
|
+
}
|
|
4600
5361
|
break;
|
|
5362
|
+
}
|
|
4601
5363
|
case "path":
|
|
4602
5364
|
setPreview(resolvePathPreview([...gesture.points, input.point]));
|
|
4603
5365
|
break;
|
|
@@ -4659,6 +5421,10 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4659
5421
|
commit(preview, active.detection);
|
|
4660
5422
|
return;
|
|
4661
5423
|
}
|
|
5424
|
+
if (!active.moved) {
|
|
5425
|
+
setState(idleState());
|
|
5426
|
+
return;
|
|
5427
|
+
}
|
|
4662
5428
|
const preview = applyAnnotationHandleDrag(active.detection, active.handle, input.point);
|
|
4663
5429
|
commit(preview, active.detection);
|
|
4664
5430
|
},
|
|
@@ -4679,6 +5445,7 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4679
5445
|
detection,
|
|
4680
5446
|
handle,
|
|
4681
5447
|
kind: "resize",
|
|
5448
|
+
moved: false,
|
|
4682
5449
|
start: input,
|
|
4683
5450
|
};
|
|
4684
5451
|
setState({
|
|
@@ -4686,7 +5453,7 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4686
5453
|
activeHandleId: handle.id,
|
|
4687
5454
|
kind: AnnotationGestureStateKind.Resizing,
|
|
4688
5455
|
pointerId: input.pointerId ?? null,
|
|
4689
|
-
preview:
|
|
5456
|
+
preview: null,
|
|
4690
5457
|
});
|
|
4691
5458
|
},
|
|
4692
5459
|
deleteVertex(detection, vertexIndex) {
|
|
@@ -4864,6 +5631,10 @@ function createAnnotationEditingEngine(options = {}) {
|
|
|
4864
5631
|
function scale() {
|
|
4865
5632
|
return Math.max(options.viewportScale?.() ?? 1, Number.EPSILON);
|
|
4866
5633
|
}
|
|
5634
|
+
function exceedsMoveThreshold(start, input) {
|
|
5635
|
+
return (Math.hypot(input.point.x - start.point.x, input.point.y - start.point.y) >=
|
|
5636
|
+
MOVE_THRESHOLD / scale());
|
|
5637
|
+
}
|
|
4867
5638
|
function capture(pointerId) {
|
|
4868
5639
|
if (pointerId !== undefined)
|
|
4869
5640
|
options.capturePointer?.(pointerId);
|
|
@@ -5218,8 +5989,8 @@ const DEFAULT_SELECTED_STROKE_WIDTH = 4;
|
|
|
5218
5989
|
* Default configurable interaction style.
|
|
5219
5990
|
*
|
|
5220
5991
|
* It resolves hover and selected picks into state-specific presentations using
|
|
5221
|
-
* the same style contracts as normal boxes, masks, and labels.
|
|
5222
|
-
*
|
|
5992
|
+
* the same style contracts as normal boxes, masks, and labels. A state left
|
|
5993
|
+
* unconfigured falls back to a highlight rectangle over the detection.
|
|
5223
5994
|
*/
|
|
5224
5995
|
class BaseInteractionStyle {
|
|
5225
5996
|
options;
|
|
@@ -5241,60 +6012,22 @@ class BaseInteractionStyle {
|
|
|
5241
6012
|
return undefined;
|
|
5242
6013
|
}
|
|
5243
6014
|
return {
|
|
5244
|
-
boxStyle:
|
|
5245
|
-
};
|
|
5246
|
-
}
|
|
5247
|
-
resolveBoxInstruction(detection, context, rect) {
|
|
5248
|
-
const shape = this.resolveShape(detection, context);
|
|
5249
|
-
const cornerRadius = this.resolveCornerRadius(detection, context, shape);
|
|
5250
|
-
const instruction = {
|
|
5251
|
-
fill: this.resolveFill(detection, context),
|
|
5252
|
-
rect,
|
|
5253
|
-
shape,
|
|
5254
|
-
stroke: this.resolveStroke(detection, context),
|
|
6015
|
+
boxStyle: createDefaultHighlightBoxStyle(context.state, rect),
|
|
5255
6016
|
};
|
|
5256
|
-
if (cornerRadius !== undefined) {
|
|
5257
|
-
return {
|
|
5258
|
-
...instruction,
|
|
5259
|
-
cornerRadius,
|
|
5260
|
-
};
|
|
5261
|
-
}
|
|
5262
|
-
return instruction;
|
|
5263
|
-
}
|
|
5264
|
-
resolveShape(detection, context) {
|
|
5265
|
-
return (resolveStyleValue(this.options.shape, detection, context) ?? BoxShape.Rect);
|
|
5266
6017
|
}
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
if (stroke === null) {
|
|
5276
|
-
return undefined;
|
|
5277
|
-
}
|
|
5278
|
-
const defaults = getStateDefaults(context.state);
|
|
5279
|
-
return resolveStrokeStyle(stroke, {
|
|
6018
|
+
}
|
|
6019
|
+
function createDefaultHighlightBoxStyle(state, rect) {
|
|
6020
|
+
const defaults = getStateDefaults(state);
|
|
6021
|
+
const instruction = {
|
|
6022
|
+
fill: { alpha: defaults.fillAlpha, color: defaults.fillColor },
|
|
6023
|
+
rect,
|
|
6024
|
+
shape: BoxShape.Rect,
|
|
6025
|
+
stroke: {
|
|
5280
6026
|
alpha: defaults.strokeAlpha,
|
|
5281
6027
|
color: defaults.strokeColor,
|
|
5282
6028
|
width: defaults.strokeWidth,
|
|
5283
|
-
}
|
|
5284
|
-
}
|
|
5285
|
-
resolveFill(detection, context) {
|
|
5286
|
-
const fill = resolveStyleValue(this.options.fill, detection, context);
|
|
5287
|
-
if (fill === null) {
|
|
5288
|
-
return undefined;
|
|
5289
|
-
}
|
|
5290
|
-
const defaults = getStateDefaults(context.state);
|
|
5291
|
-
return {
|
|
5292
|
-
alpha: fill?.alpha ?? defaults.fillAlpha,
|
|
5293
|
-
color: fill?.color ?? defaults.fillColor,
|
|
5294
|
-
};
|
|
5295
|
-
}
|
|
5296
|
-
}
|
|
5297
|
-
function createResolvedBoxStyle(instruction) {
|
|
6029
|
+
},
|
|
6030
|
+
};
|
|
5298
6031
|
return {
|
|
5299
6032
|
resolve() {
|
|
5300
6033
|
return instruction;
|
|
@@ -5694,8 +6427,18 @@ class BasePolylineStyle {
|
|
|
5694
6427
|
if (stroke === null) {
|
|
5695
6428
|
return undefined;
|
|
5696
6429
|
}
|
|
6430
|
+
const shadowStroke = resolveStyleValue(this.options.shadowStroke, detection, context);
|
|
5697
6431
|
return {
|
|
5698
6432
|
points: detection.polyline.points,
|
|
6433
|
+
...(shadowStroke === undefined || shadowStroke === null
|
|
6434
|
+
? {}
|
|
6435
|
+
: {
|
|
6436
|
+
shadowStroke: resolveStrokeStyle(shadowStroke, {
|
|
6437
|
+
alpha: 0.65,
|
|
6438
|
+
color: 0x000000,
|
|
6439
|
+
width: 4,
|
|
6440
|
+
}),
|
|
6441
|
+
}),
|
|
5699
6442
|
stroke: resolveStrokeStyle(stroke, {
|
|
5700
6443
|
alpha: 1,
|
|
5701
6444
|
color: 0x00ff66,
|
|
@@ -5865,6 +6608,8 @@ const DEFAULT_KEYPOINT_EDGE_WIDTH = 1.5;
|
|
|
5865
6608
|
const DEFAULT_KEYPOINT_SHADOW_WIDTH = 3;
|
|
5866
6609
|
const DEFAULT_KEYPOINT_SHADOW_ALPHA = 0.25;
|
|
5867
6610
|
const DEFAULT_KEYPOINT_RADIUS = 3.5;
|
|
6611
|
+
const DEFAULT_POLYLINE_SHADOW_WIDTH = DEFAULT_OUTLINE_WIDTH + 1;
|
|
6612
|
+
const DEFAULT_POLYLINE_SHADOW_ALPHA = 0.55;
|
|
5868
6613
|
const DEFAULT_LABEL_CORNER_RADIUS = 4;
|
|
5869
6614
|
const DEFAULT_LABEL_PADDING_X = 6;
|
|
5870
6615
|
const DEFAULT_LABEL_PADDING_Y = 3;
|
|
@@ -6053,6 +6798,11 @@ function createDefaultPolygonStyle(options = {}) {
|
|
|
6053
6798
|
function createDefaultPolylineStyle(options = {}) {
|
|
6054
6799
|
const getClassColor = createClassColorResolver(options);
|
|
6055
6800
|
return new BasePolylineStyle({
|
|
6801
|
+
shadowStroke: {
|
|
6802
|
+
alpha: DEFAULT_POLYLINE_SHADOW_ALPHA,
|
|
6803
|
+
color: 0x000000,
|
|
6804
|
+
width: DEFAULT_POLYLINE_SHADOW_WIDTH,
|
|
6805
|
+
},
|
|
6056
6806
|
stroke: (detection) => ({
|
|
6057
6807
|
alpha: 1,
|
|
6058
6808
|
color: getClassColor(detection),
|
|
@@ -6092,12 +6842,28 @@ const annotationRendererKinds = [
|
|
|
6092
6842
|
/** Sources currently supported by a region renderer. */
|
|
6093
6843
|
const RegionRendererSourceKind = {
|
|
6094
6844
|
Asset: "asset",
|
|
6845
|
+
Media: "media",
|
|
6095
6846
|
};
|
|
6096
6847
|
/** Media-space regions currently supported by a region renderer. */
|
|
6097
6848
|
const RegionRendererRegionKind = {
|
|
6098
6849
|
Bounds: "bounds",
|
|
6099
6850
|
KeypointAnchor: "keypoint-anchor",
|
|
6100
6851
|
};
|
|
6852
|
+
/** Semantic coverage used to clip pixels sampled from the current media. */
|
|
6853
|
+
const RegionRendererCoverageKind = {
|
|
6854
|
+
Mask: "mask",
|
|
6855
|
+
Polygon: "polygon",
|
|
6856
|
+
};
|
|
6857
|
+
/** Filter effects that can be applied to a current-frame media region. */
|
|
6858
|
+
const RegionRendererMediaEffectKind = {
|
|
6859
|
+
Blur: "blur",
|
|
6860
|
+
Pixelate: "pixelate",
|
|
6861
|
+
};
|
|
6862
|
+
/** Coordinate space used by an explicit region-renderer size. */
|
|
6863
|
+
const RegionRendererSizeSpace = {
|
|
6864
|
+
Media: "media",
|
|
6865
|
+
Screen: "screen",
|
|
6866
|
+
};
|
|
6101
6867
|
/** Composition modes currently supported by a region renderer. */
|
|
6102
6868
|
const RegionRendererComposeMode = {
|
|
6103
6869
|
Over: "over",
|
|
@@ -6476,10 +7242,10 @@ function rasterizeRectToMask(rect, dimensions) {
|
|
|
6476
7242
|
const topLeft = centerRectToTopLeftRect(rect);
|
|
6477
7243
|
const left = Math.max(0, Math.round(topLeft.x));
|
|
6478
7244
|
const top = Math.max(0, Math.round(topLeft.y));
|
|
6479
|
-
const right = Math.min(dimensions.width
|
|
6480
|
-
const bottom = Math.min(dimensions.height
|
|
6481
|
-
for (let y = top; y
|
|
6482
|
-
for (let x = left; x
|
|
7245
|
+
const right = Math.min(dimensions.width, Math.round(topLeft.x + rect.width));
|
|
7246
|
+
const bottom = Math.min(dimensions.height, Math.round(topLeft.y + rect.height));
|
|
7247
|
+
for (let y = top; y < bottom; y += 1) {
|
|
7248
|
+
for (let x = left; x < right; x += 1) {
|
|
6483
7249
|
data[y * dimensions.width + x] = 1;
|
|
6484
7250
|
}
|
|
6485
7251
|
}
|
|
@@ -6507,9 +7273,9 @@ function rasterizePolygonToMask(points, dimensions) {
|
|
|
6507
7273
|
}
|
|
6508
7274
|
intersections.sort((left, right) => left - right);
|
|
6509
7275
|
for (let index = 0; index < intersections.length - 1; index += 2) {
|
|
6510
|
-
const left = Math.max(0, Math.
|
|
6511
|
-
const right = Math.min(dimensions.width
|
|
6512
|
-
for (let x = left; x
|
|
7276
|
+
const left = Math.max(0, Math.round(intersections[index]));
|
|
7277
|
+
const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));
|
|
7278
|
+
for (let x = left; x < right; x += 1) {
|
|
6513
7279
|
data[y * dimensions.width + x] = 1;
|
|
6514
7280
|
}
|
|
6515
7281
|
}
|
|
@@ -6649,14 +7415,28 @@ function resolveMaskStyleOpacity(maskStyle) {
|
|
|
6649
7415
|
return Number.isFinite(opacity) ? Math.max(0, Math.min(opacity, 1)) : 1;
|
|
6650
7416
|
}
|
|
6651
7417
|
|
|
6652
|
-
|
|
7418
|
+
/**
|
|
7419
|
+
* Palette slots an id raster can name, one of them the background. A GLSL
|
|
7420
|
+
* fragment stage is guaranteed only 224 uniform vectors, and a palette entry
|
|
7421
|
+
* costs 2.25 of them, so this stays a multiple of four and well inside that
|
|
7422
|
+
* budget; past it a frame draws from the RGBA composite instead.
|
|
7423
|
+
*/
|
|
7424
|
+
const MAX_ID_MASK_PALETTE_ENTRIES = 80;
|
|
6653
7425
|
const MAX_ID_MASK_STROKE_WIDTH = 16;
|
|
6654
|
-
function createIdMaskFrame(instructions) {
|
|
7426
|
+
function createIdMaskFrame(instructions, options = {}) {
|
|
6655
7427
|
if (instructions.length === 0) {
|
|
6656
7428
|
return undefined;
|
|
6657
7429
|
}
|
|
6658
|
-
const
|
|
6659
|
-
const
|
|
7430
|
+
const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));
|
|
7431
|
+
const maskHeight = Math.max(...instructions.map(({ mask }) => mask.height));
|
|
7432
|
+
const width = resolveRasterWidth(maskWidth, options.maxWidth);
|
|
7433
|
+
const height = width === maskWidth
|
|
7434
|
+
? maskHeight
|
|
7435
|
+
: Math.max(1, Math.round((width * maskHeight) / maskWidth));
|
|
7436
|
+
const strokeScale = width / maskWidth;
|
|
7437
|
+
const scaledAxes = width === maskWidth
|
|
7438
|
+
? undefined
|
|
7439
|
+
: createScaledMaskAxes({ height, maskHeight, maskWidth, width });
|
|
6660
7440
|
const data = new Uint8Array(new ArrayBuffer(width * height));
|
|
6661
7441
|
const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
6662
7442
|
const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
@@ -6671,20 +7451,17 @@ function createIdMaskFrame(instructions) {
|
|
|
6671
7451
|
}
|
|
6672
7452
|
writePaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);
|
|
6673
7453
|
if (instruction.stroke && instruction.stroke.width > 0) {
|
|
6674
|
-
const strokeWidth = Math.min(
|
|
7454
|
+
const strokeWidth = Math.min(resolveStrokeTexels(instruction.stroke.width, strokeScale), MAX_ID_MASK_STROKE_WIDTH);
|
|
6675
7455
|
hasStroke = true;
|
|
6676
7456
|
strokeWidths[detectionMaskId] = strokeWidth;
|
|
6677
7457
|
maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);
|
|
6678
7458
|
writePaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
6679
7459
|
}
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
data[y * width + x] = detectionMaskId;
|
|
6686
|
-
}
|
|
6687
|
-
}
|
|
7460
|
+
if (scaledAxes) {
|
|
7461
|
+
writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);
|
|
7462
|
+
}
|
|
7463
|
+
else {
|
|
7464
|
+
writeMaskRuns(data, width, instruction.mask, detectionMaskId);
|
|
6688
7465
|
}
|
|
6689
7466
|
}
|
|
6690
7467
|
return {
|
|
@@ -6698,6 +7475,103 @@ function createIdMaskFrame(instructions) {
|
|
|
6698
7475
|
width,
|
|
6699
7476
|
};
|
|
6700
7477
|
}
|
|
7478
|
+
/**
|
|
7479
|
+
* Compressed RLE counts runs down each column in turn, so a foreground run is
|
|
7480
|
+
* a contiguous walk down one column that wraps into the next.
|
|
7481
|
+
*/
|
|
7482
|
+
function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {
|
|
7483
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
7484
|
+
const maskWidth = mask.width;
|
|
7485
|
+
const maskHeight = mask.height;
|
|
7486
|
+
let maskOffset = 0;
|
|
7487
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
7488
|
+
const runLength = counts[index] ?? 0;
|
|
7489
|
+
if (index % 2 === 0 || runLength <= 0) {
|
|
7490
|
+
maskOffset += runLength;
|
|
7491
|
+
continue;
|
|
7492
|
+
}
|
|
7493
|
+
let x = Math.floor(maskOffset / maskHeight);
|
|
7494
|
+
let y = maskOffset - x * maskHeight;
|
|
7495
|
+
let frameOffset = y * frameWidth + x;
|
|
7496
|
+
for (let step = 0; step < runLength; step += 1) {
|
|
7497
|
+
if (x >= maskWidth) {
|
|
7498
|
+
break;
|
|
7499
|
+
}
|
|
7500
|
+
data[frameOffset] = detectionMaskId;
|
|
7501
|
+
y += 1;
|
|
7502
|
+
frameOffset += frameWidth;
|
|
7503
|
+
if (y === maskHeight) {
|
|
7504
|
+
y = 0;
|
|
7505
|
+
x += 1;
|
|
7506
|
+
frameOffset = x;
|
|
7507
|
+
}
|
|
7508
|
+
}
|
|
7509
|
+
maskOffset += runLength;
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
/**
|
|
7513
|
+
* The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands
|
|
7514
|
+
* in, so a mask keeps every part of itself the smaller raster can hold and
|
|
7515
|
+
* gains up to a texel at its edges.
|
|
7516
|
+
*/
|
|
7517
|
+
function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {
|
|
7518
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
7519
|
+
const maskWidth = mask.width;
|
|
7520
|
+
const maskHeight = mask.height;
|
|
7521
|
+
let maskOffset = 0;
|
|
7522
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
7523
|
+
const runLength = counts[index] ?? 0;
|
|
7524
|
+
if (index % 2 === 0 || runLength <= 0) {
|
|
7525
|
+
maskOffset += runLength;
|
|
7526
|
+
continue;
|
|
7527
|
+
}
|
|
7528
|
+
let x = Math.floor(maskOffset / maskHeight);
|
|
7529
|
+
let y = maskOffset - x * maskHeight;
|
|
7530
|
+
let column = axes.columns[x];
|
|
7531
|
+
for (let step = 0; step < runLength; step += 1) {
|
|
7532
|
+
if (x >= maskWidth) {
|
|
7533
|
+
break;
|
|
7534
|
+
}
|
|
7535
|
+
data[axes.rows[y] + column] = detectionMaskId;
|
|
7536
|
+
y += 1;
|
|
7537
|
+
if (y === maskHeight) {
|
|
7538
|
+
y = 0;
|
|
7539
|
+
x += 1;
|
|
7540
|
+
column = axes.columns[x];
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
maskOffset += runLength;
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
function resolveRasterWidth(maskWidth, maxWidth) {
|
|
7547
|
+
return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth
|
|
7548
|
+
? Math.max(1, Math.floor(maxWidth))
|
|
7549
|
+
: maskWidth;
|
|
7550
|
+
}
|
|
7551
|
+
/**
|
|
7552
|
+
* A stroke is measured in texels of the raster it is drawn on, so a coarser
|
|
7553
|
+
* raster measures it in coarser texels. A stroke of a texel or more keeps at
|
|
7554
|
+
* least one, the thinnest line the shader can draw; a narrower one keeps its
|
|
7555
|
+
* own width, which the shader draws as an inner boundary at any scale.
|
|
7556
|
+
*/
|
|
7557
|
+
function resolveStrokeTexels(strokeWidth, scale) {
|
|
7558
|
+
return Math.max(strokeWidth * scale, Math.min(strokeWidth, 1));
|
|
7559
|
+
}
|
|
7560
|
+
function createScaledMaskAxes(frame) {
|
|
7561
|
+
return {
|
|
7562
|
+
columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),
|
|
7563
|
+
rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
/** Rows hold their destination offset, so writing a run is two lookups. */
|
|
7567
|
+
function createMaskAxisMap(sourceLength, targetLength, stride) {
|
|
7568
|
+
const map = new Int32Array(sourceLength);
|
|
7569
|
+
const scale = targetLength / sourceLength;
|
|
7570
|
+
for (let index = 0; index < sourceLength; index += 1) {
|
|
7571
|
+
map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;
|
|
7572
|
+
}
|
|
7573
|
+
return map;
|
|
7574
|
+
}
|
|
6701
7575
|
function writePaletteEntry(palette, id, color, alpha) {
|
|
6702
7576
|
const offset = id * 4;
|
|
6703
7577
|
palette[offset] = ((color >> 16) & 0xff) / 255;
|
|
@@ -6962,6 +7836,30 @@ var MediaSourceStatus;
|
|
|
6962
7836
|
MediaSourceStatus["Error"] = "error";
|
|
6963
7837
|
MediaSourceStatus["Destroyed"] = "destroyed";
|
|
6964
7838
|
})(MediaSourceStatus || (MediaSourceStatus = {}));
|
|
7839
|
+
/**
|
|
7840
|
+
* Current renderer state.
|
|
7841
|
+
*/
|
|
7842
|
+
/**
|
|
7843
|
+
* What a playback gate holds back.
|
|
7844
|
+
*
|
|
7845
|
+
* A source the renderer pulls samples from can be held between any two frames,
|
|
7846
|
+
* because the renderer decides when each one is drawn. A source that presents
|
|
7847
|
+
* its own frames owns the playhead, so holding it means stopping the producer
|
|
7848
|
+
* and starting it again, which the detection gate does and the
|
|
7849
|
+
* render-preparation gate does not.
|
|
7850
|
+
*/
|
|
7851
|
+
var PlaybackGateReach;
|
|
7852
|
+
(function (PlaybackGateReach) {
|
|
7853
|
+
/** No gate: the picture moves and unprepared layers are absent from it. */
|
|
7854
|
+
PlaybackGateReach["Off"] = "off";
|
|
7855
|
+
/**
|
|
7856
|
+
* Playback waits to begin, and stops again at any frame whose artifacts are
|
|
7857
|
+
* missing, until they arrive or the gate's own wait bound gives up on them.
|
|
7858
|
+
*/
|
|
7859
|
+
PlaybackGateReach["EveryFrame"] = "everyFrame";
|
|
7860
|
+
/** Playback waits to begin; frames after that are not held. */
|
|
7861
|
+
PlaybackGateReach["StartOfPlayback"] = "startOfPlayback";
|
|
7862
|
+
})(PlaybackGateReach || (PlaybackGateReach = {}));
|
|
6965
7863
|
|
|
6966
7864
|
/**
|
|
6967
7865
|
* Media-session operating mode.
|
|
@@ -7002,6 +7900,13 @@ var MediaSessionStatus;
|
|
|
7002
7900
|
*/
|
|
7003
7901
|
var MediaSessionActivityKind;
|
|
7004
7902
|
(function (MediaSessionActivityKind) {
|
|
7903
|
+
/**
|
|
7904
|
+
* Playback is held waiting for a producer to emit detections for the frame
|
|
7905
|
+
* about to be shown, which is a wait on inference rather than on transfer or
|
|
7906
|
+
* decode. `DetectionsBuffering` is the transfer of detections that already
|
|
7907
|
+
* exist, and `PlaybackBuffering` is the wait for media bytes.
|
|
7908
|
+
*/
|
|
7909
|
+
MediaSessionActivityKind["DetectionsAwaitingCoverage"] = "detectionsAwaitingCoverage";
|
|
7005
7910
|
MediaSessionActivityKind["DetectionsBuffering"] = "detectionsBuffering";
|
|
7006
7911
|
MediaSessionActivityKind["DetectionsLoading"] = "detectionsLoading";
|
|
7007
7912
|
MediaSessionActivityKind["Error"] = "error";
|
|
@@ -7020,5 +7925,5 @@ var MediaSessionActivityStatus;
|
|
|
7020
7925
|
MediaSessionActivityStatus["Waiting"] = "waiting";
|
|
7021
7926
|
})(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
|
|
7022
7927
|
|
|
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 };
|
|
7928
|
+
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, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, 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, extractDetectionMaskRectRuns, 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, resolveAnnotationRendererStyleFields, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
|
|
7024
7929
|
//# sourceMappingURL=index.js.map
|