@camstack/ui-library 1.2.348 → 1.2.349
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/CHANGELOG.md +7 -0
- package/dist/hooks/merge-detections.d.ts +16 -0
- package/dist/index.cjs +154 -22
- package/dist/index.js +154 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.349] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Live detection boxes ignore out-of-order frames and fade by frame age
|
|
15
|
+
- Live detection boxes come straight from the detector; faces, plates and names attach as they arrive
|
|
16
|
+
|
|
10
17
|
## [1.2.348] - 2026-10-29
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge of the two live overlay sources into the one flat list the overlay
|
|
3
|
+
* renders. Root boxes ALWAYS come from the raw detector frame; the enriched
|
|
4
|
+
* (post-analysis) frame contributes only EXTRAS — detail children (face,
|
|
5
|
+
* plate…) and identity labels — attached to the matching raw root.
|
|
6
|
+
*/
|
|
7
|
+
import type { ObjectDetection } from "@camstack/types";
|
|
8
|
+
/** Minimum IoU between an enriched root and a raw root to call them one subject
|
|
9
|
+
* when no trackId pairs them. */
|
|
10
|
+
export declare const EXTRA_MATCH_IOU_THRESHOLD = .3;
|
|
11
|
+
export interface FramePart {
|
|
12
|
+
readonly detections: readonly ObjectDetection[];
|
|
13
|
+
readonly frameWidth: number;
|
|
14
|
+
readonly frameHeight: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function mergeDetections(raw: FramePart | null, extras: FramePart | null): FramePart | null;
|
package/dist/index.cjs
CHANGED
|
@@ -71030,6 +71030,90 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71030
71030
|
return useDeviceStateSlice(handleRef.current);
|
|
71031
71031
|
}
|
|
71032
71032
|
//#endregion
|
|
71033
|
+
//#region src/hooks/merge-detections.ts
|
|
71034
|
+
/** Minimum IoU between an enriched root and a raw root to call them one subject
|
|
71035
|
+
* when no trackId pairs them. */
|
|
71036
|
+
var EXTRA_MATCH_IOU_THRESHOLD = .3;
|
|
71037
|
+
function iou(a, b) {
|
|
71038
|
+
const x1 = Math.max(a.x, b.x);
|
|
71039
|
+
const y1 = Math.max(a.y, b.y);
|
|
71040
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
71041
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
71042
|
+
const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
71043
|
+
const union = a.width * a.height + b.width * b.height - inter;
|
|
71044
|
+
return union > 0 ? inter / union : 0;
|
|
71045
|
+
}
|
|
71046
|
+
function isRoot(d) {
|
|
71047
|
+
return d.kind !== "detail";
|
|
71048
|
+
}
|
|
71049
|
+
function scaleBox(b, sx, sy) {
|
|
71050
|
+
return {
|
|
71051
|
+
x: b.x * sx,
|
|
71052
|
+
y: b.y * sy,
|
|
71053
|
+
width: b.width * sx,
|
|
71054
|
+
height: b.height * sy
|
|
71055
|
+
};
|
|
71056
|
+
}
|
|
71057
|
+
/** The raw root an enriched root describes: same trackId, else best IoU. */
|
|
71058
|
+
function matchRawRoot(enrichedRoot, rawRoots, sx, sy) {
|
|
71059
|
+
const trackId = enrichedRoot.track?.trackId;
|
|
71060
|
+
if (trackId !== void 0) {
|
|
71061
|
+
const byTrack = rawRoots.find((r) => r.track?.trackId === trackId);
|
|
71062
|
+
if (byTrack) return byTrack;
|
|
71063
|
+
}
|
|
71064
|
+
const scaled = scaleBox(enrichedRoot.bbox, sx, sy);
|
|
71065
|
+
let best = null;
|
|
71066
|
+
let bestIou = EXTRA_MATCH_IOU_THRESHOLD;
|
|
71067
|
+
for (const r of rawRoots) {
|
|
71068
|
+
const v = iou(scaled, r.bbox);
|
|
71069
|
+
if (v >= bestIou) {
|
|
71070
|
+
best = r;
|
|
71071
|
+
bestIou = v;
|
|
71072
|
+
}
|
|
71073
|
+
}
|
|
71074
|
+
return best;
|
|
71075
|
+
}
|
|
71076
|
+
function mergeDetections(raw, extras) {
|
|
71077
|
+
if (!extras) return raw;
|
|
71078
|
+
const frameWidth = raw?.frameWidth ?? extras.frameWidth;
|
|
71079
|
+
const frameHeight = raw?.frameHeight ?? extras.frameHeight;
|
|
71080
|
+
const sx = extras.frameWidth > 0 ? frameWidth / extras.frameWidth : 1;
|
|
71081
|
+
const sy = extras.frameHeight > 0 ? frameHeight / extras.frameHeight : 1;
|
|
71082
|
+
const rawDets = raw?.detections ?? [];
|
|
71083
|
+
const rawRoots = rawDets.filter(isRoot);
|
|
71084
|
+
const rawRootFor = /* @__PURE__ */ new Map();
|
|
71085
|
+
const extraLabels = /* @__PURE__ */ new Map();
|
|
71086
|
+
for (const root of extras.detections.filter(isRoot)) {
|
|
71087
|
+
const match = matchRawRoot(root, rawRoots, sx, sy);
|
|
71088
|
+
rawRootFor.set(root.id, match);
|
|
71089
|
+
if (match && root.labels.length > 0) extraLabels.set(match, [...extraLabels.get(match) ?? [], ...root.labels]);
|
|
71090
|
+
}
|
|
71091
|
+
const out = rawDets.map((d) => {
|
|
71092
|
+
const add = extraLabels.get(d);
|
|
71093
|
+
if (!add) return d;
|
|
71094
|
+
const have = new Set(d.labels.map((l) => l.label));
|
|
71095
|
+
const fresh = add.filter((l) => !have.has(l.label));
|
|
71096
|
+
return fresh.length > 0 ? {
|
|
71097
|
+
...d,
|
|
71098
|
+
labels: [...d.labels, ...fresh]
|
|
71099
|
+
} : d;
|
|
71100
|
+
});
|
|
71101
|
+
for (const d of extras.detections) {
|
|
71102
|
+
if (isRoot(d)) continue;
|
|
71103
|
+
const parent = d.parentId !== void 0 ? rawRootFor.get(d.parentId) : void 0;
|
|
71104
|
+
out.push({
|
|
71105
|
+
...d,
|
|
71106
|
+
bbox: scaleBox(d.bbox, sx, sy),
|
|
71107
|
+
...parent ? { parentId: parent.id } : {}
|
|
71108
|
+
});
|
|
71109
|
+
}
|
|
71110
|
+
return {
|
|
71111
|
+
detections: out,
|
|
71112
|
+
frameWidth,
|
|
71113
|
+
frameHeight
|
|
71114
|
+
};
|
|
71115
|
+
}
|
|
71116
|
+
//#endregion
|
|
71033
71117
|
//#region src/hooks/use-device-detections.ts
|
|
71034
71118
|
/**
|
|
71035
71119
|
* useDeviceDetections — subscribes to motion, raw motion, and detection events for a device.
|
|
@@ -71038,12 +71122,18 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71038
71122
|
* admin-ui (BackendClient.trpc) and addon pages (AddonPageProps.trpc).
|
|
71039
71123
|
*/
|
|
71040
71124
|
var MOTION_FADE_MS = 500;
|
|
71041
|
-
/**
|
|
71042
|
-
*
|
|
71043
|
-
* frames are ignored (post-analysis is the richer source; raw remains the
|
|
71044
|
-
* fallback when post-analysis is absent). Keeping the two equal means raw
|
|
71045
|
-
* resumes exactly when the enriched display fades — no blank gap. */
|
|
71125
|
+
/** Fade of each detection source (raw roots, enriched extras), counted from the
|
|
71126
|
+
* frame's capture time so a late frame is not kept for the full window. */
|
|
71046
71127
|
var DETECTION_FADE_MS = 800;
|
|
71128
|
+
/** A frame older than this at arrival is replayed pre-roll and never drawn.
|
|
71129
|
+
* Same meaning as the server's OVERLAY_MAX_FRAME_AGE_MS. */
|
|
71130
|
+
var OVERLAY_MAX_FRAME_AGE_MS = 1e3;
|
|
71131
|
+
/**
|
|
71132
|
+
* The most of a frame's age (arrival minus its capture timestamp) the fade
|
|
71133
|
+
* deducts. Bounded so a skewed server/browser clock can never make boxes vanish
|
|
71134
|
+
* on arrival.
|
|
71135
|
+
*/
|
|
71136
|
+
var MAX_FRAME_AGE_FADE_DEDUCTION_MS = DETECTION_FADE_MS / 2;
|
|
71047
71137
|
/** Helper: subscribe to live.onEvent without optional chaining on subscribe() */
|
|
71048
71138
|
function safeSubscribe(trpc, category, onData) {
|
|
71049
71139
|
try {
|
|
@@ -71066,8 +71156,26 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71066
71156
|
const [phase, setPhase] = (0, react$1.useState)("watching");
|
|
71067
71157
|
const motionTimer = (0, react$1.useRef)(void 0);
|
|
71068
71158
|
const motionRawTimer = (0, react$1.useRef)(void 0);
|
|
71069
|
-
const
|
|
71070
|
-
const
|
|
71159
|
+
const rawTimer = (0, react$1.useRef)(void 0);
|
|
71160
|
+
const extrasTimer = (0, react$1.useRef)(void 0);
|
|
71161
|
+
/** Live part of each source: raw roots and enriched extras fade independently. */
|
|
71162
|
+
const rawPart = (0, react$1.useRef)(null);
|
|
71163
|
+
const extrasPart = (0, react$1.useRef)(null);
|
|
71164
|
+
/** Capture timestamp of the newest frame shown, per source; older arrivals are dropped. */
|
|
71165
|
+
const lastRawTs = (0, react$1.useRef)(0);
|
|
71166
|
+
const lastExtrasTs = (0, react$1.useRef)(0);
|
|
71167
|
+
const publish = (0, react$1.useCallback)(() => {
|
|
71168
|
+
setDetection(mergeDetections(rawPart.current, extrasPart.current));
|
|
71169
|
+
}, []);
|
|
71170
|
+
/** Admit a frame: null = rejected (stale or out of order), else its fade in ms. */
|
|
71171
|
+
const admit = (0, react$1.useCallback)((frameTs, lastTs) => {
|
|
71172
|
+
if (frameTs !== null) {
|
|
71173
|
+
if (Date.now() - frameTs > OVERLAY_MAX_FRAME_AGE_MS) return null;
|
|
71174
|
+
if (frameTs < lastTs.current) return null;
|
|
71175
|
+
lastTs.current = frameTs;
|
|
71176
|
+
}
|
|
71177
|
+
return DETECTION_FADE_MS - (frameTs === null ? 0 : Math.min(Math.max(Date.now() - frameTs, 0), MAX_FRAME_AGE_FADE_DEDUCTION_MS));
|
|
71178
|
+
}, []);
|
|
71071
71179
|
(0, react$1.useEffect)(() => {
|
|
71072
71180
|
if (deviceId === null) return;
|
|
71073
71181
|
const sub = safeSubscribe(trpc, "detection.motion-analysis", (event) => {
|
|
@@ -71116,48 +71224,72 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71116
71224
|
};
|
|
71117
71225
|
}, [trpc, deviceId]);
|
|
71118
71226
|
(0, react$1.useEffect)(() => {
|
|
71119
|
-
|
|
71227
|
+
extrasPart.current = null;
|
|
71228
|
+
rawPart.current = null;
|
|
71229
|
+
lastExtrasTs.current = 0;
|
|
71230
|
+
lastRawTs.current = 0;
|
|
71231
|
+
setDetection(null);
|
|
71120
71232
|
if (deviceId === null) return;
|
|
71121
71233
|
const sub = safeSubscribe(trpc, "pipeline-analytics.frame-tracked", (event) => {
|
|
71122
71234
|
const data = event?.data;
|
|
71123
71235
|
if (!data || data.deviceId !== deviceId) return;
|
|
71124
71236
|
if (!data.detections?.length) return;
|
|
71125
71237
|
if (typeof data.detections[0]?.kind !== "string") return;
|
|
71126
|
-
|
|
71127
|
-
|
|
71238
|
+
const fadeInMs = admit(typeof data.timestamp === "number" ? data.timestamp : null, lastExtrasTs);
|
|
71239
|
+
if (fadeInMs === null) return;
|
|
71240
|
+
extrasPart.current = {
|
|
71128
71241
|
detections: data.detections,
|
|
71129
71242
|
frameWidth: data.frameWidth,
|
|
71130
71243
|
frameHeight: data.frameHeight
|
|
71131
|
-
}
|
|
71132
|
-
|
|
71133
|
-
|
|
71244
|
+
};
|
|
71245
|
+
publish();
|
|
71246
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71247
|
+
extrasTimer.current = setTimeout(() => {
|
|
71248
|
+
extrasPart.current = null;
|
|
71249
|
+
publish();
|
|
71250
|
+
}, fadeInMs);
|
|
71134
71251
|
});
|
|
71135
71252
|
return () => {
|
|
71136
71253
|
sub?.unsubscribe();
|
|
71137
|
-
if (
|
|
71254
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71138
71255
|
};
|
|
71139
|
-
}, [
|
|
71256
|
+
}, [
|
|
71257
|
+
trpc,
|
|
71258
|
+
deviceId,
|
|
71259
|
+
admit,
|
|
71260
|
+
publish
|
|
71261
|
+
]);
|
|
71140
71262
|
(0, react$1.useEffect)(() => {
|
|
71141
71263
|
if (deviceId === null) return;
|
|
71142
71264
|
const sub = safeSubscribe(trpc, "detection.result", (event) => {
|
|
71143
71265
|
const e = event;
|
|
71144
71266
|
if (!matchesDevice(e?.source, deviceId)) return;
|
|
71145
|
-
if (Date.now() - lastEnrichedAt.current < DETECTION_FADE_MS) return;
|
|
71146
71267
|
const frame = e.data?.frame;
|
|
71147
71268
|
if (!frame?.detections?.length) return;
|
|
71148
|
-
|
|
71269
|
+
const fadeInMs = admit(typeof frame.timestamp === "number" ? frame.timestamp : null, lastRawTs);
|
|
71270
|
+
if (fadeInMs === null) return;
|
|
71271
|
+
rawPart.current = {
|
|
71149
71272
|
detections: frame.detections,
|
|
71150
71273
|
frameWidth: frame.width,
|
|
71151
71274
|
frameHeight: frame.height
|
|
71152
|
-
}
|
|
71153
|
-
|
|
71154
|
-
|
|
71275
|
+
};
|
|
71276
|
+
publish();
|
|
71277
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71278
|
+
rawTimer.current = setTimeout(() => {
|
|
71279
|
+
rawPart.current = null;
|
|
71280
|
+
publish();
|
|
71281
|
+
}, fadeInMs);
|
|
71155
71282
|
});
|
|
71156
71283
|
return () => {
|
|
71157
71284
|
sub?.unsubscribe();
|
|
71158
|
-
if (
|
|
71285
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71159
71286
|
};
|
|
71160
|
-
}, [
|
|
71287
|
+
}, [
|
|
71288
|
+
trpc,
|
|
71289
|
+
deviceId,
|
|
71290
|
+
admit,
|
|
71291
|
+
publish
|
|
71292
|
+
]);
|
|
71161
71293
|
(0, react$1.useEffect)(() => {
|
|
71162
71294
|
if (deviceId === null) return;
|
|
71163
71295
|
const sub = safeSubscribe(trpc, "detection.phase-transition", (event) => {
|
package/dist/index.js
CHANGED
|
@@ -71006,6 +71006,90 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71006
71006
|
return useDeviceStateSlice(handleRef.current);
|
|
71007
71007
|
}
|
|
71008
71008
|
//#endregion
|
|
71009
|
+
//#region src/hooks/merge-detections.ts
|
|
71010
|
+
/** Minimum IoU between an enriched root and a raw root to call them one subject
|
|
71011
|
+
* when no trackId pairs them. */
|
|
71012
|
+
var EXTRA_MATCH_IOU_THRESHOLD = .3;
|
|
71013
|
+
function iou(a, b) {
|
|
71014
|
+
const x1 = Math.max(a.x, b.x);
|
|
71015
|
+
const y1 = Math.max(a.y, b.y);
|
|
71016
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
71017
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
71018
|
+
const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
71019
|
+
const union = a.width * a.height + b.width * b.height - inter;
|
|
71020
|
+
return union > 0 ? inter / union : 0;
|
|
71021
|
+
}
|
|
71022
|
+
function isRoot(d) {
|
|
71023
|
+
return d.kind !== "detail";
|
|
71024
|
+
}
|
|
71025
|
+
function scaleBox(b, sx, sy) {
|
|
71026
|
+
return {
|
|
71027
|
+
x: b.x * sx,
|
|
71028
|
+
y: b.y * sy,
|
|
71029
|
+
width: b.width * sx,
|
|
71030
|
+
height: b.height * sy
|
|
71031
|
+
};
|
|
71032
|
+
}
|
|
71033
|
+
/** The raw root an enriched root describes: same trackId, else best IoU. */
|
|
71034
|
+
function matchRawRoot(enrichedRoot, rawRoots, sx, sy) {
|
|
71035
|
+
const trackId = enrichedRoot.track?.trackId;
|
|
71036
|
+
if (trackId !== void 0) {
|
|
71037
|
+
const byTrack = rawRoots.find((r) => r.track?.trackId === trackId);
|
|
71038
|
+
if (byTrack) return byTrack;
|
|
71039
|
+
}
|
|
71040
|
+
const scaled = scaleBox(enrichedRoot.bbox, sx, sy);
|
|
71041
|
+
let best = null;
|
|
71042
|
+
let bestIou = EXTRA_MATCH_IOU_THRESHOLD;
|
|
71043
|
+
for (const r of rawRoots) {
|
|
71044
|
+
const v = iou(scaled, r.bbox);
|
|
71045
|
+
if (v >= bestIou) {
|
|
71046
|
+
best = r;
|
|
71047
|
+
bestIou = v;
|
|
71048
|
+
}
|
|
71049
|
+
}
|
|
71050
|
+
return best;
|
|
71051
|
+
}
|
|
71052
|
+
function mergeDetections(raw, extras) {
|
|
71053
|
+
if (!extras) return raw;
|
|
71054
|
+
const frameWidth = raw?.frameWidth ?? extras.frameWidth;
|
|
71055
|
+
const frameHeight = raw?.frameHeight ?? extras.frameHeight;
|
|
71056
|
+
const sx = extras.frameWidth > 0 ? frameWidth / extras.frameWidth : 1;
|
|
71057
|
+
const sy = extras.frameHeight > 0 ? frameHeight / extras.frameHeight : 1;
|
|
71058
|
+
const rawDets = raw?.detections ?? [];
|
|
71059
|
+
const rawRoots = rawDets.filter(isRoot);
|
|
71060
|
+
const rawRootFor = /* @__PURE__ */ new Map();
|
|
71061
|
+
const extraLabels = /* @__PURE__ */ new Map();
|
|
71062
|
+
for (const root of extras.detections.filter(isRoot)) {
|
|
71063
|
+
const match = matchRawRoot(root, rawRoots, sx, sy);
|
|
71064
|
+
rawRootFor.set(root.id, match);
|
|
71065
|
+
if (match && root.labels.length > 0) extraLabels.set(match, [...extraLabels.get(match) ?? [], ...root.labels]);
|
|
71066
|
+
}
|
|
71067
|
+
const out = rawDets.map((d) => {
|
|
71068
|
+
const add = extraLabels.get(d);
|
|
71069
|
+
if (!add) return d;
|
|
71070
|
+
const have = new Set(d.labels.map((l) => l.label));
|
|
71071
|
+
const fresh = add.filter((l) => !have.has(l.label));
|
|
71072
|
+
return fresh.length > 0 ? {
|
|
71073
|
+
...d,
|
|
71074
|
+
labels: [...d.labels, ...fresh]
|
|
71075
|
+
} : d;
|
|
71076
|
+
});
|
|
71077
|
+
for (const d of extras.detections) {
|
|
71078
|
+
if (isRoot(d)) continue;
|
|
71079
|
+
const parent = d.parentId !== void 0 ? rawRootFor.get(d.parentId) : void 0;
|
|
71080
|
+
out.push({
|
|
71081
|
+
...d,
|
|
71082
|
+
bbox: scaleBox(d.bbox, sx, sy),
|
|
71083
|
+
...parent ? { parentId: parent.id } : {}
|
|
71084
|
+
});
|
|
71085
|
+
}
|
|
71086
|
+
return {
|
|
71087
|
+
detections: out,
|
|
71088
|
+
frameWidth,
|
|
71089
|
+
frameHeight
|
|
71090
|
+
};
|
|
71091
|
+
}
|
|
71092
|
+
//#endregion
|
|
71009
71093
|
//#region src/hooks/use-device-detections.ts
|
|
71010
71094
|
/**
|
|
71011
71095
|
* useDeviceDetections — subscribes to motion, raw motion, and detection events for a device.
|
|
@@ -71014,12 +71098,18 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71014
71098
|
* admin-ui (BackendClient.trpc) and addon pages (AddonPageProps.trpc).
|
|
71015
71099
|
*/
|
|
71016
71100
|
var MOTION_FADE_MS = 500;
|
|
71017
|
-
/**
|
|
71018
|
-
*
|
|
71019
|
-
* frames are ignored (post-analysis is the richer source; raw remains the
|
|
71020
|
-
* fallback when post-analysis is absent). Keeping the two equal means raw
|
|
71021
|
-
* resumes exactly when the enriched display fades — no blank gap. */
|
|
71101
|
+
/** Fade of each detection source (raw roots, enriched extras), counted from the
|
|
71102
|
+
* frame's capture time so a late frame is not kept for the full window. */
|
|
71022
71103
|
var DETECTION_FADE_MS = 800;
|
|
71104
|
+
/** A frame older than this at arrival is replayed pre-roll and never drawn.
|
|
71105
|
+
* Same meaning as the server's OVERLAY_MAX_FRAME_AGE_MS. */
|
|
71106
|
+
var OVERLAY_MAX_FRAME_AGE_MS = 1e3;
|
|
71107
|
+
/**
|
|
71108
|
+
* The most of a frame's age (arrival minus its capture timestamp) the fade
|
|
71109
|
+
* deducts. Bounded so a skewed server/browser clock can never make boxes vanish
|
|
71110
|
+
* on arrival.
|
|
71111
|
+
*/
|
|
71112
|
+
var MAX_FRAME_AGE_FADE_DEDUCTION_MS = DETECTION_FADE_MS / 2;
|
|
71023
71113
|
/** Helper: subscribe to live.onEvent without optional chaining on subscribe() */
|
|
71024
71114
|
function safeSubscribe(trpc, category, onData) {
|
|
71025
71115
|
try {
|
|
@@ -71042,8 +71132,26 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71042
71132
|
const [phase, setPhase] = useState("watching");
|
|
71043
71133
|
const motionTimer = useRef(void 0);
|
|
71044
71134
|
const motionRawTimer = useRef(void 0);
|
|
71045
|
-
const
|
|
71046
|
-
const
|
|
71135
|
+
const rawTimer = useRef(void 0);
|
|
71136
|
+
const extrasTimer = useRef(void 0);
|
|
71137
|
+
/** Live part of each source: raw roots and enriched extras fade independently. */
|
|
71138
|
+
const rawPart = useRef(null);
|
|
71139
|
+
const extrasPart = useRef(null);
|
|
71140
|
+
/** Capture timestamp of the newest frame shown, per source; older arrivals are dropped. */
|
|
71141
|
+
const lastRawTs = useRef(0);
|
|
71142
|
+
const lastExtrasTs = useRef(0);
|
|
71143
|
+
const publish = useCallback(() => {
|
|
71144
|
+
setDetection(mergeDetections(rawPart.current, extrasPart.current));
|
|
71145
|
+
}, []);
|
|
71146
|
+
/** Admit a frame: null = rejected (stale or out of order), else its fade in ms. */
|
|
71147
|
+
const admit = useCallback((frameTs, lastTs) => {
|
|
71148
|
+
if (frameTs !== null) {
|
|
71149
|
+
if (Date.now() - frameTs > OVERLAY_MAX_FRAME_AGE_MS) return null;
|
|
71150
|
+
if (frameTs < lastTs.current) return null;
|
|
71151
|
+
lastTs.current = frameTs;
|
|
71152
|
+
}
|
|
71153
|
+
return DETECTION_FADE_MS - (frameTs === null ? 0 : Math.min(Math.max(Date.now() - frameTs, 0), MAX_FRAME_AGE_FADE_DEDUCTION_MS));
|
|
71154
|
+
}, []);
|
|
71047
71155
|
useEffect(() => {
|
|
71048
71156
|
if (deviceId === null) return;
|
|
71049
71157
|
const sub = safeSubscribe(trpc, "detection.motion-analysis", (event) => {
|
|
@@ -71092,48 +71200,72 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71092
71200
|
};
|
|
71093
71201
|
}, [trpc, deviceId]);
|
|
71094
71202
|
useEffect(() => {
|
|
71095
|
-
|
|
71203
|
+
extrasPart.current = null;
|
|
71204
|
+
rawPart.current = null;
|
|
71205
|
+
lastExtrasTs.current = 0;
|
|
71206
|
+
lastRawTs.current = 0;
|
|
71207
|
+
setDetection(null);
|
|
71096
71208
|
if (deviceId === null) return;
|
|
71097
71209
|
const sub = safeSubscribe(trpc, "pipeline-analytics.frame-tracked", (event) => {
|
|
71098
71210
|
const data = event?.data;
|
|
71099
71211
|
if (!data || data.deviceId !== deviceId) return;
|
|
71100
71212
|
if (!data.detections?.length) return;
|
|
71101
71213
|
if (typeof data.detections[0]?.kind !== "string") return;
|
|
71102
|
-
|
|
71103
|
-
|
|
71214
|
+
const fadeInMs = admit(typeof data.timestamp === "number" ? data.timestamp : null, lastExtrasTs);
|
|
71215
|
+
if (fadeInMs === null) return;
|
|
71216
|
+
extrasPart.current = {
|
|
71104
71217
|
detections: data.detections,
|
|
71105
71218
|
frameWidth: data.frameWidth,
|
|
71106
71219
|
frameHeight: data.frameHeight
|
|
71107
|
-
}
|
|
71108
|
-
|
|
71109
|
-
|
|
71220
|
+
};
|
|
71221
|
+
publish();
|
|
71222
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71223
|
+
extrasTimer.current = setTimeout(() => {
|
|
71224
|
+
extrasPart.current = null;
|
|
71225
|
+
publish();
|
|
71226
|
+
}, fadeInMs);
|
|
71110
71227
|
});
|
|
71111
71228
|
return () => {
|
|
71112
71229
|
sub?.unsubscribe();
|
|
71113
|
-
if (
|
|
71230
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71114
71231
|
};
|
|
71115
|
-
}, [
|
|
71232
|
+
}, [
|
|
71233
|
+
trpc,
|
|
71234
|
+
deviceId,
|
|
71235
|
+
admit,
|
|
71236
|
+
publish
|
|
71237
|
+
]);
|
|
71116
71238
|
useEffect(() => {
|
|
71117
71239
|
if (deviceId === null) return;
|
|
71118
71240
|
const sub = safeSubscribe(trpc, "detection.result", (event) => {
|
|
71119
71241
|
const e = event;
|
|
71120
71242
|
if (!matchesDevice(e?.source, deviceId)) return;
|
|
71121
|
-
if (Date.now() - lastEnrichedAt.current < DETECTION_FADE_MS) return;
|
|
71122
71243
|
const frame = e.data?.frame;
|
|
71123
71244
|
if (!frame?.detections?.length) return;
|
|
71124
|
-
|
|
71245
|
+
const fadeInMs = admit(typeof frame.timestamp === "number" ? frame.timestamp : null, lastRawTs);
|
|
71246
|
+
if (fadeInMs === null) return;
|
|
71247
|
+
rawPart.current = {
|
|
71125
71248
|
detections: frame.detections,
|
|
71126
71249
|
frameWidth: frame.width,
|
|
71127
71250
|
frameHeight: frame.height
|
|
71128
|
-
}
|
|
71129
|
-
|
|
71130
|
-
|
|
71251
|
+
};
|
|
71252
|
+
publish();
|
|
71253
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71254
|
+
rawTimer.current = setTimeout(() => {
|
|
71255
|
+
rawPart.current = null;
|
|
71256
|
+
publish();
|
|
71257
|
+
}, fadeInMs);
|
|
71131
71258
|
});
|
|
71132
71259
|
return () => {
|
|
71133
71260
|
sub?.unsubscribe();
|
|
71134
|
-
if (
|
|
71261
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71135
71262
|
};
|
|
71136
|
-
}, [
|
|
71263
|
+
}, [
|
|
71264
|
+
trpc,
|
|
71265
|
+
deviceId,
|
|
71266
|
+
admit,
|
|
71267
|
+
publish
|
|
71268
|
+
]);
|
|
71137
71269
|
useEffect(() => {
|
|
71138
71270
|
if (deviceId === null) return;
|
|
71139
71271
|
const sub = safeSubscribe(trpc, "detection.phase-transition", (event) => {
|