@camstack/addon-pipeline-orchestrator 1.2.27 → 1.2.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
@@ -6490,7 +6490,20 @@ var BrokerStatsSchema = object({
6490
6490
  sampleRate: number(),
6491
6491
  channels: number(),
6492
6492
  supported: boolean()
6493
- }).nullable().optional()
6493
+ }).nullable().optional(),
6494
+ /**
6495
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
6496
+ * distributing none of the device's audio, on live or recording.
6497
+ *
6498
+ * Present so a silent camera can be told apart from a broken one on the
6499
+ * stream panel itself, without cross-referencing the switch group: a
6500
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
6501
+ * working exactly as asked. `audioMutedDropped` counts the audio units
6502
+ * thrown away since the current dial — it is how you confirm from stats
6503
+ * alone that the mute is on the packet path and not merely persisted.
6504
+ */
6505
+ audioMuted: boolean().optional(),
6506
+ audioMutedDropped: number().optional()
6494
6507
  });
6495
6508
  /**
6496
6509
  * Exporter-facing "profile restream" entry. Returned by
@@ -7657,6 +7670,19 @@ object({
7657
7670
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7658
7671
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7659
7672
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7673
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
7674
+ *
7675
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
7676
+ *
7677
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
7678
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
7679
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
7680
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
7681
+ * instant, vendor-independent and reversible without touching the camera, and
7682
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
7683
+ * D62 forbids a second switch that *disagrees* with the first; these two
7684
+ * cannot disagree, because neither reads the other's store — the camera holds
7685
+ * one, the broker holds the other, and each reports its own fact.
7660
7686
  *
7661
7687
  * ## The two switches whose authority is not on this server
7662
7688
  *
@@ -7718,6 +7744,7 @@ var CameraSwitchIdSchema = _enum([
7718
7744
  "object-detection",
7719
7745
  "privacy-mask",
7720
7746
  "device-audio",
7747
+ "broker-audio",
7721
7748
  "audio-analysis",
7722
7749
  "recording",
7723
7750
  "notifications"
@@ -7733,12 +7760,17 @@ var CameraSwitchIdSchema = _enum([
7733
7760
  * `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
7734
7761
  * radius is partial, and the "broadest first" rule does not rank a partial
7735
7762
  * control above a whole-function one.
7763
+ *
7764
+ * `broker-audio` sits directly BELOW `device-audio` by the same source-first
7765
+ * rule: the camera's microphone feeds the broker, so silencing the camera
7766
+ * leaves the broker's mute with nothing to suppress; the reverse is not true.
7736
7767
  */
7737
7768
  var CAMERA_SWITCH_ORDER = [
7738
7769
  "stream-broker",
7739
7770
  "object-detection",
7740
7771
  "privacy-mask",
7741
7772
  "device-audio",
7773
+ "broker-audio",
7742
7774
  "audio-analysis",
7743
7775
  "recording",
7744
7776
  "notifications"
@@ -7764,7 +7796,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7764
7796
  object({
7765
7797
  kind: literal("camera-mask"),
7766
7798
  capName: string()
7767
- })
7799
+ }),
7800
+ object({ kind: literal("broker-audio-mute") })
7768
7801
  ]);
7769
7802
  /**
7770
7803
  * Why a switch is not offered for this camera. Rendered instead of the
@@ -7863,6 +7896,13 @@ var CAMERA_SWITCH_CATALOG = {
7863
7896
  },
7864
7897
  countsAsSwitchedOff: true
7865
7898
  },
7899
+ "broker-audio": {
7900
+ id: "broker-audio",
7901
+ label: "Audio distribution",
7902
+ costWhenOff: "Off: this server distributes no sound for this camera — live view is silent and everything recorded while it is off is silent FOREVER, even after you turn it back on. Audio detection and classification also have nothing to analyse. The camera keeps capturing sound, so nothing about the camera changes and turning this back on is instant, with no interruption to the picture.",
7903
+ authority: { kind: "broker-audio-mute" },
7904
+ countsAsSwitchedOff: true
7905
+ },
7866
7906
  "audio-analysis": {
7867
7907
  id: "audio-analysis",
7868
7908
  label: "Audio detection & classification",
@@ -7981,6 +8021,18 @@ function resolveState(descriptor, input) {
7981
8021
  enabled: mask.enabled
7982
8022
  };
7983
8023
  }
8024
+ case "broker-audio-mute": {
8025
+ const broker = input.brokerAudio;
8026
+ if (broker === null) return {
8027
+ available: false,
8028
+ enabled: true,
8029
+ unavailableReason: "source-unreachable"
8030
+ };
8031
+ return {
8032
+ available: true,
8033
+ enabled: !broker.muted
8034
+ };
8035
+ }
7984
8036
  }
7985
8037
  }
7986
8038
  /**
@@ -10223,7 +10275,25 @@ method(object({
10223
10275
  }), _void(), {
10224
10276
  kind: "mutation",
10225
10277
  auth: "admin"
10226
- }), method(object({ brokerId: string() }), boolean()), object({
10278
+ }), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
10279
+ muted: boolean(),
10280
+ /**
10281
+ * How many live non-derived brokers currently hold the mute. Purely
10282
+ * diagnostic: `muted` is the policy and is authoritative on its own
10283
+ * (it applies to brokers that do not exist yet), while this says
10284
+ * whether anything is presently being silenced.
10285
+ */
10286
+ appliedBrokers: number().int().nonnegative()
10287
+ })), method(object({
10288
+ deviceId: number().int(),
10289
+ muted: boolean()
10290
+ }), object({
10291
+ muted: boolean(),
10292
+ appliedBrokers: number().int().nonnegative()
10293
+ }), {
10294
+ kind: "mutation",
10295
+ auth: "admin"
10296
+ }), object({
10227
10297
  deviceId: number().int().nonnegative(),
10228
10298
  camStreamId: string(),
10229
10299
  profile: CamProfileSchema
@@ -15487,6 +15557,30 @@ var TrackSourceSchema = _enum([
15487
15557
  "audio"
15488
15558
  ]);
15489
15559
  /**
15560
+ * Where a track sits in the RETRAIN lifecycle (D81).
15561
+ *
15562
+ * - `none` — never marked, or un-marked. Evictable.
15563
+ * - `staging` — the operator wants this track as training material and has not
15564
+ * finished with it. **This is the only state retention holds**: the track and
15565
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
15566
+ * the device's age window.
15567
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
15568
+ * were COPIED into the retrain dataset at selection time, so the dataset no
15569
+ * longer depends on the track's media and the track becomes EVICTABLE again.
15570
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
15571
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
15572
+ *
15573
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
15574
+ * the store's filter language has only positive equality and `whereIn` — no
15575
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
15576
+ * would make the entire pre-column history immortal in one deploy.
15577
+ */
15578
+ var RetrainStatusSchema = _enum([
15579
+ "none",
15580
+ "staging",
15581
+ "trained"
15582
+ ]);
15583
+ /**
15490
15584
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15491
15585
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15492
15586
  * so the two surfaces cannot drift.
@@ -15496,18 +15590,31 @@ var TrackSourceSchema = _enum([
15496
15590
  * columns existed read as absent, and a consumer that needs a boolean should say
15497
15591
  * `flag === true`, not `flag !== false`.
15498
15592
  *
15499
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15500
- * operator curation, and the behaviour they drive will be specified separately.
15501
- * In particular a `markForTrain` track is NOT pinned against retention — see
15502
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15593
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
15594
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
15595
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
15596
+ * `trained` track reports `false` while refusing both writes. The boolean is
15597
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
15598
+ * "never marked" from "already trained" must read `retrainStatus`.
15599
+ *
15600
+ * `debug` does NOT pin; it is attention, not durability.
15503
15601
  */
15504
15602
  var TrackFlagFields = {
15505
- /** Operator marked this track as training material. */
15603
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
15604
+ * `'staging'`. */
15506
15605
  markForTrain: boolean().optional(),
15507
15606
  /** Operator marked this track for diagnostic attention. */
15508
15607
  debug: boolean().optional()
15509
15608
  };
15510
15609
  /**
15610
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
15611
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
15612
+ * write patch, and the status is not something the toggle sets — it is what the
15613
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
15614
+ * always present on a persisted row (the column default materialises `'none'`).
15615
+ */
15616
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
15617
+ /**
15511
15618
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15512
15619
  * one flag can never clear the other — the toggles are independent and are
15513
15620
  * driven from three surfaces that do not know about each other.
@@ -15521,7 +15628,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
15521
15628
  var TrackFlagsSchema = object({
15522
15629
  trackId: string(),
15523
15630
  markForTrain: boolean(),
15524
- debug: boolean()
15631
+ debug: boolean(),
15632
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
15633
+ * a track row) because this shape is only ever produced by the write body,
15634
+ * which always knows it — and a surface that has just written needs to render
15635
+ * `trained` without a re-fetch. */
15636
+ retrainStatus: RetrainStatusSchema
15637
+ });
15638
+ /** Per-camera slice of a training-export estimate. */
15639
+ var TrainingExportDeviceTotalsSchema = object({
15640
+ deviceId: number(),
15641
+ tracks: number().int(),
15642
+ files: number().int(),
15643
+ bytes: number().int()
15644
+ });
15645
+ /**
15646
+ * What a training export WOULD contain. Computed from media index rows only —
15647
+ * no blob is read to produce this.
15648
+ */
15649
+ var TrainingExportSummarySchema = object({
15650
+ generatedAt: number(),
15651
+ trackCount: number().int(),
15652
+ fileCount: number().int(),
15653
+ byteCount: number().int(),
15654
+ /** More marked tracks exist than a single pass carries. */
15655
+ truncated: boolean(),
15656
+ devices: array(TrainingExportDeviceTotalsSchema).readonly()
15525
15657
  });
15526
15658
  var TrackSchema = object({
15527
15659
  trackId: string(),
@@ -15566,7 +15698,8 @@ var TrackSchema = object({
15566
15698
  * Populated from the persisted envelope columns on historical reads;
15567
15699
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15568
15700
  envelope: TrackEnvelopeSchema.optional(),
15569
- ...TrackFlagFields
15701
+ ...TrackFlagFields,
15702
+ ...TrackRetrainFields
15570
15703
  });
15571
15704
  var BaseEventFields = {
15572
15705
  id: string(),
@@ -15780,7 +15913,8 @@ var KeyEventSchema = object({
15780
15913
  bestEventId: string(),
15781
15914
  /** Track lifetime in ms (lastSeen - firstSeen). */
15782
15915
  windowMs: number().optional(),
15783
- ...TrackFlagFields
15916
+ ...TrackFlagFields,
15917
+ ...TrackRetrainFields
15784
15918
  });
15785
15919
  object({
15786
15920
  trackId: string(),
@@ -16041,6 +16175,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16041
16175
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
16042
16176
  kind: "query",
16043
16177
  auth: "admin"
16178
+ }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
16179
+ kind: "query",
16180
+ auth: "admin"
16181
+ }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16182
+ kind: "query",
16183
+ auth: "admin"
16044
16184
  }), method(object({
16045
16185
  eventId: string(),
16046
16186
  kind: MediaFileKindEnum.optional()
@@ -22317,6 +22457,173 @@ DeviceType.Camera, method(object({
22317
22457
  status: OsdStatusSchema
22318
22458
  });
22319
22459
  /**
22460
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
22461
+ *
22462
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
22463
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
22464
+ * comes from, and it must not — a driver that grew a "show the temperature
22465
+ * here" feature would grow it once per vendor.
22466
+ *
22467
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
22468
+ * which value feeds the slot, how it is formatted, and under which
22469
+ * conditions it is shown at all. One addon renders every binding on every
22470
+ * camera, so a new source costs zero driver code.
22471
+ *
22472
+ * Three deliberate choices, each with a rejected alternative:
22473
+ *
22474
+ * 1. A source is `(capName, valuePath)` over the kernel's device
22475
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
22476
+ * cap-keyed slice a device publishes is bindable the day the cap
22477
+ * ships. The rejected alternative (one enum member per source, with
22478
+ * a resolver branch each) is what makes "add the humidity too" a
22479
+ * code change.
22480
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
22481
+ * notification centre's condition vocabulary — rather than a parallel
22482
+ * model. An operator who has learned one condition editor has learned
22483
+ * both.
22484
+ * 3. Because the renderer's facts are device STATE and not a detection
22485
+ * record, only a SUBSET of that vocabulary can be answered here.
22486
+ * `setSlotBinding` REJECTS the rest at write time (see
22487
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
22488
+ * condition that can never be true renders a permanently blank
22489
+ * overlay, and a blank overlay looks exactly like a broken camera.
22490
+ */
22491
+ /** Where a slot's value comes from. */
22492
+ var OsdSourceSchema = discriminatedUnion("kind", [
22493
+ object({
22494
+ kind: literal("static"),
22495
+ text: string().max(64)
22496
+ }),
22497
+ object({
22498
+ kind: literal("clock"),
22499
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
22500
+ pattern: string().min(1).max(32).default("HH:mm"),
22501
+ /** IANA zone. Omitted = the server's zone. */
22502
+ timezone: string().min(1).max(64).optional()
22503
+ }),
22504
+ object({
22505
+ kind: literal("device-state"),
22506
+ deviceId: number().int().optional(),
22507
+ capName: string().min(1).max(64),
22508
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
22509
+ valuePath: string().min(1).max(64)
22510
+ })
22511
+ ]);
22512
+ var OsdSlotBindingSchema = object({
22513
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
22514
+ enabled: boolean().default(true),
22515
+ source: OsdSourceSchema,
22516
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
22517
+ template: string().max(96).default("${value}"),
22518
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
22519
+ maxCharacters: number().int().min(4).max(64).optional(),
22520
+ /**
22521
+ * Decimal places for a numeric value. `0` yields an integer — the
22522
+ * documented workaround for firmwares that reject `.` in overlay text.
22523
+ */
22524
+ maxDecimals: number().int().min(0).max(4).default(1),
22525
+ /** Appended via `${unit}`. The state mirror does not carry units. */
22526
+ unitLabel: string().max(8).optional(),
22527
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
22528
+ valueMap: record(string(), string()).optional(),
22529
+ /** Time windows in which the slot is shown. Absent = always. */
22530
+ schedule: NcScheduleSchema.optional(),
22531
+ /**
22532
+ * Display gate, in the notification centre's condition vocabulary.
22533
+ * Only the keys reported by `getConditionSupport` are accepted.
22534
+ */
22535
+ conditions: NcConditionsSchema.optional(),
22536
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
22537
+ fallbackText: string().max(64).default("")
22538
+ });
22539
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
22540
+ var OsdSlotViewSchema = object({
22541
+ slotId: string(),
22542
+ kind: OsdOverlayKindEnum,
22543
+ /** Firmware refuses text edits (a timestamp, the channel name). */
22544
+ readOnly: boolean(),
22545
+ cameraEnabled: boolean(),
22546
+ cameraText: string().optional(),
22547
+ binding: OsdSlotBindingSchema.nullable()
22548
+ });
22549
+ /**
22550
+ * What happened to one slot on one render pass. `unchanged` exists so the
22551
+ * operator can tell "we are driving this and the value is steady" from
22552
+ * "we never got there" — and so the loop can prove it is not rewriting
22553
+ * identical text to the camera every tick.
22554
+ */
22555
+ var OsdRenderOutcomeEnum = _enum([
22556
+ "written",
22557
+ "unchanged",
22558
+ "gated",
22559
+ "unreadable",
22560
+ "disabled",
22561
+ "unbound",
22562
+ "failed"
22563
+ ]);
22564
+ var OsdRenderResultSchema = object({
22565
+ slotId: string(),
22566
+ outcome: OsdRenderOutcomeEnum,
22567
+ /** The text the slot should carry. Empty = the slot is switched off. */
22568
+ text: string(),
22569
+ /** Why, whenever the outcome is not a plain write. Never silent. */
22570
+ reason: string().optional()
22571
+ });
22572
+ var OsdSourceValueTypeEnum = _enum([
22573
+ "number",
22574
+ "boolean",
22575
+ "string",
22576
+ "enum"
22577
+ ]);
22578
+ /**
22579
+ * One bindable value, derived from a cap's `runtimeState` schema — never
22580
+ * hand-listed. The editor renders from this, so a cap that ships a new
22581
+ * state field becomes bindable with no UI change.
22582
+ */
22583
+ var OsdSourceOptionSchema = object({
22584
+ deviceId: number().int(),
22585
+ deviceName: string(),
22586
+ capName: string(),
22587
+ valuePath: string(),
22588
+ label: string(),
22589
+ valueType: OsdSourceValueTypeEnum,
22590
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
22591
+ enumValues: array(string()).readonly().optional()
22592
+ });
22593
+ method(object({ deviceId: number().int() }), object({
22594
+ supported: boolean(),
22595
+ slots: array(OsdSlotViewSchema)
22596
+ }), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
22597
+ supported: array(string()),
22598
+ catalog: array(NcConditionDescriptorSchema)
22599
+ }), { auth: "admin" }), method(object({
22600
+ deviceId: number().int(),
22601
+ slotId: string().min(1),
22602
+ binding: OsdSlotBindingSchema
22603
+ }), object({
22604
+ slot: OsdSlotViewSchema,
22605
+ render: OsdRenderResultSchema
22606
+ }), {
22607
+ kind: "mutation",
22608
+ auth: "admin"
22609
+ }), method(object({
22610
+ deviceId: number().int(),
22611
+ slotId: string().min(1)
22612
+ }), object({ success: literal(true) }), {
22613
+ kind: "mutation",
22614
+ auth: "admin"
22615
+ }), method(object({
22616
+ deviceId: number().int(),
22617
+ slotId: string().min(1),
22618
+ binding: OsdSlotBindingSchema.optional()
22619
+ }), OsdRenderResultSchema, {
22620
+ kind: "mutation",
22621
+ auth: "admin"
22622
+ }), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
22623
+ kind: "mutation",
22624
+ auth: "admin"
22625
+ });
22626
+ /**
22320
22627
  * Feeder connectivity / power status — mirrors the HA petkit device-status
22321
22628
  * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
22322
22629
  * `on_batteries` (running on battery backup). `null` until first reported.
@@ -27349,6 +27656,48 @@ Object.freeze({
27349
27656
  addonId: null,
27350
27657
  access: "create"
27351
27658
  },
27659
+ "osdManager.clearSlotBinding": {
27660
+ capName: "osd-manager",
27661
+ capScope: "system",
27662
+ addonId: null,
27663
+ access: "delete"
27664
+ },
27665
+ "osdManager.getConditionSupport": {
27666
+ capName: "osd-manager",
27667
+ capScope: "system",
27668
+ addonId: null,
27669
+ access: "view"
27670
+ },
27671
+ "osdManager.getDeviceOsd": {
27672
+ capName: "osd-manager",
27673
+ capScope: "system",
27674
+ addonId: null,
27675
+ access: "view"
27676
+ },
27677
+ "osdManager.getSourceCatalog": {
27678
+ capName: "osd-manager",
27679
+ capScope: "system",
27680
+ addonId: null,
27681
+ access: "view"
27682
+ },
27683
+ "osdManager.previewSlot": {
27684
+ capName: "osd-manager",
27685
+ capScope: "system",
27686
+ addonId: null,
27687
+ access: "create"
27688
+ },
27689
+ "osdManager.renderDevice": {
27690
+ capName: "osd-manager",
27691
+ capScope: "system",
27692
+ addonId: null,
27693
+ access: "create"
27694
+ },
27695
+ "osdManager.setSlotBinding": {
27696
+ capName: "osd-manager",
27697
+ capScope: "system",
27698
+ addonId: null,
27699
+ access: "create"
27700
+ },
27352
27701
  "petFeeder.callPet": {
27353
27702
  capName: "pet-feeder",
27354
27703
  capScope: "device",
@@ -27511,6 +27860,18 @@ Object.freeze({
27511
27860
  addonId: null,
27512
27861
  access: "view"
27513
27862
  },
27863
+ "pipelineAnalytics.getTrainingExportSummary": {
27864
+ capName: "pipeline-analytics",
27865
+ capScope: "device",
27866
+ addonId: null,
27867
+ access: "view"
27868
+ },
27869
+ "pipelineAnalytics.getTrainingExportUrl": {
27870
+ capName: "pipeline-analytics",
27871
+ capScope: "device",
27872
+ addonId: null,
27873
+ access: "view"
27874
+ },
27514
27875
  "pipelineAnalytics.listEventKinds": {
27515
27876
  capName: "pipeline-analytics",
27516
27877
  capScope: "device",
@@ -28987,6 +29348,12 @@ Object.freeze({
28987
29348
  addonId: null,
28988
29349
  access: "view"
28989
29350
  },
29351
+ "streamBroker.getDeviceAudioMute": {
29352
+ capName: "stream-broker",
29353
+ capScope: "system",
29354
+ addonId: null,
29355
+ access: "view"
29356
+ },
28990
29357
  "streamBroker.getPreBufferInfo": {
28991
29358
  capName: "stream-broker",
28992
29359
  capScope: "system",
@@ -29107,6 +29474,12 @@ Object.freeze({
29107
29474
  addonId: null,
29108
29475
  access: "create"
29109
29476
  },
29477
+ "streamBroker.setDeviceAudioMute": {
29478
+ capName: "stream-broker",
29479
+ capScope: "system",
29480
+ addonId: null,
29481
+ access: "create"
29482
+ },
29110
29483
  "streamBroker.setPreBufferDuration": {
29111
29484
  capName: "stream-broker",
29112
29485
  capScope: "system",
@@ -32079,6 +32452,18 @@ function parseDeviceKeyEngine(deviceKey) {
32079
32452
  }
32080
32453
  /** Per-stage timeout applied by `boundedStage` to every remote-fetch stage. */
32081
32454
  var STAGE_TIMEOUT_MS = 3e3;
32455
+ /**
32456
+ * Every switch whose OFF position leaves audio analysis with nothing to
32457
+ * analyse — the camera not capturing, this server not distributing, or the
32458
+ * analyzer not running. Named once because `CameraStatus.audio.enabled` must
32459
+ * agree with all three, and a list rebuilt at the call site is a list that
32460
+ * will miss the next one.
32461
+ */
32462
+ var AUDIO_SILENCING_SWITCH_IDS = [
32463
+ "device-audio",
32464
+ "broker-audio",
32465
+ "audio-analysis"
32466
+ ];
32082
32467
  var CameraStatusService = class {
32083
32468
  deps;
32084
32469
  constructor(deps) {
@@ -32273,12 +32658,19 @@ var CameraStatusService = class {
32273
32658
  * is precisely the failure the switch group exists to remove, and leaving
32274
32659
  * the lie in place would have made the group's own status block disagree
32275
32660
  * with it.
32661
+ *
32662
+ * THREE switches can silence this stage, not one (D83). `audio-analysis`
32663
+ * stops the analyzer; `device-audio` stops the camera capturing; and
32664
+ * `broker-audio` stops this server distributing. Any of them leaves the
32665
+ * analyzer with nothing, and reporting `enabled: true` for the other two
32666
+ * would reintroduce the exact hardcoded lie the previous paragraph is about
32667
+ * — one row further down the group.
32276
32668
  */
32277
32669
  buildAudioStage(audioNodeId, switchedOff) {
32278
32670
  if (audioNodeId === null) return null;
32279
32671
  return {
32280
32672
  nodeId: audioNodeId,
32281
- enabled: !switchedOff.includes("audio-analysis")
32673
+ enabled: !AUDIO_SILENCING_SWITCH_IDS.some((id) => switchedOff.includes(id))
32282
32674
  };
32283
32675
  }
32284
32676
  /**
@@ -32525,6 +32917,12 @@ var CameraSwitchService = class {
32525
32917
  patch: { enabled }
32526
32918
  });
32527
32919
  return;
32920
+ case "broker-audio-mute":
32921
+ await api.streamBroker.setDeviceAudioMute.mutate({
32922
+ deviceId,
32923
+ muted: !enabled
32924
+ }, nodePin(this.deps.assignSource(deviceId)));
32925
+ return;
32528
32926
  }
32529
32927
  }
32530
32928
  /**
@@ -32543,7 +32941,8 @@ var CameraSwitchService = class {
32543
32941
  recordingEnabled: null,
32544
32942
  notificationsMuted: null,
32545
32943
  deviceAudio: null,
32546
- privacyMask: null
32944
+ privacyMask: null,
32945
+ brokerAudio: null
32547
32946
  },
32548
32947
  wrapperAddonIdByCap: /* @__PURE__ */ new Map(),
32549
32948
  recordingConfig: null
@@ -32583,11 +32982,16 @@ var CameraSwitchService = class {
32583
32982
  this.warn(deviceId, "notificationRules.listDeviceMutes", err);
32584
32983
  return null;
32585
32984
  });
32586
- const [device, bindings, recordingConfig, mutedDeviceIds] = await Promise.all([
32985
+ const brokerAudioPromise = api.streamBroker.getDeviceAudioMute.query({ deviceId }, nodePin(this.deps.assignSource(deviceId))).then((r) => ({ muted: r.muted })).catch((err) => {
32986
+ this.warn(deviceId, "streamBroker.getDeviceAudioMute", err);
32987
+ return null;
32988
+ });
32989
+ const [device, bindings, recordingConfig, mutedDeviceIds, brokerAudio] = await Promise.all([
32587
32990
  devicePromise,
32588
32991
  bindingsPromise,
32589
32992
  recordingPromise,
32590
- mutesPromise
32993
+ mutesPromise,
32994
+ brokerAudioPromise
32591
32995
  ]);
32592
32996
  const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : api.deviceManager.listBindableCapsForDeviceType.query({ deviceType: device.type }).catch((err) => {
32593
32997
  this.warn(deviceId, "listBindableCapsForDeviceType", err);
@@ -32608,7 +33012,8 @@ var CameraSwitchService = class {
32608
33012
  recordingEnabled: recordingConfig === null ? null : recordingConfig.enabled,
32609
33013
  notificationsMuted: mutedDeviceIds === null ? null : mutedDeviceIds.includes(deviceId),
32610
33014
  deviceAudio: privacy.deviceAudio,
32611
- privacyMask: privacy.privacyMask
33015
+ privacyMask: privacy.privacyMask,
33016
+ brokerAudio
32612
33017
  },
32613
33018
  wrapperAddonIdByCap,
32614
33019
  recordingConfig
@@ -38388,7 +38793,8 @@ async function buildOrchestratorControllers(deps) {
38388
38793
  });
38389
38794
  const cameraSwitchService = new CameraSwitchService({
38390
38795
  api: () => deps.ctx().api ?? null,
38391
- logger: deps.ctx().logger
38796
+ logger: deps.ctx().logger,
38797
+ assignSource: (deviceId) => topology.assignSource(deviceId)
38392
38798
  });
38393
38799
  const cameraStatusService = new CameraStatusService({
38394
38800
  api: () => deps.ctx().api,
@@ -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-DZurdE2f.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Myx4bNgp.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.2.27",
3
+ "version": "1.2.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",