@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.mjs CHANGED
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-BiDFW0E7.mjs
4630
+ //#region ../types/dist/sleep-CZDdRBua.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7634,7 +7634,16 @@ var DecoderStatsSchema = object({
7634
7634
  inputFps: number(),
7635
7635
  outputFps: number(),
7636
7636
  avgDecodeTimeMs: number(),
7637
- droppedFrames: number()
7637
+ droppedFrames: number(),
7638
+ /**
7639
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
7640
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
7641
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
7642
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
7643
+ */
7644
+ lagMs: number().optional(),
7645
+ effectiveFps: number().optional(),
7646
+ adaptiveFps: number().optional()
7638
7647
  });
7639
7648
  var DecoderSessionConfigSchema = object({
7640
7649
  codec: string(),
@@ -7675,7 +7684,15 @@ var DecoderSessionConfigSchema = object({
7675
7684
  * other — `pullFrames` returns nothing for an `'shm'` session and
7676
7685
  * `pullHandles` returns nothing for a `'callback'` session.
7677
7686
  */
7678
- frameSink: _enum(["callback", "shm"]).default("callback")
7687
+ frameSink: _enum(["callback", "shm"]).default("callback"),
7688
+ /**
7689
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
7690
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
7691
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
7692
+ * stream-broker's `streamingDebug` gate — off by default so production logs
7693
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
7694
+ */
7695
+ debug: boolean().optional()
7679
7696
  });
7680
7697
  var EncodeProfileSchema = object({
7681
7698
  video: object({
@@ -9894,6 +9911,75 @@ DeviceType.Cover, method(object({ deviceId: number().int().nonnegative() }), _vo
9894
9911
  auth: "admin"
9895
9912
  });
9896
9913
  /**
9914
+ * Vendor-neutral day/night (IR-cut) control — the per-camera config cap
9915
+ * shared by reolink / hikvision / amcrest. Models the common firmware
9916
+ * surface: the IR-cut switching MODE plus the two knobs that gate it
9917
+ * (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
9918
+ * onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
9919
+ * the shape so ONE derived-form renders every camera.
9920
+ *
9921
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
9922
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
9923
+ * injected from `status`) reports the live values, and a single
9924
+ * `setSettings` mutation applies a partial change. No hand-written
9925
+ * settings-contribution methods — the framework derives the UI + save
9926
+ * routing from this surface.
9927
+ */
9928
+ /** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
9929
+ var DayNightModeSchema = _enum([
9930
+ "auto",
9931
+ "day",
9932
+ "night",
9933
+ "schedule"
9934
+ ]);
9935
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
9936
+ * getOptions availability convention. Normalized values are 0–100. */
9937
+ var NormalizedRangeSchema$1 = object({
9938
+ min: number(),
9939
+ max: number(),
9940
+ step: number()
9941
+ });
9942
+ object({
9943
+ mode: DayNightModeSchema,
9944
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
9945
+ sensitivity: number().optional(),
9946
+ /** Delay before the IR-cut filter flips, in seconds. */
9947
+ switchDelaySec: number().optional(),
9948
+ lastFetchedAt: number()
9949
+ });
9950
+ /**
9951
+ * Per-camera availability descriptor — drives which controls the admin UI
9952
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
9953
+ * (normalized 0–100); the mode choice-set as an array. A provider returns
9954
+ * honest, camera-probed values — never hardcoded.
9955
+ */
9956
+ var DayNightOptionsSchema = object({
9957
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
9958
+ modes: array(DayNightModeSchema),
9959
+ supportsSensitivity: boolean(),
9960
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
9961
+ sensitivity: NormalizedRangeSchema$1.optional(),
9962
+ supportsSwitchDelay: boolean(),
9963
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
9964
+ switchDelaySec: NormalizedRangeSchema$1.optional()
9965
+ });
9966
+ /**
9967
+ * Partial change to the day/night config — every field optional. A
9968
+ * provider ignores fields it does not support.
9969
+ */
9970
+ var DayNightSettingsPatchSchema = object({
9971
+ mode: DayNightModeSchema.optional(),
9972
+ sensitivity: number().optional(),
9973
+ switchDelaySec: number().optional()
9974
+ });
9975
+ DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema), method(object({
9976
+ deviceId: number(),
9977
+ settings: DayNightSettingsPatchSchema
9978
+ }), _void(), {
9979
+ kind: "mutation",
9980
+ auth: "admin"
9981
+ });
9982
+ /**
9897
9983
  * Identity envelope for a device's upstream-system metadata.
9898
9984
  *
9899
9985
  * Two jobs:
@@ -10249,6 +10335,130 @@ object({
10249
10335
  });
10250
10336
  DeviceType.Image;
10251
10337
  /**
10338
+ * Vendor-neutral image / picture-adjustment cap — the per-camera config
10339
+ * cap shared by reolink / hikvision / amcrest. Models the common ISP
10340
+ * surface: the four picture sliders (brightness / contrast / saturation /
10341
+ * sharpness), orientation (mirror / flip / rotate), white-balance,
10342
+ * exposure and backlight-compensation modes.
10343
+ *
10344
+ * NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
10345
+ * these natively as 0–100 or 0–255 (or other ranges); each provider maps
10346
+ * its native range to/from this normalized 0–100 space so the cap surface
10347
+ * (and the derived form) is identical across cameras. `warmth` (manual
10348
+ * white-balance) is likewise normalized 0–100.
10349
+ *
10350
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
10351
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
10352
+ * injected from `status`) reports the live values, and a single
10353
+ * `setSettings` mutation applies a partial change. No hand-written
10354
+ * settings-contribution methods — the framework derives the UI + save
10355
+ * routing from this surface.
10356
+ */
10357
+ /** Sensor/image rotation, degrees clockwise. */
10358
+ var ImageRotateSchema = _enum([
10359
+ "0",
10360
+ "90",
10361
+ "180",
10362
+ "270"
10363
+ ]);
10364
+ /** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
10365
+ var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
10366
+ /** Exposure mode. */
10367
+ var ExposureModeSchema = _enum(["auto", "manual"]);
10368
+ /**
10369
+ * Backlight-compensation mode:
10370
+ * - `off` — disabled
10371
+ * - `blc` — backlight compensation
10372
+ * - `wdr` — wide dynamic range
10373
+ * - `hlc` — highlight compensation
10374
+ */
10375
+ var BacklightModeSchema = _enum([
10376
+ "off",
10377
+ "blc",
10378
+ "wdr",
10379
+ "hlc"
10380
+ ]);
10381
+ /** Normalized numeric range descriptor — `{ min, max, step }` per the
10382
+ * getOptions availability convention. Slider values are normalized 0–100. */
10383
+ var NormalizedRangeSchema = object({
10384
+ min: number(),
10385
+ max: number(),
10386
+ step: number()
10387
+ });
10388
+ object({
10389
+ /** Normalized 0–100. */
10390
+ brightness: number().optional(),
10391
+ /** Normalized 0–100. */
10392
+ contrast: number().optional(),
10393
+ /** Normalized 0–100. */
10394
+ saturation: number().optional(),
10395
+ /** Normalized 0–100. */
10396
+ sharpness: number().optional(),
10397
+ mirror: boolean().optional(),
10398
+ flip: boolean().optional(),
10399
+ rotate: ImageRotateSchema.optional(),
10400
+ whiteBalance: WhiteBalanceModeSchema.optional(),
10401
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
10402
+ warmth: number().optional(),
10403
+ exposureMode: ExposureModeSchema.optional(),
10404
+ backlightMode: BacklightModeSchema.optional(),
10405
+ lastFetchedAt: number()
10406
+ });
10407
+ /**
10408
+ * Per-camera availability descriptor — drives which controls the admin UI
10409
+ * renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
10410
+ * (the normalized 0–100 range); enums as arrays of supported values (empty
10411
+ * array → control hidden). A provider returns honest, camera-probed values
10412
+ * — never hardcoded.
10413
+ */
10414
+ var ImageSettingsOptionsSchema = object({
10415
+ supportsBrightness: boolean(),
10416
+ brightness: NormalizedRangeSchema.optional(),
10417
+ supportsContrast: boolean(),
10418
+ contrast: NormalizedRangeSchema.optional(),
10419
+ supportsSaturation: boolean(),
10420
+ saturation: NormalizedRangeSchema.optional(),
10421
+ supportsSharpness: boolean(),
10422
+ sharpness: NormalizedRangeSchema.optional(),
10423
+ supportsMirror: boolean(),
10424
+ supportsFlip: boolean(),
10425
+ /** Supported rotation values. Empty → rotation not configurable. */
10426
+ rotateOptions: array(ImageRotateSchema),
10427
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
10428
+ whiteBalanceModes: array(WhiteBalanceModeSchema),
10429
+ supportsWarmth: boolean(),
10430
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
10431
+ warmth: NormalizedRangeSchema.optional(),
10432
+ /** Supported exposure modes. Empty → exposure not configurable. */
10433
+ exposureModes: array(ExposureModeSchema),
10434
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
10435
+ backlightModes: array(BacklightModeSchema)
10436
+ });
10437
+ /**
10438
+ * Partial change to the image config — every field optional. Slider values
10439
+ * are normalized 0–100. A provider ignores fields it does not support.
10440
+ */
10441
+ var ImageSettingsPatchSchema = object({
10442
+ brightness: number().optional(),
10443
+ contrast: number().optional(),
10444
+ saturation: number().optional(),
10445
+ sharpness: number().optional(),
10446
+ mirror: boolean().optional(),
10447
+ flip: boolean().optional(),
10448
+ rotate: ImageRotateSchema.optional(),
10449
+ whiteBalance: WhiteBalanceModeSchema.optional(),
10450
+ warmth: number().optional(),
10451
+ exposureMode: ExposureModeSchema.optional(),
10452
+ backlightMode: BacklightModeSchema.optional()
10453
+ });
10454
+ DeviceType.Camera, method(object({ deviceId: number() }), ImageSettingsOptionsSchema), method(object({
10455
+ deviceId: number(),
10456
+ settings: ImageSettingsPatchSchema
10457
+ }), _void(), {
10458
+ kind: "mutation",
10459
+ auth: "admin"
10460
+ });
10461
+ /**
10252
10462
  * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10253
10463
  * with a mowing lifecycle plus a dock action.
10254
10464
  *
@@ -11311,6 +11521,16 @@ var RunnerCameraConfigSchema = object({
11311
11521
  * this gate is bypassed.
11312
11522
  */
11313
11523
  onboardMotionDrivesAnalyzer: boolean().default(true),
11524
+ /**
11525
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
11526
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
11527
+ * this is off by default because the recheck re-subscribes a detection session
11528
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
11529
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
11530
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
11531
+ * (and only render) when this is enabled.
11532
+ */
11533
+ occupancyRecheckEnabled: boolean().default(false),
11314
11534
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
11315
11535
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
11316
11536
  /**
@@ -11361,6 +11581,8 @@ var RunnerCameraDeviceUIFields = [
11361
11581
  default: motionFpsField.default,
11362
11582
  showValue: true,
11363
11583
  unit: "fps",
11584
+ nullable: true,
11585
+ nullLabel: "Default",
11364
11586
  showWhen: {
11365
11587
  field: "motionSources",
11366
11588
  includes: "analyzer"
@@ -11375,7 +11597,9 @@ var RunnerCameraDeviceUIFields = [
11375
11597
  step: detectionFpsField.step,
11376
11598
  default: detectionFpsField.default,
11377
11599
  showValue: true,
11378
- unit: "fps"
11600
+ unit: "fps",
11601
+ nullable: true,
11602
+ nullLabel: "Default"
11379
11603
  },
11380
11604
  {
11381
11605
  key: "motionCooldownMs",
@@ -11388,7 +11612,9 @@ var RunnerCameraDeviceUIFields = [
11388
11612
  default: motionCooldownMsField.default,
11389
11613
  showValue: true,
11390
11614
  unit: "s",
11391
- displayScale: 1e3
11615
+ displayScale: 1e3,
11616
+ nullable: true,
11617
+ nullLabel: "Default"
11392
11618
  },
11393
11619
  {
11394
11620
  key: "onboardMotionDrivesAnalyzer",
@@ -11401,17 +11627,29 @@ var RunnerCameraDeviceUIFields = [
11401
11627
  includes: "onboard"
11402
11628
  }
11403
11629
  },
11630
+ {
11631
+ key: "occupancyRecheckEnabled",
11632
+ type: "boolean",
11633
+ style: "checkbox",
11634
+ label: "Occupancy re-check",
11635
+ 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.",
11636
+ default: false
11637
+ },
11404
11638
  {
11405
11639
  key: "occupancyRecheckSec",
11406
11640
  type: "slider",
11407
11641
  label: "Occupancy re-check interval",
11408
- 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.",
11642
+ description: "How often (in seconds) the runner re-samples a few frames to confirm the scene is truly empty during the watching phase.",
11409
11643
  min: occupancyRecheckSecField.min,
11410
11644
  max: occupancyRecheckSecField.max,
11411
11645
  step: occupancyRecheckSecField.step,
11412
11646
  default: occupancyRecheckSecField.default,
11413
11647
  showValue: true,
11414
- unit: "s"
11648
+ unit: "s",
11649
+ showWhen: {
11650
+ field: "occupancyRecheckEnabled",
11651
+ equals: true
11652
+ }
11415
11653
  },
11416
11654
  {
11417
11655
  key: "occupancyRecheckFrames",
@@ -11422,7 +11660,11 @@ var RunnerCameraDeviceUIFields = [
11422
11660
  max: occupancyRecheckFramesField.max,
11423
11661
  step: occupancyRecheckFramesField.step,
11424
11662
  default: occupancyRecheckFramesField.default,
11425
- showValue: true
11663
+ showValue: true,
11664
+ showWhen: {
11665
+ field: "occupancyRecheckEnabled",
11666
+ equals: true
11667
+ }
11426
11668
  }
11427
11669
  ];
11428
11670
  /**
@@ -20001,6 +20243,18 @@ Object.freeze({
20001
20243
  addonId: null,
20002
20244
  access: "view"
20003
20245
  },
20246
+ "dayNight.getOptions": {
20247
+ capName: "day-night",
20248
+ capScope: "device",
20249
+ addonId: null,
20250
+ access: "view"
20251
+ },
20252
+ "dayNight.setSettings": {
20253
+ capName: "day-night",
20254
+ capScope: "device",
20255
+ addonId: null,
20256
+ access: "create"
20257
+ },
20004
20258
  "decoder.createSession": {
20005
20259
  capName: "decoder",
20006
20260
  capScope: "system",
@@ -20931,6 +21185,18 @@ Object.freeze({
20931
21185
  addonId: null,
20932
21186
  access: "create"
20933
21187
  },
21188
+ "imageSettings.getOptions": {
21189
+ capName: "image-settings",
21190
+ capScope: "device",
21191
+ addonId: null,
21192
+ access: "view"
21193
+ },
21194
+ "imageSettings.setSettings": {
21195
+ capName: "image-settings",
21196
+ capScope: "device",
21197
+ addonId: null,
21198
+ access: "create"
21199
+ },
20934
21200
  "integrations.create": {
20935
21201
  capName: "integrations",
20936
21202
  capScope: "system",
@@ -24510,7 +24776,8 @@ var DEFAULT_WATCHDOG_THRESHOLDS = {
24510
24776
  audioMs: 9e4,
24511
24777
  motionMs: 3e4,
24512
24778
  detectionMs: 6e4,
24513
- maxRecoveryAttempts: 3
24779
+ maxRecoveryAttempts: 3,
24780
+ exhaustedRetryMs: 6e4
24514
24781
  };
24515
24782
  var STAGE_THRESHOLD_KEY = {
24516
24783
  audio: "audioMs",
@@ -24531,7 +24798,8 @@ var PipelineWatchdog = class {
24531
24798
  const stages = /* @__PURE__ */ new Map();
24532
24799
  for (const [stage] of cam.continuousStages) stages.set(stage, {
24533
24800
  lastSeenMs: now,
24534
- attempts: 0
24801
+ attempts: 0,
24802
+ lastAttemptMs: 0
24535
24803
  });
24536
24804
  this.state.set(cam.deviceId, stages);
24537
24805
  }
@@ -24554,7 +24822,10 @@ var PipelineWatchdog = class {
24554
24822
  else this.deps.logger.info(line);
24555
24823
  for (const r of recoveries) {
24556
24824
  const rt = this.state.get(cam.deviceId)?.get(r.stage);
24557
- if (rt) rt.attempts += 1;
24825
+ if (rt) {
24826
+ if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
24827
+ rt.lastAttemptMs = now;
24828
+ }
24558
24829
  this.deps.recover(cam.deviceId, r.stage, r.streamId);
24559
24830
  }
24560
24831
  }
@@ -24598,7 +24869,13 @@ var PipelineWatchdog = class {
24598
24869
  if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
24599
24870
  else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
24600
24871
  stalled = true;
24601
- parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts})`);
24872
+ if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
24873
+ recoveries.push({
24874
+ stage,
24875
+ streamId
24876
+ });
24877
+ parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts} · retrying)`);
24878
+ } else parts.push(`${stage}=${stageMode[stage]}@${streamId}(STALLED ${sSec}s · exhausted ${rt.attempts})`);
24602
24879
  } else {
24603
24880
  stalled = true;
24604
24881
  recoveries.push({
@@ -24950,6 +25227,24 @@ function isPipelinePhaseMode(v) {
24950
25227
  var PREFERRED_AGENT_SETTING = "preferredAgent";
24951
25228
  /** Key under which the orchestrator stores the per-device audio node pin. */
24952
25229
  var AUDIO_NODE_SETTING = "audioNodeId";
25230
+ /**
25231
+ * Target PCM window duration, in ms, for audio classification.
25232
+ *
25233
+ * Sized for the ~1 s inference window the downstream classifiers expect —
25234
+ * YAMNet consumes a 0.975 s frame and Apple SoundAnalysis a ~1 s frame.
25235
+ * The broker's `AudioCodecSession` no longer batches (its 500 ms
25236
+ * accumulator was removed for WebRTC latency), so decoded codec chunks now
25237
+ * arrive at ~21–64 ms each — ~16–48 chunks per second per camera. Feeding
25238
+ * each tiny chunk to the analyzer meant one cross-process `analyseChunk`
25239
+ * RPC + one `PipelineAudioInferenceResult` event PER chunk for a classifier
25240
+ * that (a) throttles to ~2 Hz internally anyway and (b) needs a ~1 s window
25241
+ * for accurate inference. We instead accumulate raw PCM per device and flush
25242
+ * ONE classify + ONE event per ~1 s window: ~1 RPC/event/s/camera and a
25243
+ * correctly-sized inference frame.
25244
+ */
25245
+ var AUDIO_WINDOW_TARGET_MS = 1e3;
25246
+ /** f32le PCM = 4 bytes per sample. */
25247
+ var F32_BYTES_PER_SAMPLE = 4;
24953
25248
  var DEFAULT_BROKER_CALL_TIMEOUT_MS = 5e3;
24954
25249
  /** Debounce window for `scheduleReconcile` — coalesces bursts of topology/slot-change signals. */
24955
25250
  var RECONCILE_DEBOUNCE_MS = 200;
@@ -25002,7 +25297,6 @@ var OrchestratorDiagnosticsSchema = object({
25002
25297
  enabledAudioNodes: array(string()),
25003
25298
  clusterRoles: object({
25004
25299
  ingestNode: string(),
25005
- recordingNode: string(),
25006
25300
  audioNode: string()
25007
25301
  }),
25008
25302
  assignedDeviceCount: number().int().min(0),
@@ -25163,12 +25457,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25163
25457
  /**
25164
25458
  * Cluster-wide singleton role assignments (placement-model §6). Each role
25165
25459
  * is the ONE node that serves it for every camera. Driven by the
25166
- * `ingestNode` / `recordingNode` / `audioNode` node-selects in the addon
25167
- * global schema; defaults to the hub for all three.
25460
+ * `ingestNode` / `audioNode` node-selects in the addon global schema;
25461
+ * defaults to the hub for both. NOTE: the recording node is NOT a role here
25462
+ * — the `recorder` addon owns it as the single source of truth (its
25463
+ * `recordingNodeId` global setting, obeyed by the recorder's gate). The
25464
+ * Pipeline placement board's "Recording" picker reads/writes that canonical
25465
+ * setting directly via `addonSettings`, not an orchestrator-owned duplicate.
25168
25466
  */
25169
25467
  clusterRoles = {
25170
25468
  ingestNode: "hub",
25171
- recordingNode: "hub",
25172
25469
  audioNode: "hub"
25173
25470
  };
25174
25471
  /**
@@ -28142,32 +28439,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28142
28439
  title: "Cluster Roles",
28143
28440
  tab: "pipeline",
28144
28441
  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.",
28145
- fields: [
28146
- {
28147
- key: "ingestNode",
28148
- type: "node-select",
28149
- label: "Ingest Node",
28150
- 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.)",
28151
- default: "hub",
28152
- showOffline: true
28153
- },
28154
- {
28155
- key: "recordingNode",
28156
- type: "node-select",
28157
- label: "Recording Node",
28158
- description: "The node that records every camera (passthrough copy to its storage location). Defaults to the hub.",
28159
- default: "hub",
28160
- showOffline: true
28161
- },
28162
- {
28163
- key: "audioNode",
28164
- type: "node-select",
28165
- label: "Audio Node",
28166
- description: "The node that runs all audio analysis. Defaults to the hub. Must be an audio-capable node.",
28167
- default: "hub",
28168
- showOffline: true
28169
- }
28170
- ]
28442
+ fields: [{
28443
+ key: "ingestNode",
28444
+ type: "node-select",
28445
+ label: "Ingest Node",
28446
+ 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.)",
28447
+ default: "hub",
28448
+ showOffline: true
28449
+ }, {
28450
+ key: "audioNode",
28451
+ type: "node-select",
28452
+ label: "Audio Node",
28453
+ description: "The node that runs all audio analysis. Defaults to the hub. Must be an audio-capable node.",
28454
+ default: "hub",
28455
+ showOffline: true
28456
+ }]
28171
28457
  },
28172
28458
  {
28173
28459
  id: "decoder",
@@ -28693,7 +28979,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28693
28979
  };
28694
28980
  this.clusterRoles = {
28695
28981
  ingestNode: readRole("ingestNode"),
28696
- recordingNode: readRole("recordingNode"),
28697
28982
  audioNode: readRole("audioNode")
28698
28983
  };
28699
28984
  this.schedulePendingRetry();
@@ -28868,6 +29153,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28868
29153
  if (typeof v !== "number") throw new Error(`orchestrator schema missing required number '${key}' for device ${deviceId}`);
28869
29154
  return v;
28870
29155
  };
29156
+ const numberOrDefault = (key) => {
29157
+ const v = flat[key];
29158
+ if (typeof v === "number") return v;
29159
+ const uiField = RunnerCameraDeviceUIFields.find((f) => f.key === key);
29160
+ const dflt = uiField && "default" in uiField ? uiField.default : void 0;
29161
+ return typeof dflt === "number" ? dflt : 0;
29162
+ };
28871
29163
  try {
28872
29164
  const features = await this.lookupDeviceFeatures(deviceId);
28873
29165
  const profile = resolveDeviceProfile(features);
@@ -28885,6 +29177,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28885
29177
  const audioMode = typeof userAudioMode === "string" && isPipelinePhaseMode(userAudioMode) ? userAudioMode : profile?.defaults.audioMode ?? "always-on";
28886
29178
  const userOnboardMotionDrivesAnalyzer = raw["onboardMotionDrivesAnalyzer"];
28887
29179
  const onboardMotionDrivesAnalyzer = typeof userOnboardMotionDrivesAnalyzer === "boolean" ? userOnboardMotionDrivesAnalyzer : true;
29180
+ const userOccupancyRecheckEnabled = raw["occupancyRecheckEnabled"];
29181
+ const occupancyRecheckEnabled = typeof userOccupancyRecheckEnabled === "boolean" ? userOccupancyRecheckEnabled : false;
28888
29182
  const occupancyRecheckSec = mustNumber("occupancyRecheckSec");
28889
29183
  const occupancyRecheckFrames = mustNumber("occupancyRecheckFrames");
28890
29184
  return {
@@ -28893,12 +29187,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28893
29187
  motionSources,
28894
29188
  motionStreamProfile: mustString("motionStreamProfile"),
28895
29189
  detectionStreamProfile: mustString("detectionStreamProfile"),
28896
- motionFps: mustNumber("motionFps"),
28897
- detectionFps: mustNumber("detectionFps"),
28898
- motionCooldownMs: mustNumber("motionCooldownMs"),
29190
+ motionFps: numberOrDefault("motionFps"),
29191
+ detectionFps: numberOrDefault("detectionFps"),
29192
+ motionCooldownMs: numberOrDefault("motionCooldownMs"),
28899
29193
  detectionMode,
28900
29194
  audioMode,
28901
29195
  onboardMotionDrivesAnalyzer,
29196
+ occupancyRecheckEnabled,
28902
29197
  occupancyRecheckSec,
28903
29198
  occupancyRecheckFrames
28904
29199
  };
@@ -29009,6 +29304,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29009
29304
  detectionMode: resolved.detectionMode,
29010
29305
  audioMode: resolved.audioMode,
29011
29306
  onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer,
29307
+ occupancyRecheckEnabled: resolved.occupancyRecheckEnabled,
29012
29308
  occupancyRecheckSec: resolved.occupancyRecheckSec,
29013
29309
  occupancyRecheckFrames: resolved.occupancyRecheckFrames
29014
29310
  };
@@ -29043,6 +29339,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29043
29339
  detectionMode: config.detectionMode,
29044
29340
  audioMode: config.audioMode,
29045
29341
  onboardMotionDrivesAnalyzer: config.onboardMotionDrivesAnalyzer,
29342
+ occupancyRecheckEnabled: config.occupancyRecheckEnabled,
29046
29343
  occupancyRecheckSec: config.occupancyRecheckSec,
29047
29344
  occupancyRecheckFrames: config.occupancyRecheckFrames
29048
29345
  };
@@ -29208,6 +29505,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29208
29505
  if (a.motionCooldownMs !== b.motionCooldownMs) return false;
29209
29506
  if (a.audioStreamId !== b.audioStreamId) return false;
29210
29507
  if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
29508
+ if (a.occupancyRecheckEnabled !== b.occupancyRecheckEnabled) return false;
29211
29509
  if (a.occupancyRecheckSec !== b.occupancyRecheckSec) return false;
29212
29510
  if (a.occupancyRecheckFrames !== b.occupancyRecheckFrames) return false;
29213
29511
  if (a.motionSources.length !== b.motionSources.length) return false;
@@ -29737,6 +30035,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29737
30035
  isRemote: isRemoteAudio
29738
30036
  }
29739
30037
  });
30038
+ let pcmParts = [];
30039
+ let accumulatedBytes = 0;
30040
+ let accumulatedMs = 0;
30041
+ let windowSampleRate = 0;
30042
+ let windowChannels = 0;
30043
+ let windowTimestamp = 0;
30044
+ let windowOpen = false;
30045
+ const resetWindow = () => {
30046
+ pcmParts = [];
30047
+ accumulatedBytes = 0;
30048
+ accumulatedMs = 0;
30049
+ windowOpen = false;
30050
+ };
29740
30051
  const teardown = startAudioChunkPoller({
29741
30052
  api,
29742
30053
  brokerId: audioBrokerId,
@@ -29746,13 +30057,35 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29746
30057
  this.pipelineWatchdog?.noteSignal(deviceId, "audio");
29747
30058
  try {
29748
30059
  const byteLength = chunk.data.byteLength;
29749
- const data = new Uint8Array(byteLength);
29750
- data.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
30060
+ const bytes = new Uint8Array(byteLength);
30061
+ bytes.set(new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, byteLength));
30062
+ if (!windowOpen) {
30063
+ windowSampleRate = chunk.sampleRate;
30064
+ windowChannels = chunk.channels;
30065
+ windowTimestamp = chunk.timestamp;
30066
+ windowOpen = true;
30067
+ }
30068
+ pcmParts.push(bytes);
30069
+ accumulatedBytes += byteLength;
30070
+ const channels = chunk.channels > 0 ? chunk.channels : 1;
30071
+ const framesPerChannel = byteLength / F32_BYTES_PER_SAMPLE / channels;
30072
+ accumulatedMs += framesPerChannel / chunk.sampleRate * 1e3;
30073
+ if (accumulatedMs < AUDIO_WINDOW_TARGET_MS) return;
30074
+ const windowData = new Uint8Array(accumulatedBytes);
30075
+ let offset = 0;
30076
+ for (const part of pcmParts) {
30077
+ windowData.set(part, offset);
30078
+ offset += part.byteLength;
30079
+ }
30080
+ const flushSampleRate = windowSampleRate;
30081
+ const flushChannels = windowChannels;
30082
+ const flushTimestamp = windowTimestamp;
30083
+ resetWindow();
29751
30084
  const audioChunkInput = {
29752
- data,
29753
- sampleRate: chunk.sampleRate,
29754
- channels: chunk.channels,
29755
- timestamp: chunk.timestamp,
30085
+ data: windowData,
30086
+ sampleRate: flushSampleRate,
30087
+ channels: flushChannels,
30088
+ timestamp: flushTimestamp,
29756
30089
  deviceId
29757
30090
  };
29758
30091
  const result = await api.audioAnalyzer.analyseChunk.mutate({
@@ -29829,6 +30162,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29829
30162
  this.ctx.logger.info("Audio stream subscribed", { tags: { deviceId } });
29830
30163
  return () => {
29831
30164
  teardown();
30165
+ resetWindow();
29832
30166
  };
29833
30167
  }
29834
30168
  };
@@ -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_pipeline_orchestrator_widgets-0vYngRZJ.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DaBzEEJK.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-orchestrator",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",