@camstack/addon-pipeline-orchestrator 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.
package/dist/index.js CHANGED
@@ -4631,7 +4631,7 @@ function _instanceof(cls, params = {}) {
4631
4631
  return inst;
4632
4632
  }
4633
4633
  //#endregion
4634
- //#region ../types/dist/sleep-BiDFW0E7.mjs
4634
+ //#region ../types/dist/sleep-CZDdRBua.mjs
4635
4635
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4636
4636
  EventCategory["SystemBoot"] = "system.boot";
4637
4637
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7638,7 +7638,16 @@ var DecoderStatsSchema = object({
7638
7638
  inputFps: number(),
7639
7639
  outputFps: number(),
7640
7640
  avgDecodeTimeMs: number(),
7641
- droppedFrames: number()
7641
+ droppedFrames: number(),
7642
+ /**
7643
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
7644
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
7645
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
7646
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
7647
+ */
7648
+ lagMs: number().optional(),
7649
+ effectiveFps: number().optional(),
7650
+ adaptiveFps: number().optional()
7642
7651
  });
7643
7652
  var DecoderSessionConfigSchema = object({
7644
7653
  codec: string(),
@@ -7679,7 +7688,15 @@ var DecoderSessionConfigSchema = object({
7679
7688
  * other — `pullFrames` returns nothing for an `'shm'` session and
7680
7689
  * `pullHandles` returns nothing for a `'callback'` session.
7681
7690
  */
7682
- frameSink: _enum(["callback", "shm"]).default("callback")
7691
+ frameSink: _enum(["callback", "shm"]).default("callback"),
7692
+ /**
7693
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
7694
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
7695
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
7696
+ * stream-broker's `streamingDebug` gate — off by default so production logs
7697
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
7698
+ */
7699
+ debug: boolean().optional()
7683
7700
  });
7684
7701
  var EncodeProfileSchema = object({
7685
7702
  video: object({
@@ -9898,6 +9915,75 @@ DeviceType.Cover, method(object({ deviceId: number().int().nonnegative() }), _vo
9898
9915
  auth: "admin"
9899
9916
  });
9900
9917
  /**
9918
+ * Vendor-neutral day/night (IR-cut) control — the per-camera config cap
9919
+ * shared by reolink / hikvision / amcrest. Models the common firmware
9920
+ * surface: the IR-cut switching MODE plus the two knobs that gate it
9921
+ * (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
9922
+ * onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
9923
+ * the shape so ONE derived-form renders every camera.
9924
+ *
9925
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
9926
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
9927
+ * injected from `status`) reports the live values, and a single
9928
+ * `setSettings` mutation applies a partial change. No hand-written
9929
+ * settings-contribution methods — the framework derives the UI + save
9930
+ * routing from this surface.
9931
+ */
9932
+ /** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
9933
+ var DayNightModeSchema = _enum([
9934
+ "auto",
9935
+ "day",
9936
+ "night",
9937
+ "schedule"
9938
+ ]);
9939
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
9940
+ * getOptions availability convention. Normalized values are 0–100. */
9941
+ var NormalizedRangeSchema$1 = object({
9942
+ min: number(),
9943
+ max: number(),
9944
+ step: number()
9945
+ });
9946
+ object({
9947
+ mode: DayNightModeSchema,
9948
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
9949
+ sensitivity: number().optional(),
9950
+ /** Delay before the IR-cut filter flips, in seconds. */
9951
+ switchDelaySec: number().optional(),
9952
+ lastFetchedAt: number()
9953
+ });
9954
+ /**
9955
+ * Per-camera availability descriptor — drives which controls the admin UI
9956
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
9957
+ * (normalized 0–100); the mode choice-set as an array. A provider returns
9958
+ * honest, camera-probed values — never hardcoded.
9959
+ */
9960
+ var DayNightOptionsSchema = object({
9961
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
9962
+ modes: array(DayNightModeSchema),
9963
+ supportsSensitivity: boolean(),
9964
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
9965
+ sensitivity: NormalizedRangeSchema$1.optional(),
9966
+ supportsSwitchDelay: boolean(),
9967
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
9968
+ switchDelaySec: NormalizedRangeSchema$1.optional()
9969
+ });
9970
+ /**
9971
+ * Partial change to the day/night config — every field optional. A
9972
+ * provider ignores fields it does not support.
9973
+ */
9974
+ var DayNightSettingsPatchSchema = object({
9975
+ mode: DayNightModeSchema.optional(),
9976
+ sensitivity: number().optional(),
9977
+ switchDelaySec: number().optional()
9978
+ });
9979
+ DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema), method(object({
9980
+ deviceId: number(),
9981
+ settings: DayNightSettingsPatchSchema
9982
+ }), _void(), {
9983
+ kind: "mutation",
9984
+ auth: "admin"
9985
+ });
9986
+ /**
9901
9987
  * Identity envelope for a device's upstream-system metadata.
9902
9988
  *
9903
9989
  * Two jobs:
@@ -10253,6 +10339,130 @@ object({
10253
10339
  });
10254
10340
  DeviceType.Image;
10255
10341
  /**
10342
+ * Vendor-neutral image / picture-adjustment cap — the per-camera config
10343
+ * cap shared by reolink / hikvision / amcrest. Models the common ISP
10344
+ * surface: the four picture sliders (brightness / contrast / saturation /
10345
+ * sharpness), orientation (mirror / flip / rotate), white-balance,
10346
+ * exposure and backlight-compensation modes.
10347
+ *
10348
+ * NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
10349
+ * these natively as 0–100 or 0–255 (or other ranges); each provider maps
10350
+ * its native range to/from this normalized 0–100 space so the cap surface
10351
+ * (and the derived form) is identical across cameras. `warmth` (manual
10352
+ * white-balance) is likewise normalized 0–100.
10353
+ *
10354
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
10355
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
10356
+ * injected from `status`) reports the live values, and a single
10357
+ * `setSettings` mutation applies a partial change. No hand-written
10358
+ * settings-contribution methods — the framework derives the UI + save
10359
+ * routing from this surface.
10360
+ */
10361
+ /** Sensor/image rotation, degrees clockwise. */
10362
+ var ImageRotateSchema = _enum([
10363
+ "0",
10364
+ "90",
10365
+ "180",
10366
+ "270"
10367
+ ]);
10368
+ /** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
10369
+ var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
10370
+ /** Exposure mode. */
10371
+ var ExposureModeSchema = _enum(["auto", "manual"]);
10372
+ /**
10373
+ * Backlight-compensation mode:
10374
+ * - `off` — disabled
10375
+ * - `blc` — backlight compensation
10376
+ * - `wdr` — wide dynamic range
10377
+ * - `hlc` — highlight compensation
10378
+ */
10379
+ var BacklightModeSchema = _enum([
10380
+ "off",
10381
+ "blc",
10382
+ "wdr",
10383
+ "hlc"
10384
+ ]);
10385
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
10386
+ * getOptions availability convention. Slider values are normalized 0–100. */
10387
+ var NormalizedRangeSchema = object({
10388
+ min: number(),
10389
+ max: number(),
10390
+ step: number()
10391
+ });
10392
+ object({
10393
+ /** Normalized 0–100. */
10394
+ brightness: number().optional(),
10395
+ /** Normalized 0–100. */
10396
+ contrast: number().optional(),
10397
+ /** Normalized 0–100. */
10398
+ saturation: number().optional(),
10399
+ /** Normalized 0–100. */
10400
+ sharpness: number().optional(),
10401
+ mirror: boolean().optional(),
10402
+ flip: boolean().optional(),
10403
+ rotate: ImageRotateSchema.optional(),
10404
+ whiteBalance: WhiteBalanceModeSchema.optional(),
10405
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
10406
+ warmth: number().optional(),
10407
+ exposureMode: ExposureModeSchema.optional(),
10408
+ backlightMode: BacklightModeSchema.optional(),
10409
+ lastFetchedAt: number()
10410
+ });
10411
+ /**
10412
+ * Per-camera availability descriptor — drives which controls the admin UI
10413
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
10414
+ * (the normalized 0–100 range); enums as arrays of supported values (empty
10415
+ * array → control hidden). A provider returns honest, camera-probed values
10416
+ * — never hardcoded.
10417
+ */
10418
+ var ImageSettingsOptionsSchema = object({
10419
+ supportsBrightness: boolean(),
10420
+ brightness: NormalizedRangeSchema.optional(),
10421
+ supportsContrast: boolean(),
10422
+ contrast: NormalizedRangeSchema.optional(),
10423
+ supportsSaturation: boolean(),
10424
+ saturation: NormalizedRangeSchema.optional(),
10425
+ supportsSharpness: boolean(),
10426
+ sharpness: NormalizedRangeSchema.optional(),
10427
+ supportsMirror: boolean(),
10428
+ supportsFlip: boolean(),
10429
+ /** Supported rotation values. Empty → rotation not configurable. */
10430
+ rotateOptions: array(ImageRotateSchema),
10431
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
10432
+ whiteBalanceModes: array(WhiteBalanceModeSchema),
10433
+ supportsWarmth: boolean(),
10434
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
10435
+ warmth: NormalizedRangeSchema.optional(),
10436
+ /** Supported exposure modes. Empty → exposure not configurable. */
10437
+ exposureModes: array(ExposureModeSchema),
10438
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
10439
+ backlightModes: array(BacklightModeSchema)
10440
+ });
10441
+ /**
10442
+ * Partial change to the image config — every field optional. Slider values
10443
+ * are normalized 0–100. A provider ignores fields it does not support.
10444
+ */
10445
+ var ImageSettingsPatchSchema = object({
10446
+ brightness: number().optional(),
10447
+ contrast: number().optional(),
10448
+ saturation: number().optional(),
10449
+ sharpness: number().optional(),
10450
+ mirror: boolean().optional(),
10451
+ flip: boolean().optional(),
10452
+ rotate: ImageRotateSchema.optional(),
10453
+ whiteBalance: WhiteBalanceModeSchema.optional(),
10454
+ warmth: number().optional(),
10455
+ exposureMode: ExposureModeSchema.optional(),
10456
+ backlightMode: BacklightModeSchema.optional()
10457
+ });
10458
+ DeviceType.Camera, method(object({ deviceId: number() }), ImageSettingsOptionsSchema), method(object({
10459
+ deviceId: number(),
10460
+ settings: ImageSettingsPatchSchema
10461
+ }), _void(), {
10462
+ kind: "mutation",
10463
+ auth: "admin"
10464
+ });
10465
+ /**
10256
10466
  * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10257
10467
  * with a mowing lifecycle plus a dock action.
10258
10468
  *
@@ -11315,6 +11525,16 @@ var RunnerCameraConfigSchema = object({
11315
11525
  * this gate is bypassed.
11316
11526
  */
11317
11527
  onboardMotionDrivesAnalyzer: boolean().default(true),
11528
+ /**
11529
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
11530
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
11531
+ * this is off by default because the recheck re-subscribes a detection session
11532
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
11533
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
11534
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
11535
+ * (and only render) when this is enabled.
11536
+ */
11537
+ occupancyRecheckEnabled: boolean().default(false),
11318
11538
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
11319
11539
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
11320
11540
  /**
@@ -11365,6 +11585,8 @@ var RunnerCameraDeviceUIFields = [
11365
11585
  default: motionFpsField.default,
11366
11586
  showValue: true,
11367
11587
  unit: "fps",
11588
+ nullable: true,
11589
+ nullLabel: "Default",
11368
11590
  showWhen: {
11369
11591
  field: "motionSources",
11370
11592
  includes: "analyzer"
@@ -11379,7 +11601,9 @@ var RunnerCameraDeviceUIFields = [
11379
11601
  step: detectionFpsField.step,
11380
11602
  default: detectionFpsField.default,
11381
11603
  showValue: true,
11382
- unit: "fps"
11604
+ unit: "fps",
11605
+ nullable: true,
11606
+ nullLabel: "Default"
11383
11607
  },
11384
11608
  {
11385
11609
  key: "motionCooldownMs",
@@ -11392,7 +11616,9 @@ var RunnerCameraDeviceUIFields = [
11392
11616
  default: motionCooldownMsField.default,
11393
11617
  showValue: true,
11394
11618
  unit: "s",
11395
- displayScale: 1e3
11619
+ displayScale: 1e3,
11620
+ nullable: true,
11621
+ nullLabel: "Default"
11396
11622
  },
11397
11623
  {
11398
11624
  key: "onboardMotionDrivesAnalyzer",
@@ -11405,17 +11631,29 @@ var RunnerCameraDeviceUIFields = [
11405
11631
  includes: "onboard"
11406
11632
  }
11407
11633
  },
11634
+ {
11635
+ key: "occupancyRecheckEnabled",
11636
+ type: "boolean",
11637
+ style: "checkbox",
11638
+ label: "Occupancy re-check",
11639
+ description: "Periodically re-sample a few frames during the watching phase to confirm the scene is truly empty (catches stationary objects motion-gating would miss). Off by default — it adds decoder re-dial churn.",
11640
+ default: false
11641
+ },
11408
11642
  {
11409
11643
  key: "occupancyRecheckSec",
11410
11644
  type: "slider",
11411
11645
  label: "Occupancy re-check interval",
11412
- description: "How often (in seconds) the runner re-samples a few frames to confirm the scene is truly empty during the watching phase. 0 = disabled.",
11646
+ description: "How often (in seconds) the runner re-samples a few frames to confirm the scene is truly empty during the watching phase.",
11413
11647
  min: occupancyRecheckSecField.min,
11414
11648
  max: occupancyRecheckSecField.max,
11415
11649
  step: occupancyRecheckSecField.step,
11416
11650
  default: occupancyRecheckSecField.default,
11417
11651
  showValue: true,
11418
- unit: "s"
11652
+ unit: "s",
11653
+ showWhen: {
11654
+ field: "occupancyRecheckEnabled",
11655
+ equals: true
11656
+ }
11419
11657
  },
11420
11658
  {
11421
11659
  key: "occupancyRecheckFrames",
@@ -11426,7 +11664,11 @@ var RunnerCameraDeviceUIFields = [
11426
11664
  max: occupancyRecheckFramesField.max,
11427
11665
  step: occupancyRecheckFramesField.step,
11428
11666
  default: occupancyRecheckFramesField.default,
11429
- showValue: true
11667
+ showValue: true,
11668
+ showWhen: {
11669
+ field: "occupancyRecheckEnabled",
11670
+ equals: true
11671
+ }
11430
11672
  }
11431
11673
  ];
11432
11674
  /**
@@ -20005,6 +20247,18 @@ Object.freeze({
20005
20247
  addonId: null,
20006
20248
  access: "view"
20007
20249
  },
20250
+ "dayNight.getOptions": {
20251
+ capName: "day-night",
20252
+ capScope: "device",
20253
+ addonId: null,
20254
+ access: "view"
20255
+ },
20256
+ "dayNight.setSettings": {
20257
+ capName: "day-night",
20258
+ capScope: "device",
20259
+ addonId: null,
20260
+ access: "create"
20261
+ },
20008
20262
  "decoder.createSession": {
20009
20263
  capName: "decoder",
20010
20264
  capScope: "system",
@@ -20935,6 +21189,18 @@ Object.freeze({
20935
21189
  addonId: null,
20936
21190
  access: "create"
20937
21191
  },
21192
+ "imageSettings.getOptions": {
21193
+ capName: "image-settings",
21194
+ capScope: "device",
21195
+ addonId: null,
21196
+ access: "view"
21197
+ },
21198
+ "imageSettings.setSettings": {
21199
+ capName: "image-settings",
21200
+ capScope: "device",
21201
+ addonId: null,
21202
+ access: "create"
21203
+ },
20938
21204
  "integrations.create": {
20939
21205
  capName: "integrations",
20940
21206
  capScope: "system",
@@ -24514,7 +24780,8 @@ var DEFAULT_WATCHDOG_THRESHOLDS = {
24514
24780
  audioMs: 9e4,
24515
24781
  motionMs: 3e4,
24516
24782
  detectionMs: 6e4,
24517
- maxRecoveryAttempts: 3
24783
+ maxRecoveryAttempts: 3,
24784
+ exhaustedRetryMs: 6e4
24518
24785
  };
24519
24786
  var STAGE_THRESHOLD_KEY = {
24520
24787
  audio: "audioMs",
@@ -24535,7 +24802,8 @@ var PipelineWatchdog = class {
24535
24802
  const stages = /* @__PURE__ */ new Map();
24536
24803
  for (const [stage] of cam.continuousStages) stages.set(stage, {
24537
24804
  lastSeenMs: now,
24538
- attempts: 0
24805
+ attempts: 0,
24806
+ lastAttemptMs: 0
24539
24807
  });
24540
24808
  this.state.set(cam.deviceId, stages);
24541
24809
  }
@@ -24558,7 +24826,10 @@ var PipelineWatchdog = class {
24558
24826
  else this.deps.logger.info(line);
24559
24827
  for (const r of recoveries) {
24560
24828
  const rt = this.state.get(cam.deviceId)?.get(r.stage);
24561
- if (rt) rt.attempts += 1;
24829
+ if (rt) {
24830
+ if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
24831
+ rt.lastAttemptMs = now;
24832
+ }
24562
24833
  this.deps.recover(cam.deviceId, r.stage, r.streamId);
24563
24834
  }
24564
24835
  }
@@ -24602,7 +24873,13 @@ var PipelineWatchdog = class {
24602
24873
  if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
24603
24874
  else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
24604
24875
  stalled = true;
24605
- parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts})`);
24876
+ if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
24877
+ recoveries.push({
24878
+ stage,
24879
+ streamId
24880
+ });
24881
+ parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts} · retrying)`);
24882
+ } else parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts})`);
24606
24883
  } else {
24607
24884
  stalled = true;
24608
24885
  recoveries.push({
@@ -24954,6 +25231,24 @@ function isPipelinePhaseMode(v) {
24954
25231
  var PREFERRED_AGENT_SETTING = "preferredAgent";
24955
25232
  /** Key under which the orchestrator stores the per-device audio node pin. */
24956
25233
  var AUDIO_NODE_SETTING = "audioNodeId";
25234
+ /**
25235
+ * Target PCM window duration, in ms, for audio classification.
25236
+ *
25237
+ * Sized for the ~1 s inference window the downstream classifiers expect —
25238
+ * YAMNet consumes a 0.975 s frame and Apple SoundAnalysis a ~1 s frame.
25239
+ * The broker's `AudioCodecSession` no longer batches (its 500 ms
25240
+ * accumulator was removed for WebRTC latency), so decoded codec chunks now
25241
+ * arrive at ~21–64 ms each — ~16–48 chunks per second per camera. Feeding
25242
+ * each tiny chunk to the analyzer meant one cross-process `analyseChunk`
25243
+ * RPC + one `PipelineAudioInferenceResult` event PER chunk for a classifier
25244
+ * that (a) throttles to ~2 Hz internally anyway and (b) needs a ~1 s window
25245
+ * for accurate inference. We instead accumulate raw PCM per device and flush
25246
+ * ONE classify + ONE event per ~1 s window: ~1 RPC/event/s/camera and a
25247
+ * correctly-sized inference frame.
25248
+ */
25249
+ var AUDIO_WINDOW_TARGET_MS = 1e3;
25250
+ /** f32le PCM = 4 bytes per sample. */
25251
+ var F32_BYTES_PER_SAMPLE = 4;
24957
25252
  var DEFAULT_BROKER_CALL_TIMEOUT_MS = 5e3;
24958
25253
  /** Debounce window for `scheduleReconcile` — coalesces bursts of topology/slot-change signals. */
24959
25254
  var RECONCILE_DEBOUNCE_MS = 200;
@@ -25006,7 +25301,6 @@ var OrchestratorDiagnosticsSchema = object({
25006
25301
  enabledAudioNodes: array(string()),
25007
25302
  clusterRoles: object({
25008
25303
  ingestNode: string(),
25009
- recordingNode: string(),
25010
25304
  audioNode: string()
25011
25305
  }),
25012
25306
  assignedDeviceCount: number().int().min(0),
@@ -25167,12 +25461,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25167
25461
  /**
25168
25462
  * Cluster-wide singleton role assignments (placement-model §6). Each role
25169
25463
  * is the ONE node that serves it for every camera. Driven by the
25170
- * `ingestNode` / `recordingNode` / `audioNode` node-selects in the addon
25171
- * global schema; defaults to the hub for all three.
25464
+ * `ingestNode` / `audioNode` node-selects in the addon global schema;
25465
+ * defaults to the hub for both. NOTE: the recording node is NOT a role here
25466
+ * — the `recorder` addon owns it as the single source of truth (its
25467
+ * `recordingNodeId` global setting, obeyed by the recorder's gate). The
25468
+ * Pipeline placement board's "Recording" picker reads/writes that canonical
25469
+ * setting directly via `addonSettings`, not an orchestrator-owned duplicate.
25172
25470
  */
25173
25471
  clusterRoles = {
25174
25472
  ingestNode: "hub",
25175
- recordingNode: "hub",
25176
25473
  audioNode: "hub"
25177
25474
  };
25178
25475
  /**
@@ -28146,32 +28443,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28146
28443
  title: "Cluster Roles",
28147
28444
  tab: "pipeline",
28148
28445
  description: "Cluster-wide singleton roles: the single node that serves each role for every camera. Per-node detection/decode/audio CAPABILITIES + weights + camera caps are edited in the per-node capability table (agent settings), not here.",
28149
- fields: [
28150
- {
28151
- key: "ingestNode",
28152
- type: "node-select",
28153
- label: "Ingest Node",
28154
- description: "The node that connects every camera and serves the compressed restream. Defaults to the hub. (Arbitrary ingest ≠ hub is a later phase; kept here so the role is modeled.)",
28155
- default: "hub",
28156
- showOffline: true
28157
- },
28158
- {
28159
- key: "recordingNode",
28160
- type: "node-select",
28161
- label: "Recording Node",
28162
- description: "The node that records every camera (passthrough copy to its storage location). Defaults to the hub.",
28163
- default: "hub",
28164
- showOffline: true
28165
- },
28166
- {
28167
- key: "audioNode",
28168
- type: "node-select",
28169
- label: "Audio Node",
28170
- description: "The node that runs all audio analysis. Defaults to the hub. Must be an audio-capable node.",
28171
- default: "hub",
28172
- showOffline: true
28173
- }
28174
- ]
28446
+ fields: [{
28447
+ key: "ingestNode",
28448
+ type: "node-select",
28449
+ label: "Ingest Node",
28450
+ description: "The node that connects every camera and serves the compressed restream. Defaults to the hub. (Arbitrary ingest ≠ hub is a later phase; kept here so the role is modeled.)",
28451
+ default: "hub",
28452
+ showOffline: true
28453
+ }, {
28454
+ key: "audioNode",
28455
+ type: "node-select",
28456
+ label: "Audio Node",
28457
+ description: "The node that runs all audio analysis. Defaults to the hub. Must be an audio-capable node.",
28458
+ default: "hub",
28459
+ showOffline: true
28460
+ }]
28175
28461
  },
28176
28462
  {
28177
28463
  id: "decoder",
@@ -28697,7 +28983,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28697
28983
  };
28698
28984
  this.clusterRoles = {
28699
28985
  ingestNode: readRole("ingestNode"),
28700
- recordingNode: readRole("recordingNode"),
28701
28986
  audioNode: readRole("audioNode")
28702
28987
  };
28703
28988
  this.schedulePendingRetry();
@@ -28872,6 +29157,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28872
29157
  if (typeof v !== "number") throw new Error(`orchestrator schema missing required number '${key}' for device ${deviceId}`);
28873
29158
  return v;
28874
29159
  };
29160
+ const numberOrDefault = (key) => {
29161
+ const v = flat[key];
29162
+ if (typeof v === "number") return v;
29163
+ const uiField = RunnerCameraDeviceUIFields.find((f) => f.key === key);
29164
+ const dflt = uiField && "default" in uiField ? uiField.default : void 0;
29165
+ return typeof dflt === "number" ? dflt : 0;
29166
+ };
28875
29167
  try {
28876
29168
  const features = await this.lookupDeviceFeatures(deviceId);
28877
29169
  const profile = resolveDeviceProfile(features);
@@ -28889,6 +29181,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28889
29181
  const audioMode = typeof userAudioMode === "string" && isPipelinePhaseMode(userAudioMode) ? userAudioMode : profile?.defaults.audioMode ?? "always-on";
28890
29182
  const userOnboardMotionDrivesAnalyzer = raw["onboardMotionDrivesAnalyzer"];
28891
29183
  const onboardMotionDrivesAnalyzer = typeof userOnboardMotionDrivesAnalyzer === "boolean" ? userOnboardMotionDrivesAnalyzer : true;
29184
+ const userOccupancyRecheckEnabled = raw["occupancyRecheckEnabled"];
29185
+ const occupancyRecheckEnabled = typeof userOccupancyRecheckEnabled === "boolean" ? userOccupancyRecheckEnabled : false;
28892
29186
  const occupancyRecheckSec = mustNumber("occupancyRecheckSec");
28893
29187
  const occupancyRecheckFrames = mustNumber("occupancyRecheckFrames");
28894
29188
  return {
@@ -28897,12 +29191,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28897
29191
  motionSources,
28898
29192
  motionStreamProfile: mustString("motionStreamProfile"),
28899
29193
  detectionStreamProfile: mustString("detectionStreamProfile"),
28900
- motionFps: mustNumber("motionFps"),
28901
- detectionFps: mustNumber("detectionFps"),
28902
- motionCooldownMs: mustNumber("motionCooldownMs"),
29194
+ motionFps: numberOrDefault("motionFps"),
29195
+ detectionFps: numberOrDefault("detectionFps"),
29196
+ motionCooldownMs: numberOrDefault("motionCooldownMs"),
28903
29197
  detectionMode,
28904
29198
  audioMode,
28905
29199
  onboardMotionDrivesAnalyzer,
29200
+ occupancyRecheckEnabled,
28906
29201
  occupancyRecheckSec,
28907
29202
  occupancyRecheckFrames
28908
29203
  };
@@ -29013,6 +29308,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29013
29308
  detectionMode: resolved.detectionMode,
29014
29309
  audioMode: resolved.audioMode,
29015
29310
  onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer,
29311
+ occupancyRecheckEnabled: resolved.occupancyRecheckEnabled,
29016
29312
  occupancyRecheckSec: resolved.occupancyRecheckSec,
29017
29313
  occupancyRecheckFrames: resolved.occupancyRecheckFrames
29018
29314
  };
@@ -29047,6 +29343,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29047
29343
  detectionMode: config.detectionMode,
29048
29344
  audioMode: config.audioMode,
29049
29345
  onboardMotionDrivesAnalyzer: config.onboardMotionDrivesAnalyzer,
29346
+ occupancyRecheckEnabled: config.occupancyRecheckEnabled,
29050
29347
  occupancyRecheckSec: config.occupancyRecheckSec,
29051
29348
  occupancyRecheckFrames: config.occupancyRecheckFrames
29052
29349
  };
@@ -29212,6 +29509,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29212
29509
  if (a.motionCooldownMs !== b.motionCooldownMs) return false;
29213
29510
  if (a.audioStreamId !== b.audioStreamId) return false;
29214
29511
  if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
29512
+ if (a.occupancyRecheckEnabled !== b.occupancyRecheckEnabled) return false;
29215
29513
  if (a.occupancyRecheckSec !== b.occupancyRecheckSec) return false;
29216
29514
  if (a.occupancyRecheckFrames !== b.occupancyRecheckFrames) return false;
29217
29515
  if (a.motionSources.length !== b.motionSources.length) return false;
@@ -29741,6 +30039,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29741
30039
  isRemote: isRemoteAudio
29742
30040
  }
29743
30041
  });
30042
+ let pcmParts = [];
30043
+ let accumulatedBytes = 0;
30044
+ let accumulatedMs = 0;
30045
+ let windowSampleRate = 0;
30046
+ let windowChannels = 0;
30047
+ let windowTimestamp = 0;
30048
+ let windowOpen = false;
30049
+ const resetWindow = () => {
30050
+ pcmParts = [];
30051
+ accumulatedBytes = 0;
30052
+ accumulatedMs = 0;
30053
+ windowOpen = false;
30054
+ };
29744
30055
  const teardown = startAudioChunkPoller({
29745
30056
  api,
29746
30057
  brokerId: audioBrokerId,
@@ -29750,13 +30061,35 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29750
30061
  this.pipelineWatchdog?.noteSignal(deviceId, "audio");
29751
30062
  try {
29752
30063
  const byteLength = chunk.data.byteLength;
29753
- const data = new Uint8Array(byteLength);
29754
- data.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
30064
+ const bytes = new Uint8Array(byteLength);
30065
+ bytes.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
30066
+ if (!windowOpen) {
30067
+ windowSampleRate = chunk.sampleRate;
30068
+ windowChannels = chunk.channels;
30069
+ windowTimestamp = chunk.timestamp;
30070
+ windowOpen = true;
30071
+ }
30072
+ pcmParts.push(bytes);
30073
+ accumulatedBytes += byteLength;
30074
+ const channels = chunk.channels > 0 ? chunk.channels : 1;
30075
+ const framesPerChannel = byteLength / F32_BYTES_PER_SAMPLE / channels;
30076
+ accumulatedMs += framesPerChannel / chunk.sampleRate * 1e3;
30077
+ if (accumulatedMs < AUDIO_WINDOW_TARGET_MS) return;
30078
+ const windowData = new Uint8Array(accumulatedBytes);
30079
+ let offset = 0;
30080
+ for (const part of pcmParts) {
30081
+ windowData.set(part, offset);
30082
+ offset += part.byteLength;
30083
+ }
30084
+ const flushSampleRate = windowSampleRate;
30085
+ const flushChannels = windowChannels;
30086
+ const flushTimestamp = windowTimestamp;
30087
+ resetWindow();
29755
30088
  const audioChunkInput = {
29756
- data,
29757
- sampleRate: chunk.sampleRate,
29758
- channels: chunk.channels,
29759
- timestamp: chunk.timestamp,
30089
+ data: windowData,
30090
+ sampleRate: flushSampleRate,
30091
+ channels: flushChannels,
30092
+ timestamp: flushTimestamp,
29760
30093
  deviceId
29761
30094
  };
29762
30095
  const result = await api.audioAnalyzer.analyseChunk.mutate({
@@ -29833,6 +30166,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29833
30166
  this.ctx.logger.info("Audio stream subscribed", { tags: { deviceId } });
29834
30167
  return () => {
29835
30168
  teardown();
30169
+ resetWindow();
29836
30170
  };
29837
30171
  }
29838
30172
  };