supervision 0.1.7 → 0.2.0-next.1
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/constants/media-renderer.d.ts +3 -0
- package/dist/constants/media-renderer.d.ts.map +1 -1
- package/dist/detections/chunked-detection-frame-source.d.ts.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9281 -5038
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +409 -185
- 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-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-state.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 +54 -0
- package/dist/media/video-engine-media-source.d.ts.map +1 -0
- package/dist/media/video-engine-media-source.js +3 -0
- package/dist/media/video-engine-media-source.js.map +1 -0
- package/dist/playback/media-playback-controller.d.ts +7 -1
- package/dist/playback/media-playback-controller.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-artifact.d.ts +48 -12
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts +12 -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 +13 -3
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
- package/dist/render-preparation/prepared-render-window.d.ts +44 -2
- 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/injected-pixi.d.ts +38 -0
- package/dist/renderers/injected-pixi.d.ts.map +1 -0
- 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 +9 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +50 -2
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/media-renderer-state.d.ts +20 -1
- package/dist/renderers/media-renderer-state.d.ts.map +1 -1
- package/dist/renderers/media-renderer-transport.d.ts +72 -0
- package/dist/renderers/media-renderer-transport.d.ts.map +1 -0
- 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 +19 -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 +6 -25
- 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 +12 -2
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-halo.d.ts +18 -23
- package/dist/renderers/pixi-mask-halo.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-layer.d.ts +94 -32
- 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 +9 -2
- package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-region-coverage-mask.d.ts +62 -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 +43 -2
- package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-shader-lifecycle.d.ts +14 -0
- package/dist/renderers/pixi-shader-lifecycle.d.ts.map +1 -0
- 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 +138 -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 +73 -4
- package/dist/tracking.worker.js.map +1 -1
- 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 -15
- package/dist/types/media-session.d.ts.map +1 -1
- package/dist/types/render-preparation.d.ts +149 -5
- package/dist/types/render-preparation.d.ts.map +1 -1
- package/dist/video-engine-media-source-CUXnOJOV.js +450 -0
- package/dist/video-engine-media-source-CUXnOJOV.js.map +1 -0
- package/dist/web-video-engine/analysis-session.d.ts +75 -0
- package/dist/web-video-engine/analysis.d.ts +6 -0
- package/dist/web-video-engine/analysis.js +1850 -0
- package/dist/web-video-engine/cache-budget.d.ts +18 -0
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +89 -0
- package/dist/web-video-engine/clock.d.ts +82 -0
- package/dist/web-video-engine/constants.d.ts +261 -0
- package/dist/web-video-engine/create-scrub-cursor.d.ts +65 -0
- package/dist/web-video-engine/decode-resolution.d.ts +114 -0
- package/dist/web-video-engine/decode-scheduler.d.ts +371 -0
- package/dist/web-video-engine/decode-session.d.ts +283 -0
- package/dist/web-video-engine/decode-source.d.ts +258 -0
- package/dist/web-video-engine/diagnostics-store.d.ts +17 -0
- package/dist/web-video-engine/diagnostics.d.ts +316 -0
- package/dist/web-video-engine/embedded-engine-worker.d.ts +1 -0
- package/dist/web-video-engine/engine-core.d.ts +269 -0
- package/dist/web-video-engine/engine.d.ts +25 -0
- package/dist/web-video-engine/engine.js +912 -0
- package/dist/web-video-engine/engine.worker.d.ts +1 -0
- package/dist/web-video-engine/engine.worker.js +33624 -0
- package/dist/web-video-engine/frame-cache.d.ts +236 -0
- package/dist/web-video-engine/frame-extractor.d.ts +27 -0
- package/dist/web-video-engine/frame-timeline-DreEwsRY.js +616 -0
- package/dist/web-video-engine/frame-timeline.d.ts +80 -0
- package/dist/web-video-engine/frame-walker.d.ts +73 -0
- package/dist/web-video-engine/index.d.ts +13 -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 +64 -0
- package/dist/web-video-engine/keyframe-index.d.ts +90 -0
- package/dist/web-video-engine/mirror-store.d.ts +55 -0
- package/dist/web-video-engine/renderer.d.ts +93 -0
- package/dist/web-video-engine/rotation.d.ts +34 -0
- package/dist/web-video-engine/scrub-controller.d.ts +375 -0
- package/dist/web-video-engine/scrub-cursor.d.ts +347 -0
- package/dist/web-video-engine/scrub-trajectory.d.ts +29 -0
- package/dist/web-video-engine/source-residency.d.ts +91 -0
- package/dist/web-video-engine/trace-recorder.d.ts +163 -0
- package/dist/web-video-engine/types.d.ts +200 -0
- package/dist/web-video-engine/video-engine.d.ts +437 -0
- package/dist/web-video-engine/webgpu-renderer.d.ts +61 -0
- package/dist/web-video-engine/worker-bridge.d.ts +11 -0
- package/dist/web-video-engine/worker-dispatch.d.ts +16 -0
- package/dist/web-video-engine/worker-protocol.d.ts +282 -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 +5 -4
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +1167 -280
- package/node_modules/supervision-js-core/dist/index.js.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 +191 -17
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +83 -5
- 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 +32 -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 +34 -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 +24 -3
|
@@ -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,15 +78,32 @@ 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
|
? {
|
|
68
109
|
boxRelative: detection.keypoints.boxRelative
|
|
@@ -97,16 +138,56 @@ function copySortedDetectionFrames(detectionFrames) {
|
|
|
97
138
|
endTime: frame.endTime,
|
|
98
139
|
frameIndex: frame.frameIndex,
|
|
99
140
|
mediaTime: frame.mediaTime,
|
|
100
|
-
}
|
|
101
|
-
.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
141
|
+
};
|
|
102
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");
|
|
103
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
|
+
}
|
|
104
155
|
const clone = globalThis.structuredClone;
|
|
105
156
|
if (clone) {
|
|
106
157
|
return clone(metadata);
|
|
107
158
|
}
|
|
108
159
|
return copyMetadataValue(metadata);
|
|
109
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
|
+
}
|
|
110
191
|
function copyMetadataValue(value) {
|
|
111
192
|
if (Array.isArray(value)) {
|
|
112
193
|
return value.map(copyMetadataValue);
|
|
@@ -120,95 +201,85 @@ function copyMetadataValue(value) {
|
|
|
120
201
|
return value;
|
|
121
202
|
}
|
|
122
203
|
function validateDetectionFrames(detectionFrames) {
|
|
123
|
-
for (
|
|
124
|
-
|
|
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`, {
|
|
125
208
|
min: 0,
|
|
126
209
|
});
|
|
127
210
|
if (frame.endTime !== undefined) {
|
|
128
|
-
validateNumber(frame.endTime,
|
|
211
|
+
validateNumber(frame.endTime, () => `${framePath()}.endTime`, {
|
|
129
212
|
exclusiveMin: frame.mediaTime,
|
|
130
213
|
});
|
|
131
214
|
}
|
|
132
215
|
if (frame.frameIndex !== undefined) {
|
|
133
|
-
validateNumber(frame.frameIndex,
|
|
216
|
+
validateNumber(frame.frameIndex, () => `${framePath()}.frameIndex`, {
|
|
134
217
|
integer: true,
|
|
135
218
|
min: 0,
|
|
136
219
|
});
|
|
137
220
|
}
|
|
138
221
|
if (frame.coordinateSpace !== undefined) {
|
|
139
|
-
validateNumber(frame.coordinateSpace.width,
|
|
140
|
-
validateNumber(frame.coordinateSpace.height,
|
|
222
|
+
validateNumber(frame.coordinateSpace.width, () => `${framePath()}.coordinateSpace.width`, { exclusiveMin: 0 });
|
|
223
|
+
validateNumber(frame.coordinateSpace.height, () => `${framePath()}.coordinateSpace.height`, { exclusiveMin: 0 });
|
|
141
224
|
}
|
|
142
|
-
for (
|
|
143
|
-
const
|
|
225
|
+
for (let detectionOffset = 0; detectionOffset < frame.detections.length; detectionOffset += 1) {
|
|
226
|
+
const detection = frame.detections[detectionOffset];
|
|
227
|
+
const detectionPath = () => `${framePath()}.detections[${detectionOffset}]`;
|
|
144
228
|
if (detection.confidence !== undefined) {
|
|
145
|
-
validateNumber(detection.confidence, `${detectionPath}.confidence`, {
|
|
146
|
-
max: 1,
|
|
147
|
-
min: 0,
|
|
148
|
-
});
|
|
229
|
+
validateNumber(detection.confidence, () => `${detectionPath()}.confidence`, { max: 1, min: 0 });
|
|
149
230
|
}
|
|
150
231
|
if (detection.zIndex !== undefined) {
|
|
151
|
-
validateNumber(detection.zIndex, `${detectionPath}.zIndex`);
|
|
232
|
+
validateNumber(detection.zIndex, () => `${detectionPath()}.zIndex`);
|
|
152
233
|
}
|
|
153
234
|
if (detection.trackerId !== undefined) {
|
|
154
|
-
validateNumber(detection.trackerId, `${detectionPath}.trackerId`, {
|
|
155
|
-
integer: true,
|
|
156
|
-
min: 0,
|
|
157
|
-
});
|
|
235
|
+
validateNumber(detection.trackerId, () => `${detectionPath()}.trackerId`, { integer: true, min: 0 });
|
|
158
236
|
}
|
|
159
237
|
if (detection.sourceId !== undefined &&
|
|
160
238
|
typeof detection.sourceId !== "string") {
|
|
161
|
-
throw new Error(`${detectionPath}.sourceId must be a string.`);
|
|
239
|
+
throw new Error(`${detectionPath()}.sourceId must be a string.`);
|
|
162
240
|
}
|
|
163
241
|
if (detection.sourceDetectionIndex !== undefined) {
|
|
164
|
-
validateNumber(detection.sourceDetectionIndex, `${detectionPath}.sourceDetectionIndex`, {
|
|
242
|
+
validateNumber(detection.sourceDetectionIndex, () => `${detectionPath()}.sourceDetectionIndex`, {
|
|
165
243
|
integer: true,
|
|
166
244
|
min: 0,
|
|
167
245
|
});
|
|
168
246
|
}
|
|
169
247
|
if (detection.rect) {
|
|
170
|
-
validateNumber(detection.rect.x, `${detectionPath}.rect.x`);
|
|
171
|
-
validateNumber(detection.rect.y, `${detectionPath}.rect.y`);
|
|
172
|
-
validateNumber(detection.rect.width, `${detectionPath}.rect.width`, {
|
|
173
|
-
|
|
174
|
-
});
|
|
175
|
-
validateNumber(detection.rect.height, `${detectionPath}.rect.height`, {
|
|
176
|
-
exclusiveMin: 0,
|
|
177
|
-
});
|
|
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 });
|
|
178
252
|
}
|
|
179
|
-
validatePoints(detection.polygon?.points, `${detectionPath}.polygon`, 3);
|
|
180
|
-
validatePoints(detection.polyline?.points, `${detectionPath}.polyline`, 2);
|
|
253
|
+
validatePoints(detection.polygon?.points, () => `${detectionPath()}.polygon`, 3);
|
|
254
|
+
validatePoints(detection.polyline?.points, () => `${detectionPath()}.polyline`, 2);
|
|
181
255
|
if (detection.keypoints) {
|
|
182
|
-
validatePoints(detection.keypoints.points, `${detectionPath}.keypoints`, 1);
|
|
256
|
+
validatePoints(detection.keypoints.points, () => `${detectionPath()}.keypoints`, 1);
|
|
183
257
|
if (detection.keypoints.visibility !== undefined &&
|
|
184
258
|
detection.keypoints.visibility.length !==
|
|
185
259
|
detection.keypoints.points.length) {
|
|
186
|
-
throw new Error(`${detectionPath}.keypoints.visibility must match points length.`);
|
|
260
|
+
throw new Error(`${detectionPath()}.keypoints.visibility must match points length.`);
|
|
187
261
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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}]`, {
|
|
192
269
|
integer: true,
|
|
193
270
|
min: 0,
|
|
194
271
|
});
|
|
195
272
|
if (endpoint >= detection.keypoints.points.length) {
|
|
196
|
-
throw new Error(`${edgePath}[${endpointOffset}] is out of range.`);
|
|
273
|
+
throw new Error(`${edgePath()}[${endpointOffset}] is out of range.`);
|
|
197
274
|
}
|
|
198
275
|
}
|
|
199
276
|
}
|
|
200
277
|
}
|
|
201
278
|
if (detection.mask) {
|
|
202
|
-
validateNumber(detection.mask.width, `${detectionPath}.mask.width`, {
|
|
203
|
-
|
|
204
|
-
exclusiveMin: 0,
|
|
205
|
-
});
|
|
206
|
-
validateNumber(detection.mask.height, `${detectionPath}.mask.height`, {
|
|
207
|
-
integer: true,
|
|
208
|
-
exclusiveMin: 0,
|
|
209
|
-
});
|
|
279
|
+
validateNumber(detection.mask.width, () => `${detectionPath()}.mask.width`, { integer: true, exclusiveMin: 0 });
|
|
280
|
+
validateNumber(detection.mask.height, () => `${detectionPath()}.mask.height`, { integer: true, exclusiveMin: 0 });
|
|
210
281
|
if (detection.mask.counts.length === 0) {
|
|
211
|
-
throw new Error(`${detectionPath}.mask.counts must not be empty.`);
|
|
282
|
+
throw new Error(`${detectionPath()}.mask.counts must not be empty.`);
|
|
212
283
|
}
|
|
213
284
|
}
|
|
214
285
|
}
|
|
@@ -219,28 +290,30 @@ function validatePoints(points, path, minimumLength) {
|
|
|
219
290
|
return;
|
|
220
291
|
}
|
|
221
292
|
if (points.length < minimumLength) {
|
|
222
|
-
throw new Error(`${path}.points must contain at least ${minimumLength} points.`);
|
|
293
|
+
throw new Error(`${path()}.points must contain at least ${minimumLength} points.`);
|
|
223
294
|
}
|
|
224
|
-
for (
|
|
225
|
-
|
|
226
|
-
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`);
|
|
227
299
|
}
|
|
228
300
|
}
|
|
229
|
-
|
|
301
|
+
const NO_BOUNDS = {};
|
|
302
|
+
function validateNumber(value, path, options = NO_BOUNDS) {
|
|
230
303
|
if (!Number.isFinite(value)) {
|
|
231
|
-
throw new Error(`${path} must be a finite number.`);
|
|
304
|
+
throw new Error(`${path()} must be a finite number.`);
|
|
232
305
|
}
|
|
233
306
|
if (options.integer && !Number.isInteger(value)) {
|
|
234
|
-
throw new Error(`${path} must be an integer.`);
|
|
307
|
+
throw new Error(`${path()} must be an integer.`);
|
|
235
308
|
}
|
|
236
309
|
if (options.min !== undefined && value < options.min) {
|
|
237
|
-
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}.`);
|
|
238
311
|
}
|
|
239
312
|
if (options.exclusiveMin !== undefined && value <= options.exclusiveMin) {
|
|
240
|
-
throw new Error(`${path} must be greater than ${options.exclusiveMin}.`);
|
|
313
|
+
throw new Error(`${path()} must be greater than ${options.exclusiveMin}.`);
|
|
241
314
|
}
|
|
242
315
|
if (options.max !== undefined && value > options.max) {
|
|
243
|
-
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}.`);
|
|
244
317
|
}
|
|
245
318
|
}
|
|
246
319
|
function filterDetectionFramesForRange(detectionFrames, startTime, endTime) {
|
|
@@ -248,7 +321,7 @@ function filterDetectionFramesForRange(detectionFrames, startTime, endTime) {
|
|
|
248
321
|
}
|
|
249
322
|
function selectDetectionFrame(detectionFrames, mediaTime, options = {}) {
|
|
250
323
|
if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
251
|
-
const selection = selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, options.frameRate
|
|
324
|
+
const selection = selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, options.frameRate);
|
|
252
325
|
if (selection.isApplicable) {
|
|
253
326
|
return selection.frame;
|
|
254
327
|
}
|
|
@@ -285,13 +358,77 @@ function decodeCompressedRleMask(mask) {
|
|
|
285
358
|
};
|
|
286
359
|
}
|
|
287
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) {
|
|
288
425
|
let selectedFrame;
|
|
289
426
|
let low = 0;
|
|
290
427
|
let high = detectionFrames.length - 1;
|
|
291
428
|
while (low <= high) {
|
|
292
429
|
const middle = Math.floor((low + high) / 2);
|
|
293
430
|
const frame = detectionFrames[middle];
|
|
294
|
-
if (frame
|
|
431
|
+
if (hasDetectionFrameStarted(frame, mediaTime)) {
|
|
295
432
|
selectedFrame = frame;
|
|
296
433
|
low = middle + 1;
|
|
297
434
|
}
|
|
@@ -299,44 +436,16 @@ function selectIntervalDetectionFrame(detectionFrames, mediaTime) {
|
|
|
299
436
|
high = middle - 1;
|
|
300
437
|
}
|
|
301
438
|
}
|
|
302
|
-
return selectedFrame
|
|
303
|
-
? selectedFrame
|
|
304
|
-
: undefined;
|
|
439
|
+
return selectedFrame;
|
|
305
440
|
}
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (!firstIndexedFrame || firstIndexedFrame.frameIndex === undefined) {
|
|
312
|
-
return { frame: undefined, isApplicable: false };
|
|
313
|
-
}
|
|
314
|
-
const originTime = frameIndexOriginTime !== undefined
|
|
315
|
-
? frameIndexOriginTime
|
|
316
|
-
: firstIndexedFrame.mediaTime - firstIndexedFrame.frameIndex / frameRate;
|
|
317
|
-
const targetFrameIndex = Math.round((mediaTime - originTime) * frameRate);
|
|
318
|
-
let nearestFrame;
|
|
319
|
-
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
320
|
-
for (const frame of detectionFrames) {
|
|
321
|
-
if (frame.frameIndex === undefined) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const distance = Math.abs(frame.frameIndex - targetFrameIndex);
|
|
325
|
-
if (distance < nearestDistance ||
|
|
326
|
-
(distance === nearestDistance &&
|
|
327
|
-
nearestFrame?.frameIndex !== undefined &&
|
|
328
|
-
frame.frameIndex > nearestFrame.frameIndex)) {
|
|
329
|
-
nearestFrame = frame;
|
|
330
|
-
nearestDistance = distance;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return {
|
|
334
|
-
frame: nearestDistance <= 1 ? nearestFrame : undefined,
|
|
335
|
-
isApplicable: true,
|
|
336
|
-
};
|
|
441
|
+
function isIndexedDetectionFrame(frame) {
|
|
442
|
+
return frame.frameIndex !== undefined;
|
|
443
|
+
}
|
|
444
|
+
function hasDetectionFrameStarted(frame, mediaTime) {
|
|
445
|
+
return frame.mediaTime - PLAYHEAD_QUANTIZATION_TOLERANCE_SECONDS <= mediaTime;
|
|
337
446
|
}
|
|
338
447
|
function isDetectionFrameActive(frame, mediaTime) {
|
|
339
|
-
return (frame
|
|
448
|
+
return (hasDetectionFrameStarted(frame, mediaTime) &&
|
|
340
449
|
(frame.endTime === undefined || mediaTime < frame.endTime));
|
|
341
450
|
}
|
|
342
451
|
function detectionFrameOverlapsRange(frame, startTime, endTime) {
|
|
@@ -398,8 +507,84 @@ function createArrayDetectionFrameSource(frames) {
|
|
|
398
507
|
};
|
|
399
508
|
}
|
|
400
509
|
|
|
401
|
-
|
|
402
|
-
|
|
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
|
+
|
|
566
|
+
const DEFAULT_BUFFER_AHEAD_SECONDS = 10;
|
|
567
|
+
const DEFAULT_BUFFER_BEHIND_SECONDS = 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;
|
|
403
588
|
const bufferedFrameSnapshots = new WeakMap();
|
|
404
589
|
/**
|
|
405
590
|
* Returns the timeline-owned hot-buffer snapshot without copying it.
|
|
@@ -417,6 +602,9 @@ function createBufferedDetectionTimeline(options) {
|
|
|
417
602
|
? null
|
|
418
603
|
: Math.max(0, options.refreshIntervalSeconds);
|
|
419
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));
|
|
420
608
|
let buffer = [];
|
|
421
609
|
let state = createIdleDetectionBufferState();
|
|
422
610
|
let destroyed = false;
|
|
@@ -429,30 +617,53 @@ function createBufferedDetectionTimeline(options) {
|
|
|
429
617
|
};
|
|
430
618
|
let inFlight;
|
|
431
619
|
let incrementalRefresh;
|
|
620
|
+
/** Source version the gate last gave up on, or null while it still waits. */
|
|
621
|
+
let abandonedGateSourceVersion = null;
|
|
432
622
|
let pendingPrefetch;
|
|
433
623
|
let prefetchPump;
|
|
624
|
+
const listeners = new Set();
|
|
625
|
+
const notifyBufferChanged = () => {
|
|
626
|
+
for (const listener of listeners) {
|
|
627
|
+
listener();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
434
630
|
const getSourceVersion = (ranges) => {
|
|
435
631
|
if (!ranges) {
|
|
436
632
|
return options.source.getVersion?.() ?? 0;
|
|
437
633
|
}
|
|
438
634
|
return ranges.reduce((version, range) => Math.max(version, options.source.getVersion?.(range) ?? 0), 0);
|
|
439
635
|
};
|
|
636
|
+
const isLoadingEnabled = () => typeof options.enabled === "function"
|
|
637
|
+
? options.enabled()
|
|
638
|
+
: options.enabled !== false;
|
|
440
639
|
const isBufferFresh = () => bufferedVersionRange !== null &&
|
|
441
640
|
bufferedSourceVersion === getSourceVersion(getBufferedSourceRanges());
|
|
442
|
-
const getLoadRange = (mediaTime) => {
|
|
641
|
+
const getLoadRange = (mediaTime, lead = "full") => {
|
|
443
642
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
444
643
|
const startTime = comparableMediaTime - bufferBehindSeconds;
|
|
445
|
-
const endTime = comparableMediaTime + bufferAheadSeconds;
|
|
644
|
+
const endTime = comparableMediaTime + (lead === "full" ? bufferAheadSeconds : 0);
|
|
446
645
|
return createLoadPlan(startTime, endTime);
|
|
447
646
|
};
|
|
448
|
-
const loadWindow = (mediaTime) => {
|
|
449
|
-
const
|
|
647
|
+
const loadWindow = (mediaTime, lead = "full") => {
|
|
648
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
649
|
+
const { endTime, sourceRanges, startTime } = getLoadRange(mediaTime, lead);
|
|
450
650
|
const versionRange = { endTime, startTime };
|
|
451
651
|
const sourceVersion = getSourceVersion(sourceRanges);
|
|
452
|
-
if (inFlight &&
|
|
453
|
-
inFlight.
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
}
|
|
456
667
|
}
|
|
457
668
|
const currentLoadId = loadId + 1;
|
|
458
669
|
loadId = currentLoadId;
|
|
@@ -469,12 +680,12 @@ function createBufferedDetectionTimeline(options) {
|
|
|
469
680
|
return;
|
|
470
681
|
}
|
|
471
682
|
const committedSourceVersion = getSourceVersion(sourceRanges);
|
|
472
|
-
const
|
|
683
|
+
const loadedFrames = frameRanges.flat();
|
|
473
684
|
buffer =
|
|
474
685
|
bufferedSourceVersion !== null &&
|
|
475
686
|
bufferedSourceVersion === committedSourceVersion
|
|
476
|
-
? reuseBufferedFrameSnapshots(buffer,
|
|
477
|
-
:
|
|
687
|
+
? reuseBufferedFrameSnapshots(buffer, loadedFrames)
|
|
688
|
+
: copySortedDetectionFrames(loadedFrames);
|
|
478
689
|
bufferedVersionRange = versionRange;
|
|
479
690
|
bufferedSourceVersion = committedSourceVersion;
|
|
480
691
|
state = {
|
|
@@ -487,6 +698,7 @@ function createBufferedDetectionTimeline(options) {
|
|
|
487
698
|
requestedStartTime: startTime,
|
|
488
699
|
status: DetectionBufferStatus.Ready,
|
|
489
700
|
};
|
|
701
|
+
notifyBufferChanged();
|
|
490
702
|
})
|
|
491
703
|
.catch((error) => {
|
|
492
704
|
if (!destroyed && currentLoadId === loadId) {
|
|
@@ -503,15 +715,56 @@ function createBufferedDetectionTimeline(options) {
|
|
|
503
715
|
inFlight = undefined;
|
|
504
716
|
}
|
|
505
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
|
+
}
|
|
506
729
|
inFlight = {
|
|
507
730
|
endTime,
|
|
508
731
|
id: currentLoadId,
|
|
732
|
+
mediaTime: comparableMediaTime,
|
|
509
733
|
promise,
|
|
510
734
|
sourceVersion,
|
|
511
735
|
startTime,
|
|
512
736
|
};
|
|
513
737
|
return promise;
|
|
514
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
|
+
};
|
|
515
768
|
const isBuffered = (mediaTime) => {
|
|
516
769
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
517
770
|
return (isBufferFresh() &&
|
|
@@ -520,6 +773,32 @@ function createBufferedDetectionTimeline(options) {
|
|
|
520
773
|
comparableMediaTime >= state.bufferStartTime &&
|
|
521
774
|
comparableMediaTime <= state.bufferEndTime);
|
|
522
775
|
};
|
|
776
|
+
const inFlightCovers = (mediaTime) => {
|
|
777
|
+
if (!inFlight) {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
781
|
+
return (comparableMediaTime >= inFlight.startTime &&
|
|
782
|
+
comparableMediaTime <= inFlight.endTime);
|
|
783
|
+
};
|
|
784
|
+
const supersedeLoadOutside = (mediaTime) => {
|
|
785
|
+
if (!inFlight || inFlightCovers(mediaTime)) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const keepsCurrentBuffer = isBuffered(mediaTime);
|
|
789
|
+
loadId += 1;
|
|
790
|
+
inFlight = undefined;
|
|
791
|
+
if (keepsCurrentBuffer) {
|
|
792
|
+
state = {
|
|
793
|
+
...state,
|
|
794
|
+
errorMessage: null,
|
|
795
|
+
requestedEndTime: state.bufferEndTime,
|
|
796
|
+
requestedStartTime: state.bufferStartTime,
|
|
797
|
+
status: DetectionBufferStatus.Ready,
|
|
798
|
+
};
|
|
799
|
+
notifyBufferChanged();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
523
802
|
const isInsideBufferedRange = (mediaTime) => {
|
|
524
803
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
525
804
|
return (bufferedVersionRange !== null &&
|
|
@@ -528,8 +807,13 @@ function createBufferedDetectionTimeline(options) {
|
|
|
528
807
|
comparableMediaTime >= state.bufferStartTime &&
|
|
529
808
|
comparableMediaTime <= state.bufferEndTime);
|
|
530
809
|
};
|
|
531
|
-
const refreshBuffer = async (mediaTime) => {
|
|
810
|
+
const refreshBuffer = async (mediaTime, lead = "full") => {
|
|
532
811
|
if (isBuffered(mediaTime)) {
|
|
812
|
+
// A backwards navigation may land in the retained buffer while a rolling
|
|
813
|
+
// prefetch for the old playhead is about to replace it. The navigation
|
|
814
|
+
// is current truth; do not let that older load evict its detections after
|
|
815
|
+
// the frame has been accepted.
|
|
816
|
+
supersedeLoadOutside(mediaTime);
|
|
533
817
|
return;
|
|
534
818
|
}
|
|
535
819
|
if (isInsideBufferedRange(mediaTime) &&
|
|
@@ -544,11 +828,18 @@ function createBufferedDetectionTimeline(options) {
|
|
|
544
828
|
if (isBuffered(mediaTime)) {
|
|
545
829
|
return;
|
|
546
830
|
}
|
|
547
|
-
return refreshBuffer(mediaTime);
|
|
831
|
+
return refreshBuffer(mediaTime, lead);
|
|
548
832
|
}
|
|
549
|
-
await loadWindow(mediaTime);
|
|
833
|
+
await loadWindow(mediaTime, lead);
|
|
550
834
|
};
|
|
835
|
+
/**
|
|
836
|
+
* Whether the next window is worth fetching while the current one still
|
|
837
|
+
* answers, so the load lands behind annotations that never went away.
|
|
838
|
+
*/
|
|
551
839
|
const shouldPrefetch = (mediaTime) => {
|
|
840
|
+
if (!isLoadingEnabled()) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
552
843
|
if (!isBuffered(mediaTime)) {
|
|
553
844
|
return true;
|
|
554
845
|
}
|
|
@@ -558,12 +849,19 @@ function createBufferedDetectionTimeline(options) {
|
|
|
558
849
|
if (state.bufferEndTime === null || bufferAheadSeconds <= 0) {
|
|
559
850
|
return false;
|
|
560
851
|
}
|
|
561
|
-
|
|
562
|
-
state.bufferEndTime)
|
|
852
|
+
if (getComparableMediaTime(mediaTime) + bufferAheadSeconds / 2 <
|
|
853
|
+
state.bufferEndTime) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
const { endTime, startTime } = getLoadRange(mediaTime);
|
|
857
|
+
// A window already spanning everything the source can offer has nowhere to
|
|
858
|
+
// advance to, so its lead only shrinks from here. Refetching it would
|
|
859
|
+
// repeat for every remaining frame of playback and buy no coverage.
|
|
860
|
+
return (startTime !== state.bufferStartTime || endTime !== state.bufferEndTime);
|
|
563
861
|
};
|
|
564
862
|
const timeline = {
|
|
565
863
|
async prepare(mediaTime, prepareOptions) {
|
|
566
|
-
if (destroyed) {
|
|
864
|
+
if (destroyed || !isLoadingEnabled()) {
|
|
567
865
|
return;
|
|
568
866
|
}
|
|
569
867
|
if (shouldWaitForPlaybackGate(prepareOptions)) {
|
|
@@ -573,9 +871,32 @@ function createBufferedDetectionTimeline(options) {
|
|
|
573
871
|
}
|
|
574
872
|
}
|
|
575
873
|
await refreshBuffer(mediaTime);
|
|
874
|
+
anchorWindowToPlayhead(mediaTime);
|
|
875
|
+
},
|
|
876
|
+
needsBufferPrepare(mediaTime) {
|
|
877
|
+
return (!destroyed &&
|
|
878
|
+
isLoadingEnabled() &&
|
|
879
|
+
(!isBuffered(mediaTime) || !inFlightCovers(mediaTime)));
|
|
880
|
+
},
|
|
881
|
+
needsPlaybackGateWait(mediaTime, prepareOptions) {
|
|
882
|
+
if (destroyed ||
|
|
883
|
+
!isLoadingEnabled() ||
|
|
884
|
+
!playbackGate?.enabled ||
|
|
885
|
+
!options.source.waitForRange ||
|
|
886
|
+
!options.source.getAvailableRanges ||
|
|
887
|
+
hasAbandonedGate()) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const availableRanges = options.source.getAvailableRanges();
|
|
891
|
+
return !createPlaybackGateCoveragePlan(mediaTime, prepareOptions).sourceRanges.every((range) => isRangeCovered(range, availableRanges));
|
|
576
892
|
},
|
|
577
893
|
prefetch(mediaTime) {
|
|
578
|
-
if (destroyed || !
|
|
894
|
+
if (destroyed || !isLoadingEnabled()) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
anchorWindowToPlayhead(mediaTime);
|
|
898
|
+
supersedeLoadOutside(mediaTime);
|
|
899
|
+
if (!shouldPrefetch(mediaTime)) {
|
|
579
900
|
return;
|
|
580
901
|
}
|
|
581
902
|
pendingPrefetch = { loadId, mediaTime };
|
|
@@ -598,11 +919,18 @@ function createBufferedDetectionTimeline(options) {
|
|
|
598
919
|
getState() {
|
|
599
920
|
return { ...state };
|
|
600
921
|
},
|
|
922
|
+
subscribe(listener) {
|
|
923
|
+
listeners.add(listener);
|
|
924
|
+
return () => {
|
|
925
|
+
listeners.delete(listener);
|
|
926
|
+
};
|
|
927
|
+
},
|
|
601
928
|
destroy() {
|
|
602
929
|
if (destroyed) {
|
|
603
930
|
return;
|
|
604
931
|
}
|
|
605
932
|
destroyed = true;
|
|
933
|
+
listeners.clear();
|
|
606
934
|
pendingPrefetch = undefined;
|
|
607
935
|
buffer = [];
|
|
608
936
|
bufferedSourceVersion = null;
|
|
@@ -646,9 +974,16 @@ function createBufferedDetectionTimeline(options) {
|
|
|
646
974
|
if (!shouldPrefetch(mediaTime)) {
|
|
647
975
|
continue;
|
|
648
976
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
977
|
+
// A playhead the window does not reach is a jump rather than the window
|
|
978
|
+
// rolling forward, and the frame under it is the one thing the picture
|
|
979
|
+
// cannot draw without. A window that still spans it, however stale, is
|
|
980
|
+
// missing no such frame.
|
|
981
|
+
const lead = isInsideBufferedRange(mediaTime)
|
|
982
|
+
? "full"
|
|
983
|
+
: "covering";
|
|
984
|
+
await (isBuffered(mediaTime) || shouldRefreshRollingWindow(mediaTime)
|
|
985
|
+
? loadWindow(mediaTime, lead)
|
|
986
|
+
: refreshBuffer(mediaTime, lead)).catch(() => undefined);
|
|
652
987
|
}
|
|
653
988
|
}
|
|
654
989
|
async function applyIncrementalChanges() {
|
|
@@ -690,6 +1025,7 @@ function createBufferedDetectionTimeline(options) {
|
|
|
690
1025
|
frameCount: buffer.length,
|
|
691
1026
|
status: DetectionBufferStatus.Ready,
|
|
692
1027
|
};
|
|
1028
|
+
notifyBufferChanged();
|
|
693
1029
|
}
|
|
694
1030
|
catch (error) {
|
|
695
1031
|
if (!destroyed) {
|
|
@@ -707,31 +1043,44 @@ function createBufferedDetectionTimeline(options) {
|
|
|
707
1043
|
playbackGate?.enabled === true &&
|
|
708
1044
|
Boolean(options.source.waitForRange));
|
|
709
1045
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const requiredAheadSeconds = Math.max(0, playbackGate.requiredAheadSeconds ?? 0);
|
|
1046
|
+
function createPlaybackGateCoveragePlan(mediaTime, prepareOptions) {
|
|
1047
|
+
const requiredAheadSeconds = Math.max(0, playbackGate?.requiredAheadSeconds ?? 0);
|
|
715
1048
|
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
716
1049
|
const endTime = getRequiredCoverageEndTime({
|
|
1050
|
+
// A looping window counts past the end of media and wraps into the
|
|
1051
|
+
// replay, so clamping it to duration would ask for less than it needs.
|
|
717
1052
|
duration: isLoopingTimeline() ? null : prepareOptions?.duration,
|
|
718
1053
|
firstTimestamp: prepareOptions?.firstTimestamp,
|
|
719
1054
|
mediaTime: comparableMediaTime,
|
|
720
1055
|
requiredAheadSeconds,
|
|
721
1056
|
});
|
|
722
|
-
|
|
1057
|
+
// A lead clamped away at the end of media, or asked for as zero, still
|
|
1058
|
+
// leaves the frame under the playhead to wait for.
|
|
1059
|
+
return createLoadPlan(comparableMediaTime, Math.max(comparableMediaTime, endTime));
|
|
1060
|
+
}
|
|
1061
|
+
function hasAbandonedGate() {
|
|
1062
|
+
return (abandonedGateSourceVersion !== null &&
|
|
1063
|
+
getSourceVersion() <= abandonedGateSourceVersion);
|
|
1064
|
+
}
|
|
1065
|
+
async function waitForPlaybackGate(mediaTime, prepareOptions) {
|
|
1066
|
+
if (!playbackGate?.enabled || !options.source.waitForRange) {
|
|
723
1067
|
return;
|
|
724
1068
|
}
|
|
725
|
-
|
|
1069
|
+
if (hasAbandonedGate()) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const coveragePlan = createPlaybackGateCoveragePlan(mediaTime, prepareOptions);
|
|
726
1073
|
state = {
|
|
727
1074
|
...state,
|
|
728
1075
|
errorMessage: null,
|
|
729
1076
|
requestedEndTime: coveragePlan.endTime,
|
|
730
1077
|
requestedStartTime: coveragePlan.startTime,
|
|
731
|
-
status: DetectionBufferStatus.
|
|
1078
|
+
status: DetectionBufferStatus.AwaitingCoverage,
|
|
732
1079
|
};
|
|
733
1080
|
try {
|
|
734
|
-
await
|
|
1081
|
+
const covered = await waitForSourceCoverage(coveragePlan.sourceRanges, Math.max(0, playbackGate.maxWaitSeconds ?? DEFAULT_PLAYBACK_GATE_MAX_WAIT_SECONDS));
|
|
1082
|
+
abandonedGateSourceVersion =
|
|
1083
|
+
covered || !options.source.getVersion ? null : getSourceVersion();
|
|
735
1084
|
}
|
|
736
1085
|
catch (error) {
|
|
737
1086
|
if (!destroyed) {
|
|
@@ -744,6 +1093,33 @@ function createBufferedDetectionTimeline(options) {
|
|
|
744
1093
|
throw error;
|
|
745
1094
|
}
|
|
746
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Resolves true once the source covers every range, false once the wait has
|
|
1098
|
+
* run longer than `maxWaitSeconds`.
|
|
1099
|
+
*/
|
|
1100
|
+
async function waitForSourceCoverage(sourceRanges, maxWaitSeconds) {
|
|
1101
|
+
const covered = whenRangesCovered(sourceRanges);
|
|
1102
|
+
if (!Number.isFinite(maxWaitSeconds)) {
|
|
1103
|
+
return covered;
|
|
1104
|
+
}
|
|
1105
|
+
const bound = startWaitBound(maxWaitSeconds * 1000);
|
|
1106
|
+
try {
|
|
1107
|
+
const result = await Promise.race([covered, bound.expired]);
|
|
1108
|
+
if (!result) {
|
|
1109
|
+
// The abandoned wait outlives this call, and a rejection it reaches
|
|
1110
|
+
// afterwards has nobody left to hand it to.
|
|
1111
|
+
void covered.catch(() => undefined);
|
|
1112
|
+
}
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
bound.cancel();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function whenRangesCovered(sourceRanges) {
|
|
1120
|
+
await Promise.all(sourceRanges.map((range) => options.source.waitForRange?.(range)));
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
747
1123
|
function shouldRefreshRollingWindow(mediaTime) {
|
|
748
1124
|
if (refreshIntervalSeconds === null ||
|
|
749
1125
|
refreshIntervalSeconds <= 0 ||
|
|
@@ -752,8 +1128,24 @@ function createBufferedDetectionTimeline(options) {
|
|
|
752
1128
|
return false;
|
|
753
1129
|
}
|
|
754
1130
|
const { endTime, startTime } = getLoadRange(mediaTime);
|
|
755
|
-
return (
|
|
756
|
-
|
|
1131
|
+
return (getWindowDrift(startTime, state.bufferStartTime) >=
|
|
1132
|
+
refreshIntervalSeconds ||
|
|
1133
|
+
getWindowDrift(endTime, state.bufferEndTime) >= refreshIntervalSeconds);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* How far a planned edge sits from the window's, counted the short way around
|
|
1137
|
+
* a looping timeline. A plan is stated on the first lap and the window on the
|
|
1138
|
+
* playhead's, so across the loop point the two state the same edge a whole lap
|
|
1139
|
+
* apart; measured straight, that reads as a window a full lap stale and
|
|
1140
|
+
* rebuilds it on every frame until the plan stops reaching behind zero.
|
|
1141
|
+
*/
|
|
1142
|
+
function getWindowDrift(planTime, bufferTime) {
|
|
1143
|
+
const drift = Math.abs(planTime - bufferTime);
|
|
1144
|
+
if (!isLoopingTimeline() || timelineContext.duration === null) {
|
|
1145
|
+
return drift;
|
|
1146
|
+
}
|
|
1147
|
+
const wrappedDrift = modulo(drift, timelineContext.duration);
|
|
1148
|
+
return Math.min(wrappedDrift, timelineContext.duration - wrappedDrift);
|
|
757
1149
|
}
|
|
758
1150
|
function createLoadPlan(requestedStartTime, requestedEndTime) {
|
|
759
1151
|
const startTime = Math.min(requestedStartTime, requestedEndTime);
|
|
@@ -780,10 +1172,15 @@ function createBufferedDetectionTimeline(options) {
|
|
|
780
1172
|
startTime: 0,
|
|
781
1173
|
};
|
|
782
1174
|
}
|
|
1175
|
+
// Both ends move by the same whole number of laps, so the window keeps its
|
|
1176
|
+
// span and its source ranges while its start reads on the media clock: a
|
|
1177
|
+
// window counted in the laps playback accumulated is one no host can hold
|
|
1178
|
+
// against a current time.
|
|
1179
|
+
const laps = Math.floor(startTime / duration);
|
|
783
1180
|
return {
|
|
784
|
-
endTime,
|
|
1181
|
+
endTime: endTime - laps * duration,
|
|
785
1182
|
sourceRanges: getLoopingSourceRanges(startTime, endTime, duration),
|
|
786
|
-
startTime,
|
|
1183
|
+
startTime: startTime - laps * duration,
|
|
787
1184
|
};
|
|
788
1185
|
}
|
|
789
1186
|
function getBufferedSourceRanges() {
|
|
@@ -805,14 +1202,13 @@ function createBufferedDetectionTimeline(options) {
|
|
|
805
1202
|
return mediaTime;
|
|
806
1203
|
}
|
|
807
1204
|
const duration = timelineContext.duration;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return comparableMediaTime;
|
|
1205
|
+
// The representative of mediaTime (mod duration) anchored at the window
|
|
1206
|
+
// start. The mapping must depend only on where the window sits, never on
|
|
1207
|
+
// how many laps playback has accumulated, or membership drifts away from
|
|
1208
|
+
// what the buffer actually holds. A time less than one lap past the
|
|
1209
|
+
// anchor never wraps, so ordinary forward playback cannot ratchet.
|
|
1210
|
+
return (state.bufferStartTime +
|
|
1211
|
+
modulo(mediaTime - state.bufferStartTime, duration));
|
|
816
1212
|
}
|
|
817
1213
|
function getSourceMediaTime(mediaTime) {
|
|
818
1214
|
if (!isLoopingTimeline() || timelineContext.duration === null) {
|
|
@@ -900,9 +1296,19 @@ function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
|
|
|
900
1296
|
}
|
|
901
1297
|
return Array.from(framesByIdentity.values()).sort(compareDetectionFrames$1);
|
|
902
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Keeps the frame already held wherever the source returned one this buffer
|
|
1301
|
+
* knows, and copies only what is new. Copying everything first and then
|
|
1302
|
+
* discarding it is the same result for a great deal more work: a window rebuilt
|
|
1303
|
+
* while a gesture moves inside it re-derives hundreds of frames it already has.
|
|
1304
|
+
*/
|
|
903
1305
|
function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
|
|
1306
|
+
validateDetectionFrames(loadedFrames);
|
|
904
1307
|
const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
|
|
905
|
-
return loadedFrames
|
|
1308
|
+
return loadedFrames
|
|
1309
|
+
.map((frame) => currentFramesByIdentity.get(getDetectionFrameIdentity(frame)) ??
|
|
1310
|
+
copyDetectionFrame(frame))
|
|
1311
|
+
.sort(compareDetectionFrames$1);
|
|
906
1312
|
}
|
|
907
1313
|
function getDetectionFrameIdentity(frame) {
|
|
908
1314
|
return frame.frameIndex === undefined
|
|
@@ -1043,7 +1449,7 @@ function createCompositeDetectionFrameSource(options) {
|
|
|
1043
1449
|
},
|
|
1044
1450
|
async waitForRange(range) {
|
|
1045
1451
|
await Promise.all(sources
|
|
1046
|
-
.filter((source) => source.
|
|
1452
|
+
.filter((source) => source.requiredForCoverage)
|
|
1047
1453
|
.map((source) => source.source.waitForRange?.(range)));
|
|
1048
1454
|
},
|
|
1049
1455
|
getAvailableRanges() {
|
|
@@ -1078,7 +1484,7 @@ function normalizeCompositeSources(entries) {
|
|
|
1078
1484
|
declarationIndex,
|
|
1079
1485
|
id: entry.id,
|
|
1080
1486
|
order: entry.order ?? 0,
|
|
1081
|
-
|
|
1487
|
+
requiredForCoverage: entry.requiredForCoverage !== false,
|
|
1082
1488
|
source: entry.source ?? createArrayDetectionFrameSource(entry.frames ?? []),
|
|
1083
1489
|
sync: entry.sync,
|
|
1084
1490
|
};
|
|
@@ -1108,47 +1514,181 @@ function composeIntervalFrames(sources, startTime, endTime, options, coordinateS
|
|
|
1108
1514
|
}
|
|
1109
1515
|
const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
|
|
1110
1516
|
const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
|
|
1111
|
-
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval },
|
|
1517
|
+
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval }, coordinateSpace);
|
|
1112
1518
|
if (frame) {
|
|
1113
1519
|
frames.push(frame);
|
|
1114
1520
|
}
|
|
1115
1521
|
}
|
|
1116
1522
|
return frames;
|
|
1117
1523
|
}
|
|
1524
|
+
/**
|
|
1525
|
+
* One composed frame per inference grid index the children wrote, and one per
|
|
1526
|
+
* child frame that carries no index.
|
|
1527
|
+
*
|
|
1528
|
+
* Children are paired on the index their producer labelled, and the composed
|
|
1529
|
+
* frame keeps the media time they reported for it. `frameRate` states the grid
|
|
1530
|
+
* a producer was asked for, not the one the clip plays at, and a slower clip
|
|
1531
|
+
* answers consecutive indexes with the same decoded sample: a timeline placed
|
|
1532
|
+
* by that rate then asks for a time two indexes share, which can only ever
|
|
1533
|
+
* reach one of them and leaves the other's detections stranded. Pairing on the
|
|
1534
|
+
* index reaches every one and keeps each frame's detections on the frame they
|
|
1535
|
+
* were computed for.
|
|
1536
|
+
*
|
|
1537
|
+
* A frame carrying no index has no grid position to be paired on, so it
|
|
1538
|
+
* composes at its own media time, and every child answers there with whatever
|
|
1539
|
+
* it has standing. That is how a child sitting on a grid index keeps its
|
|
1540
|
+
* detections on screen while an unindexed frame from another child overlaps it.
|
|
1541
|
+
* The composed frame carries no index of its own, because only part of what it
|
|
1542
|
+
* holds sits on the grid.
|
|
1543
|
+
*/
|
|
1118
1544
|
function composeNearestFrameIndexFrames(sources, startTime, endTime, options, coordinateSpace) {
|
|
1119
1545
|
const frameRate = options.frameRate;
|
|
1120
1546
|
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
1121
1547
|
return null;
|
|
1122
1548
|
}
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1549
|
+
const gridSources = sources.map((source) => ({
|
|
1550
|
+
...source,
|
|
1551
|
+
framesByIndex: indexFramesByFrameIndex(source, options),
|
|
1552
|
+
}));
|
|
1553
|
+
const indexedSlots = mergeGridSlots(gridSources);
|
|
1554
|
+
if (indexedSlots.length === 0) {
|
|
1126
1555
|
return null;
|
|
1127
1556
|
}
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
...
|
|
1132
|
-
|
|
1133
|
-
.filter((frameIndex) => frameIndex !== undefined)),
|
|
1134
|
-
].sort((left, right) => left - right);
|
|
1557
|
+
const gridStep = measureGridStep(indexedSlots) ?? 1 / frameRate;
|
|
1558
|
+
const slots = [
|
|
1559
|
+
...indexedSlots,
|
|
1560
|
+
...mergeUnindexedGridSlots(gridSources, indexedSlots),
|
|
1561
|
+
];
|
|
1135
1562
|
const frames = [];
|
|
1136
|
-
for (const
|
|
1137
|
-
|
|
1138
|
-
if (mediaTime < startTime || mediaTime >= endTime) {
|
|
1563
|
+
for (const slot of slots) {
|
|
1564
|
+
if (slot.mediaTime < startTime || slot.mediaTime >= endTime) {
|
|
1139
1565
|
continue;
|
|
1140
1566
|
}
|
|
1141
|
-
const frame =
|
|
1567
|
+
const frame = composeFrameAtGridSlot(gridSources, slot, Math.min(slot.mediaTime + gridStep, endTime), {
|
|
1142
1568
|
...options,
|
|
1143
1569
|
selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
|
|
1144
|
-
},
|
|
1570
|
+
}, coordinateSpace);
|
|
1145
1571
|
if (frame) {
|
|
1146
1572
|
frames.push(frame);
|
|
1147
1573
|
}
|
|
1148
1574
|
}
|
|
1149
|
-
return frames;
|
|
1575
|
+
return frames.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
1576
|
+
}
|
|
1577
|
+
function indexFramesByFrameIndex(source, options) {
|
|
1578
|
+
const framesByIndex = new Map();
|
|
1579
|
+
const selectionMode = source.sync?.selectionMode ?? options.selectionMode;
|
|
1580
|
+
if (selectionMode !== DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
1581
|
+
return framesByIndex;
|
|
1582
|
+
}
|
|
1583
|
+
for (const frame of source.frames) {
|
|
1584
|
+
if (frame.frameIndex !== undefined) {
|
|
1585
|
+
framesByIndex.set(frame.frameIndex, frame);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return framesByIndex;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Where each grid index sits in the media, taken as the earliest time any child
|
|
1592
|
+
* reported for it. Children on one grid watched the same source frame, and the
|
|
1593
|
+
* earliest of their answers is where that frame starts.
|
|
1594
|
+
*/
|
|
1595
|
+
function mergeGridSlots(sources) {
|
|
1596
|
+
const mediaTimes = new Map();
|
|
1597
|
+
for (const source of sources) {
|
|
1598
|
+
for (const [frameIndex, frame] of source.framesByIndex) {
|
|
1599
|
+
const mediaTime = mediaTimes.get(frameIndex);
|
|
1600
|
+
if (mediaTime === undefined || frame.mediaTime < mediaTime) {
|
|
1601
|
+
mediaTimes.set(frameIndex, frame.mediaTime);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return [...mediaTimes]
|
|
1606
|
+
.map(([frameIndex, mediaTime]) => ({ frameIndex, mediaTime }))
|
|
1607
|
+
.sort((left, right) => left.frameIndex - right.frameIndex);
|
|
1150
1608
|
}
|
|
1151
|
-
|
|
1609
|
+
/**
|
|
1610
|
+
* Where a frame no producer labelled sits: at the media time it reported, which
|
|
1611
|
+
* is the only handle it has.
|
|
1612
|
+
*/
|
|
1613
|
+
function mergeUnindexedGridSlots(sources, indexedSlots) {
|
|
1614
|
+
const indexedSlotTimes = new Set(indexedSlots.map((slot) => slot.mediaTime));
|
|
1615
|
+
const mediaTimes = new Set();
|
|
1616
|
+
for (const source of sources) {
|
|
1617
|
+
for (const frame of source.frames) {
|
|
1618
|
+
if (frame.frameIndex === undefined &&
|
|
1619
|
+
!indexedSlotTimes.has(frame.mediaTime)) {
|
|
1620
|
+
mediaTimes.add(frame.mediaTime);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return [...mediaTimes]
|
|
1625
|
+
.sort((left, right) => left - right)
|
|
1626
|
+
.map((mediaTime) => ({ mediaTime }));
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* How long one grid index stands, read off the slots across the widest index
|
|
1630
|
+
* span they cover. A clip whose real rate differs from `frameRate` would end
|
|
1631
|
+
* every composed frame a little short or a little long of where the next index
|
|
1632
|
+
* is due, and that error accumulates for as long as the clip plays.
|
|
1633
|
+
*/
|
|
1634
|
+
function measureGridStep(gridSlots) {
|
|
1635
|
+
const firstSlot = gridSlots[0];
|
|
1636
|
+
const lastSlot = gridSlots[gridSlots.length - 1];
|
|
1637
|
+
const indexSpan = lastSlot.frameIndex - firstSlot.frameIndex;
|
|
1638
|
+
const step = (lastSlot.mediaTime - firstSlot.mediaTime) / indexSpan;
|
|
1639
|
+
return indexSpan > 0 && Number.isFinite(step) && step > 0 ? step : undefined;
|
|
1640
|
+
}
|
|
1641
|
+
function composeFrameAtGridSlot(sources, slot, endTime, options, coordinateSpace) {
|
|
1642
|
+
const detections = [];
|
|
1643
|
+
for (const source of sources) {
|
|
1644
|
+
const activeFrame = selectGridSlotFrame(source, slot, options);
|
|
1645
|
+
if (!activeFrame) {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
activeFrame.detections.forEach((detection, sourceDetectionIndex) => {
|
|
1649
|
+
detections.push(copyDetectionWithSource(detection, source.id, sourceDetectionIndex));
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
if (detections.length === 0) {
|
|
1653
|
+
return undefined;
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
detections,
|
|
1657
|
+
endTime,
|
|
1658
|
+
frameIndex: slot.frameIndex,
|
|
1659
|
+
mediaTime: slot.mediaTime,
|
|
1660
|
+
// Children were projected before composition, so a composed frame is
|
|
1661
|
+
// already in the coordinate space the renderer presents.
|
|
1662
|
+
...(coordinateSpace ? { coordinateSpace } : {}),
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* What one child has to say at a slot.
|
|
1667
|
+
*
|
|
1668
|
+
* A child that wrote this index answers with the frame it wrote for it, however
|
|
1669
|
+
* far its own media time sits from the slot's. A child on the grid that never
|
|
1670
|
+
* wrote this index answers only with a frame of its own that carries no index
|
|
1671
|
+
* and is standing here: its neighbouring indexes describe media the slot is not
|
|
1672
|
+
* on, and lending one of those would put those detections on a frame they were
|
|
1673
|
+
* not computed for.
|
|
1674
|
+
*/
|
|
1675
|
+
function selectGridSlotFrame(source, slot, options) {
|
|
1676
|
+
const pairedFrame = slot.frameIndex === undefined
|
|
1677
|
+
? undefined
|
|
1678
|
+
: source.framesByIndex.get(slot.frameIndex);
|
|
1679
|
+
if (pairedFrame) {
|
|
1680
|
+
return pairedFrame;
|
|
1681
|
+
}
|
|
1682
|
+
const selectedFrame = selectDetectionFrame(source.frames, slot.mediaTime, {
|
|
1683
|
+
...options,
|
|
1684
|
+
...source.sync,
|
|
1685
|
+
});
|
|
1686
|
+
const isGridChildAtIndexedSlot = source.framesByIndex.size > 0 && slot.frameIndex !== undefined;
|
|
1687
|
+
return isGridChildAtIndexedSlot && selectedFrame?.frameIndex !== undefined
|
|
1688
|
+
? undefined
|
|
1689
|
+
: selectedFrame;
|
|
1690
|
+
}
|
|
1691
|
+
function composeFrameAtTime(sources, mediaTime, endTime, options, coordinateSpace) {
|
|
1152
1692
|
const detections = [];
|
|
1153
1693
|
const activeFrameIndexes = [];
|
|
1154
1694
|
for (const source of sources) {
|
|
@@ -1172,7 +1712,7 @@ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex, co
|
|
|
1172
1712
|
return {
|
|
1173
1713
|
detections,
|
|
1174
1714
|
endTime,
|
|
1175
|
-
frameIndex:
|
|
1715
|
+
frameIndex: resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
|
|
1176
1716
|
mediaTime,
|
|
1177
1717
|
// Children were projected before composition, so a composed frame is
|
|
1178
1718
|
// already in the coordinate space the renderer presents.
|
|
@@ -1645,7 +2185,6 @@ function createProjectedDetectionFrameSource(source, resolveTarget) {
|
|
|
1645
2185
|
};
|
|
1646
2186
|
}
|
|
1647
2187
|
|
|
1648
|
-
const RANGE_EPSILON_SECONDS = 1e-6;
|
|
1649
2188
|
const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
|
|
1650
2189
|
const DEFAULT_LIVE_HOLD_SECONDS = 60;
|
|
1651
2190
|
const NO_COVERAGE_INVALIDATION = { ranges: [] };
|
|
@@ -1786,8 +2325,8 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1786
2325
|
// earlier, though, so report it rather than assume.
|
|
1787
2326
|
invalidation);
|
|
1788
2327
|
// Closing a held live frame shortens coverage. Reported availability
|
|
1789
|
-
// has to shrink with it, or
|
|
1790
|
-
// source covers time past the end of media.
|
|
2328
|
+
// has to shrink with it, or `waitForRange` and the buffered window
|
|
2329
|
+
// would still believe the source covers time past the end of media.
|
|
1791
2330
|
clipAvailableRangesAfter(endTime);
|
|
1792
2331
|
return finalSummary;
|
|
1793
2332
|
});
|
|
@@ -2252,10 +2791,6 @@ function recordAvailableRange(range, availableRanges) {
|
|
|
2252
2791
|
}
|
|
2253
2792
|
availableRanges.length = writeIndex;
|
|
2254
2793
|
}
|
|
2255
|
-
function isRangeCovered(range, availableRanges) {
|
|
2256
|
-
return availableRanges.some((availableRange) => availableRange.startTime <= range.startTime + RANGE_EPSILON_SECONDS &&
|
|
2257
|
-
availableRange.endTime + RANGE_EPSILON_SECONDS >= range.endTime);
|
|
2258
|
-
}
|
|
2259
2794
|
|
|
2260
2795
|
/**
|
|
2261
2796
|
* Creates one stateful SORT tracker for a single ordered media sequence.
|
|
@@ -3535,9 +4070,16 @@ function computeMaskBounds(data, width, height) {
|
|
|
3535
4070
|
};
|
|
3536
4071
|
}
|
|
3537
4072
|
function computeDetectionMaskRect(mask) {
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
4073
|
+
const { bounds } = readCompressedRleMask(mask);
|
|
4074
|
+
if (!bounds) {
|
|
4075
|
+
return undefined;
|
|
4076
|
+
}
|
|
4077
|
+
return {
|
|
4078
|
+
height: bounds.height,
|
|
4079
|
+
width: bounds.width,
|
|
4080
|
+
x: bounds.left + bounds.width / 2,
|
|
4081
|
+
y: bounds.top + bounds.height / 2,
|
|
4082
|
+
};
|
|
3541
4083
|
}
|
|
3542
4084
|
function detectMaskBorders(data, width, height) {
|
|
3543
4085
|
assertMaskDimensions(data, width, height);
|
|
@@ -3587,53 +4129,236 @@ function extractMaskContour(data, width, height) {
|
|
|
3587
4129
|
}
|
|
3588
4130
|
function extractMaskRectRuns(data, width, height) {
|
|
3589
4131
|
assertMaskDimensions(data, width, height);
|
|
4132
|
+
const rects = mergeRowSpansIntoRects(data, width, height, 0, 0);
|
|
4133
|
+
return rects.length > 0 ? rects : undefined;
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* The rect runs of a compressed-RLE mask, without holding the mask's raster.
|
|
4137
|
+
*
|
|
4138
|
+
* A mask is the size of the whole frame, so reading a silhouette that covers a
|
|
4139
|
+
* fraction of one percent of it otherwise costs a megabytes-wide allocation and
|
|
4140
|
+
* a pass over every pixel. The counts carry the occupied box, so the raster and
|
|
4141
|
+
* the pass are that box.
|
|
4142
|
+
*/
|
|
4143
|
+
function extractDetectionMaskRectRuns(mask) {
|
|
4144
|
+
const { height, width } = mask;
|
|
4145
|
+
const { bounds, counts } = readCompressedRleMask(mask);
|
|
4146
|
+
if (!bounds) {
|
|
4147
|
+
return undefined;
|
|
4148
|
+
}
|
|
4149
|
+
const { left, top } = bounds;
|
|
4150
|
+
const boundsWidth = bounds.width;
|
|
4151
|
+
const boundsHeight = bounds.height;
|
|
4152
|
+
const boundsBuffer = new Uint8Array(boundsWidth * boundsHeight);
|
|
4153
|
+
const pixels = width * height;
|
|
4154
|
+
let offset = 0;
|
|
4155
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
4156
|
+
const start = offset;
|
|
4157
|
+
offset += counts[index] ?? 0;
|
|
4158
|
+
if (!isForegroundRun(index) || start >= pixels) {
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
const end = Math.min(offset, pixels);
|
|
4162
|
+
let cursor = start;
|
|
4163
|
+
while (cursor < end) {
|
|
4164
|
+
const column = Math.floor(cursor / height);
|
|
4165
|
+
const columnEnd = Math.min(end, (column + 1) * height);
|
|
4166
|
+
let target = (cursor - column * height - top) * boundsWidth + (column - left);
|
|
4167
|
+
for (let step = columnEnd - cursor; step > 0; step -= 1) {
|
|
4168
|
+
boundsBuffer[target] = 1;
|
|
4169
|
+
target += boundsWidth;
|
|
4170
|
+
}
|
|
4171
|
+
cursor = columnEnd;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
const rects = mergeRowSpansIntoRects(boundsBuffer, boundsWidth, boundsHeight, left, top);
|
|
4175
|
+
return rects.length > 0 ? rects : undefined;
|
|
4176
|
+
}
|
|
4177
|
+
function readCompressedRleMask(mask) {
|
|
4178
|
+
if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {
|
|
4179
|
+
throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);
|
|
4180
|
+
}
|
|
4181
|
+
assertMaskSize(mask.width, mask.height);
|
|
4182
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
4183
|
+
return {
|
|
4184
|
+
bounds: computeCompressedRleBounds(counts, mask.width, mask.height),
|
|
4185
|
+
counts,
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
/** Counts alternate background, foreground, starting on background. */
|
|
4189
|
+
function isForegroundRun(index) {
|
|
4190
|
+
return index % 2 === 1;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* The occupied box of a compressed-RLE mask, read from the counts alone.
|
|
4194
|
+
*
|
|
4195
|
+
* COCO runs are column-major, so a run confined to one column bounds its own
|
|
4196
|
+
* rows, and a run that reaches the next column has already spanned a full
|
|
4197
|
+
* column top to bottom.
|
|
4198
|
+
*/
|
|
4199
|
+
function computeCompressedRleBounds(counts, width, height) {
|
|
4200
|
+
const pixels = width * height;
|
|
4201
|
+
let left = width;
|
|
4202
|
+
let top = height;
|
|
4203
|
+
let right = -1;
|
|
4204
|
+
let bottom = -1;
|
|
4205
|
+
let offset = 0;
|
|
4206
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
4207
|
+
const start = offset;
|
|
4208
|
+
offset += counts[index] ?? 0;
|
|
4209
|
+
if (!isForegroundRun(index) || start >= pixels) {
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
const end = Math.min(offset, pixels) - 1;
|
|
4213
|
+
const startColumn = Math.floor(start / height);
|
|
4214
|
+
const endColumn = Math.floor(end / height);
|
|
4215
|
+
if (startColumn < left) {
|
|
4216
|
+
left = startColumn;
|
|
4217
|
+
}
|
|
4218
|
+
if (endColumn > right) {
|
|
4219
|
+
right = endColumn;
|
|
4220
|
+
}
|
|
4221
|
+
if (startColumn !== endColumn) {
|
|
4222
|
+
top = 0;
|
|
4223
|
+
bottom = height - 1;
|
|
4224
|
+
continue;
|
|
4225
|
+
}
|
|
4226
|
+
const startRow = start - startColumn * height;
|
|
4227
|
+
const endRow = end - endColumn * height;
|
|
4228
|
+
if (startRow < top) {
|
|
4229
|
+
top = startRow;
|
|
4230
|
+
}
|
|
4231
|
+
if (endRow > bottom) {
|
|
4232
|
+
bottom = endRow;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
return right < 0
|
|
4236
|
+
? null
|
|
4237
|
+
: {
|
|
4238
|
+
height: bottom - top + 1,
|
|
4239
|
+
left,
|
|
4240
|
+
top,
|
|
4241
|
+
width: right - left + 1,
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
function createOpenMaskSpans(capacity) {
|
|
4245
|
+
return {
|
|
4246
|
+
openedAt: new Int32Array(capacity),
|
|
4247
|
+
runWidth: new Int32Array(capacity),
|
|
4248
|
+
startX: new Int32Array(capacity),
|
|
4249
|
+
topY: new Int32Array(capacity),
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* Emits rects in the coordinates of the raster the given window was cut from,
|
|
4254
|
+
* not the window's own.
|
|
4255
|
+
*
|
|
4256
|
+
* A span continues the rect above it when its start and width both match, and
|
|
4257
|
+
* the row above holds exactly one open rect per span, so walking both in
|
|
4258
|
+
* ascending x pairs them without a lookup.
|
|
4259
|
+
*/
|
|
4260
|
+
function mergeRowSpansIntoRects(data, width, height, offsetX, offsetY) {
|
|
3590
4261
|
const rects = [];
|
|
3591
|
-
|
|
4262
|
+
// Spans need a gap between them, so a row holds at most half its width.
|
|
4263
|
+
const capacity = (width >> 1) + 2;
|
|
4264
|
+
const closed = new Int32Array(capacity);
|
|
4265
|
+
let open = createOpenMaskSpans(capacity);
|
|
4266
|
+
let next = createOpenMaskSpans(capacity);
|
|
4267
|
+
let openCount = 0;
|
|
4268
|
+
let opened = 0;
|
|
3592
4269
|
for (let y = 0; y < height; y += 1) {
|
|
3593
|
-
const
|
|
4270
|
+
const row = y * width;
|
|
4271
|
+
let nextCount = 0;
|
|
4272
|
+
let closedCount = 0;
|
|
4273
|
+
let cursor = 0;
|
|
3594
4274
|
let x = 0;
|
|
3595
4275
|
while (x < width) {
|
|
3596
|
-
while (x < width &&
|
|
4276
|
+
while (x < width && data[row + x] === 0) {
|
|
3597
4277
|
x += 1;
|
|
3598
4278
|
}
|
|
3599
4279
|
if (x >= width) {
|
|
3600
4280
|
break;
|
|
3601
4281
|
}
|
|
3602
4282
|
const startX = x;
|
|
3603
|
-
while (x < width && data[
|
|
4283
|
+
while (x < width && data[row + x] !== 0) {
|
|
3604
4284
|
x += 1;
|
|
3605
4285
|
}
|
|
3606
4286
|
const runWidth = x - startX;
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
4287
|
+
while (cursor < openCount && open.startX[cursor] < startX) {
|
|
4288
|
+
closed[closedCount] = cursor;
|
|
4289
|
+
closedCount += 1;
|
|
4290
|
+
cursor += 1;
|
|
4291
|
+
}
|
|
4292
|
+
if (cursor < openCount &&
|
|
4293
|
+
open.startX[cursor] === startX &&
|
|
4294
|
+
open.runWidth[cursor] === runWidth) {
|
|
4295
|
+
next.openedAt[nextCount] = open.openedAt[cursor];
|
|
4296
|
+
next.topY[nextCount] = open.topY[cursor];
|
|
4297
|
+
cursor += 1;
|
|
3612
4298
|
}
|
|
3613
4299
|
else {
|
|
3614
|
-
if (
|
|
3615
|
-
|
|
4300
|
+
if (cursor < openCount && open.startX[cursor] === startX) {
|
|
4301
|
+
closed[closedCount] = cursor;
|
|
4302
|
+
closedCount += 1;
|
|
4303
|
+
cursor += 1;
|
|
3616
4304
|
}
|
|
3617
|
-
|
|
4305
|
+
next.openedAt[nextCount] = opened;
|
|
4306
|
+
next.topY[nextCount] = y;
|
|
4307
|
+
opened += 1;
|
|
3618
4308
|
}
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
4309
|
+
next.runWidth[nextCount] = runWidth;
|
|
4310
|
+
next.startX[nextCount] = startX;
|
|
4311
|
+
nextCount += 1;
|
|
4312
|
+
}
|
|
4313
|
+
while (cursor < openCount) {
|
|
4314
|
+
closed[closedCount] = cursor;
|
|
4315
|
+
closedCount += 1;
|
|
4316
|
+
cursor += 1;
|
|
4317
|
+
}
|
|
4318
|
+
pushClosedRects(rects, open, closed, closedCount, y, offsetX, offsetY);
|
|
4319
|
+
const carried = open;
|
|
4320
|
+
open = next;
|
|
4321
|
+
next = carried;
|
|
4322
|
+
openCount = nextCount;
|
|
4323
|
+
}
|
|
4324
|
+
for (let index = 0; index < openCount; index += 1) {
|
|
4325
|
+
closed[index] = index;
|
|
4326
|
+
}
|
|
4327
|
+
pushClosedRects(rects, open, closed, openCount, height, offsetX, offsetY);
|
|
4328
|
+
return rects;
|
|
4329
|
+
}
|
|
4330
|
+
/** Rects leave in the order they opened, which is not their order across a row. */
|
|
4331
|
+
function pushClosedRects(rects, open, closed, closedCount, bottomY, offsetX, offsetY) {
|
|
4332
|
+
for (let index = 1; index < closedCount; index += 1) {
|
|
4333
|
+
const span = closed[index];
|
|
4334
|
+
const openedAt = open.openedAt[span];
|
|
4335
|
+
let sorted = index - 1;
|
|
4336
|
+
while (sorted >= 0 && open.openedAt[closed[sorted]] > openedAt) {
|
|
4337
|
+
closed[sorted + 1] = closed[sorted];
|
|
4338
|
+
sorted -= 1;
|
|
4339
|
+
}
|
|
4340
|
+
closed[sorted + 1] = span;
|
|
4341
|
+
}
|
|
4342
|
+
for (let index = 0; index < closedCount; index += 1) {
|
|
4343
|
+
const span = closed[index];
|
|
4344
|
+
rects.push({
|
|
4345
|
+
height: bottomY - open.topY[span],
|
|
4346
|
+
width: open.runWidth[span],
|
|
4347
|
+
x: open.startX[span] + offsetX,
|
|
4348
|
+
y: open.topY[span] + offsetY,
|
|
4349
|
+
});
|
|
3626
4350
|
}
|
|
3627
|
-
rects.push(...openRects.values());
|
|
3628
|
-
return rects.length > 0 ? rects : undefined;
|
|
3629
4351
|
}
|
|
3630
|
-
function
|
|
4352
|
+
function assertMaskSize(width, height) {
|
|
3631
4353
|
if (!Number.isInteger(width) || width <= 0) {
|
|
3632
4354
|
throw new Error("Mask width must be a positive integer.");
|
|
3633
4355
|
}
|
|
3634
4356
|
if (!Number.isInteger(height) || height <= 0) {
|
|
3635
4357
|
throw new Error("Mask height must be a positive integer.");
|
|
3636
4358
|
}
|
|
4359
|
+
}
|
|
4360
|
+
function assertMaskDimensions(data, width, height) {
|
|
4361
|
+
assertMaskSize(width, height);
|
|
3637
4362
|
if (data.length !== width * height) {
|
|
3638
4363
|
throw new Error("Mask data length must equal width * height.");
|
|
3639
4364
|
}
|
|
@@ -5314,8 +6039,8 @@ const DEFAULT_SELECTED_STROKE_WIDTH = 4;
|
|
|
5314
6039
|
* Default configurable interaction style.
|
|
5315
6040
|
*
|
|
5316
6041
|
* It resolves hover and selected picks into state-specific presentations using
|
|
5317
|
-
* the same style contracts as normal boxes, masks, and labels.
|
|
5318
|
-
*
|
|
6042
|
+
* the same style contracts as normal boxes, masks, and labels. A state left
|
|
6043
|
+
* unconfigured falls back to a highlight rectangle over the detection.
|
|
5319
6044
|
*/
|
|
5320
6045
|
class BaseInteractionStyle {
|
|
5321
6046
|
options;
|
|
@@ -5337,60 +6062,22 @@ class BaseInteractionStyle {
|
|
|
5337
6062
|
return undefined;
|
|
5338
6063
|
}
|
|
5339
6064
|
return {
|
|
5340
|
-
boxStyle:
|
|
6065
|
+
boxStyle: createDefaultHighlightBoxStyle(context.state, rect),
|
|
5341
6066
|
};
|
|
5342
6067
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
};
|
|
5352
|
-
if (cornerRadius !== undefined) {
|
|
5353
|
-
return {
|
|
5354
|
-
...instruction,
|
|
5355
|
-
cornerRadius,
|
|
5356
|
-
};
|
|
5357
|
-
}
|
|
5358
|
-
return instruction;
|
|
5359
|
-
}
|
|
5360
|
-
resolveShape(detection, context) {
|
|
5361
|
-
return (resolveStyleValue(this.options.shape, detection, context) ?? BoxShape.Rect);
|
|
5362
|
-
}
|
|
5363
|
-
resolveCornerRadius(detection, context, shape) {
|
|
5364
|
-
if (shape !== BoxShape.RoundedRect) {
|
|
5365
|
-
return undefined;
|
|
5366
|
-
}
|
|
5367
|
-
return resolveStyleValue(this.options.cornerRadius, detection, context);
|
|
5368
|
-
}
|
|
5369
|
-
resolveStroke(detection, context) {
|
|
5370
|
-
const stroke = resolveStyleValue(this.options.stroke, detection, context);
|
|
5371
|
-
if (stroke === null) {
|
|
5372
|
-
return undefined;
|
|
5373
|
-
}
|
|
5374
|
-
const defaults = getStateDefaults(context.state);
|
|
5375
|
-
return resolveStrokeStyle(stroke, {
|
|
6068
|
+
}
|
|
6069
|
+
function createDefaultHighlightBoxStyle(state, rect) {
|
|
6070
|
+
const defaults = getStateDefaults(state);
|
|
6071
|
+
const instruction = {
|
|
6072
|
+
fill: { alpha: defaults.fillAlpha, color: defaults.fillColor },
|
|
6073
|
+
rect,
|
|
6074
|
+
shape: BoxShape.Rect,
|
|
6075
|
+
stroke: {
|
|
5376
6076
|
alpha: defaults.strokeAlpha,
|
|
5377
6077
|
color: defaults.strokeColor,
|
|
5378
6078
|
width: defaults.strokeWidth,
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
resolveFill(detection, context) {
|
|
5382
|
-
const fill = resolveStyleValue(this.options.fill, detection, context);
|
|
5383
|
-
if (fill === null) {
|
|
5384
|
-
return undefined;
|
|
5385
|
-
}
|
|
5386
|
-
const defaults = getStateDefaults(context.state);
|
|
5387
|
-
return {
|
|
5388
|
-
alpha: fill?.alpha ?? defaults.fillAlpha,
|
|
5389
|
-
color: fill?.color ?? defaults.fillColor,
|
|
5390
|
-
};
|
|
5391
|
-
}
|
|
5392
|
-
}
|
|
5393
|
-
function createResolvedBoxStyle(instruction) {
|
|
6079
|
+
},
|
|
6080
|
+
};
|
|
5394
6081
|
return {
|
|
5395
6082
|
resolve() {
|
|
5396
6083
|
return instruction;
|
|
@@ -5790,8 +6477,18 @@ class BasePolylineStyle {
|
|
|
5790
6477
|
if (stroke === null) {
|
|
5791
6478
|
return undefined;
|
|
5792
6479
|
}
|
|
6480
|
+
const shadowStroke = resolveStyleValue(this.options.shadowStroke, detection, context);
|
|
5793
6481
|
return {
|
|
5794
6482
|
points: detection.polyline.points,
|
|
6483
|
+
...(shadowStroke === undefined || shadowStroke === null
|
|
6484
|
+
? {}
|
|
6485
|
+
: {
|
|
6486
|
+
shadowStroke: resolveStrokeStyle(shadowStroke, {
|
|
6487
|
+
alpha: 0.65,
|
|
6488
|
+
color: 0x000000,
|
|
6489
|
+
width: 4,
|
|
6490
|
+
}),
|
|
6491
|
+
}),
|
|
5795
6492
|
stroke: resolveStrokeStyle(stroke, {
|
|
5796
6493
|
alpha: 1,
|
|
5797
6494
|
color: 0x00ff66,
|
|
@@ -5961,6 +6658,8 @@ const DEFAULT_KEYPOINT_EDGE_WIDTH = 1.5;
|
|
|
5961
6658
|
const DEFAULT_KEYPOINT_SHADOW_WIDTH = 3;
|
|
5962
6659
|
const DEFAULT_KEYPOINT_SHADOW_ALPHA = 0.25;
|
|
5963
6660
|
const DEFAULT_KEYPOINT_RADIUS = 3.5;
|
|
6661
|
+
const DEFAULT_POLYLINE_SHADOW_WIDTH = DEFAULT_OUTLINE_WIDTH + 1;
|
|
6662
|
+
const DEFAULT_POLYLINE_SHADOW_ALPHA = 0.55;
|
|
5964
6663
|
const DEFAULT_LABEL_CORNER_RADIUS = 4;
|
|
5965
6664
|
const DEFAULT_LABEL_PADDING_X = 6;
|
|
5966
6665
|
const DEFAULT_LABEL_PADDING_Y = 3;
|
|
@@ -6149,6 +6848,11 @@ function createDefaultPolygonStyle(options = {}) {
|
|
|
6149
6848
|
function createDefaultPolylineStyle(options = {}) {
|
|
6150
6849
|
const getClassColor = createClassColorResolver(options);
|
|
6151
6850
|
return new BasePolylineStyle({
|
|
6851
|
+
shadowStroke: {
|
|
6852
|
+
alpha: DEFAULT_POLYLINE_SHADOW_ALPHA,
|
|
6853
|
+
color: 0x000000,
|
|
6854
|
+
width: DEFAULT_POLYLINE_SHADOW_WIDTH,
|
|
6855
|
+
},
|
|
6152
6856
|
stroke: (detection) => ({
|
|
6153
6857
|
alpha: 1,
|
|
6154
6858
|
color: getClassColor(detection),
|
|
@@ -6188,12 +6892,28 @@ const annotationRendererKinds = [
|
|
|
6188
6892
|
/** Sources currently supported by a region renderer. */
|
|
6189
6893
|
const RegionRendererSourceKind = {
|
|
6190
6894
|
Asset: "asset",
|
|
6895
|
+
Media: "media",
|
|
6191
6896
|
};
|
|
6192
6897
|
/** Media-space regions currently supported by a region renderer. */
|
|
6193
6898
|
const RegionRendererRegionKind = {
|
|
6194
6899
|
Bounds: "bounds",
|
|
6195
6900
|
KeypointAnchor: "keypoint-anchor",
|
|
6196
6901
|
};
|
|
6902
|
+
/** Semantic coverage used to clip pixels sampled from the current media. */
|
|
6903
|
+
const RegionRendererCoverageKind = {
|
|
6904
|
+
Mask: "mask",
|
|
6905
|
+
Polygon: "polygon",
|
|
6906
|
+
};
|
|
6907
|
+
/** Filter effects that can be applied to a current-frame media region. */
|
|
6908
|
+
const RegionRendererMediaEffectKind = {
|
|
6909
|
+
Blur: "blur",
|
|
6910
|
+
Pixelate: "pixelate",
|
|
6911
|
+
};
|
|
6912
|
+
/** Coordinate space used by an explicit region-renderer size. */
|
|
6913
|
+
const RegionRendererSizeSpace = {
|
|
6914
|
+
Media: "media",
|
|
6915
|
+
Screen: "screen",
|
|
6916
|
+
};
|
|
6197
6917
|
/** Composition modes currently supported by a region renderer. */
|
|
6198
6918
|
const RegionRendererComposeMode = {
|
|
6199
6919
|
Over: "over",
|
|
@@ -6572,10 +7292,10 @@ function rasterizeRectToMask(rect, dimensions) {
|
|
|
6572
7292
|
const topLeft = centerRectToTopLeftRect(rect);
|
|
6573
7293
|
const left = Math.max(0, Math.round(topLeft.x));
|
|
6574
7294
|
const top = Math.max(0, Math.round(topLeft.y));
|
|
6575
|
-
const right = Math.min(dimensions.width
|
|
6576
|
-
const bottom = Math.min(dimensions.height
|
|
6577
|
-
for (let y = top; y
|
|
6578
|
-
for (let x = left; x
|
|
7295
|
+
const right = Math.min(dimensions.width, Math.round(topLeft.x + rect.width));
|
|
7296
|
+
const bottom = Math.min(dimensions.height, Math.round(topLeft.y + rect.height));
|
|
7297
|
+
for (let y = top; y < bottom; y += 1) {
|
|
7298
|
+
for (let x = left; x < right; x += 1) {
|
|
6579
7299
|
data[y * dimensions.width + x] = 1;
|
|
6580
7300
|
}
|
|
6581
7301
|
}
|
|
@@ -6603,9 +7323,9 @@ function rasterizePolygonToMask(points, dimensions) {
|
|
|
6603
7323
|
}
|
|
6604
7324
|
intersections.sort((left, right) => left - right);
|
|
6605
7325
|
for (let index = 0; index < intersections.length - 1; index += 2) {
|
|
6606
|
-
const left = Math.max(0, Math.
|
|
6607
|
-
const right = Math.min(dimensions.width
|
|
6608
|
-
for (let x = left; x
|
|
7326
|
+
const left = Math.max(0, Math.round(intersections[index]));
|
|
7327
|
+
const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));
|
|
7328
|
+
for (let x = left; x < right; x += 1) {
|
|
6609
7329
|
data[y * dimensions.width + x] = 1;
|
|
6610
7330
|
}
|
|
6611
7331
|
}
|
|
@@ -6745,14 +7465,27 @@ function resolveMaskStyleOpacity(maskStyle) {
|
|
|
6745
7465
|
return Number.isFinite(opacity) ? Math.max(0, Math.min(opacity, 1)) : 1;
|
|
6746
7466
|
}
|
|
6747
7467
|
|
|
6748
|
-
|
|
7468
|
+
/**
|
|
7469
|
+
* Palette slots an id raster can name, one of them the background. A GLSL
|
|
7470
|
+
* fragment stage is guaranteed only 224 uniform vectors, and a palette entry
|
|
7471
|
+
* costs 2.25 of them, so this stays a multiple of four and well inside that
|
|
7472
|
+
* budget; past it a frame draws from the RGBA composite instead.
|
|
7473
|
+
*/
|
|
7474
|
+
const MAX_ID_MASK_PALETTE_ENTRIES = 80;
|
|
6749
7475
|
const MAX_ID_MASK_STROKE_WIDTH = 16;
|
|
6750
|
-
function createIdMaskFrame(instructions) {
|
|
7476
|
+
function createIdMaskFrame(instructions, options = {}) {
|
|
6751
7477
|
if (instructions.length === 0) {
|
|
6752
7478
|
return undefined;
|
|
6753
7479
|
}
|
|
6754
|
-
const
|
|
6755
|
-
const
|
|
7480
|
+
const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));
|
|
7481
|
+
const maskHeight = Math.max(...instructions.map(({ mask }) => mask.height));
|
|
7482
|
+
const width = resolveRasterWidth(maskWidth, options.maxWidth);
|
|
7483
|
+
const height = width === maskWidth
|
|
7484
|
+
? maskHeight
|
|
7485
|
+
: Math.max(1, Math.round((width * maskHeight) / maskWidth));
|
|
7486
|
+
const scaledAxes = width === maskWidth
|
|
7487
|
+
? undefined
|
|
7488
|
+
: createScaledMaskAxes({ height, maskHeight, maskWidth, width });
|
|
6756
7489
|
const data = new Uint8Array(new ArrayBuffer(width * height));
|
|
6757
7490
|
const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
6758
7491
|
const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
@@ -6760,27 +7493,23 @@ function createIdMaskFrame(instructions) {
|
|
|
6760
7493
|
let hasStroke = false;
|
|
6761
7494
|
let maxStrokeWidth = 0;
|
|
6762
7495
|
for (const instruction of instructions) {
|
|
6763
|
-
const detectionMaskId = instruction.detectionIndex
|
|
6764
|
-
if (detectionMaskId
|
|
6765
|
-
detectionMaskId >= MAX_ID_MASK_PALETTE_ENTRIES) {
|
|
7496
|
+
const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);
|
|
7497
|
+
if (detectionMaskId === undefined) {
|
|
6766
7498
|
return undefined;
|
|
6767
7499
|
}
|
|
6768
|
-
|
|
7500
|
+
writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);
|
|
6769
7501
|
if (instruction.stroke && instruction.stroke.width > 0) {
|
|
6770
|
-
const strokeWidth =
|
|
7502
|
+
const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);
|
|
6771
7503
|
hasStroke = true;
|
|
6772
7504
|
strokeWidths[detectionMaskId] = strokeWidth;
|
|
6773
7505
|
maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);
|
|
6774
|
-
|
|
6775
|
-
}
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
data[y * width + x] = detectionMaskId;
|
|
6782
|
-
}
|
|
6783
|
-
}
|
|
7506
|
+
writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
7507
|
+
}
|
|
7508
|
+
if (scaledAxes) {
|
|
7509
|
+
writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);
|
|
7510
|
+
}
|
|
7511
|
+
else {
|
|
7512
|
+
writeMaskRuns(data, width, instruction.mask, detectionMaskId);
|
|
6784
7513
|
}
|
|
6785
7514
|
}
|
|
6786
7515
|
return {
|
|
@@ -6789,13 +7518,126 @@ function createIdMaskFrame(instructions) {
|
|
|
6789
7518
|
hasStroke,
|
|
6790
7519
|
height,
|
|
6791
7520
|
maxStrokeWidth,
|
|
7521
|
+
sourceWidth: maskWidth,
|
|
6792
7522
|
strokePalette,
|
|
6793
7523
|
strokeWidths,
|
|
6794
7524
|
width,
|
|
6795
7525
|
};
|
|
6796
7526
|
}
|
|
6797
|
-
|
|
6798
|
-
|
|
7527
|
+
/**
|
|
7528
|
+
* Compressed RLE counts runs down each column in turn, so a foreground run is
|
|
7529
|
+
* a contiguous walk down one column that wraps into the next.
|
|
7530
|
+
*/
|
|
7531
|
+
function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {
|
|
7532
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
7533
|
+
const maskWidth = mask.width;
|
|
7534
|
+
const maskHeight = mask.height;
|
|
7535
|
+
let maskOffset = 0;
|
|
7536
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
7537
|
+
const runLength = counts[index] ?? 0;
|
|
7538
|
+
if (index % 2 === 0 || runLength <= 0) {
|
|
7539
|
+
maskOffset += runLength;
|
|
7540
|
+
continue;
|
|
7541
|
+
}
|
|
7542
|
+
let x = Math.floor(maskOffset / maskHeight);
|
|
7543
|
+
let y = maskOffset - x * maskHeight;
|
|
7544
|
+
let frameOffset = y * frameWidth + x;
|
|
7545
|
+
for (let step = 0; step < runLength; step += 1) {
|
|
7546
|
+
if (x >= maskWidth) {
|
|
7547
|
+
break;
|
|
7548
|
+
}
|
|
7549
|
+
data[frameOffset] = detectionMaskId;
|
|
7550
|
+
y += 1;
|
|
7551
|
+
frameOffset += frameWidth;
|
|
7552
|
+
if (y === maskHeight) {
|
|
7553
|
+
y = 0;
|
|
7554
|
+
x += 1;
|
|
7555
|
+
frameOffset = x;
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
maskOffset += runLength;
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
/**
|
|
7562
|
+
* The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands
|
|
7563
|
+
* in, so a mask keeps every part of itself the smaller raster can hold and
|
|
7564
|
+
* gains up to a texel at its edges.
|
|
7565
|
+
*/
|
|
7566
|
+
function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {
|
|
7567
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
7568
|
+
const maskWidth = mask.width;
|
|
7569
|
+
const maskHeight = mask.height;
|
|
7570
|
+
let maskOffset = 0;
|
|
7571
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
7572
|
+
const runLength = counts[index] ?? 0;
|
|
7573
|
+
if (index % 2 === 0 || runLength <= 0) {
|
|
7574
|
+
maskOffset += runLength;
|
|
7575
|
+
continue;
|
|
7576
|
+
}
|
|
7577
|
+
let x = Math.floor(maskOffset / maskHeight);
|
|
7578
|
+
let y = maskOffset - x * maskHeight;
|
|
7579
|
+
let column = axes.columns[x];
|
|
7580
|
+
for (let step = 0; step < runLength; step += 1) {
|
|
7581
|
+
if (x >= maskWidth) {
|
|
7582
|
+
break;
|
|
7583
|
+
}
|
|
7584
|
+
data[axes.rows[y] + column] = detectionMaskId;
|
|
7585
|
+
y += 1;
|
|
7586
|
+
if (y === maskHeight) {
|
|
7587
|
+
y = 0;
|
|
7588
|
+
x += 1;
|
|
7589
|
+
column = axes.columns[x];
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
maskOffset += runLength;
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
function resolveRasterWidth(maskWidth, maxWidth) {
|
|
7596
|
+
return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth
|
|
7597
|
+
? Math.max(1, Math.floor(maxWidth))
|
|
7598
|
+
: maskWidth;
|
|
7599
|
+
}
|
|
7600
|
+
/**
|
|
7601
|
+
* A stroke is measured in texels of the raster it is drawn on, so a coarser
|
|
7602
|
+
* raster measures it in coarser texels. A stroke of a texel or more keeps at
|
|
7603
|
+
* least one, the thinnest line the shader can draw; a narrower one keeps its
|
|
7604
|
+
* own width, which the shader draws as an inner boundary at any scale. The
|
|
7605
|
+
* ceiling is the widest neighbourhood the shaders scan, so a wider stroke would
|
|
7606
|
+
* be drawn at the ceiling anyway and every layer drawing the same annotation
|
|
7607
|
+
* has to arrive at the same width.
|
|
7608
|
+
*/
|
|
7609
|
+
function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {
|
|
7610
|
+
const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;
|
|
7611
|
+
return Math.min(Math.max(strokeWidth * scale, Math.min(strokeWidth, 1)), MAX_ID_MASK_STROKE_WIDTH);
|
|
7612
|
+
}
|
|
7613
|
+
function createScaledMaskAxes(frame) {
|
|
7614
|
+
return {
|
|
7615
|
+
columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),
|
|
7616
|
+
rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),
|
|
7617
|
+
};
|
|
7618
|
+
}
|
|
7619
|
+
/** Rows hold their destination offset, so writing a run is two lookups. */
|
|
7620
|
+
function createMaskAxisMap(sourceLength, targetLength, stride) {
|
|
7621
|
+
const map = new Int32Array(sourceLength);
|
|
7622
|
+
const scale = targetLength / sourceLength;
|
|
7623
|
+
for (let index = 0; index < sourceLength; index += 1) {
|
|
7624
|
+
map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;
|
|
7625
|
+
}
|
|
7626
|
+
return map;
|
|
7627
|
+
}
|
|
7628
|
+
/**
|
|
7629
|
+
* The palette slot a detection index names, or undefined when the palette has
|
|
7630
|
+
* no slot for it. Slot zero is the background, so the ids start at one and the
|
|
7631
|
+
* last detection the palette can name sits one index below the last slot.
|
|
7632
|
+
*/
|
|
7633
|
+
function resolveIdMaskPaletteId(detectionIndex) {
|
|
7634
|
+
const detectionMaskId = detectionIndex + 1;
|
|
7635
|
+
return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES
|
|
7636
|
+
? detectionMaskId
|
|
7637
|
+
: undefined;
|
|
7638
|
+
}
|
|
7639
|
+
function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {
|
|
7640
|
+
const offset = detectionMaskId * 4;
|
|
6799
7641
|
palette[offset] = ((color >> 16) & 0xff) / 255;
|
|
6800
7642
|
palette[offset + 1] = ((color >> 8) & 0xff) / 255;
|
|
6801
7643
|
palette[offset + 2] = (color & 0xff) / 255;
|
|
@@ -7033,7 +7875,7 @@ var MediaRendererPlaybackState;
|
|
|
7033
7875
|
*/
|
|
7034
7876
|
var MediaErrorKind;
|
|
7035
7877
|
(function (MediaErrorKind) {
|
|
7036
|
-
/** The media could not be opened or read
|
|
7878
|
+
/** The media could not be opened or read, or was refused before any read. */
|
|
7037
7879
|
MediaErrorKind["Unreadable"] = "unreadable";
|
|
7038
7880
|
/** The container or codec is not supported by this platform. */
|
|
7039
7881
|
MediaErrorKind["UnsupportedFormat"] = "unsupportedFormat";
|
|
@@ -7058,6 +7900,28 @@ var MediaSourceStatus;
|
|
|
7058
7900
|
MediaSourceStatus["Error"] = "error";
|
|
7059
7901
|
MediaSourceStatus["Destroyed"] = "destroyed";
|
|
7060
7902
|
})(MediaSourceStatus || (MediaSourceStatus = {}));
|
|
7903
|
+
/**
|
|
7904
|
+
* Current renderer state.
|
|
7905
|
+
*/
|
|
7906
|
+
/**
|
|
7907
|
+
* What a playback gate holds back.
|
|
7908
|
+
*
|
|
7909
|
+
* A source the renderer pulls samples from can be held between any two frames,
|
|
7910
|
+
* because the renderer decides when each one is drawn. A source that presents
|
|
7911
|
+
* its own frames owns the playhead, so holding it means stopping the producer
|
|
7912
|
+
* and starting it again, and a gate that stops one has to bound its own wait
|
|
7913
|
+
* or a producer nothing answers for never runs again.
|
|
7914
|
+
*/
|
|
7915
|
+
var PlaybackGateReach;
|
|
7916
|
+
(function (PlaybackGateReach) {
|
|
7917
|
+
/** No gate: the picture moves and unprepared layers are absent from it. */
|
|
7918
|
+
PlaybackGateReach["Off"] = "off";
|
|
7919
|
+
/**
|
|
7920
|
+
* Playback waits to begin, and stops again at any frame whose artifacts are
|
|
7921
|
+
* missing, until they arrive or the gate's own wait bound gives up on them.
|
|
7922
|
+
*/
|
|
7923
|
+
PlaybackGateReach["EveryFrame"] = "everyFrame";
|
|
7924
|
+
})(PlaybackGateReach || (PlaybackGateReach = {}));
|
|
7061
7925
|
|
|
7062
7926
|
/**
|
|
7063
7927
|
* Media-session operating mode.
|
|
@@ -7098,12 +7962,35 @@ var MediaSessionStatus;
|
|
|
7098
7962
|
*/
|
|
7099
7963
|
var MediaSessionActivityKind;
|
|
7100
7964
|
(function (MediaSessionActivityKind) {
|
|
7965
|
+
/**
|
|
7966
|
+
* Playback is held waiting for a producer to emit detections for the frame
|
|
7967
|
+
* about to be shown, which is a wait on inference rather than on transfer or
|
|
7968
|
+
* decode. `DetectionsBuffering` is the transfer of detections that already
|
|
7969
|
+
* exist, and `PlaybackBuffering` is the wait for media bytes.
|
|
7970
|
+
*/
|
|
7971
|
+
MediaSessionActivityKind["DetectionsAwaitingCoverage"] = "detectionsAwaitingCoverage";
|
|
7101
7972
|
MediaSessionActivityKind["DetectionsBuffering"] = "detectionsBuffering";
|
|
7102
7973
|
MediaSessionActivityKind["DetectionsLoading"] = "detectionsLoading";
|
|
7103
7974
|
MediaSessionActivityKind["Error"] = "error";
|
|
7104
7975
|
MediaSessionActivityKind["MediaNormalizing"] = "mediaNormalizing";
|
|
7105
7976
|
MediaSessionActivityKind["MediaOpening"] = "mediaOpening";
|
|
7977
|
+
/**
|
|
7978
|
+
* The picture is waiting on media the source has not handed over yet, which
|
|
7979
|
+
* is the bytes and their decode rather than anything downstream of them.
|
|
7980
|
+
* `PlaybackBuffering` is what a transport reports once it has already
|
|
7981
|
+
* stopped; this is reported from the read itself, including the reads a seek
|
|
7982
|
+
* makes while the transport still reads as paused.
|
|
7983
|
+
*/
|
|
7984
|
+
MediaSessionActivityKind["MediaSourceReading"] = "mediaSourceReading";
|
|
7106
7985
|
MediaSessionActivityKind["PlaybackBuffering"] = "playbackBuffering";
|
|
7986
|
+
/**
|
|
7987
|
+
* The render-preparation gate held the picture for artifacts that never
|
|
7988
|
+
* arrived and let it go. Nothing is blocked: the frames reaching the screen
|
|
7989
|
+
* are the ones whose artifacts were given up on. What is waited on is
|
|
7990
|
+
* preparation finishing another frame, which is what lets the gate hold the
|
|
7991
|
+
* picture again; `RenderPreparing` covers the holds that are running.
|
|
7992
|
+
*/
|
|
7993
|
+
MediaSessionActivityKind["RenderPreparationAbandoned"] = "renderPreparationAbandoned";
|
|
7107
7994
|
MediaSessionActivityKind["RenderPreparing"] = "renderPreparing";
|
|
7108
7995
|
})(MediaSessionActivityKind || (MediaSessionActivityKind = {}));
|
|
7109
7996
|
/**
|
|
@@ -7116,5 +8003,5 @@ var MediaSessionActivityStatus;
|
|
|
7116
8003
|
MediaSessionActivityStatus["Waiting"] = "waiting";
|
|
7117
8004
|
})(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
|
|
7118
8005
|
|
|
7119
|
-
export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, StrokeAlignment as BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionMaskPayloadFormat, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MAX_ID_MASK_PALETTE_ENTRIES, MAX_ID_MASK_STROKE_WIDTH, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, ShapeInstructionKind, StrokeAlignment, TrackingGeometry, annotationRendererKinds, annotationRenderers, applyAnnotationHandleDrag, canReuseMaskStyleArtifacts, centerRectToTopLeftRect, computeDetectionMaskRect, computeMaskBounds, containsPoint, convertDetectionBoxToMask, convertDetectionBoxToPolygon, convertDetectionMaskToBox, convertDetectionMaskToPolygon, convertDetectionPolygonToBox, convertDetectionPolygonToMask, copySortedDetectionFrames, createAnnotationEditingEngine, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createDetectionPickKey, createEditableAnnotationFrameSession, createIdMaskFrame, createIdleDetectionBufferState, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createSourceAwarePresentation, createViewportController, createWritableDetectionFrameSource, decodeCompressedRleCounts, decodeCompressedRleMask, decodeDetectionMaskPayload, deleteAnnotationVertex, detectMaskBorders, detectionFrameOverlapsRange, detectionPostProcessors, distanceToSegment, encodeBinaryMask, encodeBinaryMaskWithBounds, encodeCompressedRleCounts, encodeDetectionMaskPayload, extractMaskContour, extractMaskRectRuns, filterDetectionFramesForRange, findClosestAnnotationSegment, followDetectionPickAcrossFrames, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, haveSameDetectionPickIdentities, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationRendererPresentation, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
|
|
8006
|
+
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, resolveIdMaskPaletteId, resolveIdMaskStrokeTexels, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames, writeIdMaskPaletteEntry };
|
|
7120
8007
|
//# sourceMappingURL=index.js.map
|