@camstack/addon-pipeline 1.1.26 → 1.1.28

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 (48) hide show
  1. package/dist/audio-analyzer/index.js +7 -6
  2. package/dist/audio-analyzer/index.mjs +7 -6
  3. package/dist/audio-codec-ffmpeg/index.js +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/constants-CAqYVigN.js +6615 -0
  6. package/dist/constants-oYgW36Ty.mjs +3587 -0
  7. package/dist/decoder-ffmpeg/index.js +74 -281
  8. package/dist/decoder-ffmpeg/index.mjs +73 -280
  9. package/dist/decoder-nodeav/index.js +1218 -0
  10. package/dist/decoder-nodeav/index.mjs +1210 -0
  11. package/dist/detection-pipeline/index.js +157 -219
  12. package/dist/detection-pipeline/index.mjs +157 -219
  13. package/dist/{dist-DAIlCdAx.js → dist-Biq62zt4.js} +372 -35
  14. package/dist/{dist-CgEP_0OL.mjs → dist-C6_wgXqF.mjs} +373 -24
  15. package/dist/dist-D3Ytt-_Y.js +41111 -0
  16. package/dist/dist-DluJuxV5.mjs +37976 -0
  17. package/dist/{frame-handle-plane-Dq20KtKL.mjs → frame-handle-plane-BIoY6nRV.mjs} +13 -5
  18. package/dist/{frame-handle-plane-DtTRX_0n.js → frame-handle-plane-D6BzyEgy.js} +13 -5
  19. package/dist/frame-ring-sink-9J0wCdLF.js +444 -0
  20. package/dist/frame-ring-sink-Cs9vby6v.mjs +409 -0
  21. package/dist/motion-wasm/index.js +55 -3
  22. package/dist/motion-wasm/index.mjs +55 -3
  23. package/dist/node-topology-platform-BkR_k6WT.mjs +15 -0
  24. package/dist/node-topology-platform-CFZ7F4xW.js +20 -0
  25. package/dist/pipeline-runner/index.js +50 -16
  26. package/dist/pipeline-runner/index.mjs +50 -16
  27. package/dist/recorder/index.js +36 -40
  28. package/dist/recorder/index.mjs +36 -40
  29. package/dist/stream-broker/_stub.js +2 -2
  30. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs} +3 -3
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +26 -0
  32. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs} +1 -1
  33. package/dist/stream-broker/{hostInit-DyLqyJaS.mjs → hostInit-C0SuwQhL.mjs} +3 -3
  34. package/dist/stream-broker/index.js +81 -40
  35. package/dist/stream-broker/index.mjs +81 -40
  36. package/dist/stream-broker/remoteEntry.js +1 -1
  37. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js → MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js} +1 -1
  38. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CoLjNiUN.js → MotionZonesSettings-NcxxQN8r-BSCcPdPf.js} +1 -1
  39. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js → PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js} +1 -1
  40. package/embed-dist/assets/index-C5UpuPr8.css +2 -0
  41. package/embed-dist/assets/{index-C-pL8ETk.js → index-CeZV1B-2.js} +10 -10
  42. package/embed-dist/index.html +2 -2
  43. package/package.json +31 -3
  44. package/python/inference_pool.py +108 -121
  45. package/python/test_inference_pool_device_selection.py +47 -43
  46. package/python/yamnet_audio.py +1 -1
  47. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +0 -26
  48. package/embed-dist/assets/index-DrJ0ee3f.css +0 -2
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DAIlCdAx.js");
6
- const require_frame_handle_plane = require("../frame-handle-plane-DtTRX_0n.js");
5
+ const require_dist = require("../dist-Biq62zt4.js");
6
+ const require_frame_handle_plane = require("../frame-handle-plane-D6BzyEgy.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  //#region src/pipeline-runner/bench-actions.ts
@@ -950,6 +950,34 @@ function ownDetectionEntry(entry) {
950
950
  handle: entry.handle
951
951
  };
952
952
  }
953
+ /**
954
+ * Clone a {@link MotionQueueEntry} for the queue's copy-on-retain boundary —
955
+ * same contract as {@link ownDetectionEntry}: detach the borrowed shm pixels,
956
+ * carry the plain-record `FrameHandle` through unchanged.
957
+ */
958
+ function ownMotionEntry(entry) {
959
+ return {
960
+ frame: ownFrame(entry.frame),
961
+ handle: entry.handle
962
+ };
963
+ }
964
+ /**
965
+ * CB5 shm FrameHandle passthrough gate. The runner hands the executor / motion
966
+ * provider a zero-pixel shm `FrameHandle` (instead of re-serialising ~1.2MB of
967
+ * pixels over UDS) ONLY while the pipeline is keeping up. The FrameRing is a
968
+ * latest-wins seqlock with NO refcount, so deferring the pixel read from the
969
+ * poll tick to the executor (after queue + semaphore + UDS) widens the
970
+ * publish→read window — under backpressure the producer can recycle the slot
971
+ * before the executor reads it (a silent drop). When the pipeline is backed up
972
+ * we therefore fall back to copying pixels inline (today's behaviour).
973
+ *
974
+ * The per-camera detection `FrameQueue` is latest-only (capacity 1), so raw
975
+ * queue length is 0/1 and the operative backpressure signal is inference
976
+ * saturation on the shared semaphore — no spare inference permit ⇒ frames are
977
+ * waiting ⇒ ship pixels. The queue-depth clause is kept for defence in depth
978
+ * and future multi-slot queues.
979
+ */
980
+ var HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH = 1;
953
981
  var DEFAULT_MOTION_COOLDOWN_MS = 3e4;
954
982
  function toFrameInput$1(frame) {
955
983
  return {
@@ -1032,7 +1060,7 @@ var PipelineRunner = class {
1032
1060
  this.detectionStreamHandler = handler;
1033
1061
  }
1034
1062
  registerCamera(deviceId, registration) {
1035
- const motionQueue = new FrameQueue(this.config.maxQueueDepth, ownFrame);
1063
+ const motionQueue = new FrameQueue(this.config.maxQueueDepth, ownMotionEntry);
1036
1064
  const detectionQueue = new FrameQueue(this.config.maxQueueDepth, ownDetectionEntry);
1037
1065
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
1038
1066
  const state = {
@@ -1087,10 +1115,13 @@ var PipelineRunner = class {
1087
1115
  if (this.defaultRoundRobinIndex >= this.defaultRoundRobinKeys.length) this.defaultRoundRobinIndex = 0;
1088
1116
  }
1089
1117
  }
1090
- enqueueMotionFrame(deviceId, frame) {
1118
+ enqueueMotionFrame(deviceId, frame, handle) {
1091
1119
  const state = this.cameras.get(deviceId);
1092
1120
  if (!state) return;
1093
- state.motionQueue.enqueue(frame);
1121
+ state.motionQueue.enqueue({
1122
+ frame,
1123
+ handle
1124
+ });
1094
1125
  }
1095
1126
  enqueueDetectionFrame(deviceId, frame, handle) {
1096
1127
  const state = this.cameras.get(deviceId);
@@ -1274,8 +1305,8 @@ var PipelineRunner = class {
1274
1305
  }
1275
1306
  drainMotionQueues() {
1276
1307
  for (const [deviceId, state] of this.cameras) while (state.motionQueue.size > 0) {
1277
- const frame = state.motionQueue.dequeue();
1278
- if (frame) this.config.analyzeMotion(deviceId, frame);
1308
+ const entry = state.motionQueue.dequeue();
1309
+ if (entry) this.config.analyzeMotion(deviceId, entry.frame, entry.handle);
1279
1310
  }
1280
1311
  }
1281
1312
  async processWithSemaphore(deviceId, entry, frameInput, state, streamType) {
@@ -1284,8 +1315,9 @@ var PipelineRunner = class {
1284
1315
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
1285
1316
  const release = await this.semaphore.acquire();
1286
1317
  const semAcquiredAt = Date.now();
1318
+ const executorHandle = state.detectionQueue.size <= HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH && this.semaphore.available > 0 ? handle : void 0;
1287
1319
  try {
1288
- const result = await this.config.processFrame(deviceId, frameInput);
1320
+ const result = await this.config.processFrame(deviceId, frameInput, executorHandle);
1289
1321
  const inferDoneAt = Date.now();
1290
1322
  const inferenceMs = inferDoneAt - semAcquiredAt;
1291
1323
  state.inferenceTimes.push(inferenceMs);
@@ -1463,8 +1495,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1463
1495
  maxConcurrentInferences: this.config.maxConcurrentInferences,
1464
1496
  targetLoadPercent: this.config.targetLoadPercent,
1465
1497
  minThrottledFps: this.config.minThrottledFps,
1466
- processFrame: (deviceId, frame) => this.runInference(deviceId, frame),
1467
- analyzeMotion: (deviceId, frame) => this.runMotionAnalysis(deviceId, frame),
1498
+ processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
1499
+ analyzeMotion: (deviceId, frame, handle) => this.runMotionAnalysis(deviceId, frame, handle),
1468
1500
  onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
1469
1501
  logger: this.ctx.logger,
1470
1502
  onOccupancyRecheck: (deviceId, frames) => {
@@ -2129,8 +2161,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2129
2161
  ownNodeId: this.nodeId,
2130
2162
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2131
2163
  ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
2132
- onFrame: (frame, _handle) => {
2133
- runner.enqueueMotionFrame(config.deviceId, frame);
2164
+ onFrame: (frame, handle) => {
2165
+ runner.enqueueMotionFrame(config.deviceId, frame, handle);
2134
2166
  }
2135
2167
  });
2136
2168
  }
@@ -2221,7 +2253,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2221
2253
  }
2222
2254
  });
2223
2255
  }
2224
- async runInference(deviceId, frame) {
2256
+ async runInference(deviceId, frame, handle) {
2225
2257
  if (!this.ctx) return null;
2226
2258
  const log = this.ctx.logger.withTags({ deviceId });
2227
2259
  const api = this.ctx.api;
@@ -2237,10 +2269,11 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2237
2269
  return null;
2238
2270
  }
2239
2271
  if (steps.length === 0) return null;
2272
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
2240
2273
  try {
2241
2274
  return await api.pipelineExecutor.runPipeline.mutate({
2242
2275
  steps: [...steps],
2243
- frame,
2276
+ ...useHandle ? { frameHandle: handle } : { frame },
2244
2277
  deviceId,
2245
2278
  ...engine ? { engine } : {}
2246
2279
  });
@@ -2250,7 +2283,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2250
2283
  return null;
2251
2284
  }
2252
2285
  }
2253
- async runMotionAnalysis(deviceId, frame) {
2286
+ async runMotionAnalysis(deviceId, frame, handle) {
2254
2287
  const ctx = this.ctx;
2255
2288
  const runner = this.runner;
2256
2289
  if (!ctx || !runner) return;
@@ -2262,9 +2295,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2262
2295
  log.warn("runMotionAnalysis: this.ctx.api not available");
2263
2296
  return;
2264
2297
  }
2298
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
2265
2299
  const result = await api.motionDetection.analyze.mutate({
2266
2300
  deviceId,
2267
- frame: toFrameInput(frame)
2301
+ ...useHandle ? { frameHandle: handle } : { frame: toFrameInput(frame) }
2268
2302
  });
2269
2303
  if (!result) return;
2270
2304
  const detected = result.regions.length > 0;
@@ -1,5 +1,5 @@
1
- import { B as errMsg, C as defineCustomActions, G as EventCategory, J as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Z as makeSourceBrokerId, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, rt as array } from "../dist-CgEP_0OL.mjs";
2
- import { t as FrameHandlePlane } from "../frame-handle-plane-Dq20KtKL.mjs";
1
+ import { B as errMsg, C as defineCustomActions, G as EventCategory, K as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Y as makeSourceBrokerId, b as customAction, et as _enum, it as lazy, j as nodePin, lt as string, nt as boolean, ot as number, st as object, tt as array } from "../dist-C6_wgXqF.mjs";
2
+ import { t as FrameHandlePlane } from "../frame-handle-plane-BIoY6nRV.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
5
  //#region src/pipeline-runner/bench-actions.ts
@@ -946,6 +946,34 @@ function ownDetectionEntry(entry) {
946
946
  handle: entry.handle
947
947
  };
948
948
  }
949
+ /**
950
+ * Clone a {@link MotionQueueEntry} for the queue's copy-on-retain boundary —
951
+ * same contract as {@link ownDetectionEntry}: detach the borrowed shm pixels,
952
+ * carry the plain-record `FrameHandle` through unchanged.
953
+ */
954
+ function ownMotionEntry(entry) {
955
+ return {
956
+ frame: ownFrame(entry.frame),
957
+ handle: entry.handle
958
+ };
959
+ }
960
+ /**
961
+ * CB5 shm FrameHandle passthrough gate. The runner hands the executor / motion
962
+ * provider a zero-pixel shm `FrameHandle` (instead of re-serialising ~1.2MB of
963
+ * pixels over UDS) ONLY while the pipeline is keeping up. The FrameRing is a
964
+ * latest-wins seqlock with NO refcount, so deferring the pixel read from the
965
+ * poll tick to the executor (after queue + semaphore + UDS) widens the
966
+ * publish→read window — under backpressure the producer can recycle the slot
967
+ * before the executor reads it (a silent drop). When the pipeline is backed up
968
+ * we therefore fall back to copying pixels inline (today's behaviour).
969
+ *
970
+ * The per-camera detection `FrameQueue` is latest-only (capacity 1), so raw
971
+ * queue length is 0/1 and the operative backpressure signal is inference
972
+ * saturation on the shared semaphore — no spare inference permit ⇒ frames are
973
+ * waiting ⇒ ship pixels. The queue-depth clause is kept for defence in depth
974
+ * and future multi-slot queues.
975
+ */
976
+ var HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH = 1;
949
977
  var DEFAULT_MOTION_COOLDOWN_MS = 3e4;
950
978
  function toFrameInput$1(frame) {
951
979
  return {
@@ -1028,7 +1056,7 @@ var PipelineRunner = class {
1028
1056
  this.detectionStreamHandler = handler;
1029
1057
  }
1030
1058
  registerCamera(deviceId, registration) {
1031
- const motionQueue = new FrameQueue(this.config.maxQueueDepth, ownFrame);
1059
+ const motionQueue = new FrameQueue(this.config.maxQueueDepth, ownMotionEntry);
1032
1060
  const detectionQueue = new FrameQueue(this.config.maxQueueDepth, ownDetectionEntry);
1033
1061
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
1034
1062
  const state = {
@@ -1083,10 +1111,13 @@ var PipelineRunner = class {
1083
1111
  if (this.defaultRoundRobinIndex >= this.defaultRoundRobinKeys.length) this.defaultRoundRobinIndex = 0;
1084
1112
  }
1085
1113
  }
1086
- enqueueMotionFrame(deviceId, frame) {
1114
+ enqueueMotionFrame(deviceId, frame, handle) {
1087
1115
  const state = this.cameras.get(deviceId);
1088
1116
  if (!state) return;
1089
- state.motionQueue.enqueue(frame);
1117
+ state.motionQueue.enqueue({
1118
+ frame,
1119
+ handle
1120
+ });
1090
1121
  }
1091
1122
  enqueueDetectionFrame(deviceId, frame, handle) {
1092
1123
  const state = this.cameras.get(deviceId);
@@ -1270,8 +1301,8 @@ var PipelineRunner = class {
1270
1301
  }
1271
1302
  drainMotionQueues() {
1272
1303
  for (const [deviceId, state] of this.cameras) while (state.motionQueue.size > 0) {
1273
- const frame = state.motionQueue.dequeue();
1274
- if (frame) this.config.analyzeMotion(deviceId, frame);
1304
+ const entry = state.motionQueue.dequeue();
1305
+ if (entry) this.config.analyzeMotion(deviceId, entry.frame, entry.handle);
1275
1306
  }
1276
1307
  }
1277
1308
  async processWithSemaphore(deviceId, entry, frameInput, state, streamType) {
@@ -1280,8 +1311,9 @@ var PipelineRunner = class {
1280
1311
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
1281
1312
  const release = await this.semaphore.acquire();
1282
1313
  const semAcquiredAt = Date.now();
1314
+ const executorHandle = state.detectionQueue.size <= HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH && this.semaphore.available > 0 ? handle : void 0;
1283
1315
  try {
1284
- const result = await this.config.processFrame(deviceId, frameInput);
1316
+ const result = await this.config.processFrame(deviceId, frameInput, executorHandle);
1285
1317
  const inferDoneAt = Date.now();
1286
1318
  const inferenceMs = inferDoneAt - semAcquiredAt;
1287
1319
  state.inferenceTimes.push(inferenceMs);
@@ -1459,8 +1491,8 @@ var PipelineRunnerAddon = class extends BaseAddon {
1459
1491
  maxConcurrentInferences: this.config.maxConcurrentInferences,
1460
1492
  targetLoadPercent: this.config.targetLoadPercent,
1461
1493
  minThrottledFps: this.config.minThrottledFps,
1462
- processFrame: (deviceId, frame) => this.runInference(deviceId, frame),
1463
- analyzeMotion: (deviceId, frame) => this.runMotionAnalysis(deviceId, frame),
1494
+ processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
1495
+ analyzeMotion: (deviceId, frame, handle) => this.runMotionAnalysis(deviceId, frame, handle),
1464
1496
  onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
1465
1497
  logger: this.ctx.logger,
1466
1498
  onOccupancyRecheck: (deviceId, frames) => {
@@ -2125,8 +2157,8 @@ var PipelineRunnerAddon = class extends BaseAddon {
2125
2157
  ownNodeId: this.nodeId,
2126
2158
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2127
2159
  ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
2128
- onFrame: (frame, _handle) => {
2129
- runner.enqueueMotionFrame(config.deviceId, frame);
2160
+ onFrame: (frame, handle) => {
2161
+ runner.enqueueMotionFrame(config.deviceId, frame, handle);
2130
2162
  }
2131
2163
  });
2132
2164
  }
@@ -2217,7 +2249,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
2217
2249
  }
2218
2250
  });
2219
2251
  }
2220
- async runInference(deviceId, frame) {
2252
+ async runInference(deviceId, frame, handle) {
2221
2253
  if (!this.ctx) return null;
2222
2254
  const log = this.ctx.logger.withTags({ deviceId });
2223
2255
  const api = this.ctx.api;
@@ -2233,10 +2265,11 @@ var PipelineRunnerAddon = class extends BaseAddon {
2233
2265
  return null;
2234
2266
  }
2235
2267
  if (steps.length === 0) return null;
2268
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
2236
2269
  try {
2237
2270
  return await api.pipelineExecutor.runPipeline.mutate({
2238
2271
  steps: [...steps],
2239
- frame,
2272
+ ...useHandle ? { frameHandle: handle } : { frame },
2240
2273
  deviceId,
2241
2274
  ...engine ? { engine } : {}
2242
2275
  });
@@ -2246,7 +2279,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
2246
2279
  return null;
2247
2280
  }
2248
2281
  }
2249
- async runMotionAnalysis(deviceId, frame) {
2282
+ async runMotionAnalysis(deviceId, frame, handle) {
2250
2283
  const ctx = this.ctx;
2251
2284
  const runner = this.runner;
2252
2285
  if (!ctx || !runner) return;
@@ -2258,9 +2291,10 @@ var PipelineRunnerAddon = class extends BaseAddon {
2258
2291
  log.warn("runMotionAnalysis: this.ctx.api not available");
2259
2292
  return;
2260
2293
  }
2294
+ const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
2261
2295
  const result = await api.motionDetection.analyze.mutate({
2262
2296
  deviceId,
2263
- frame: toFrameInput(frame)
2297
+ ...useHandle ? { frameHandle: handle } : { frame: toFrameInput(frame) }
2264
2298
  });
2265
2299
  if (!result) return;
2266
2300
  const detected = result.regions.length > 0;
@@ -1,5 +1,5 @@
1
1
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
2
- const require_dist = require("../dist-DAIlCdAx.js");
2
+ const require_dist = require("../dist-Biq62zt4.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
4
  let node_fs = require("node:fs");
5
5
  let node_path = require("node:path");
@@ -726,9 +726,15 @@ function buildRecordingDeviceSchema() {
726
726
  * (stringified) numeric deviceId — enumerable for boot hydrate (so the addon
727
727
  * knows which devices have persisted footage to index without scanning every
728
728
  * device store). The whole Zod-validated blob round-trips on every read/write
729
- * via a {@link createDurableState} handle, so no per-device field can be
730
- * silently dropped on persist. A corrupt blob reads back as EMPTY rather than
731
- * crashing boot.
729
+ * via a {@link DurableState} handle, so no per-device field can be silently
730
+ * dropped on persist. A corrupt blob reads back as EMPTY rather than crashing
731
+ * boot.
732
+ *
733
+ * P3b settings-API migration: the functions here consume the durable handle
734
+ * directly — in production the recorder addon builds it via the sanctioned
735
+ * `BaseAddon.state()` (retry budget + validated fallback + parse-warning
736
+ * logging); {@link recordingConfigsState} adapts a raw store slice for test
737
+ * fakes. Same store key, same persisted blob shape as before.
732
738
  *
733
739
  * On read the legacy `mode`/`schedules`/`triggers` fields are derived once into
734
740
  * the authoritative `bands` via `migrateConfigToBands`, then persisted, so the
@@ -743,23 +749,14 @@ var DEFAULT_DEVICE_CONFIG = {
743
749
  enabled: false,
744
750
  bands: []
745
751
  };
746
- function blobState(store) {
747
- return require_dist.createDurableState({
748
- key: RECORDING_CONFIGS_KEY,
749
- schema: RecordingConfigsBlobSchema,
750
- fallback: {},
751
- read: () => store.readAddonStore(),
752
- write: (patch) => store.writeAddonStore(patch)
753
- });
754
- }
755
752
  function serialize(map) {
756
753
  const out = {};
757
754
  for (const [id, config] of map) out[String(id)] = config;
758
755
  return out;
759
756
  }
760
757
  /** Read every persisted device config, keyed by numeric deviceId. */
761
- async function readDeviceConfigs(store) {
762
- const blob = await blobState(store).get();
758
+ async function readDeviceConfigs(state) {
759
+ const blob = await state.get();
763
760
  const map = /* @__PURE__ */ new Map();
764
761
  for (const [key, config] of Object.entries(blob)) {
765
762
  const id = Number(key);
@@ -773,15 +770,15 @@ async function readDeviceConfigs(store) {
773
770
  * re-persisted so the stored shape converges on the authoritative model. An
774
771
  * unconfigured device returns {@link DEFAULT_DEVICE_CONFIG}.
775
772
  */
776
- async function loadDeviceConfig(store, deviceId) {
777
- const stored = (await readDeviceConfigs(store)).get(deviceId);
773
+ async function loadDeviceConfig(state, deviceId) {
774
+ const stored = (await readDeviceConfigs(state)).get(deviceId);
778
775
  if (!stored) return DEFAULT_DEVICE_CONFIG;
779
776
  if (stored.bands) return stored;
780
777
  const migrated = {
781
778
  ...stored,
782
779
  bands: require_dist.migrateConfigToBands(stored)
783
780
  };
784
- await saveDeviceConfig(store, deviceId, migrated);
781
+ await saveDeviceConfig(state, deviceId, migrated);
785
782
  return migrated;
786
783
  }
787
784
  /**
@@ -789,11 +786,11 @@ async function loadDeviceConfig(store, deviceId) {
789
786
  * `RecordingConfigSchema` at the boundary (untrusted cap input). Returns the
790
787
  * validated config that was persisted.
791
788
  */
792
- async function saveDeviceConfig(store, deviceId, config) {
789
+ async function saveDeviceConfig(state, deviceId, config) {
793
790
  const validated = require_dist.RecordingConfigSchema.parse(config);
794
- const map = await readDeviceConfigs(store);
791
+ const map = await readDeviceConfigs(state);
795
792
  map.set(deviceId, validated);
796
- await blobState(store).set(serialize(map));
793
+ await state.set(serialize(map));
797
794
  return validated;
798
795
  }
799
796
  /**
@@ -2052,9 +2049,16 @@ var resolveRecordingHubHostname = require_hub_hostname.resolveHubHostname;
2052
2049
  * provider until the controller swaps the manifest entry in a later step — this
2053
2050
  * file ships dormant (separate tree, not wired into the manifest yet).
2054
2051
  */
2052
+ /** Default designated recording node (`recordingNodeId` global setting). The
2053
+ * stream-broker is hub-only and recording is a single-node global function, so
2054
+ * the hub is the safe default — mirrors pipeline-analytics' post-processing
2055
+ * node gate. */
2056
+ var RECORDING_NODE_DEFAULT = "hub";
2055
2057
  var DEFAULT_CONFIG = {
2056
2058
  segmentSeconds: 10,
2057
- watchIntervalMs: 2e3
2059
+ watchIntervalMs: 2e3,
2060
+ recordingNodeId: RECORDING_NODE_DEFAULT,
2061
+ recordingHubHostname: ""
2058
2062
  };
2059
2063
  /** Playback-marker window padding around a motion event (EventMap only; the
2060
2064
  * controller's events-band gating uses the raw trigger ms, not this pad).
@@ -2069,11 +2073,6 @@ var MARKER_PAD_POST_MS = require_dist.EVENT_PAD_MS.postMs;
2069
2073
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
2070
2074
  var RECORDER_ADDON_ID = "recorder";
2071
2075
  var PLAYBACK_PREFIX = "playback";
2072
- /** Default designated recording node (`recordingNodeId` global setting). The
2073
- * stream-broker is hub-only and recording is a single-node global function, so
2074
- * the hub is the safe default — mirrors pipeline-analytics' post-processing
2075
- * node gate. */
2076
- var RECORDING_NODE_DEFAULT = "hub";
2077
2076
  /** Node the hub-only `stream-broker` runs on. Every recorder — on the hub or on
2078
2077
  * a designated agent — gates on, and pulls from, THIS node's broker. */
2079
2078
  var BROKER_HOST_NODE_ID = "hub";
@@ -2101,9 +2100,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2101
2100
  async onInitialize() {
2102
2101
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
2103
2102
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
2104
- const store = await this.ctx.settings?.readAddonStore() ?? {};
2105
- const rawNode = store["recordingNodeId"];
2106
- const designated = typeof rawNode === "string" && rawNode.length > 0 ? rawNode : RECORDING_NODE_DEFAULT;
2103
+ const configuredNode = this.config.recordingNodeId;
2104
+ const designated = configuredNode.length > 0 ? configuredNode : RECORDING_NODE_DEFAULT;
2107
2105
  this.isRecordingNode = this.nodeId === designated;
2108
2106
  if (!this.isRecordingNode) {
2109
2107
  this.ctx.logger.info("recorder INERT on this node — recording assigned elsewhere", { meta: {
@@ -2112,8 +2110,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2112
2110
  } });
2113
2111
  return { providers: [] };
2114
2112
  }
2115
- const rawHubHostname = store["recordingHubHostname"];
2116
- const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(typeof rawHubHostname === "string" ? rawHubHostname : void 0, process.env["CAMSTACK_HUB_URL"]);
2113
+ const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2117
2114
  if (this.nodeId !== BROKER_HOST_NODE_ID) {
2118
2115
  this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
2119
2116
  ownNodeId: this.nodeId,
@@ -2318,7 +2315,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2318
2315
  * Global settings surfaced in the admin UI (and persisted to the addon
2319
2316
  * store). The cluster Recording tab drives `recordingNodeId` via a node
2320
2317
  * picker; `recordingHubHostname` is an advanced override for cross-node
2321
- * pulls. Both take effect on addon restart (read once in `onInitialize`).
2318
+ * pulls. Both are SHARED cluster keys (not perNode) hydrated into
2319
+ * `this.config` by `resolveConfig()` and take effect on addon restart
2320
+ * (read once in `onInitialize`).
2322
2321
  */
2323
2322
  globalSettingsSchema() {
2324
2323
  return this.schema({ sections: [{
@@ -2341,14 +2340,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2341
2340
  }]
2342
2341
  }] });
2343
2342
  }
2344
- /** The addon-store slice config-store reads/writes the deviceId→config blob on. */
2343
+ /** Durable handle config-store reads/writes the deviceId→config blob on
2344
+ * the sanctioned `BaseAddon.state()` wrapper (same key, same blob shape;
2345
+ * adds the read-retry budget + parse-warning logging over the raw slice). */
2345
2346
  configStore() {
2346
- const settings = this.ctx.settings;
2347
- if (!settings) throw new Error("RecorderV2Addon: ctx.settings unavailable — cannot persist recording config");
2348
- return {
2349
- readAddonStore: () => settings.readAddonStore(),
2350
- writeAddonStore: (patch) => settings.writeAddonStore(patch)
2351
- };
2347
+ return this.state(RECORDING_CONFIGS_KEY, RecordingConfigsBlobSchema, {});
2352
2348
  }
2353
2349
  };
2354
2350
  //#endregion
@@ -1,4 +1,4 @@
1
- import { B as errMsg, G as EventCategory, I as storageEvictableCapability, P as recordingCapability, V as BaseAddon, W as DeviceType, Y as hydrateSchema, c as EVENT_PAD_MS, dt as string, et as selectAssignedProfileSlots, f as RecordingConfigSchema, k as migrateConfigToBands, q as createDurableState, ut as record } from "../dist-CgEP_0OL.mjs";
1
+ import { B as errMsg, G as EventCategory, I as storageEvictableCapability, P as recordingCapability, Q as selectAssignedProfileSlots, V as BaseAddon, W as DeviceType, c as EVENT_PAD_MS, ct as record, f as RecordingConfigSchema, k as migrateConfigToBands, lt as string, q as hydrateSchema } from "../dist-C6_wgXqF.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { promises } from "node:fs";
@@ -725,9 +725,15 @@ function buildRecordingDeviceSchema() {
725
725
  * (stringified) numeric deviceId — enumerable for boot hydrate (so the addon
726
726
  * knows which devices have persisted footage to index without scanning every
727
727
  * device store). The whole Zod-validated blob round-trips on every read/write
728
- * via a {@link createDurableState} handle, so no per-device field can be
729
- * silently dropped on persist. A corrupt blob reads back as EMPTY rather than
730
- * crashing boot.
728
+ * via a {@link DurableState} handle, so no per-device field can be silently
729
+ * dropped on persist. A corrupt blob reads back as EMPTY rather than crashing
730
+ * boot.
731
+ *
732
+ * P3b settings-API migration: the functions here consume the durable handle
733
+ * directly — in production the recorder addon builds it via the sanctioned
734
+ * `BaseAddon.state()` (retry budget + validated fallback + parse-warning
735
+ * logging); {@link recordingConfigsState} adapts a raw store slice for test
736
+ * fakes. Same store key, same persisted blob shape as before.
731
737
  *
732
738
  * On read the legacy `mode`/`schedules`/`triggers` fields are derived once into
733
739
  * the authoritative `bands` via `migrateConfigToBands`, then persisted, so the
@@ -742,23 +748,14 @@ var DEFAULT_DEVICE_CONFIG = {
742
748
  enabled: false,
743
749
  bands: []
744
750
  };
745
- function blobState(store) {
746
- return createDurableState({
747
- key: RECORDING_CONFIGS_KEY,
748
- schema: RecordingConfigsBlobSchema,
749
- fallback: {},
750
- read: () => store.readAddonStore(),
751
- write: (patch) => store.writeAddonStore(patch)
752
- });
753
- }
754
751
  function serialize(map) {
755
752
  const out = {};
756
753
  for (const [id, config] of map) out[String(id)] = config;
757
754
  return out;
758
755
  }
759
756
  /** Read every persisted device config, keyed by numeric deviceId. */
760
- async function readDeviceConfigs(store) {
761
- const blob = await blobState(store).get();
757
+ async function readDeviceConfigs(state) {
758
+ const blob = await state.get();
762
759
  const map = /* @__PURE__ */ new Map();
763
760
  for (const [key, config] of Object.entries(blob)) {
764
761
  const id = Number(key);
@@ -772,15 +769,15 @@ async function readDeviceConfigs(store) {
772
769
  * re-persisted so the stored shape converges on the authoritative model. An
773
770
  * unconfigured device returns {@link DEFAULT_DEVICE_CONFIG}.
774
771
  */
775
- async function loadDeviceConfig(store, deviceId) {
776
- const stored = (await readDeviceConfigs(store)).get(deviceId);
772
+ async function loadDeviceConfig(state, deviceId) {
773
+ const stored = (await readDeviceConfigs(state)).get(deviceId);
777
774
  if (!stored) return DEFAULT_DEVICE_CONFIG;
778
775
  if (stored.bands) return stored;
779
776
  const migrated = {
780
777
  ...stored,
781
778
  bands: migrateConfigToBands(stored)
782
779
  };
783
- await saveDeviceConfig(store, deviceId, migrated);
780
+ await saveDeviceConfig(state, deviceId, migrated);
784
781
  return migrated;
785
782
  }
786
783
  /**
@@ -788,11 +785,11 @@ async function loadDeviceConfig(store, deviceId) {
788
785
  * `RecordingConfigSchema` at the boundary (untrusted cap input). Returns the
789
786
  * validated config that was persisted.
790
787
  */
791
- async function saveDeviceConfig(store, deviceId, config) {
788
+ async function saveDeviceConfig(state, deviceId, config) {
792
789
  const validated = RecordingConfigSchema.parse(config);
793
- const map = await readDeviceConfigs(store);
790
+ const map = await readDeviceConfigs(state);
794
791
  map.set(deviceId, validated);
795
- await blobState(store).set(serialize(map));
792
+ await state.set(serialize(map));
796
793
  return validated;
797
794
  }
798
795
  /**
@@ -2051,9 +2048,16 @@ var resolveRecordingHubHostname = resolveHubHostname;
2051
2048
  * provider until the controller swaps the manifest entry in a later step — this
2052
2049
  * file ships dormant (separate tree, not wired into the manifest yet).
2053
2050
  */
2051
+ /** Default designated recording node (`recordingNodeId` global setting). The
2052
+ * stream-broker is hub-only and recording is a single-node global function, so
2053
+ * the hub is the safe default — mirrors pipeline-analytics' post-processing
2054
+ * node gate. */
2055
+ var RECORDING_NODE_DEFAULT = "hub";
2054
2056
  var DEFAULT_CONFIG = {
2055
2057
  segmentSeconds: 10,
2056
- watchIntervalMs: 2e3
2058
+ watchIntervalMs: 2e3,
2059
+ recordingNodeId: RECORDING_NODE_DEFAULT,
2060
+ recordingHubHostname: ""
2057
2061
  };
2058
2062
  /** Playback-marker window padding around a motion event (EventMap only; the
2059
2063
  * controller's events-band gating uses the raw trigger ms, not this pad).
@@ -2068,11 +2072,6 @@ var MARKER_PAD_POST_MS = EVENT_PAD_MS.postMs;
2068
2072
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
2069
2073
  var RECORDER_ADDON_ID = "recorder";
2070
2074
  var PLAYBACK_PREFIX = "playback";
2071
- /** Default designated recording node (`recordingNodeId` global setting). The
2072
- * stream-broker is hub-only and recording is a single-node global function, so
2073
- * the hub is the safe default — mirrors pipeline-analytics' post-processing
2074
- * node gate. */
2075
- var RECORDING_NODE_DEFAULT = "hub";
2076
2075
  /** Node the hub-only `stream-broker` runs on. Every recorder — on the hub or on
2077
2076
  * a designated agent — gates on, and pulls from, THIS node's broker. */
2078
2077
  var BROKER_HOST_NODE_ID = "hub";
@@ -2100,9 +2099,8 @@ var RecorderV2Addon = class extends BaseAddon {
2100
2099
  async onInitialize() {
2101
2100
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
2102
2101
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
2103
- const store = await this.ctx.settings?.readAddonStore() ?? {};
2104
- const rawNode = store["recordingNodeId"];
2105
- const designated = typeof rawNode === "string" && rawNode.length > 0 ? rawNode : RECORDING_NODE_DEFAULT;
2102
+ const configuredNode = this.config.recordingNodeId;
2103
+ const designated = configuredNode.length > 0 ? configuredNode : RECORDING_NODE_DEFAULT;
2106
2104
  this.isRecordingNode = this.nodeId === designated;
2107
2105
  if (!this.isRecordingNode) {
2108
2106
  this.ctx.logger.info("recorder INERT on this node — recording assigned elsewhere", { meta: {
@@ -2111,8 +2109,7 @@ var RecorderV2Addon = class extends BaseAddon {
2111
2109
  } });
2112
2110
  return { providers: [] };
2113
2111
  }
2114
- const rawHubHostname = store["recordingHubHostname"];
2115
- const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(typeof rawHubHostname === "string" ? rawHubHostname : void 0, process.env["CAMSTACK_HUB_URL"]);
2112
+ const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2116
2113
  if (this.nodeId !== BROKER_HOST_NODE_ID) {
2117
2114
  this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
2118
2115
  ownNodeId: this.nodeId,
@@ -2317,7 +2314,9 @@ var RecorderV2Addon = class extends BaseAddon {
2317
2314
  * Global settings surfaced in the admin UI (and persisted to the addon
2318
2315
  * store). The cluster Recording tab drives `recordingNodeId` via a node
2319
2316
  * picker; `recordingHubHostname` is an advanced override for cross-node
2320
- * pulls. Both take effect on addon restart (read once in `onInitialize`).
2317
+ * pulls. Both are SHARED cluster keys (not perNode) hydrated into
2318
+ * `this.config` by `resolveConfig()` and take effect on addon restart
2319
+ * (read once in `onInitialize`).
2321
2320
  */
2322
2321
  globalSettingsSchema() {
2323
2322
  return this.schema({ sections: [{
@@ -2340,14 +2339,11 @@ var RecorderV2Addon = class extends BaseAddon {
2340
2339
  }]
2341
2340
  }] });
2342
2341
  }
2343
- /** The addon-store slice config-store reads/writes the deviceId→config blob on. */
2342
+ /** Durable handle config-store reads/writes the deviceId→config blob on
2343
+ * the sanctioned `BaseAddon.state()` wrapper (same key, same blob shape;
2344
+ * adds the read-retry budget + parse-warning logging over the raw slice). */
2344
2345
  configStore() {
2345
- const settings = this.ctx.settings;
2346
- if (!settings) throw new Error("RecorderV2Addon: ctx.settings unavailable — cannot persist recording config");
2347
- return {
2348
- readAddonStore: () => settings.readAddonStore(),
2349
- writeAddonStore: (patch) => settings.writeAddonStore(patch)
2350
- };
2346
+ return this.state(RECORDING_CONFIGS_KEY, RecordingConfigsBlobSchema, {});
2351
2347
  }
2352
2348
  };
2353
2349
  //#endregion
@@ -1,8 +1,8 @@
1
- import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs";
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs";
2
2
  import { a as u, i as d, n as f, o as p, r as m, t as h } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
4
  import { n as y, t as b } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
5
- import { t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs";
5
+ import { t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var S = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), C = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), w = (e) => {
8
8
  let t = C(e);