@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 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
- /** Detection overlay fade — ALSO the raw-fallback suppression window: while an
71042
- * enriched frame-tracked event arrived within this window, raw detection.result
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 detectionTimer = (0, react$1.useRef)(void 0);
71070
- const lastEnrichedAt = (0, react$1.useRef)(0);
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
- lastEnrichedAt.current = 0;
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
- lastEnrichedAt.current = Date.now();
71127
- setDetection({
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
- if (detectionTimer.current) clearTimeout(detectionTimer.current);
71133
- detectionTimer.current = setTimeout(() => setDetection(null), DETECTION_FADE_MS);
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 (detectionTimer.current) clearTimeout(detectionTimer.current);
71254
+ if (extrasTimer.current) clearTimeout(extrasTimer.current);
71138
71255
  };
71139
- }, [trpc, deviceId]);
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
- setDetection({
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
- if (detectionTimer.current) clearTimeout(detectionTimer.current);
71154
- detectionTimer.current = setTimeout(() => setDetection(null), DETECTION_FADE_MS);
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 (detectionTimer.current) clearTimeout(detectionTimer.current);
71285
+ if (rawTimer.current) clearTimeout(rawTimer.current);
71159
71286
  };
71160
- }, [trpc, deviceId]);
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
- /** Detection overlay fade — ALSO the raw-fallback suppression window: while an
71018
- * enriched frame-tracked event arrived within this window, raw detection.result
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 detectionTimer = useRef(void 0);
71046
- const lastEnrichedAt = useRef(0);
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
- lastEnrichedAt.current = 0;
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
- lastEnrichedAt.current = Date.now();
71103
- setDetection({
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
- if (detectionTimer.current) clearTimeout(detectionTimer.current);
71109
- detectionTimer.current = setTimeout(() => setDetection(null), DETECTION_FADE_MS);
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 (detectionTimer.current) clearTimeout(detectionTimer.current);
71230
+ if (extrasTimer.current) clearTimeout(extrasTimer.current);
71114
71231
  };
71115
- }, [trpc, deviceId]);
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
- setDetection({
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
- if (detectionTimer.current) clearTimeout(detectionTimer.current);
71130
- detectionTimer.current = setTimeout(() => setDetection(null), DETECTION_FADE_MS);
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 (detectionTimer.current) clearTimeout(detectionTimer.current);
71261
+ if (rawTimer.current) clearTimeout(rawTimer.current);
71135
71262
  };
71136
- }, [trpc, deviceId]);
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.348",
3
+ "version": "1.2.349",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",