@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.js CHANGED
@@ -6518,7 +6518,20 @@ var BrokerStatsSchema = object({
6518
6518
  sampleRate: number(),
6519
6519
  channels: number(),
6520
6520
  supported: boolean()
6521
- }).nullable().optional()
6521
+ }).nullable().optional(),
6522
+ /**
6523
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
6524
+ * distributing none of the device's audio, on live or recording.
6525
+ *
6526
+ * Present so a silent camera can be told apart from a broken one on the
6527
+ * stream panel itself, without cross-referencing the switch group: a
6528
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
6529
+ * working exactly as asked. `audioMutedDropped` counts the audio units
6530
+ * thrown away since the current dial — it is how you confirm from stats
6531
+ * alone that the mute is on the packet path and not merely persisted.
6532
+ */
6533
+ audioMuted: boolean().optional(),
6534
+ audioMutedDropped: number().optional()
6522
6535
  });
6523
6536
  /**
6524
6537
  * Exporter-facing "profile restream" entry. Returned by
@@ -7685,6 +7698,19 @@ object({
7685
7698
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7686
7699
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7687
7700
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7701
+ * | `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 |
7702
+ *
7703
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
7704
+ *
7705
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
7706
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
7707
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
7708
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
7709
+ * instant, vendor-independent and reversible without touching the camera, and
7710
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
7711
+ * D62 forbids a second switch that *disagrees* with the first; these two
7712
+ * cannot disagree, because neither reads the other's store — the camera holds
7713
+ * one, the broker holds the other, and each reports its own fact.
7688
7714
  *
7689
7715
  * ## The two switches whose authority is not on this server
7690
7716
  *
@@ -7746,6 +7772,7 @@ var CameraSwitchIdSchema = _enum([
7746
7772
  "object-detection",
7747
7773
  "privacy-mask",
7748
7774
  "device-audio",
7775
+ "broker-audio",
7749
7776
  "audio-analysis",
7750
7777
  "recording",
7751
7778
  "notifications"
@@ -7761,12 +7788,17 @@ var CameraSwitchIdSchema = _enum([
7761
7788
  * `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
7762
7789
  * radius is partial, and the "broadest first" rule does not rank a partial
7763
7790
  * control above a whole-function one.
7791
+ *
7792
+ * `broker-audio` sits directly BELOW `device-audio` by the same source-first
7793
+ * rule: the camera's microphone feeds the broker, so silencing the camera
7794
+ * leaves the broker's mute with nothing to suppress; the reverse is not true.
7764
7795
  */
7765
7796
  var CAMERA_SWITCH_ORDER = [
7766
7797
  "stream-broker",
7767
7798
  "object-detection",
7768
7799
  "privacy-mask",
7769
7800
  "device-audio",
7801
+ "broker-audio",
7770
7802
  "audio-analysis",
7771
7803
  "recording",
7772
7804
  "notifications"
@@ -7792,7 +7824,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7792
7824
  object({
7793
7825
  kind: literal("camera-mask"),
7794
7826
  capName: string()
7795
- })
7827
+ }),
7828
+ object({ kind: literal("broker-audio-mute") })
7796
7829
  ]);
7797
7830
  /**
7798
7831
  * Why a switch is not offered for this camera. Rendered instead of the
@@ -7891,6 +7924,13 @@ var CAMERA_SWITCH_CATALOG = {
7891
7924
  },
7892
7925
  countsAsSwitchedOff: true
7893
7926
  },
7927
+ "broker-audio": {
7928
+ id: "broker-audio",
7929
+ label: "Audio distribution",
7930
+ 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.",
7931
+ authority: { kind: "broker-audio-mute" },
7932
+ countsAsSwitchedOff: true
7933
+ },
7894
7934
  "audio-analysis": {
7895
7935
  id: "audio-analysis",
7896
7936
  label: "Audio detection & classification",
@@ -8009,6 +8049,18 @@ function resolveState(descriptor, input) {
8009
8049
  enabled: mask.enabled
8010
8050
  };
8011
8051
  }
8052
+ case "broker-audio-mute": {
8053
+ const broker = input.brokerAudio;
8054
+ if (broker === null) return {
8055
+ available: false,
8056
+ enabled: true,
8057
+ unavailableReason: "source-unreachable"
8058
+ };
8059
+ return {
8060
+ available: true,
8061
+ enabled: !broker.muted
8062
+ };
8063
+ }
8012
8064
  }
8013
8065
  }
8014
8066
  /**
@@ -10251,7 +10303,25 @@ method(object({
10251
10303
  }), _void(), {
10252
10304
  kind: "mutation",
10253
10305
  auth: "admin"
10254
- }), method(object({ brokerId: string() }), boolean()), object({
10306
+ }), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
10307
+ muted: boolean(),
10308
+ /**
10309
+ * How many live non-derived brokers currently hold the mute. Purely
10310
+ * diagnostic: `muted` is the policy and is authoritative on its own
10311
+ * (it applies to brokers that do not exist yet), while this says
10312
+ * whether anything is presently being silenced.
10313
+ */
10314
+ appliedBrokers: number().int().nonnegative()
10315
+ })), method(object({
10316
+ deviceId: number().int(),
10317
+ muted: boolean()
10318
+ }), object({
10319
+ muted: boolean(),
10320
+ appliedBrokers: number().int().nonnegative()
10321
+ }), {
10322
+ kind: "mutation",
10323
+ auth: "admin"
10324
+ }), object({
10255
10325
  deviceId: number().int().nonnegative(),
10256
10326
  camStreamId: string(),
10257
10327
  profile: CamProfileSchema
@@ -15515,6 +15585,30 @@ var TrackSourceSchema = _enum([
15515
15585
  "audio"
15516
15586
  ]);
15517
15587
  /**
15588
+ * Where a track sits in the RETRAIN lifecycle (D81).
15589
+ *
15590
+ * - `none` — never marked, or un-marked. Evictable.
15591
+ * - `staging` — the operator wants this track as training material and has not
15592
+ * finished with it. **This is the only state retention holds**: the track and
15593
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
15594
+ * the device's age window.
15595
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
15596
+ * were COPIED into the retrain dataset at selection time, so the dataset no
15597
+ * longer depends on the track's media and the track becomes EVICTABLE again.
15598
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
15599
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
15600
+ *
15601
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
15602
+ * the store's filter language has only positive equality and `whereIn` — no
15603
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
15604
+ * would make the entire pre-column history immortal in one deploy.
15605
+ */
15606
+ var RetrainStatusSchema = _enum([
15607
+ "none",
15608
+ "staging",
15609
+ "trained"
15610
+ ]);
15611
+ /**
15518
15612
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15519
15613
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15520
15614
  * so the two surfaces cannot drift.
@@ -15524,18 +15618,31 @@ var TrackSourceSchema = _enum([
15524
15618
  * columns existed read as absent, and a consumer that needs a boolean should say
15525
15619
  * `flag === true`, not `flag !== false`.
15526
15620
  *
15527
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15528
- * operator curation, and the behaviour they drive will be specified separately.
15529
- * In particular a `markForTrain` track is NOT pinned against retention — see
15530
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15621
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
15622
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
15623
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
15624
+ * `trained` track reports `false` while refusing both writes. The boolean is
15625
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
15626
+ * "never marked" from "already trained" must read `retrainStatus`.
15627
+ *
15628
+ * `debug` does NOT pin; it is attention, not durability.
15531
15629
  */
15532
15630
  var TrackFlagFields = {
15533
- /** Operator marked this track as training material. */
15631
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
15632
+ * `'staging'`. */
15534
15633
  markForTrain: boolean().optional(),
15535
15634
  /** Operator marked this track for diagnostic attention. */
15536
15635
  debug: boolean().optional()
15537
15636
  };
15538
15637
  /**
15638
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
15639
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
15640
+ * write patch, and the status is not something the toggle sets — it is what the
15641
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
15642
+ * always present on a persisted row (the column default materialises `'none'`).
15643
+ */
15644
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
15645
+ /**
15539
15646
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15540
15647
  * one flag can never clear the other — the toggles are independent and are
15541
15648
  * driven from three surfaces that do not know about each other.
@@ -15549,7 +15656,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
15549
15656
  var TrackFlagsSchema = object({
15550
15657
  trackId: string(),
15551
15658
  markForTrain: boolean(),
15552
- debug: boolean()
15659
+ debug: boolean(),
15660
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
15661
+ * a track row) because this shape is only ever produced by the write body,
15662
+ * which always knows it — and a surface that has just written needs to render
15663
+ * `trained` without a re-fetch. */
15664
+ retrainStatus: RetrainStatusSchema
15665
+ });
15666
+ /** Per-camera slice of a training-export estimate. */
15667
+ var TrainingExportDeviceTotalsSchema = object({
15668
+ deviceId: number(),
15669
+ tracks: number().int(),
15670
+ files: number().int(),
15671
+ bytes: number().int()
15672
+ });
15673
+ /**
15674
+ * What a training export WOULD contain. Computed from media index rows only —
15675
+ * no blob is read to produce this.
15676
+ */
15677
+ var TrainingExportSummarySchema = object({
15678
+ generatedAt: number(),
15679
+ trackCount: number().int(),
15680
+ fileCount: number().int(),
15681
+ byteCount: number().int(),
15682
+ /** More marked tracks exist than a single pass carries. */
15683
+ truncated: boolean(),
15684
+ devices: array(TrainingExportDeviceTotalsSchema).readonly()
15553
15685
  });
15554
15686
  var TrackSchema = object({
15555
15687
  trackId: string(),
@@ -15594,7 +15726,8 @@ var TrackSchema = object({
15594
15726
  * Populated from the persisted envelope columns on historical reads;
15595
15727
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15596
15728
  envelope: TrackEnvelopeSchema.optional(),
15597
- ...TrackFlagFields
15729
+ ...TrackFlagFields,
15730
+ ...TrackRetrainFields
15598
15731
  });
15599
15732
  var BaseEventFields = {
15600
15733
  id: string(),
@@ -15808,7 +15941,8 @@ var KeyEventSchema = object({
15808
15941
  bestEventId: string(),
15809
15942
  /** Track lifetime in ms (lastSeen - firstSeen). */
15810
15943
  windowMs: number().optional(),
15811
- ...TrackFlagFields
15944
+ ...TrackFlagFields,
15945
+ ...TrackRetrainFields
15812
15946
  });
15813
15947
  object({
15814
15948
  trackId: string(),
@@ -16069,6 +16203,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
16069
16203
  }), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
16070
16204
  kind: "query",
16071
16205
  auth: "admin"
16206
+ }), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
16207
+ kind: "query",
16208
+ auth: "admin"
16209
+ }), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16210
+ kind: "query",
16211
+ auth: "admin"
16072
16212
  }), method(object({
16073
16213
  eventId: string(),
16074
16214
  kind: MediaFileKindEnum.optional()
@@ -22345,6 +22485,173 @@ DeviceType.Camera, method(object({
22345
22485
  status: OsdStatusSchema
22346
22486
  });
22347
22487
  /**
22488
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
22489
+ *
22490
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
22491
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
22492
+ * comes from, and it must not — a driver that grew a "show the temperature
22493
+ * here" feature would grow it once per vendor.
22494
+ *
22495
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
22496
+ * which value feeds the slot, how it is formatted, and under which
22497
+ * conditions it is shown at all. One addon renders every binding on every
22498
+ * camera, so a new source costs zero driver code.
22499
+ *
22500
+ * Three deliberate choices, each with a rejected alternative:
22501
+ *
22502
+ * 1. A source is `(capName, valuePath)` over the kernel's device
22503
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
22504
+ * cap-keyed slice a device publishes is bindable the day the cap
22505
+ * ships. The rejected alternative (one enum member per source, with
22506
+ * a resolver branch each) is what makes "add the humidity too" a
22507
+ * code change.
22508
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
22509
+ * notification centre's condition vocabulary — rather than a parallel
22510
+ * model. An operator who has learned one condition editor has learned
22511
+ * both.
22512
+ * 3. Because the renderer's facts are device STATE and not a detection
22513
+ * record, only a SUBSET of that vocabulary can be answered here.
22514
+ * `setSlotBinding` REJECTS the rest at write time (see
22515
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
22516
+ * condition that can never be true renders a permanently blank
22517
+ * overlay, and a blank overlay looks exactly like a broken camera.
22518
+ */
22519
+ /** Where a slot's value comes from. */
22520
+ var OsdSourceSchema = discriminatedUnion("kind", [
22521
+ object({
22522
+ kind: literal("static"),
22523
+ text: string().max(64)
22524
+ }),
22525
+ object({
22526
+ kind: literal("clock"),
22527
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
22528
+ pattern: string().min(1).max(32).default("HH:mm"),
22529
+ /** IANA zone. Omitted = the server's zone. */
22530
+ timezone: string().min(1).max(64).optional()
22531
+ }),
22532
+ object({
22533
+ kind: literal("device-state"),
22534
+ deviceId: number().int().optional(),
22535
+ capName: string().min(1).max(64),
22536
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
22537
+ valuePath: string().min(1).max(64)
22538
+ })
22539
+ ]);
22540
+ var OsdSlotBindingSchema = object({
22541
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
22542
+ enabled: boolean().default(true),
22543
+ source: OsdSourceSchema,
22544
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
22545
+ template: string().max(96).default("${value}"),
22546
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
22547
+ maxCharacters: number().int().min(4).max(64).optional(),
22548
+ /**
22549
+ * Decimal places for a numeric value. `0` yields an integer — the
22550
+ * documented workaround for firmwares that reject `.` in overlay text.
22551
+ */
22552
+ maxDecimals: number().int().min(0).max(4).default(1),
22553
+ /** Appended via `${unit}`. The state mirror does not carry units. */
22554
+ unitLabel: string().max(8).optional(),
22555
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
22556
+ valueMap: record(string(), string()).optional(),
22557
+ /** Time windows in which the slot is shown. Absent = always. */
22558
+ schedule: NcScheduleSchema.optional(),
22559
+ /**
22560
+ * Display gate, in the notification centre's condition vocabulary.
22561
+ * Only the keys reported by `getConditionSupport` are accepted.
22562
+ */
22563
+ conditions: NcConditionsSchema.optional(),
22564
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
22565
+ fallbackText: string().max(64).default("")
22566
+ });
22567
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
22568
+ var OsdSlotViewSchema = object({
22569
+ slotId: string(),
22570
+ kind: OsdOverlayKindEnum,
22571
+ /** Firmware refuses text edits (a timestamp, the channel name). */
22572
+ readOnly: boolean(),
22573
+ cameraEnabled: boolean(),
22574
+ cameraText: string().optional(),
22575
+ binding: OsdSlotBindingSchema.nullable()
22576
+ });
22577
+ /**
22578
+ * What happened to one slot on one render pass. `unchanged` exists so the
22579
+ * operator can tell "we are driving this and the value is steady" from
22580
+ * "we never got there" — and so the loop can prove it is not rewriting
22581
+ * identical text to the camera every tick.
22582
+ */
22583
+ var OsdRenderOutcomeEnum = _enum([
22584
+ "written",
22585
+ "unchanged",
22586
+ "gated",
22587
+ "unreadable",
22588
+ "disabled",
22589
+ "unbound",
22590
+ "failed"
22591
+ ]);
22592
+ var OsdRenderResultSchema = object({
22593
+ slotId: string(),
22594
+ outcome: OsdRenderOutcomeEnum,
22595
+ /** The text the slot should carry. Empty = the slot is switched off. */
22596
+ text: string(),
22597
+ /** Why, whenever the outcome is not a plain write. Never silent. */
22598
+ reason: string().optional()
22599
+ });
22600
+ var OsdSourceValueTypeEnum = _enum([
22601
+ "number",
22602
+ "boolean",
22603
+ "string",
22604
+ "enum"
22605
+ ]);
22606
+ /**
22607
+ * One bindable value, derived from a cap's `runtimeState` schema — never
22608
+ * hand-listed. The editor renders from this, so a cap that ships a new
22609
+ * state field becomes bindable with no UI change.
22610
+ */
22611
+ var OsdSourceOptionSchema = object({
22612
+ deviceId: number().int(),
22613
+ deviceName: string(),
22614
+ capName: string(),
22615
+ valuePath: string(),
22616
+ label: string(),
22617
+ valueType: OsdSourceValueTypeEnum,
22618
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
22619
+ enumValues: array(string()).readonly().optional()
22620
+ });
22621
+ method(object({ deviceId: number().int() }), object({
22622
+ supported: boolean(),
22623
+ slots: array(OsdSlotViewSchema)
22624
+ }), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
22625
+ supported: array(string()),
22626
+ catalog: array(NcConditionDescriptorSchema)
22627
+ }), { auth: "admin" }), method(object({
22628
+ deviceId: number().int(),
22629
+ slotId: string().min(1),
22630
+ binding: OsdSlotBindingSchema
22631
+ }), object({
22632
+ slot: OsdSlotViewSchema,
22633
+ render: OsdRenderResultSchema
22634
+ }), {
22635
+ kind: "mutation",
22636
+ auth: "admin"
22637
+ }), method(object({
22638
+ deviceId: number().int(),
22639
+ slotId: string().min(1)
22640
+ }), object({ success: literal(true) }), {
22641
+ kind: "mutation",
22642
+ auth: "admin"
22643
+ }), method(object({
22644
+ deviceId: number().int(),
22645
+ slotId: string().min(1),
22646
+ binding: OsdSlotBindingSchema.optional()
22647
+ }), OsdRenderResultSchema, {
22648
+ kind: "mutation",
22649
+ auth: "admin"
22650
+ }), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
22651
+ kind: "mutation",
22652
+ auth: "admin"
22653
+ });
22654
+ /**
22348
22655
  * Feeder connectivity / power status — mirrors the HA petkit device-status
22349
22656
  * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
22350
22657
  * `on_batteries` (running on battery backup). `null` until first reported.
@@ -27377,6 +27684,48 @@ Object.freeze({
27377
27684
  addonId: null,
27378
27685
  access: "create"
27379
27686
  },
27687
+ "osdManager.clearSlotBinding": {
27688
+ capName: "osd-manager",
27689
+ capScope: "system",
27690
+ addonId: null,
27691
+ access: "delete"
27692
+ },
27693
+ "osdManager.getConditionSupport": {
27694
+ capName: "osd-manager",
27695
+ capScope: "system",
27696
+ addonId: null,
27697
+ access: "view"
27698
+ },
27699
+ "osdManager.getDeviceOsd": {
27700
+ capName: "osd-manager",
27701
+ capScope: "system",
27702
+ addonId: null,
27703
+ access: "view"
27704
+ },
27705
+ "osdManager.getSourceCatalog": {
27706
+ capName: "osd-manager",
27707
+ capScope: "system",
27708
+ addonId: null,
27709
+ access: "view"
27710
+ },
27711
+ "osdManager.previewSlot": {
27712
+ capName: "osd-manager",
27713
+ capScope: "system",
27714
+ addonId: null,
27715
+ access: "create"
27716
+ },
27717
+ "osdManager.renderDevice": {
27718
+ capName: "osd-manager",
27719
+ capScope: "system",
27720
+ addonId: null,
27721
+ access: "create"
27722
+ },
27723
+ "osdManager.setSlotBinding": {
27724
+ capName: "osd-manager",
27725
+ capScope: "system",
27726
+ addonId: null,
27727
+ access: "create"
27728
+ },
27380
27729
  "petFeeder.callPet": {
27381
27730
  capName: "pet-feeder",
27382
27731
  capScope: "device",
@@ -27539,6 +27888,18 @@ Object.freeze({
27539
27888
  addonId: null,
27540
27889
  access: "view"
27541
27890
  },
27891
+ "pipelineAnalytics.getTrainingExportSummary": {
27892
+ capName: "pipeline-analytics",
27893
+ capScope: "device",
27894
+ addonId: null,
27895
+ access: "view"
27896
+ },
27897
+ "pipelineAnalytics.getTrainingExportUrl": {
27898
+ capName: "pipeline-analytics",
27899
+ capScope: "device",
27900
+ addonId: null,
27901
+ access: "view"
27902
+ },
27542
27903
  "pipelineAnalytics.listEventKinds": {
27543
27904
  capName: "pipeline-analytics",
27544
27905
  capScope: "device",
@@ -29015,6 +29376,12 @@ Object.freeze({
29015
29376
  addonId: null,
29016
29377
  access: "view"
29017
29378
  },
29379
+ "streamBroker.getDeviceAudioMute": {
29380
+ capName: "stream-broker",
29381
+ capScope: "system",
29382
+ addonId: null,
29383
+ access: "view"
29384
+ },
29018
29385
  "streamBroker.getPreBufferInfo": {
29019
29386
  capName: "stream-broker",
29020
29387
  capScope: "system",
@@ -29135,6 +29502,12 @@ Object.freeze({
29135
29502
  addonId: null,
29136
29503
  access: "create"
29137
29504
  },
29505
+ "streamBroker.setDeviceAudioMute": {
29506
+ capName: "stream-broker",
29507
+ capScope: "system",
29508
+ addonId: null,
29509
+ access: "create"
29510
+ },
29138
29511
  "streamBroker.setPreBufferDuration": {
29139
29512
  capName: "stream-broker",
29140
29513
  capScope: "system",
@@ -32107,6 +32480,18 @@ function parseDeviceKeyEngine(deviceKey) {
32107
32480
  }
32108
32481
  /** Per-stage timeout applied by `boundedStage` to every remote-fetch stage. */
32109
32482
  var STAGE_TIMEOUT_MS = 3e3;
32483
+ /**
32484
+ * Every switch whose OFF position leaves audio analysis with nothing to
32485
+ * analyse — the camera not capturing, this server not distributing, or the
32486
+ * analyzer not running. Named once because `CameraStatus.audio.enabled` must
32487
+ * agree with all three, and a list rebuilt at the call site is a list that
32488
+ * will miss the next one.
32489
+ */
32490
+ var AUDIO_SILENCING_SWITCH_IDS = [
32491
+ "device-audio",
32492
+ "broker-audio",
32493
+ "audio-analysis"
32494
+ ];
32110
32495
  var CameraStatusService = class {
32111
32496
  deps;
32112
32497
  constructor(deps) {
@@ -32301,12 +32686,19 @@ var CameraStatusService = class {
32301
32686
  * is precisely the failure the switch group exists to remove, and leaving
32302
32687
  * the lie in place would have made the group's own status block disagree
32303
32688
  * with it.
32689
+ *
32690
+ * THREE switches can silence this stage, not one (D83). `audio-analysis`
32691
+ * stops the analyzer; `device-audio` stops the camera capturing; and
32692
+ * `broker-audio` stops this server distributing. Any of them leaves the
32693
+ * analyzer with nothing, and reporting `enabled: true` for the other two
32694
+ * would reintroduce the exact hardcoded lie the previous paragraph is about
32695
+ * — one row further down the group.
32304
32696
  */
32305
32697
  buildAudioStage(audioNodeId, switchedOff) {
32306
32698
  if (audioNodeId === null) return null;
32307
32699
  return {
32308
32700
  nodeId: audioNodeId,
32309
- enabled: !switchedOff.includes("audio-analysis")
32701
+ enabled: !AUDIO_SILENCING_SWITCH_IDS.some((id) => switchedOff.includes(id))
32310
32702
  };
32311
32703
  }
32312
32704
  /**
@@ -32553,6 +32945,12 @@ var CameraSwitchService = class {
32553
32945
  patch: { enabled }
32554
32946
  });
32555
32947
  return;
32948
+ case "broker-audio-mute":
32949
+ await api.streamBroker.setDeviceAudioMute.mutate({
32950
+ deviceId,
32951
+ muted: !enabled
32952
+ }, nodePin(this.deps.assignSource(deviceId)));
32953
+ return;
32556
32954
  }
32557
32955
  }
32558
32956
  /**
@@ -32571,7 +32969,8 @@ var CameraSwitchService = class {
32571
32969
  recordingEnabled: null,
32572
32970
  notificationsMuted: null,
32573
32971
  deviceAudio: null,
32574
- privacyMask: null
32972
+ privacyMask: null,
32973
+ brokerAudio: null
32575
32974
  },
32576
32975
  wrapperAddonIdByCap: /* @__PURE__ */ new Map(),
32577
32976
  recordingConfig: null
@@ -32611,11 +33010,16 @@ var CameraSwitchService = class {
32611
33010
  this.warn(deviceId, "notificationRules.listDeviceMutes", err);
32612
33011
  return null;
32613
33012
  });
32614
- const [device, bindings, recordingConfig, mutedDeviceIds] = await Promise.all([
33013
+ const brokerAudioPromise = api.streamBroker.getDeviceAudioMute.query({ deviceId }, nodePin(this.deps.assignSource(deviceId))).then((r) => ({ muted: r.muted })).catch((err) => {
33014
+ this.warn(deviceId, "streamBroker.getDeviceAudioMute", err);
33015
+ return null;
33016
+ });
33017
+ const [device, bindings, recordingConfig, mutedDeviceIds, brokerAudio] = await Promise.all([
32615
33018
  devicePromise,
32616
33019
  bindingsPromise,
32617
33020
  recordingPromise,
32618
- mutesPromise
33021
+ mutesPromise,
33022
+ brokerAudioPromise
32619
33023
  ]);
32620
33024
  const [bindable, privacy] = await Promise.all([device === null ? Promise.resolve(null) : api.deviceManager.listBindableCapsForDeviceType.query({ deviceType: device.type }).catch((err) => {
32621
33025
  this.warn(deviceId, "listBindableCapsForDeviceType", err);
@@ -32636,7 +33040,8 @@ var CameraSwitchService = class {
32636
33040
  recordingEnabled: recordingConfig === null ? null : recordingConfig.enabled,
32637
33041
  notificationsMuted: mutedDeviceIds === null ? null : mutedDeviceIds.includes(deviceId),
32638
33042
  deviceAudio: privacy.deviceAudio,
32639
- privacyMask: privacy.privacyMask
33043
+ privacyMask: privacy.privacyMask,
33044
+ brokerAudio
32640
33045
  },
32641
33046
  wrapperAddonIdByCap,
32642
33047
  recordingConfig
@@ -38416,7 +38821,8 @@ async function buildOrchestratorControllers(deps) {
38416
38821
  });
38417
38822
  const cameraSwitchService = new CameraSwitchService({
38418
38823
  api: () => deps.ctx().api ?? null,
38419
- logger: deps.ctx().logger
38824
+ logger: deps.ctx().logger,
38825
+ assignSource: (deviceId) => topology.assignSource(deviceId)
38420
38826
  });
38421
38827
  const cameraStatusService = new CameraStatusService({
38422
38828
  api: () => deps.ctx().api,