@camstack/addon-pipeline 1.2.245 → 1.2.247

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.
Files changed (28) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-DLFtoi4W.js → default-detection-model-ClJ9y701.js} +42 -1
  4. package/dist/{default-detection-model-UXQCoRj_.mjs → default-detection-model-pr2NMNpc.mjs} +31 -2
  5. package/dist/detection-pipeline/index.js +5 -16
  6. package/dist/detection-pipeline/index.mjs +5 -16
  7. package/dist/{dist-CWWQf3ci.js → dist-BjiM0e6J.js} +48 -8
  8. package/dist/{dist-CoZwNgzu.mjs → dist-Dzy3wAmT.mjs} +48 -8
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-Uh59Eo57.mjs → node-D9DBz5HF.mjs} +1 -1
  12. package/dist/{node-DLZWItaN.js → node-P2-C_t7H.js} +1 -1
  13. package/dist/pipeline-runner/index.js +45 -30
  14. package/dist/pipeline-runner/index.mjs +45 -30
  15. package/dist/{process-memory-BnFnR_nP.mjs → process-memory-BMpJ42op.mjs} +1 -1
  16. package/dist/{process-memory-Bv9SiLn-.js → process-memory-vWz15IrS.js} +1 -1
  17. package/dist/recorder/index.js +2 -9
  18. package/dist/recorder/index.mjs +2 -9
  19. package/dist/{segment-demux-js-7DukFIT6.mjs → segment-demux-js-DcF7tvrn.mjs} +1 -1
  20. package/dist/{segment-demux-js-C6B53KjT.js → segment-demux-js-Dd5asWZc.js} +1 -1
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B20FCUk4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vEbg16EK.mjs} +3 -3
  22. package/dist/stream-broker/demux-worker-child.js +1 -1
  23. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  24. package/dist/stream-broker/{hostInit-DCTAdB0n.mjs → hostInit-D1dVvyjN.mjs} +3 -3
  25. package/dist/stream-broker/index.js +69 -8
  26. package/dist/stream-broker/index.mjs +69 -8
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/package.json +1 -1
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CWWQf3ci.js");
7
- const require_process_memory = require("../process-memory-Bv9SiLn-.js");
6
+ const require_dist = require("../dist-BjiM0e6J.js");
7
+ const require_process_memory = require("../process-memory-vWz15IrS.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
10
10
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $t as hydrateSchema, Jt as audioChunkBytesPerSample, Lt as errMsg, N as audioAnalysisCapability, Ot as resolvePoolMemoryPolicy, P as audioAnalyzerCapability, Qt as expandAudioChunkToF32le, Wt as BaseAddon, bn as EventCategory, n as AUDIO_BACKEND_CHOICES, ot as mapAudioLabelToMacro, rn as nodePin, s as DEFAULT_AUDIO_ANALYZER_CONFIG, w as PoolMemoryWatchdog, y as HF_BASE_URL } from "../dist-CoZwNgzu.mjs";
3
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BnFnR_nP.mjs";
2
+ import { $t as hydrateSchema, Jt as audioChunkBytesPerSample, Lt as errMsg, N as audioAnalysisCapability, Ot as resolvePoolMemoryPolicy, P as audioAnalyzerCapability, Qt as expandAudioChunkToF32le, Wt as BaseAddon, bn as EventCategory, n as AUDIO_BACKEND_CHOICES, ot as mapAudioLabelToMacro, rn as nodePin, s as DEFAULT_AUDIO_ANALYZER_CONFIG, w as PoolMemoryWatchdog, y as HF_BASE_URL } from "../dist-Dzy3wAmT.mjs";
3
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BMpJ42op.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import { downloadFile } from "@camstack/system/addon-utils";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-CWWQf3ci.js");
1
+ const require_dist = require("./dist-BjiM0e6J.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/detection-pipeline/pipeline/landmark-precision-gate.ts
4
4
  /**
@@ -2114,6 +2114,35 @@ var localFrameRegistry = new LocalFrameRegistry({
2114
2114
  maxBytes: envPositiveInt("CAMSTACK_LOCAL_FRAME_BUDGET_MB", DEFAULT_MAX_BYTES / 1048576) * 1048576
2115
2115
  });
2116
2116
  //#endregion
2117
+ //#region src/detection-pipeline/pipeline/macro-gate.ts
2118
+ function capitalize(s) {
2119
+ if (s.length === 0) return s;
2120
+ return s.charAt(0).toUpperCase() + s.slice(1);
2121
+ }
2122
+ /** Apply the operator's class list and per-macro confidence floor. */
2123
+ function macroGateVerdict(macroClass, score, settings) {
2124
+ if (!settings) return "pass";
2125
+ const enabled = settings["enabledMacroClasses"];
2126
+ if (Array.isArray(enabled) && enabled.length > 0) {
2127
+ if (!enabled.includes(macroClass)) return "class-filter";
2128
+ }
2129
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
2130
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return "below-threshold";
2131
+ return "pass";
2132
+ }
2133
+ /**
2134
+ * The operator's gate, read off the ROOT step.
2135
+ *
2136
+ * The root is the first ENABLED step — the same one `executor.ts` picks
2137
+ * (`enabledSteps[0]`) — because that is the step whose settings the device
2138
+ * UI writes `enabledMacroClasses` and `minConfidence<Macro>` into. `undefined`
2139
+ * when there is no enabled step or it carries no settings, which the gate
2140
+ * itself reads as "pass everything": an absent gate must never mute a camera.
2141
+ */
2142
+ function rootObjectGate(steps) {
2143
+ return (steps?.find((s) => s.enabled !== false))?.settings;
2144
+ }
2145
+ //#endregion
2117
2146
  //#region src/detection-pipeline/default-detection-model.ts
2118
2147
  /** The object-detection step id — the only slot this resolver applies to. */
2119
2148
  var OBJECT_DETECTION_STEP_ID = "object-detection";
@@ -2244,6 +2273,12 @@ Object.defineProperty(exports, "localFrameRegistry", {
2244
2273
  return localFrameRegistry;
2245
2274
  }
2246
2275
  });
2276
+ Object.defineProperty(exports, "macroGateVerdict", {
2277
+ enumerable: true,
2278
+ get: function() {
2279
+ return macroGateVerdict;
2280
+ }
2281
+ });
2247
2282
  Object.defineProperty(exports, "resolveDefaultDetectionModel", {
2248
2283
  enumerable: true,
2249
2284
  get: function() {
@@ -2262,3 +2297,9 @@ Object.defineProperty(exports, "resolveModelForFormat", {
2262
2297
  return resolveModelForFormat;
2263
2298
  }
2264
2299
  });
2300
+ Object.defineProperty(exports, "rootObjectGate", {
2301
+ enumerable: true,
2302
+ get: function() {
2303
+ return rootObjectGate;
2304
+ }
2305
+ });
@@ -1,4 +1,4 @@
1
- import { F as audioKindId, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS, tt as hfModelUrl } from "./dist-CoZwNgzu.mjs";
1
+ import { F as audioKindId, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS, tt as hfModelUrl } from "./dist-Dzy3wAmT.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/detection-pipeline/pipeline/landmark-precision-gate.ts
4
4
  /**
@@ -2114,6 +2114,35 @@ var localFrameRegistry = new LocalFrameRegistry({
2114
2114
  maxBytes: envPositiveInt("CAMSTACK_LOCAL_FRAME_BUDGET_MB", DEFAULT_MAX_BYTES / 1048576) * 1048576
2115
2115
  });
2116
2116
  //#endregion
2117
+ //#region src/detection-pipeline/pipeline/macro-gate.ts
2118
+ function capitalize(s) {
2119
+ if (s.length === 0) return s;
2120
+ return s.charAt(0).toUpperCase() + s.slice(1);
2121
+ }
2122
+ /** Apply the operator's class list and per-macro confidence floor. */
2123
+ function macroGateVerdict(macroClass, score, settings) {
2124
+ if (!settings) return "pass";
2125
+ const enabled = settings["enabledMacroClasses"];
2126
+ if (Array.isArray(enabled) && enabled.length > 0) {
2127
+ if (!enabled.includes(macroClass)) return "class-filter";
2128
+ }
2129
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
2130
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return "below-threshold";
2131
+ return "pass";
2132
+ }
2133
+ /**
2134
+ * The operator's gate, read off the ROOT step.
2135
+ *
2136
+ * The root is the first ENABLED step — the same one `executor.ts` picks
2137
+ * (`enabledSteps[0]`) — because that is the step whose settings the device
2138
+ * UI writes `enabledMacroClasses` and `minConfidence<Macro>` into. `undefined`
2139
+ * when there is no enabled step or it carries no settings, which the gate
2140
+ * itself reads as "pass everything": an absent gate must never mute a camera.
2141
+ */
2142
+ function rootObjectGate(steps) {
2143
+ return (steps?.find((s) => s.enabled !== false))?.settings;
2144
+ }
2145
+ //#endregion
2117
2146
  //#region src/detection-pipeline/default-detection-model.ts
2118
2147
  /** The object-detection step id — the only slot this resolver applies to. */
2119
2148
  var OBJECT_DETECTION_STEP_ID = "object-detection";
@@ -2196,4 +2225,4 @@ function resolveDefaultDetectionModel(backend, device, format, getStepDef = getS
2196
2225
  return getDefaultModelForFormatFromDef(def, format);
2197
2226
  }
2198
2227
  //#endregion
2199
- export { ALL_PIPELINE_STEPS as a, getStep as c, landmarkPrecisionVerdict as d, resolveFrameViewGeometry as i, getStepDefinition as l, resolveDefaultDetectionModel as n, ALL_STEPS as o, localFrameRegistry as r, getDefaultModelForFormat as s, OBJECT_DETECTION_STEP_ID as t, resolveModelForFormat as u };
2228
+ export { localFrameRegistry as a, ALL_STEPS as c, getStepDefinition as d, resolveModelForFormat as f, rootObjectGate as i, getDefaultModelForFormat as l, resolveDefaultDetectionModel as n, resolveFrameViewGeometry as o, landmarkPrecisionVerdict as p, macroGateVerdict as r, ALL_PIPELINE_STEPS as s, OBJECT_DETECTION_STEP_ID as t, getStep as u };
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CWWQf3ci.js");
7
- const require_node = require("../node-DLZWItaN.js");
8
- const require_default_detection_model = require("../default-detection-model-DLFtoi4W.js");
6
+ const require_dist = require("../dist-BjiM0e6J.js");
7
+ const require_node = require("../node-P2-C_t7H.js");
8
+ const require_default_detection_model = require("../default-detection-model-ClJ9y701.js");
9
9
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
10
- const require_process_memory = require("../process-memory-Bv9SiLn-.js");
10
+ const require_process_memory = require("../process-memory-vWz15IrS.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
13
  let node_fs = require("node:fs");
@@ -5891,20 +5891,9 @@ var PipelineExecutor = class {
5891
5891
  * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
5892
5892
  */
5893
5893
  macroFilterVerdict(macroClass, score, settings) {
5894
- if (!settings) return "pass";
5895
- const enabled = settings["enabledMacroClasses"];
5896
- if (Array.isArray(enabled) && enabled.length > 0) {
5897
- if (!enabled.includes(macroClass)) return "class-filter";
5898
- }
5899
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
5900
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return "below-threshold";
5901
- return "pass";
5894
+ return require_default_detection_model.macroGateVerdict(macroClass, score, settings);
5902
5895
  }
5903
5896
  };
5904
- function capitalize(s) {
5905
- if (s.length === 0) return s;
5906
- return s.charAt(0).toUpperCase() + s.slice(1);
5907
- }
5908
5897
  //#endregion
5909
5898
  //#region src/detection-pipeline/pipeline/native-crop-compose.ts
5910
5899
  /** Clamp `v` into `[lo, hi]`. */
@@ -1,9 +1,9 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $t as hydrateSchema, At as runtimeDevices$1, Ft as supportedRuntimes$1, G as detectionPipelineCapability, J as enumerateInferenceDevices, Lt as errMsg, Ot as resolvePoolMemoryPolicy, Tt as resolveClusterStepModelId, V as defaultDeviceFor$1, Wt as BaseAddon, X as evaluateZoneRules, Yt as createEvent, bn as EventCategory, c as DEFAULT_CLUSTER_STEP_MODELS, gt as pickClusterStepSettings, hn as object, ht as pickClusterStepModels, in as parseJsonUnknown, it as loadContributionCapability, j as YAMNET_TO_MACRO, l as DEFAULT_CLUSTER_STEP_SETTINGS, ln as array, m as DEVICE_BACKEND_TO_FORMAT, nt as inferModelProvider, pt as overlayClusterStepSettings, rn as nodePin, sn as sleep, t as APPLE_SA_TO_MACRO, vn as string, w as PoolMemoryWatchdog, yn as union, yt as pipelineExecutorCapability } from "../dist-CoZwNgzu.mjs";
3
- import { o as readProcessCost } from "../node-Uh59Eo57.mjs";
4
- import { a as ALL_PIPELINE_STEPS, c as getStep, d as landmarkPrecisionVerdict, l as getStepDefinition, n as resolveDefaultDetectionModel, o as ALL_STEPS, r as localFrameRegistry, s as getDefaultModelForFormat, u as resolveModelForFormat } from "../default-detection-model-UXQCoRj_.mjs";
2
+ import { $t as hydrateSchema, At as runtimeDevices$1, Ft as supportedRuntimes$1, G as detectionPipelineCapability, J as enumerateInferenceDevices, Lt as errMsg, Ot as resolvePoolMemoryPolicy, Tt as resolveClusterStepModelId, V as defaultDeviceFor$1, Wt as BaseAddon, X as evaluateZoneRules, Yt as createEvent, bn as EventCategory, c as DEFAULT_CLUSTER_STEP_MODELS, gt as pickClusterStepSettings, hn as object, ht as pickClusterStepModels, in as parseJsonUnknown, it as loadContributionCapability, j as YAMNET_TO_MACRO, l as DEFAULT_CLUSTER_STEP_SETTINGS, ln as array, m as DEVICE_BACKEND_TO_FORMAT, nt as inferModelProvider, pt as overlayClusterStepSettings, rn as nodePin, sn as sleep, t as APPLE_SA_TO_MACRO, vn as string, w as PoolMemoryWatchdog, yn as union, yt as pipelineExecutorCapability } from "../dist-Dzy3wAmT.mjs";
3
+ import { o as readProcessCost } from "../node-D9DBz5HF.mjs";
4
+ import { a as localFrameRegistry, c as ALL_STEPS, d as getStepDefinition, f as resolveModelForFormat, l as getDefaultModelForFormat, n as resolveDefaultDetectionModel, p as landmarkPrecisionVerdict, r as macroGateVerdict, s as ALL_PIPELINE_STEPS, u as getStep } from "../default-detection-model-pr2NMNpc.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
6
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BnFnR_nP.mjs";
6
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BMpJ42op.mjs";
7
7
  import * as os from "node:os";
8
8
  import * as fs from "node:fs";
9
9
  import * as path$1 from "node:path";
@@ -5884,20 +5884,9 @@ var PipelineExecutor = class {
5884
5884
  * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
5885
5885
  */
5886
5886
  macroFilterVerdict(macroClass, score, settings) {
5887
- if (!settings) return "pass";
5888
- const enabled = settings["enabledMacroClasses"];
5889
- if (Array.isArray(enabled) && enabled.length > 0) {
5890
- if (!enabled.includes(macroClass)) return "class-filter";
5891
- }
5892
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
5893
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return "below-threshold";
5894
- return "pass";
5887
+ return macroGateVerdict(macroClass, score, settings);
5895
5888
  }
5896
5889
  };
5897
- function capitalize(s) {
5898
- if (s.length === 0) return s;
5899
- return s.charAt(0).toUpperCase() + s.slice(1);
5900
- }
5901
5890
  //#endregion
5902
5891
  //#region src/detection-pipeline/pipeline/native-crop-compose.ts
5903
5892
  /** Clamp `v` into `[lo, hi]`. */
@@ -21905,13 +21905,26 @@ var TrackAudioLabelSchema = object({
21905
21905
  * - `audio` — an audio event on the camera itself that was anomalous for
21906
21906
  * THAT camera, loud, and heard while nothing visual was happening (D62).
21907
21907
  *
21908
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
21909
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
21910
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
21911
+ * detector.
21912
+ *
21913
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
21914
+ * a persisted onboard track failed `safeParse` on read and came back with NO
21915
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
21916
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
21917
+ * could not name.
21918
+ *
21908
21919
  * The spatial subsystems (tracker association, occupancy count, re-id /
21909
21920
  * embedding, resurrection) MUST skip every synthetic source. Test for that
21910
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
21911
- * check silently readmits every source added after it was written.
21921
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
21922
+ * `!== 'sensor'` check silently readmits every source added after it was
21923
+ * written.
21912
21924
  */
21913
21925
  var TrackSourceSchema = _enum([
21914
21926
  "pipeline",
21927
+ "onboard",
21915
21928
  "sensor",
21916
21929
  "audio"
21917
21930
  ]);
@@ -22499,6 +22512,7 @@ var MediaFileKindEnum = _enum([
22499
22512
  "keyFrame",
22500
22513
  "keyFrameSmall",
22501
22514
  "thumbnailSmall",
22515
+ "confirmationCrop",
22502
22516
  "sceneFrame"
22503
22517
  ]);
22504
22518
  /**
@@ -23993,13 +24007,39 @@ var MotionSourceEnum = _enum([
23993
24007
  */
23994
24008
  var MotionSourcesSchema = array(MotionSourceEnum);
23995
24009
  /**
23996
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
23997
- * post-analysis already tags every detection with (`DetectionSource`) rather
23998
- * than a second spelling of the same two ideas — the value an operator picks
23999
- * here is the value that comes back on the track, the overlay and the debug
24000
- * row.
24010
+ * Which root detector a camera runs — EXACTLY ONE.
24011
+ *
24012
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
24013
+ * with (`DetectionSource`) rather than a second spelling of the same two
24014
+ * ideas: the value an operator picks here is the value that comes back on the
24015
+ * track, the overlay and the debug row.
24016
+ *
24017
+ * ## Why one, and why it is still an array
24018
+ *
24019
+ * Two roots on one camera is a capability nobody asked for and the operator has
24020
+ * since ruled out. It was never free: both planes feed the SAME per-device
24021
+ * stationary registry, both can promote the same parked object, and the two
24022
+ * detectors disagree about the same box by construction — which is the
24023
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
24024
+ *
24025
+ * The ARRAY shape survives because the stored settings and the attach payload
24026
+ * already speak it on every camera of every fleet, and a cap input that
24027
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
24028
+ * in the one direction that costs an operator their cameras. So the wire stays
24029
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
24030
+ * element rather than refused, and `max(1)` is what every consumer can then
24031
+ * rely on.
24032
+ *
24033
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
24034
+ * a camera an operator left with no root detector — and `planDetectionSources`
24035
+ * has always said so: inventing a root for them is how a default acts without
24036
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
24037
+ * suite caught it.
24038
+ *
24039
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
24040
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
24001
24041
  */
24002
- var DetectionSourcesSchema = array(DetectionSourceSchema);
24042
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
24003
24043
  /**
24004
24044
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
24005
24045
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -21905,13 +21905,26 @@ var TrackAudioLabelSchema = object({
21905
21905
  * - `audio` — an audio event on the camera itself that was anomalous for
21906
21906
  * THAT camera, loud, and heard while nothing visual was happening (D62).
21907
21907
  *
21908
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
21909
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
21910
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
21911
+ * detector.
21912
+ *
21913
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
21914
+ * a persisted onboard track failed `safeParse` on read and came back with NO
21915
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
21916
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
21917
+ * could not name.
21918
+ *
21908
21919
  * The spatial subsystems (tracker association, occupancy count, re-id /
21909
21920
  * embedding, resurrection) MUST skip every synthetic source. Test for that
21910
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
21911
- * check silently readmits every source added after it was written.
21921
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
21922
+ * `!== 'sensor'` check silently readmits every source added after it was
21923
+ * written.
21912
21924
  */
21913
21925
  var TrackSourceSchema = _enum([
21914
21926
  "pipeline",
21927
+ "onboard",
21915
21928
  "sensor",
21916
21929
  "audio"
21917
21930
  ]);
@@ -22499,6 +22512,7 @@ var MediaFileKindEnum = _enum([
22499
22512
  "keyFrame",
22500
22513
  "keyFrameSmall",
22501
22514
  "thumbnailSmall",
22515
+ "confirmationCrop",
22502
22516
  "sceneFrame"
22503
22517
  ]);
22504
22518
  /**
@@ -23993,13 +24007,39 @@ var MotionSourceEnum = _enum([
23993
24007
  */
23994
24008
  var MotionSourcesSchema = array(MotionSourceEnum);
23995
24009
  /**
23996
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
23997
- * post-analysis already tags every detection with (`DetectionSource`) rather
23998
- * than a second spelling of the same two ideas — the value an operator picks
23999
- * here is the value that comes back on the track, the overlay and the debug
24000
- * row.
24010
+ * Which root detector a camera runs — EXACTLY ONE.
24011
+ *
24012
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
24013
+ * with (`DetectionSource`) rather than a second spelling of the same two
24014
+ * ideas: the value an operator picks here is the value that comes back on the
24015
+ * track, the overlay and the debug row.
24016
+ *
24017
+ * ## Why one, and why it is still an array
24018
+ *
24019
+ * Two roots on one camera is a capability nobody asked for and the operator has
24020
+ * since ruled out. It was never free: both planes feed the SAME per-device
24021
+ * stationary registry, both can promote the same parked object, and the two
24022
+ * detectors disagree about the same box by construction — which is the
24023
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
24024
+ *
24025
+ * The ARRAY shape survives because the stored settings and the attach payload
24026
+ * already speak it on every camera of every fleet, and a cap input that
24027
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
24028
+ * in the one direction that costs an operator their cameras. So the wire stays
24029
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
24030
+ * element rather than refused, and `max(1)` is what every consumer can then
24031
+ * rely on.
24032
+ *
24033
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
24034
+ * a camera an operator left with no root detector — and `planDetectionSources`
24035
+ * has always said so: inventing a root for them is how a default acts without
24036
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
24037
+ * suite caught it.
24038
+ *
24039
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
24040
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
24001
24041
  */
24002
- var DetectionSourcesSchema = array(DetectionSourceSchema);
24042
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
24003
24043
  /**
24004
24044
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
24005
24045
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CWWQf3ci.js");
5
+ const require_dist = require("../dist-BjiM0e6J.js");
6
6
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { $t as hydrateSchema, Wt as BaseAddon, X as evaluateZoneRules, ft as motionDetectionCapability, qt as DeviceType } from "../dist-CoZwNgzu.mjs";
1
+ import { $t as hydrateSchema, Wt as BaseAddon, X as evaluateZoneRules, ft as motionDetectionCapability, qt as DeviceType } from "../dist-Dzy3wAmT.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -1,4 +1,4 @@
1
- import { Bt as buildFfmpegArgs, Ht as isSoftwareDecode, Lt as errMsg, zt as Fmp4BoxSplitter } from "./dist-CoZwNgzu.mjs";
1
+ import { Bt as buildFfmpegArgs, Ht as isSoftwareDecode, Lt as errMsg, zt as Fmp4BoxSplitter } from "./dist-Dzy3wAmT.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-CWWQf3ci.js");
2
+ const require_dist = require("./dist-BjiM0e6J.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs, 1);
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CWWQf3ci.js");
7
- const require_node = require("../node-DLZWItaN.js");
8
- const require_default_detection_model = require("../default-detection-model-DLFtoi4W.js");
6
+ const require_dist = require("../dist-BjiM0e6J.js");
7
+ const require_node = require("../node-P2-C_t7H.js");
8
+ const require_default_detection_model = require("../default-detection-model-ClJ9y701.js");
9
9
  const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
10
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
11
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
@@ -2129,7 +2129,7 @@ function clamp(value, max) {
2129
2129
  * the boxes travel with a real frame and a real handle, so they are scaled to
2130
2130
  * that frame — the same space every other detection on this plane lives in.
2131
2131
  */
2132
- function onboardRootDetections(slot, frame) {
2132
+ function onboardRootDetections(slot, frame, gate) {
2133
2133
  if (slot === null || slot.detections.length === 0) return { kind: "none" };
2134
2134
  const ageMs = Math.abs(frame.timestamp - slot.at);
2135
2135
  if (ageMs > 1e3) return {
@@ -2141,26 +2141,33 @@ function onboardRootDetections(slot, frame) {
2141
2141
  kind: "no-geometry",
2142
2142
  classes: slot.detections.map((d) => d.class)
2143
2143
  };
2144
+ const detections = boxed.map((d, i) => {
2145
+ const [nx, ny, nw, nh] = d.bbox;
2146
+ const x = clamp(nx * frame.width, frame.width);
2147
+ const y = clamp(ny * frame.height, frame.height);
2148
+ return {
2149
+ id: `onboard-${String(slot.at)}-${String(i)}`,
2150
+ kind: "first-level",
2151
+ macroClass: d.class,
2152
+ score: d.confidence ?? 1,
2153
+ labels: [],
2154
+ bbox: {
2155
+ x,
2156
+ y,
2157
+ width: clamp(nw * frame.width, frame.width - x),
2158
+ height: clamp(nh * frame.height, frame.height - y)
2159
+ }
2160
+ };
2161
+ });
2162
+ const admitted = detections.filter((d) => require_default_detection_model.macroGateVerdict(d.macroClass, d.score, gate) === "pass");
2163
+ if (admitted.length === 0) return {
2164
+ kind: "gated",
2165
+ refused: detections.length
2166
+ };
2144
2167
  return {
2145
2168
  kind: "paired",
2146
- detections: boxed.map((d, i) => {
2147
- const [nx, ny, nw, nh] = d.bbox;
2148
- const x = clamp(nx * frame.width, frame.width);
2149
- const y = clamp(ny * frame.height, frame.height);
2150
- return {
2151
- id: `onboard-${String(slot.at)}-${String(i)}`,
2152
- kind: "first-level",
2153
- macroClass: d.class,
2154
- score: d.confidence ?? 1,
2155
- labels: [],
2156
- bbox: {
2157
- x,
2158
- y,
2159
- width: clamp(nw * frame.width, frame.width - x),
2160
- height: clamp(nh * frame.height, frame.height - y)
2161
- }
2162
- };
2163
- })
2169
+ detections: admitted,
2170
+ ...admitted.length < detections.length ? { refused: detections.length - admitted.length } : {}
2164
2171
  };
2165
2172
  }
2166
2173
  //#endregion
@@ -5645,7 +5652,7 @@ function ownDetectionEntry(entry) {
5645
5652
  return {
5646
5653
  frame: ownFrame(entry.frame),
5647
5654
  handle: entry.handle,
5648
- ...entry.occasion !== void 0 ? { occasion: entry.occasion } : {}
5655
+ occasion: entry.occasion
5649
5656
  };
5650
5657
  }
5651
5658
  /**
@@ -5964,7 +5971,8 @@ var PipelineRunner = class {
5964
5971
  if (!state || state.phase !== "active") return false;
5965
5972
  return state.preRollQueue.enqueue({
5966
5973
  frame,
5967
- handle
5974
+ handle,
5975
+ occasion: "session"
5968
5976
  });
5969
5977
  }
5970
5978
  /**
@@ -6001,7 +6009,7 @@ var PipelineRunner = class {
6001
6009
  state.detectionQueue.enqueue({
6002
6010
  frame,
6003
6011
  handle,
6004
- occasion: "burst"
6012
+ occasion: "session"
6005
6013
  });
6006
6014
  return true;
6007
6015
  }
@@ -6313,7 +6321,7 @@ var PipelineRunner = class {
6313
6321
  state.processedCount++;
6314
6322
  if (result) {
6315
6323
  if (result.shed === true) state.shedFrames++;
6316
- await this.notifyCallbacks(deviceId, frame, plan.runsOnboard ? this.mergeOnboardDetections(state, result, frame) : result, streamType, handle, "pipeline", entry.occasion ?? "session");
6324
+ await this.notifyCallbacks(deviceId, frame, plan.runsOnboard ? this.mergeOnboardDetections(state, result, frame) : result, streamType, handle, "pipeline", entry.occasion);
6317
6325
  frameReleaseReason = "success";
6318
6326
  const emittedAt = Date.now();
6319
6327
  const capturedAt = frame.capturedAt;
@@ -6352,7 +6360,7 @@ var PipelineRunner = class {
6352
6360
  timestamp: Date.now(),
6353
6361
  width: frame.width,
6354
6362
  height: frame.height
6355
- });
6363
+ }, state.registration.objectGate);
6356
6364
  if (pairing.kind !== "paired") return result;
6357
6365
  state.onboardSlot = null;
6358
6366
  return {
@@ -6389,8 +6397,9 @@ var PipelineRunner = class {
6389
6397
  timestamp: Date.now(),
6390
6398
  width: frame.width,
6391
6399
  height: frame.height
6392
- });
6400
+ }, state.registration.objectGate);
6393
6401
  if (pairing.kind !== "paired") {
6402
+ if (pairing.kind === "gated") state.onboardSlot = null;
6394
6403
  this.noteOnboardRootIdle(deviceId, pairing.kind);
6395
6404
  return;
6396
6405
  }
@@ -6406,7 +6415,8 @@ var PipelineRunner = class {
6406
6415
  detections: pairing.detections
6407
6416
  };
6408
6417
  state.lastResultDetections = pairing.detections.length;
6409
- await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard", entry.occasion ?? "session");
6418
+ if (handle) this.config.markFrameAskable?.(handle);
6419
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard", entry.occasion);
6410
6420
  frameReleaseReason = "success";
6411
6421
  } catch {
6412
6422
  frameReleaseReason = "error";
@@ -6554,7 +6564,7 @@ var PipelineRunner = class {
6554
6564
  if (!deviceId) continue;
6555
6565
  const state = this.cameras.get(deviceId);
6556
6566
  if (!state) continue;
6557
- if (state.phase === "active" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
6567
+ if ((state.phase === "active" || state.phase === "watching") && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
6558
6568
  this.defaultRoundRobinIndex = (idx + 1) % this.defaultRoundRobinKeys.length;
6559
6569
  const entry = state.detectionQueue.dequeue();
6560
6570
  if (!entry) continue;
@@ -7587,6 +7597,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7587
7597
  this.frameCutters.drop(handle);
7588
7598
  },
7589
7599
  hydrateRetainedFrame: (handle) => this.retainedFrames?.ensureContent(handle) ?? Promise.resolve(null),
7600
+ markFrameAskable: (handle) => {
7601
+ const found = this.nativeCropRegistry.lookup(handle);
7602
+ if (found.kind === "live") found.request.markInferred?.();
7603
+ },
7590
7604
  onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
7591
7605
  logger: this.ctx.logger,
7592
7606
  onOccupancyRecheck: (deviceId, frames) => {
@@ -8455,6 +8469,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8455
8469
  runner.registerCamera(config.deviceId, {
8456
8470
  detectionMode: config.detectionMode,
8457
8471
  ...config.detectionSources !== void 0 ? { detectionSources: config.detectionSources } : {},
8472
+ ...require_default_detection_model.rootObjectGate(config.steps) !== void 0 ? { objectGate: require_default_detection_model.rootObjectGate(config.steps) } : {},
8458
8473
  fps: config.detectionFps,
8459
8474
  motionCooldownMs: config.motionCooldownMs,
8460
8475
  occupancyRecheckEnabled: config.occupancyRecheckEnabled,
@@ -1,6 +1,6 @@
1
- import { $ as failureContributionCapability, Lt as errMsg, S as NativeLeaseSettingsSchema, U as deriveDetailCropRect, Wt as BaseAddon, Xt as customAction, Yt as createEvent, Zt as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, b as MOTION_CLOSE_AFTER_MS, bn as EventCategory, bt as pipelineRunnerCapability, cn as _enum, fn as lazy, hn as object, it as loadContributionCapability, ln as array, m as DEVICE_BACKEND_TO_FORMAT, mn as number, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as literal, rn as nodePin, u as DEFAULT_DETAIL_CROP_CONVENTION, un as boolean, vn as string, vt as pickNativeLeaseOverride, x as NativeLeaseAdmissionSchema } from "../dist-CoZwNgzu.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Uh59Eo57.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-UXQCoRj_.mjs";
1
+ import { $ as failureContributionCapability, Lt as errMsg, S as NativeLeaseSettingsSchema, U as deriveDetailCropRect, Wt as BaseAddon, Xt as customAction, Yt as createEvent, Zt as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, b as MOTION_CLOSE_AFTER_MS, bn as EventCategory, bt as pipelineRunnerCapability, cn as _enum, fn as lazy, hn as object, it as loadContributionCapability, ln as array, m as DEVICE_BACKEND_TO_FORMAT, mn as number, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as literal, rn as nodePin, u as DEFAULT_DETAIL_CROP_CONVENTION, un as boolean, vn as string, vt as pickNativeLeaseOverride, x as NativeLeaseAdmissionSchema } from "../dist-Dzy3wAmT.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-D9DBz5HF.mjs";
3
+ import { a as localFrameRegistry, d as getStepDefinition, i as rootObjectGate, o as resolveFrameViewGeometry, r as macroGateVerdict } from "../default-detection-model-pr2NMNpc.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-C_W-P_g-.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
@@ -2122,7 +2122,7 @@ function clamp(value, max) {
2122
2122
  * the boxes travel with a real frame and a real handle, so they are scaled to
2123
2123
  * that frame — the same space every other detection on this plane lives in.
2124
2124
  */
2125
- function onboardRootDetections(slot, frame) {
2125
+ function onboardRootDetections(slot, frame, gate) {
2126
2126
  if (slot === null || slot.detections.length === 0) return { kind: "none" };
2127
2127
  const ageMs = Math.abs(frame.timestamp - slot.at);
2128
2128
  if (ageMs > 1e3) return {
@@ -2134,26 +2134,33 @@ function onboardRootDetections(slot, frame) {
2134
2134
  kind: "no-geometry",
2135
2135
  classes: slot.detections.map((d) => d.class)
2136
2136
  };
2137
+ const detections = boxed.map((d, i) => {
2138
+ const [nx, ny, nw, nh] = d.bbox;
2139
+ const x = clamp(nx * frame.width, frame.width);
2140
+ const y = clamp(ny * frame.height, frame.height);
2141
+ return {
2142
+ id: `onboard-${String(slot.at)}-${String(i)}`,
2143
+ kind: "first-level",
2144
+ macroClass: d.class,
2145
+ score: d.confidence ?? 1,
2146
+ labels: [],
2147
+ bbox: {
2148
+ x,
2149
+ y,
2150
+ width: clamp(nw * frame.width, frame.width - x),
2151
+ height: clamp(nh * frame.height, frame.height - y)
2152
+ }
2153
+ };
2154
+ });
2155
+ const admitted = detections.filter((d) => macroGateVerdict(d.macroClass, d.score, gate) === "pass");
2156
+ if (admitted.length === 0) return {
2157
+ kind: "gated",
2158
+ refused: detections.length
2159
+ };
2137
2160
  return {
2138
2161
  kind: "paired",
2139
- detections: boxed.map((d, i) => {
2140
- const [nx, ny, nw, nh] = d.bbox;
2141
- const x = clamp(nx * frame.width, frame.width);
2142
- const y = clamp(ny * frame.height, frame.height);
2143
- return {
2144
- id: `onboard-${String(slot.at)}-${String(i)}`,
2145
- kind: "first-level",
2146
- macroClass: d.class,
2147
- score: d.confidence ?? 1,
2148
- labels: [],
2149
- bbox: {
2150
- x,
2151
- y,
2152
- width: clamp(nw * frame.width, frame.width - x),
2153
- height: clamp(nh * frame.height, frame.height - y)
2154
- }
2155
- };
2156
- })
2162
+ detections: admitted,
2163
+ ...admitted.length < detections.length ? { refused: detections.length - admitted.length } : {}
2157
2164
  };
2158
2165
  }
2159
2166
  //#endregion
@@ -5637,7 +5644,7 @@ function ownDetectionEntry(entry) {
5637
5644
  return {
5638
5645
  frame: ownFrame(entry.frame),
5639
5646
  handle: entry.handle,
5640
- ...entry.occasion !== void 0 ? { occasion: entry.occasion } : {}
5647
+ occasion: entry.occasion
5641
5648
  };
5642
5649
  }
5643
5650
  /**
@@ -5956,7 +5963,8 @@ var PipelineRunner = class {
5956
5963
  if (!state || state.phase !== "active") return false;
5957
5964
  return state.preRollQueue.enqueue({
5958
5965
  frame,
5959
- handle
5966
+ handle,
5967
+ occasion: "session"
5960
5968
  });
5961
5969
  }
5962
5970
  /**
@@ -5993,7 +6001,7 @@ var PipelineRunner = class {
5993
6001
  state.detectionQueue.enqueue({
5994
6002
  frame,
5995
6003
  handle,
5996
- occasion: "burst"
6004
+ occasion: "session"
5997
6005
  });
5998
6006
  return true;
5999
6007
  }
@@ -6305,7 +6313,7 @@ var PipelineRunner = class {
6305
6313
  state.processedCount++;
6306
6314
  if (result) {
6307
6315
  if (result.shed === true) state.shedFrames++;
6308
- await this.notifyCallbacks(deviceId, frame, plan.runsOnboard ? this.mergeOnboardDetections(state, result, frame) : result, streamType, handle, "pipeline", entry.occasion ?? "session");
6316
+ await this.notifyCallbacks(deviceId, frame, plan.runsOnboard ? this.mergeOnboardDetections(state, result, frame) : result, streamType, handle, "pipeline", entry.occasion);
6309
6317
  frameReleaseReason = "success";
6310
6318
  const emittedAt = Date.now();
6311
6319
  const capturedAt = frame.capturedAt;
@@ -6344,7 +6352,7 @@ var PipelineRunner = class {
6344
6352
  timestamp: Date.now(),
6345
6353
  width: frame.width,
6346
6354
  height: frame.height
6347
- });
6355
+ }, state.registration.objectGate);
6348
6356
  if (pairing.kind !== "paired") return result;
6349
6357
  state.onboardSlot = null;
6350
6358
  return {
@@ -6381,8 +6389,9 @@ var PipelineRunner = class {
6381
6389
  timestamp: Date.now(),
6382
6390
  width: frame.width,
6383
6391
  height: frame.height
6384
- });
6392
+ }, state.registration.objectGate);
6385
6393
  if (pairing.kind !== "paired") {
6394
+ if (pairing.kind === "gated") state.onboardSlot = null;
6386
6395
  this.noteOnboardRootIdle(deviceId, pairing.kind);
6387
6396
  return;
6388
6397
  }
@@ -6398,7 +6407,8 @@ var PipelineRunner = class {
6398
6407
  detections: pairing.detections
6399
6408
  };
6400
6409
  state.lastResultDetections = pairing.detections.length;
6401
- await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard", entry.occasion ?? "session");
6410
+ if (handle) this.config.markFrameAskable?.(handle);
6411
+ await this.notifyCallbacks(deviceId, frame, result, streamType, handle, "onboard", entry.occasion);
6402
6412
  frameReleaseReason = "success";
6403
6413
  } catch {
6404
6414
  frameReleaseReason = "error";
@@ -6546,7 +6556,7 @@ var PipelineRunner = class {
6546
6556
  if (!deviceId) continue;
6547
6557
  const state = this.cameras.get(deviceId);
6548
6558
  if (!state) continue;
6549
- if (state.phase === "active" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
6559
+ if ((state.phase === "active" || state.phase === "watching") && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
6550
6560
  this.defaultRoundRobinIndex = (idx + 1) % this.defaultRoundRobinKeys.length;
6551
6561
  const entry = state.detectionQueue.dequeue();
6552
6562
  if (!entry) continue;
@@ -7579,6 +7589,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7579
7589
  this.frameCutters.drop(handle);
7580
7590
  },
7581
7591
  hydrateRetainedFrame: (handle) => this.retainedFrames?.ensureContent(handle) ?? Promise.resolve(null),
7592
+ markFrameAskable: (handle) => {
7593
+ const found = this.nativeCropRegistry.lookup(handle);
7594
+ if (found.kind === "live") found.request.markInferred?.();
7595
+ },
7582
7596
  onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
7583
7597
  logger: this.ctx.logger,
7584
7598
  onOccupancyRecheck: (deviceId, frames) => {
@@ -8447,6 +8461,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8447
8461
  runner.registerCamera(config.deviceId, {
8448
8462
  detectionMode: config.detectionMode,
8449
8463
  ...config.detectionSources !== void 0 ? { detectionSources: config.detectionSources } : {},
8464
+ ...rootObjectGate(config.steps) !== void 0 ? { objectGate: rootObjectGate(config.steps) } : {},
8450
8465
  fps: config.detectionFps,
8451
8466
  motionCooldownMs: config.motionCooldownMs,
8452
8467
  occupancyRecheckEnabled: config.occupancyRecheckEnabled,
@@ -1,4 +1,4 @@
1
- import { mt as parseProcStatus } from "./dist-CoZwNgzu.mjs";
1
+ import { mt as parseProcStatus } from "./dist-Dzy3wAmT.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-CWWQf3ci.js");
2
+ const require_dist = require("./dist-BjiM0e6J.js");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CWWQf3ci.js");
7
- const require_node = require("../node-DLZWItaN.js");
6
+ const require_dist = require("../dist-BjiM0e6J.js");
7
+ const require_node = require("../node-P2-C_t7H.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-C4adO0P8.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
@@ -555,8 +555,6 @@ var MfraTableStore = class {
555
555
  */
556
556
  /** Il `kind` dell'unico ramo del topic che può aprire una finestra. */
557
557
  var OBJECT_EVENT_KIND = "object";
558
- /** La sorgente implicita delle righe legacy senza `source` (`DetectionSourceSchema`). */
559
- var DEFAULT_DETECTION_SOURCE = "pipeline";
560
558
  /** Type guard, non cast: `event.data` è `unknown` finché non passa di qui. */
561
559
  function isDetectionEventEnvelope(value) {
562
560
  if (typeof value !== "object" || value === null) return false;
@@ -589,11 +587,6 @@ function qualifyObjectTrigger(data) {
589
587
  reason: "malformed-object",
590
588
  deviceId
591
589
  };
592
- if ((data.source ?? DEFAULT_DETECTION_SOURCE) !== DEFAULT_DETECTION_SOURCE) return {
593
- qualified: false,
594
- reason: "not-pipeline",
595
- deviceId
596
- };
597
590
  const className = data.className.trim().toLowerCase();
598
591
  if (!require_dist.isFirstLevelMacroClass(className)) return {
599
592
  qualified: false,
@@ -1,5 +1,5 @@
1
- import { $t as hydrateSchema, C as OpsLogEntrySchema, Ct as recordingExportCapability, D as RECORDING_EXPORT_MAX_READ_BYTES, Dt as resolveLocationMode, H as deriveBatteryPresence, I as batteryCapability, Kt as DeviceFeature, Lt as errMsg, Mt as storageOccupancyCapability, O as RecordingConfigSchema, Pt as stripRetiredBandPreBufferSec, Q as evictionPolicyOfLocation, St as recordingCapability, W as deriveRecordingMode, Wt as BaseAddon, Xt as customAction, Y as evaluateSensorEdge, Z as evictionPolicyForMode, Zt as defineCustomActions, _n as record, bn as EventCategory, cn as _enum, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dt as modeMayWrite, et as gbToBytes, g as ExportRecordSchema, gn as preprocess, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, jt as storageEvictableCapability, k as RecordingSignalStatusSchema, kt as resolveRecordingProfiles, ln as array, lt as mayWriteToLocation, mn as number, on as selectAssignedProfileSlots, qt as DeviceType, rn as nodePin, rt as isFirstLevelMacroClass, sn as sleep$1, un as boolean, ut as migrateLegacyDeviceSignalConfig, vn as string, wt as recordingSignalCapability } from "../dist-CoZwNgzu.mjs";
2
- import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Uh59Eo57.mjs";
1
+ import { $t as hydrateSchema, C as OpsLogEntrySchema, Ct as recordingExportCapability, D as RECORDING_EXPORT_MAX_READ_BYTES, Dt as resolveLocationMode, H as deriveBatteryPresence, I as batteryCapability, Kt as DeviceFeature, Lt as errMsg, Mt as storageOccupancyCapability, O as RecordingConfigSchema, Pt as stripRetiredBandPreBufferSec, Q as evictionPolicyOfLocation, St as recordingCapability, W as deriveRecordingMode, Wt as BaseAddon, Xt as customAction, Y as evaluateSensorEdge, Z as evictionPolicyForMode, Zt as defineCustomActions, _n as record, bn as EventCategory, cn as _enum, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dt as modeMayWrite, et as gbToBytes, g as ExportRecordSchema, gn as preprocess, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, jt as storageEvictableCapability, k as RecordingSignalStatusSchema, kt as resolveRecordingProfiles, ln as array, lt as mayWriteToLocation, mn as number, on as selectAssignedProfileSlots, qt as DeviceType, rn as nodePin, rt as isFirstLevelMacroClass, sn as sleep$1, un as boolean, ut as migrateLegacyDeviceSignalConfig, vn as string, wt as recordingSignalCapability } from "../dist-Dzy3wAmT.mjs";
2
+ import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-D9DBz5HF.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, r as RECORDING_PRE_ROLL_REQUEST_MS, s as withRecordingIntent } from "../restream-intent-MeJhGCK9.mjs";
5
5
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
@@ -549,8 +549,6 @@ var MfraTableStore = class {
549
549
  */
550
550
  /** Il `kind` dell'unico ramo del topic che può aprire una finestra. */
551
551
  var OBJECT_EVENT_KIND = "object";
552
- /** La sorgente implicita delle righe legacy senza `source` (`DetectionSourceSchema`). */
553
- var DEFAULT_DETECTION_SOURCE = "pipeline";
554
552
  /** Type guard, non cast: `event.data` è `unknown` finché non passa di qui. */
555
553
  function isDetectionEventEnvelope(value) {
556
554
  if (typeof value !== "object" || value === null) return false;
@@ -583,11 +581,6 @@ function qualifyObjectTrigger(data) {
583
581
  reason: "malformed-object",
584
582
  deviceId
585
583
  };
586
- if ((data.source ?? DEFAULT_DETECTION_SOURCE) !== DEFAULT_DETECTION_SOURCE) return {
587
- qualified: false,
588
- reason: "not-pipeline",
589
- deviceId
590
- };
591
584
  const className = data.className.trim().toLowerCase();
592
585
  if (!isFirstLevelMacroClass(className)) return {
593
586
  qualified: false,
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { Lt as errMsg } from "./dist-CoZwNgzu.mjs";
2
+ import { Lt as errMsg } from "./dist-Dzy3wAmT.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-CWWQf3ci.js");
2
+ const require_dist = require("./dist-BjiM0e6J.js");
3
3
  let node_child_process = require("node:child_process");
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.106",
6
+ version: "1.2.108",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.200",
21
+ version: "1.2.202",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.165",
36
+ version: "1.2.167",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",
@@ -1,4 +1,4 @@
1
- const require_segment_demux_js = require("../segment-demux-js-C6B53KjT.js");
1
+ const require_segment_demux_js = require("../segment-demux-js-Dd5asWZc.js");
2
2
  let node_worker_threads = require("node:worker_threads");
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as demuxSegmentFullJs } from "../segment-demux-js-7DukFIT6.mjs";
1
+ import { t as demuxSegmentFullJs } from "../segment-demux-js-DcF7tvrn.mjs";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.200",
39
+ version: "1.2.202",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.106",
48
+ version: "1.2.108",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.165",
84
+ version: "1.2.167",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CWWQf3ci.js");
7
- const require_node = require("../node-DLZWItaN.js");
6
+ const require_dist = require("../dist-BjiM0e6J.js");
7
+ const require_node = require("../node-P2-C_t7H.js");
8
8
  const require_restream_intent = require("../restream-intent-C4adO0P8.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-C6B53KjT.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-Dd5asWZc.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -4207,6 +4207,8 @@ var RtspListenServer = class RtspListenServer {
4207
4207
  brokerEnabled = /* @__PURE__ */ new Map();
4208
4208
  /** brokerId → warm-up closure registered alongside the restreamer */
4209
4209
  brokerWarmUps = /* @__PURE__ */ new Map();
4210
+ /** brokerId → prestream picture-window opener */
4211
+ brokerPrestreamOpeners = /* @__PURE__ */ new Map();
4210
4212
  /** Generate a random token for a stream path */
4211
4213
  static generateToken() {
4212
4214
  return (0, node_crypto.randomBytes)(16).toString("hex");
@@ -4217,7 +4219,7 @@ var RtspListenServer = class RtspListenServer {
4217
4219
  * If not, generate a new one.
4218
4220
  * Returns the token used.
4219
4221
  */
4220
- registerRestreamer(brokerId, restreamer, warmUp, existingToken) {
4222
+ registerRestreamer(brokerId, restreamer, warmUp, existingToken, prestreamOpener) {
4221
4223
  const oldToken = this.brokerTokens.get(brokerId);
4222
4224
  if (oldToken) {
4223
4225
  this.restreamers.delete(oldToken);
@@ -4228,6 +4230,7 @@ var RtspListenServer = class RtspListenServer {
4228
4230
  this.brokerTokens.set(brokerId, token);
4229
4231
  this.tokenBrokers.set(token, brokerId);
4230
4232
  if (warmUp) this.brokerWarmUps.set(brokerId, warmUp);
4233
+ if (prestreamOpener) this.brokerPrestreamOpeners.set(brokerId, prestreamOpener);
4231
4234
  return token;
4232
4235
  }
4233
4236
  unregisterRestreamer(brokerId) {
@@ -4238,6 +4241,7 @@ var RtspListenServer = class RtspListenServer {
4238
4241
  }
4239
4242
  this.brokerTokens.delete(brokerId);
4240
4243
  this.brokerWarmUps.delete(brokerId);
4244
+ this.brokerPrestreamOpeners.delete(brokerId);
4241
4245
  }
4242
4246
  /** Regenerate a random token for a broker. Returns the new token. */
4243
4247
  regenerateToken(brokerId) {
@@ -4338,8 +4342,10 @@ var RtspListenServer = class RtspListenServer {
4338
4342
  socket.destroy();
4339
4343
  return;
4340
4344
  }
4341
- const warmUp = brokerId ? this.brokerWarmUps.get(brokerId) : void 0;
4342
- const releaseWarmUp = warmUp ? warmUp() : null;
4345
+ const prestreamDial = declaration.intent === "prestream";
4346
+ const warmUp = brokerId && !prestreamDial ? this.brokerWarmUps.get(brokerId) : void 0;
4347
+ const opener = brokerId && prestreamDial ? this.brokerPrestreamOpeners.get(brokerId) : void 0;
4348
+ const releaseWarmUp = warmUp ? warmUp() : opener?.() ?? null;
4343
4349
  let released = false;
4344
4350
  const releaseOnce = () => {
4345
4351
  if (released) return;
@@ -13445,9 +13451,36 @@ var StreamBroker = class StreamBroker {
13445
13451
  */
13446
13452
  hasAnyConsumer() {
13447
13453
  if (this.hasLiveIntent()) return true;
13454
+ if (this.prestreamHolds > 0) return true;
13448
13455
  return this.rtspRestreamer.getPrestreamSessionCount() > 0;
13449
13456
  }
13450
13457
  /**
13458
+ * Somebody is DESCRIBING a prestream and has no session yet.
13459
+ *
13460
+ * The RTSP dispatch reads the SDP before `addSession` runs, so a window that
13461
+ * waits for the session is too late: the consumer is handed an empty SDP and
13462
+ * quits. Measured against the live hub (2026-09-15 14:47): the window opened
13463
+ * on the attach and closed 42 ms later having emitted zero frames, because
13464
+ * ffmpeg had already given up.
13465
+ *
13466
+ * This is the prestream counterpart of `BrokerWarmUp` and its exact opposite
13467
+ * in effect: the warm-up subscribes demand, which DIALS; this opens the
13468
+ * picture, which never does. A battery camera is not touched.
13469
+ */
13470
+ openPrestreamWindow() {
13471
+ this.prestreamHolds += 1;
13472
+ this.checkDemand();
13473
+ let released = false;
13474
+ return () => {
13475
+ if (released) return;
13476
+ released = true;
13477
+ this.prestreamHolds = Math.max(0, this.prestreamHolds - 1);
13478
+ this.checkDemand();
13479
+ };
13480
+ }
13481
+ /** DESCRIBE-window prestream holds: sessions that do not exist yet. */
13482
+ prestreamHolds = 0;
13483
+ /**
13451
13484
  * Consumers that may DIAL. Every term of the former `hasDemand`, with the
13452
13485
  * RTSP session count filtered to exclude prestream dials, plus the wake hold.
13453
13486
  *
@@ -13607,6 +13640,31 @@ var StreamBroker = class StreamBroker {
13607
13640
  }, SUSPEND_GRACE_MS);
13608
13641
  }
13609
13642
  this.prestreamWanted = this.hasAnyConsumer() && !this.hasLiveIntent();
13643
+ this.reconcilePrestreamWindow();
13644
+ }
13645
+ /**
13646
+ * Open or close the prestream window to match {@link prestreamWanted}.
13647
+ *
13648
+ * This is the half that was missing. `prestreamWanted` has been computed
13649
+ * correctly since the feature shipped, but the only thing that ever STARTED
13650
+ * the pump was the lazy branch of `start()` — a DIAL, which needs live
13651
+ * intent, which a prestream session does not have by construction. So the
13652
+ * window opened only when something else had already woken the camera, and
13653
+ * `suspend()` closed it for good.
13654
+ *
13655
+ * Driving it from the consumer edge is what the contract already claimed:
13656
+ * the pump follows CONSUMERS, and it neither dials nor wakes nor holds
13657
+ * anything open (D173). `start()` is idempotent, so being called on every
13658
+ * demand check costs one encode per window and no more.
13659
+ */
13660
+ reconcilePrestreamWindow() {
13661
+ if (!this.prestreamPump) return;
13662
+ if (this.prestreamWanted && !this.prestreamWindowClosed) {
13663
+ const url = this.resolveCurrentSource()?.url ?? this.source?.url;
13664
+ if (url !== void 0) this.prestreamPump.start(url);
13665
+ return;
13666
+ }
13667
+ if (!this.prestreamWanted) this.prestreamPump.stop("no prestream consumer");
13610
13668
  }
13611
13669
  /**
13612
13670
  * Whether a prestream picture should be flowing right now: somebody is
@@ -13642,6 +13700,7 @@ var StreamBroker = class StreamBroker {
13642
13700
  }
13643
13701
  this.prestreamPump?.stop("broker suspended");
13644
13702
  this.prestreamWindowClosed = false;
13703
+ this.reconcilePrestreamWindow();
13645
13704
  this.preBuffer.clear();
13646
13705
  this.decoderSeedBuffer.clear();
13647
13706
  this.clipBuffer?.clear();
@@ -19308,7 +19367,8 @@ var StreamBrokerManager = class StreamBrokerManager {
19308
19367
  if (!sourceBroker) continue;
19309
19368
  const existingToken = this.persistedTokens.get(profileBrokerId);
19310
19369
  const warmUp = () => sourceBroker.onEncodedData(() => {});
19311
- this.rtspServer.registerRestreamer(profileBrokerId, sourceBroker.getRtspRestreamer(), warmUp, existingToken);
19370
+ const openPrestreamWindow = () => sourceBroker.openPrestreamWindow();
19371
+ this.rtspServer.registerRestreamer(profileBrokerId, sourceBroker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
19312
19372
  this.profileRestreamerBindings.set(profileBrokerId, sourceBrokerId);
19313
19373
  if (!existingToken) this.rtspProvider.persistTokens();
19314
19374
  }
@@ -20967,7 +21027,8 @@ var StreamBrokerManager = class StreamBrokerManager {
20967
21027
  this.applyClipRetention(deviceId);
20968
21028
  const existingToken = this.persistedTokens.get(brokerId);
20969
21029
  const warmUp = () => broker.onEncodedData(() => {});
20970
- this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken);
21030
+ const openPrestreamWindow = () => broker.openPrestreamWindow();
21031
+ this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
20971
21032
  if (!existingToken) this.rtspProvider.persistTokens();
20972
21033
  await this.fanOutRegistration(deviceId, brokerId, cam, broker);
20973
21034
  this.syncProfileRestreamers(deviceId);
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { A as RingBuffer, B as declareLogChannel, Bt as buildFfmpegArgs$1, E as RATE_CONTROL_TIGHT, Et as resolveEgressDecodeHwAccel, Gt as CAM_PROFILE_ORDER, H as deriveBatteryPresence, Ht as isSoftwareDecode, I as batteryCapability, It as webrtcSessionCapability, K as egressTranscodeSharingKey, Kt as DeviceFeature, L as cameraStreamsCapability, Lt as errMsg, M as addonWidgetsSourceCapability, Nt as streamBrokerCapability, Qt as expandAudioChunkToF32le, R as createHwAccelCache, Rt as AUDIO_PRESETS, T as RATE_CONTROL_RELAXED, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Yt as createEvent, _n as record, an as parseProfileBrokerId, at as logChannelsCapability, bn as EventCategory, cn as _enum, dn as discriminatedUnion, en as isEvent, h as EncodeProfileSchema, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, ln as array, mn as number, nn as makeSourceBrokerId, pn as literal, q as egressTransportFromRequest, qt as DeviceType, rn as nodePin, st as maskUrlCredentials, tn as makeProfileBrokerId, un as boolean, v as G711_SCALE_CORRECTION_DB, vn as string, xt as rankStreams, yn as union, z as createLogChannelsProvider } from "../dist-CoZwNgzu.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-Uh59Eo57.mjs";
2
+ import { A as RingBuffer, B as declareLogChannel, Bt as buildFfmpegArgs$1, E as RATE_CONTROL_TIGHT, Et as resolveEgressDecodeHwAccel, Gt as CAM_PROFILE_ORDER, H as deriveBatteryPresence, Ht as isSoftwareDecode, I as batteryCapability, It as webrtcSessionCapability, K as egressTranscodeSharingKey, Kt as DeviceFeature, L as cameraStreamsCapability, Lt as errMsg, M as addonWidgetsSourceCapability, Nt as streamBrokerCapability, Qt as expandAudioChunkToF32le, R as createHwAccelCache, Rt as AUDIO_PRESETS, T as RATE_CONTROL_RELAXED, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Yt as createEvent, _n as record, an as parseProfileBrokerId, at as logChannelsCapability, bn as EventCategory, cn as _enum, dn as discriminatedUnion, en as isEvent, h as EncodeProfileSchema, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, ln as array, mn as number, nn as makeSourceBrokerId, pn as literal, q as egressTransportFromRequest, qt as DeviceType, rn as nodePin, st as maskUrlCredentials, tn as makeProfileBrokerId, un as boolean, v as G711_SCALE_CORRECTION_DB, vn as string, xt as rankStreams, yn as union, z as createLogChannelsProvider } from "../dist-Dzy3wAmT.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-D9DBz5HF.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-MeJhGCK9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-7DukFIT6.mjs";
7
+ import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-DcF7tvrn.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -4203,6 +4203,8 @@ var RtspListenServer = class RtspListenServer {
4203
4203
  brokerEnabled = /* @__PURE__ */ new Map();
4204
4204
  /** brokerId → warm-up closure registered alongside the restreamer */
4205
4205
  brokerWarmUps = /* @__PURE__ */ new Map();
4206
+ /** brokerId → prestream picture-window opener */
4207
+ brokerPrestreamOpeners = /* @__PURE__ */ new Map();
4206
4208
  /** Generate a random token for a stream path */
4207
4209
  static generateToken() {
4208
4210
  return randomBytes(16).toString("hex");
@@ -4213,7 +4215,7 @@ var RtspListenServer = class RtspListenServer {
4213
4215
  * If not, generate a new one.
4214
4216
  * Returns the token used.
4215
4217
  */
4216
- registerRestreamer(brokerId, restreamer, warmUp, existingToken) {
4218
+ registerRestreamer(brokerId, restreamer, warmUp, existingToken, prestreamOpener) {
4217
4219
  const oldToken = this.brokerTokens.get(brokerId);
4218
4220
  if (oldToken) {
4219
4221
  this.restreamers.delete(oldToken);
@@ -4224,6 +4226,7 @@ var RtspListenServer = class RtspListenServer {
4224
4226
  this.brokerTokens.set(brokerId, token);
4225
4227
  this.tokenBrokers.set(token, brokerId);
4226
4228
  if (warmUp) this.brokerWarmUps.set(brokerId, warmUp);
4229
+ if (prestreamOpener) this.brokerPrestreamOpeners.set(brokerId, prestreamOpener);
4227
4230
  return token;
4228
4231
  }
4229
4232
  unregisterRestreamer(brokerId) {
@@ -4234,6 +4237,7 @@ var RtspListenServer = class RtspListenServer {
4234
4237
  }
4235
4238
  this.brokerTokens.delete(brokerId);
4236
4239
  this.brokerWarmUps.delete(brokerId);
4240
+ this.brokerPrestreamOpeners.delete(brokerId);
4237
4241
  }
4238
4242
  /** Regenerate a random token for a broker. Returns the new token. */
4239
4243
  regenerateToken(brokerId) {
@@ -4334,8 +4338,10 @@ var RtspListenServer = class RtspListenServer {
4334
4338
  socket.destroy();
4335
4339
  return;
4336
4340
  }
4337
- const warmUp = brokerId ? this.brokerWarmUps.get(brokerId) : void 0;
4338
- const releaseWarmUp = warmUp ? warmUp() : null;
4341
+ const prestreamDial = declaration.intent === "prestream";
4342
+ const warmUp = brokerId && !prestreamDial ? this.brokerWarmUps.get(brokerId) : void 0;
4343
+ const opener = brokerId && prestreamDial ? this.brokerPrestreamOpeners.get(brokerId) : void 0;
4344
+ const releaseWarmUp = warmUp ? warmUp() : opener?.() ?? null;
4339
4345
  let released = false;
4340
4346
  const releaseOnce = () => {
4341
4347
  if (released) return;
@@ -13441,9 +13447,36 @@ var StreamBroker = class StreamBroker {
13441
13447
  */
13442
13448
  hasAnyConsumer() {
13443
13449
  if (this.hasLiveIntent()) return true;
13450
+ if (this.prestreamHolds > 0) return true;
13444
13451
  return this.rtspRestreamer.getPrestreamSessionCount() > 0;
13445
13452
  }
13446
13453
  /**
13454
+ * Somebody is DESCRIBING a prestream and has no session yet.
13455
+ *
13456
+ * The RTSP dispatch reads the SDP before `addSession` runs, so a window that
13457
+ * waits for the session is too late: the consumer is handed an empty SDP and
13458
+ * quits. Measured against the live hub (2026-09-15 14:47): the window opened
13459
+ * on the attach and closed 42 ms later having emitted zero frames, because
13460
+ * ffmpeg had already given up.
13461
+ *
13462
+ * This is the prestream counterpart of `BrokerWarmUp` and its exact opposite
13463
+ * in effect: the warm-up subscribes demand, which DIALS; this opens the
13464
+ * picture, which never does. A battery camera is not touched.
13465
+ */
13466
+ openPrestreamWindow() {
13467
+ this.prestreamHolds += 1;
13468
+ this.checkDemand();
13469
+ let released = false;
13470
+ return () => {
13471
+ if (released) return;
13472
+ released = true;
13473
+ this.prestreamHolds = Math.max(0, this.prestreamHolds - 1);
13474
+ this.checkDemand();
13475
+ };
13476
+ }
13477
+ /** DESCRIBE-window prestream holds: sessions that do not exist yet. */
13478
+ prestreamHolds = 0;
13479
+ /**
13447
13480
  * Consumers that may DIAL. Every term of the former `hasDemand`, with the
13448
13481
  * RTSP session count filtered to exclude prestream dials, plus the wake hold.
13449
13482
  *
@@ -13603,6 +13636,31 @@ var StreamBroker = class StreamBroker {
13603
13636
  }, SUSPEND_GRACE_MS);
13604
13637
  }
13605
13638
  this.prestreamWanted = this.hasAnyConsumer() && !this.hasLiveIntent();
13639
+ this.reconcilePrestreamWindow();
13640
+ }
13641
+ /**
13642
+ * Open or close the prestream window to match {@link prestreamWanted}.
13643
+ *
13644
+ * This is the half that was missing. `prestreamWanted` has been computed
13645
+ * correctly since the feature shipped, but the only thing that ever STARTED
13646
+ * the pump was the lazy branch of `start()` — a DIAL, which needs live
13647
+ * intent, which a prestream session does not have by construction. So the
13648
+ * window opened only when something else had already woken the camera, and
13649
+ * `suspend()` closed it for good.
13650
+ *
13651
+ * Driving it from the consumer edge is what the contract already claimed:
13652
+ * the pump follows CONSUMERS, and it neither dials nor wakes nor holds
13653
+ * anything open (D173). `start()` is idempotent, so being called on every
13654
+ * demand check costs one encode per window and no more.
13655
+ */
13656
+ reconcilePrestreamWindow() {
13657
+ if (!this.prestreamPump) return;
13658
+ if (this.prestreamWanted && !this.prestreamWindowClosed) {
13659
+ const url = this.resolveCurrentSource()?.url ?? this.source?.url;
13660
+ if (url !== void 0) this.prestreamPump.start(url);
13661
+ return;
13662
+ }
13663
+ if (!this.prestreamWanted) this.prestreamPump.stop("no prestream consumer");
13606
13664
  }
13607
13665
  /**
13608
13666
  * Whether a prestream picture should be flowing right now: somebody is
@@ -13638,6 +13696,7 @@ var StreamBroker = class StreamBroker {
13638
13696
  }
13639
13697
  this.prestreamPump?.stop("broker suspended");
13640
13698
  this.prestreamWindowClosed = false;
13699
+ this.reconcilePrestreamWindow();
13641
13700
  this.preBuffer.clear();
13642
13701
  this.decoderSeedBuffer.clear();
13643
13702
  this.clipBuffer?.clear();
@@ -19304,7 +19363,8 @@ var StreamBrokerManager = class StreamBrokerManager {
19304
19363
  if (!sourceBroker) continue;
19305
19364
  const existingToken = this.persistedTokens.get(profileBrokerId);
19306
19365
  const warmUp = () => sourceBroker.onEncodedData(() => {});
19307
- this.rtspServer.registerRestreamer(profileBrokerId, sourceBroker.getRtspRestreamer(), warmUp, existingToken);
19366
+ const openPrestreamWindow = () => sourceBroker.openPrestreamWindow();
19367
+ this.rtspServer.registerRestreamer(profileBrokerId, sourceBroker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
19308
19368
  this.profileRestreamerBindings.set(profileBrokerId, sourceBrokerId);
19309
19369
  if (!existingToken) this.rtspProvider.persistTokens();
19310
19370
  }
@@ -20963,7 +21023,8 @@ var StreamBrokerManager = class StreamBrokerManager {
20963
21023
  this.applyClipRetention(deviceId);
20964
21024
  const existingToken = this.persistedTokens.get(brokerId);
20965
21025
  const warmUp = () => broker.onEncodedData(() => {});
20966
- this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken);
21026
+ const openPrestreamWindow = () => broker.openPrestreamWindow();
21027
+ this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
20967
21028
  if (!existingToken) this.rtspProvider.persistTokens();
20968
21029
  await this.fanOutRegistration(deviceId, brokerId, cam, broker);
20969
21030
  this.syncProfileRestreamers(deviceId);
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B20FCUk4.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vEbg16EK.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.245",
3
+ "version": "1.2.247",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",