@camstack/addon-pipeline-orchestrator 1.1.27 → 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;
@@ -28862,6 +29157,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28862
29157
  if (typeof v !== "number") throw new Error(`orchestrator schema missing required number '${key}' for device ${deviceId}`);
28863
29158
  return v;
28864
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
+ };
28865
29167
  try {
28866
29168
  const features = await this.lookupDeviceFeatures(deviceId);
28867
29169
  const profile = resolveDeviceProfile(features);
@@ -28879,6 +29181,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28879
29181
  const audioMode = typeof userAudioMode === "string" && isPipelinePhaseMode(userAudioMode) ? userAudioMode : profile?.defaults.audioMode ?? "always-on";
28880
29182
  const userOnboardMotionDrivesAnalyzer = raw["onboardMotionDrivesAnalyzer"];
28881
29183
  const onboardMotionDrivesAnalyzer = typeof userOnboardMotionDrivesAnalyzer === "boolean" ? userOnboardMotionDrivesAnalyzer : true;
29184
+ const userOccupancyRecheckEnabled = raw["occupancyRecheckEnabled"];
29185
+ const occupancyRecheckEnabled = typeof userOccupancyRecheckEnabled === "boolean" ? userOccupancyRecheckEnabled : false;
28882
29186
  const occupancyRecheckSec = mustNumber("occupancyRecheckSec");
28883
29187
  const occupancyRecheckFrames = mustNumber("occupancyRecheckFrames");
28884
29188
  return {
@@ -28887,12 +29191,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
28887
29191
  motionSources,
28888
29192
  motionStreamProfile: mustString("motionStreamProfile"),
28889
29193
  detectionStreamProfile: mustString("detectionStreamProfile"),
28890
- motionFps: mustNumber("motionFps"),
28891
- detectionFps: mustNumber("detectionFps"),
28892
- motionCooldownMs: mustNumber("motionCooldownMs"),
29194
+ motionFps: numberOrDefault("motionFps"),
29195
+ detectionFps: numberOrDefault("detectionFps"),
29196
+ motionCooldownMs: numberOrDefault("motionCooldownMs"),
28893
29197
  detectionMode,
28894
29198
  audioMode,
28895
29199
  onboardMotionDrivesAnalyzer,
29200
+ occupancyRecheckEnabled,
28896
29201
  occupancyRecheckSec,
28897
29202
  occupancyRecheckFrames
28898
29203
  };
@@ -29003,6 +29308,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29003
29308
  detectionMode: resolved.detectionMode,
29004
29309
  audioMode: resolved.audioMode,
29005
29310
  onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer,
29311
+ occupancyRecheckEnabled: resolved.occupancyRecheckEnabled,
29006
29312
  occupancyRecheckSec: resolved.occupancyRecheckSec,
29007
29313
  occupancyRecheckFrames: resolved.occupancyRecheckFrames
29008
29314
  };
@@ -29037,6 +29343,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29037
29343
  detectionMode: config.detectionMode,
29038
29344
  audioMode: config.audioMode,
29039
29345
  onboardMotionDrivesAnalyzer: config.onboardMotionDrivesAnalyzer,
29346
+ occupancyRecheckEnabled: config.occupancyRecheckEnabled,
29040
29347
  occupancyRecheckSec: config.occupancyRecheckSec,
29041
29348
  occupancyRecheckFrames: config.occupancyRecheckFrames
29042
29349
  };
@@ -29202,6 +29509,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29202
29509
  if (a.motionCooldownMs !== b.motionCooldownMs) return false;
29203
29510
  if (a.audioStreamId !== b.audioStreamId) return false;
29204
29511
  if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
29512
+ if (a.occupancyRecheckEnabled !== b.occupancyRecheckEnabled) return false;
29205
29513
  if (a.occupancyRecheckSec !== b.occupancyRecheckSec) return false;
29206
29514
  if (a.occupancyRecheckFrames !== b.occupancyRecheckFrames) return false;
29207
29515
  if (a.motionSources.length !== b.motionSources.length) return false;
@@ -29731,6 +30039,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29731
30039
  isRemote: isRemoteAudio
29732
30040
  }
29733
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
+ };
29734
30055
  const teardown = startAudioChunkPoller({
29735
30056
  api,
29736
30057
  brokerId: audioBrokerId,
@@ -29740,13 +30061,35 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29740
30061
  this.pipelineWatchdog?.noteSignal(deviceId, "audio");
29741
30062
  try {
29742
30063
  const byteLength = chunk.data.byteLength;
29743
- const data = new Uint8Array(byteLength);
29744
- 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();
29745
30088
  const audioChunkInput = {
29746
- data,
29747
- sampleRate: chunk.sampleRate,
29748
- channels: chunk.channels,
29749
- timestamp: chunk.timestamp,
30089
+ data: windowData,
30090
+ sampleRate: flushSampleRate,
30091
+ channels: flushChannels,
30092
+ timestamp: flushTimestamp,
29750
30093
  deviceId
29751
30094
  };
29752
30095
  const result = await api.audioAnalyzer.analyseChunk.mutate({
@@ -29823,6 +30166,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
29823
30166
  this.ctx.logger.info("Audio stream subscribed", { tags: { deviceId } });
29824
30167
  return () => {
29825
30168
  teardown();
30169
+ resetWindow();
29826
30170
  };
29827
30171
  }
29828
30172
  };