@camstack/addon-post-analysis 1.2.47 → 1.2.49

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.
@@ -6509,9 +6509,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
6509
6509
  /**
6510
6510
  * Build the tRPC request options that pin a single capability call to `nodeId`.
6511
6511
  * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
6512
+ *
6513
+ * ## The id is normalised here, and it has to be
6514
+ *
6515
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
6516
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
6517
+ * compares a pin against real node ids, so such a pin matches nothing and the
6518
+ * call fails with `no provider registered for cap "…"`. The local-first
6519
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
6520
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
6521
+ * the thing you reach for when you specifically need THIS node — silently
6522
+ * addressed a node that does not exist.
6523
+ *
6524
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
6525
+ * own node to read the host's hardware-decode backend. It never once answered,
6526
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
6527
+ * while D67's whole premise was that the decoder addon is the authority on
6528
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
6529
+ * hardware", which was false.
6530
+ *
6531
+ * Normalising in the ONE constructor fixes every caller at once, which is why
6532
+ * it is here and not at the call sites.
6512
6533
  */
6513
6534
  function nodePin(nodeId) {
6514
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
6535
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
6536
+ }
6537
+ /**
6538
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
6539
+ * is idempotent, so passing an already-clean id costs nothing.
6540
+ */
6541
+ function toNodeId(idOrRunnerId) {
6542
+ const head = idOrRunnerId.split("/")[0];
6543
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
6515
6544
  }
6516
6545
  /**
6517
6546
  * Output schema shared by the contribution + live methods.
@@ -7068,6 +7097,30 @@ var DEVICE_SCOPED_CAPS = new Set([
7068
7097
  function isDeviceScopedCap(capName) {
7069
7098
  return DEVICE_SCOPED_CAPS.has(capName);
7070
7099
  }
7100
+ /**
7101
+ * Promise-based timer helpers — used everywhere the codebase needs to
7102
+ * wait, back off, or schedule a retry. Before these helpers landed, each
7103
+ * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
7104
+ * with subtle variations (some swallowing cancellation, some not). Two
7105
+ * shapes cover every observed use case:
7106
+ *
7107
+ * - {@link sleep} for a plain, uncancellable wait — the default choice.
7108
+ * - {@link sleepCancellable} for a wait that wakes early when an
7109
+ * abort signal trips, used by long-running pollers whose teardown
7110
+ * must stop a pending backoff promptly.
7111
+ */
7112
+ /**
7113
+ * Resolve after `ms` milliseconds. Never rejects, never cancels. The
7114
+ * sleep cannot be interrupted; for a wakeable variant use
7115
+ * {@link sleepCancellable}.
7116
+ *
7117
+ * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
7118
+ * still gives the event loop a chance to drain — useful for breaking
7119
+ * up tight async loops without changing call-site semantics.
7120
+ */
7121
+ function sleep(ms) {
7122
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7123
+ }
7071
7124
  //#endregion
7072
7125
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7073
7126
  /**
@@ -7092,6 +7145,14 @@ var EncodeProfileSchema = object({
7092
7145
  "main",
7093
7146
  "high"
7094
7147
  ]).optional(),
7148
+ /**
7149
+ * `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
7150
+ * (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
7151
+ * it, or it ships a stream that does not match its own advertisement — the
7152
+ * defect class that kept HomeKit black for a year and that Alexa carried
7153
+ * silently. Optional because a browser negotiates the level itself.
7154
+ */
7155
+ level: string().optional(),
7095
7156
  width: number().int().positive().optional(),
7096
7157
  height: number().int().positive().optional(),
7097
7158
  fps: number().positive().optional(),
@@ -7139,6 +7200,29 @@ var EncodeProfileSchema = object({
7139
7200
  outputArgs: array(string()).optional()
7140
7201
  });
7141
7202
  /**
7203
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
7204
+ * Baseline because it is the one profile every consumer in this repo decodes
7205
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
7206
+ */
7207
+ var BASE_LIVE_EGRESS_PROFILE = {
7208
+ video: {
7209
+ codec: "h264",
7210
+ profile: "baseline",
7211
+ level: "3.1",
7212
+ width: 1280,
7213
+ height: 720,
7214
+ fps: 25,
7215
+ bitrateKbps: 2500,
7216
+ gopFrames: 25,
7217
+ bf: 0,
7218
+ preset: "veryfast",
7219
+ tune: "zerolatency"
7220
+ },
7221
+ audio: "passthrough"
7222
+ };
7223
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7224
+ ({ ...BASE_LIVE_EGRESS_PROFILE });
7225
+ /**
7142
7226
  * Deep wiring healthcheck — snapshot of active reachability probes across
7143
7227
  * every declared capability + widget of every installed plugin, on every
7144
7228
  * node. Produced by the backend `WiringHealthService` and surfaced via
@@ -7188,6 +7272,154 @@ object({
7188
7272
  })
7189
7273
  });
7190
7274
  /**
7275
+ * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7276
+ * pipeline functions an operator thinks in terms of.
7277
+ *
7278
+ * ## This file adds no state
7279
+ *
7280
+ * Every switch here is a VIEW onto an authority that already existed
7281
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
7282
+ * group is that there is exactly one place each function is turned off, and
7283
+ * the group routes to it:
7284
+ *
7285
+ * | Switch | Authority | Proven "off stops the work" gate |
7286
+ * | --- | --- | --- |
7287
+ * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7288
+ * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7289
+ * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7290
+ * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7291
+ * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7292
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7293
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7294
+ *
7295
+ * ## The two switches whose authority is not on this server
7296
+ *
7297
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
7298
+ * in "the group stores nothing" — it is the purest form of it: the camera
7299
+ * holds the fact, every read is a read-through, and there is no server-side
7300
+ * copy that could drift. Their availability therefore cannot come from
7301
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
7302
+ * is filtered out there); it comes from the cap's own camera-probed
7303
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
7304
+ * THIS camera rather than for the device type
7305
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
7306
+ *
7307
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
7308
+ *
7309
+ * Every other switch means *this camera's function is doing its job*, so
7310
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
7311
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
7312
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
7313
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
7314
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
7315
+ * worse than either surface not having one.
7316
+ *
7317
+ * Two consequences follow and both are load-bearing:
7318
+ *
7319
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
7320
+ * exactly this row. With the polarity above, every camera that has NOT drawn
7321
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
7322
+ * normal, healthy state of most cameras rendered as an operator disablement.
7323
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
7324
+ * unconditionally by both clients, so for this row it has to read correctly
7325
+ * whichever way the switch is sitting.
7326
+ *
7327
+ * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7328
+ * migrated the legacy `audioEnabled` / `pipelineEnabled` /
7329
+ * `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
7330
+ * surface that decision never got.
7331
+ *
7332
+ * ## Two rules that are load-bearing
7333
+ *
7334
+ * - **Recording's switch is `enabled`, never the bands.** `bands` is the only
7335
+ * authored intent and `mode` is derived from it (`deriveRecordingMode`).
7336
+ * Expressing "off" by clearing bands destroys the operator's schedule and
7337
+ * turning the camera back on would then silently record nothing.
7338
+ * - **A switch that is off must be reported as off**, not merely produce
7339
+ * nothing. {@link CameraSwitch.enabled} is what a status surface renders as
7340
+ * "disabled by an operator" instead of "broken" — see
7341
+ * `CameraStatus.switchedOff`.
7342
+ */
7343
+ /**
7344
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
7345
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
7346
+ * recognition and plate/LPR are per-step toggles on
7347
+ * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7348
+ * editor, not in a safety group.
7349
+ */
7350
+ var CameraSwitchIdSchema = _enum([
7351
+ "stream-broker",
7352
+ "object-detection",
7353
+ "privacy-mask",
7354
+ "device-audio",
7355
+ "audio-analysis",
7356
+ "recording",
7357
+ "notifications"
7358
+ ]);
7359
+ /**
7360
+ * WHERE the switch's state actually lives. A discriminated union rather than a
7361
+ * string so both the writer (the orchestrator's `setCameraSwitch`) and any
7362
+ * reader can exhaustively narrow — and so "the group added a parallel map" is
7363
+ * a compile error rather than a review comment.
7364
+ */
7365
+ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7366
+ object({ kind: literal("device-disabled") }),
7367
+ object({
7368
+ kind: literal("wrapper-binding"),
7369
+ capName: string()
7370
+ }),
7371
+ object({ kind: literal("recording-config") }),
7372
+ object({ kind: literal("notification-mute") }),
7373
+ object({
7374
+ kind: literal("camera-audio"),
7375
+ capName: string()
7376
+ }),
7377
+ object({
7378
+ kind: literal("camera-mask"),
7379
+ capName: string()
7380
+ })
7381
+ ]);
7382
+ /**
7383
+ * Why a switch is not offered for this camera. Rendered instead of the
7384
+ * control, never as a dead control — an absent function and a broken one must
7385
+ * not look the same.
7386
+ */
7387
+ var CameraSwitchUnavailableReasonSchema = _enum([
7388
+ "no-provider",
7389
+ "source-unreachable",
7390
+ "not-configured"
7391
+ ]);
7392
+ /**
7393
+ * One switch, resolved for one camera.
7394
+ *
7395
+ * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
7396
+ * client-side: the viewer is a separate repository that does not import
7397
+ * `@camstack/types`, and a cost line duplicated in two clients is a cost line
7398
+ * that will disagree with itself. Five rows per camera is nothing.
7399
+ */
7400
+ var CameraSwitchSchema = object({
7401
+ id: CameraSwitchIdSchema,
7402
+ label: string(),
7403
+ /**
7404
+ * What the operator LOSES while this is off, in one sentence. Required, not
7405
+ * optional: a switch that cannot say what it costs should not ship.
7406
+ */
7407
+ costWhenOff: string(),
7408
+ /** False = do not render a control. `unavailableReason` says why. */
7409
+ available: boolean(),
7410
+ unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7411
+ /** Current state. Meaningless when `available` is false — read it as `true`. */
7412
+ enabled: boolean(),
7413
+ authority: CameraSwitchAuthoritySchema
7414
+ });
7415
+ /** The whole group for one camera. */
7416
+ var CameraSwitchGroupSchema = object({
7417
+ deviceId: number().int(),
7418
+ switches: array(CameraSwitchSchema).readonly(),
7419
+ /** Unix ms when the group was composed server-side. */
7420
+ fetchedAt: number()
7421
+ });
7422
+ /**
7191
7423
  * Ops-log — the durable, append-only operations audit shared by the
7192
7424
  * recordings and events management surfaces.
7193
7425
  *
@@ -7206,14 +7438,16 @@ var OpsLogOpSchema = _enum([
7206
7438
  "manual-delete",
7207
7439
  "rescan",
7208
7440
  "retention-run",
7209
- "relocate"
7441
+ "relocate",
7442
+ "orphan-audit"
7210
7443
  ]);
7211
7444
  /** Why the operation ran. */
7212
7445
  var OpsLogReasonSchema = _enum([
7213
7446
  "retention",
7214
7447
  "quota",
7215
7448
  "manual",
7216
- "operator"
7449
+ "operator",
7450
+ "maintenance"
7217
7451
  ]);
7218
7452
  /** One audit row, shared verbatim by both domains. */
7219
7453
  var OpsLogEntrySchema = object({
@@ -9099,6 +9333,126 @@ var RtpSourceSchema = object({
9099
9333
  encoder: string(),
9100
9334
  pipelineKey: string()
9101
9335
  });
9336
+ /**
9337
+ * The encode request — **structured and serialisable, with NO raw-flag escape
9338
+ * hatch.** This is deliberate and it is the one lesson taken from
9339
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
9340
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
9341
+ * adding a flag silently forks the shared child, and two consumers that mean
9342
+ * the same thing but spell it differently never share. Here every knob is a
9343
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
9344
+ * never an opaque array.
9345
+ *
9346
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
9347
+ * The operator-facing derived-stream transform editor still has them — that is
9348
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
9349
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
9350
+ */
9351
+ var EgressEncodeSchema = EncodeProfileSchema.omit({
9352
+ inputArgs: true,
9353
+ outputArgs: true
9354
+ });
9355
+ /**
9356
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
9357
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
9358
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
9359
+ * an Echo). Named rather than numeric so the INTENT survives.
9360
+ */
9361
+ var EgressRateControlSchema = _enum(["tight", "relaxed"]);
9362
+ var EgressTranscodeRequestSchema = object({
9363
+ deviceId: number().int().nonnegative(),
9364
+ /** Which published stream to read. */
9365
+ source: discriminatedUnion("kind", [object({
9366
+ kind: literal("profile"),
9367
+ profile: CamProfileSchema
9368
+ }), object({
9369
+ kind: literal("cam-stream"),
9370
+ camStreamId: string().min(1)
9371
+ })]),
9372
+ encode: EgressEncodeSchema,
9373
+ rateControl: EgressRateControlSchema.optional(),
9374
+ /**
9375
+ * `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
9376
+ * out-of-band extradata and needs `dump_extra` on both the copy and encode
9377
+ * branches. Enumerated, not free text.
9378
+ */
9379
+ bitstreamFilter: _enum([
9380
+ "dump_extra",
9381
+ "h264_mp4toannexb",
9382
+ "hevc_mp4toannexb"
9383
+ ]).optional(),
9384
+ /**
9385
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
9386
+ * consumer to dial the returned url. The broker picks the id and returns it
9387
+ * as `camStreamId` — a caller-supplied one would be circular, since the
9388
+ * sharing key is computed FROM this request.
9389
+ *
9390
+ * The url is still returned and still the contract for a transcode pinned to
9391
+ * another node. But dialling it locally costs an RTSP round trip that changes
9392
+ * the transport underneath the consumer: a dialled stream is an RTP source,
9393
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
9394
+ * repacketizer branch. The push branch — the one the derived mechanism has
9395
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
9396
+ * arriving, key frame arriving, black screen, on a chain healthy at every
9397
+ * other point.
9398
+ *
9399
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
9400
+ * gives priority to `hub-in-process` so a local call never leaves the node.
9401
+ * This is that rule for media.
9402
+ */
9403
+ publishLocally: boolean().optional(),
9404
+ pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9405
+ /**
9406
+ * Operator/consumer override for decode hardware. ABSENT is the normal case
9407
+ * and the one that matters: the broker then resolves the backend from the
9408
+ * DECODER ADDON's per-node `probedBestHwaccel` (see
9409
+ * `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
9410
+ * on this hardware — never the raw kernel resolver's qsv-first order.
9411
+ */
9412
+ decodeHwAccel: _enum([
9413
+ "auto",
9414
+ "none",
9415
+ "videotoolbox",
9416
+ "vaapi",
9417
+ "qsv",
9418
+ "cuda"
9419
+ ]).optional(),
9420
+ /**
9421
+ * Host to embed in the returned restream `url`. The broker mints hub-local
9422
+ * `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
9423
+ * host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
9424
+ * dialable from there. Same contract as `getStreamWithCodec.hostname` —
9425
+ * `substituteRtspHost` rewrites only the dial address, never the restreamer.
9426
+ */
9427
+ hostname: string().optional(),
9428
+ /** Attribution for the broker panel. Never part of the sharing key. */
9429
+ tag: string().optional()
9430
+ });
9431
+ var EgressTranscodeSchema = object({
9432
+ /** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
9433
+ url: string(),
9434
+ /** Release handle. Refcounted — the child dies when the last holder releases. */
9435
+ pipelineKey: string(),
9436
+ videoCodec: _enum(["H264", "H265"]),
9437
+ resolution: object({
9438
+ width: number().int().positive(),
9439
+ height: number().int().positive()
9440
+ }),
9441
+ transcoded: boolean(),
9442
+ encoder: string(),
9443
+ /**
9444
+ * The decode backend the child ACTUALLY ran with — `null` for software.
9445
+ * Returned rather than assumed: a consumer that asked for hardware and got
9446
+ * software needs to be able to see that without reading the broker's logs.
9447
+ */
9448
+ decodeHwAccel: string().nullable(),
9449
+ /**
9450
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
9451
+ * `url`, and the session takes the push/deframe transport rather than the
9452
+ * RTP-passthrough one. `null` means the consumer must dial.
9453
+ */
9454
+ camStreamId: string().nullable()
9455
+ });
9102
9456
  method(object({
9103
9457
  deviceId: number().int().nonnegative(),
9104
9458
  camStreamId: string().min(1),
@@ -9208,6 +9562,15 @@ method(object({
9208
9562
  }), {
9209
9563
  kind: "mutation",
9210
9564
  auth: "admin"
9565
+ }), method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
9566
+ kind: "mutation",
9567
+ auth: "admin"
9568
+ }), method(object({ pipelineKey: string() }), object({
9569
+ released: boolean(),
9570
+ refcount: number().int().nonnegative()
9571
+ }), {
9572
+ kind: "mutation",
9573
+ auth: "admin"
9211
9574
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
9212
9575
  subscriptionId: string(),
9213
9576
  maxCount: number().int().positive().default(8)
@@ -13820,12 +14183,13 @@ var NcConditionsSchema = object({
13820
14183
  * source; otherwise the subject's source must equal it. Legacy records
13821
14184
  * with no stamped source are treated as `pipeline`. The union spans both
13822
14185
  * record kinds — object events carry `pipeline` | `onboard`, synthetic
13823
- * tracks carry `sensor`.
14186
+ * tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
13824
14187
  */
13825
14188
  source: _enum([
13826
14189
  "pipeline",
13827
14190
  "onboard",
13828
14191
  "sensor",
14192
+ "audio",
13829
14193
  "any"
13830
14194
  ]).optional(),
13831
14195
  /**
@@ -14379,6 +14743,10 @@ var NC_CONDITION_CATALOG = [
14379
14743
  {
14380
14744
  value: "sensor",
14381
14745
  label: "Sensor"
14746
+ },
14747
+ {
14748
+ value: "audio",
14749
+ label: "Audio marker"
14382
14750
  }
14383
14751
  ],
14384
14752
  operator: "in",
@@ -14389,7 +14757,7 @@ var NC_CONDITION_CATALOG = [
14389
14757
  "package-event"
14390
14758
  ],
14391
14759
  phase: "P1",
14392
- description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
14760
+ description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
14393
14761
  },
14394
14762
  {
14395
14763
  id: "deviceState",
@@ -14735,6 +15103,35 @@ var notificationRulesCapability = {
14735
15103
  auth: "admin"
14736
15104
  }),
14737
15105
  /**
15106
+ * PERMANENT per-camera mute — the notifications half of the per-camera
15107
+ * function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
15108
+ *
15109
+ * Deliberately NOT a snooze. A snooze is bounded at
15110
+ * {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
15111
+ * expire would be an outage the operator asked for once and forgot" — and
15112
+ * widening it to express "this camera never notifies" would destroy that
15113
+ * property for every snooze. A mute is the other thing: an explicit,
15114
+ * indefinite, admin-only decision, visible in the switch group next to the
15115
+ * other four, and reported on `CameraStatus.switchedOff` so a silent
15116
+ * camera never reads as a working one.
15117
+ *
15118
+ * Returned as ONE list rather than a per-camera query: the group's reader
15119
+ * needs every camera's state, and a per-camera fan-out over the viewer's
15120
+ * single WebSocket is N frames serialised on one socket.
15121
+ */
15122
+ listDeviceMutes: method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }),
15123
+ /**
15124
+ * Mute or unmute one camera. Idempotent; an unmute of a camera that was
15125
+ * never muted succeeds.
15126
+ */
15127
+ setDeviceMuted: method(object({
15128
+ deviceId: number().int(),
15129
+ muted: boolean()
15130
+ }), object({ success: literal(true) }), {
15131
+ kind: "mutation",
15132
+ auth: "admin"
15133
+ }),
15134
+ /**
14738
15135
  * Dry-run a rule against recently persisted records (object events for
14739
15136
  * `immediate`, closed tracks for `track-end`). Mutation kind only to
14740
15137
  * carry the full rule object safely; no side effects.
@@ -15140,12 +15537,60 @@ var TrackAudioLabelSchema = object({
15140
15537
  });
15141
15538
  /**
15142
15539
  * How a track was produced. `pipeline` (default / absent) = the spatial
15143
- * detection+tracking pipeline. `sensor` = a SYNTHETIC track projected from a
15144
- * linked sensor/control state change (no positions; carries a snapshot). The
15145
- * spatial subsystems (tracker association, occupancy count, re-id/embedding,
15146
- * resurrection) MUST skip `sensor` tracks — they have no bbox trajectory.
15540
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
15541
+ * no positions, a single snapshot, and no bbox trajectory at all:
15542
+ *
15543
+ * - `sensor` — a linked sensor/control device state change.
15544
+ * - `audio` — an audio event on the camera itself that was anomalous for
15545
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
15546
+ *
15547
+ * The spatial subsystems (tracker association, occupancy count, re-id /
15548
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
15549
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
15550
+ * check silently readmits every source added after it was written.
15147
15551
  */
15148
- var TrackSourceSchema = _enum(["pipeline", "sensor"]);
15552
+ var TrackSourceSchema = _enum([
15553
+ "pipeline",
15554
+ "sensor",
15555
+ "audio"
15556
+ ]);
15557
+ /**
15558
+ * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15559
+ * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15560
+ * so the two surfaces cannot drift.
15561
+ *
15562
+ * **Absent ≠ false.** A track that has never been touched omits the field; an
15563
+ * explicitly un-flagged track carries `false`. Legacy rows written before the
15564
+ * columns existed read as absent, and a consumer that needs a boolean should say
15565
+ * `flag === true`, not `flag !== false`.
15566
+ *
15567
+ * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15568
+ * operator curation, and the behaviour they drive will be specified separately.
15569
+ * In particular a `markForTrain` track is NOT pinned against retention — see
15570
+ * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15571
+ */
15572
+ var TrackFlagFields = {
15573
+ /** Operator marked this track as training material. */
15574
+ markForTrain: boolean().optional(),
15575
+ /** Operator marked this track for diagnostic attention. */
15576
+ debug: boolean().optional()
15577
+ };
15578
+ /**
15579
+ * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15580
+ * one flag can never clear the other — the toggles are independent and are
15581
+ * driven from three surfaces that do not know about each other.
15582
+ */
15583
+ var TrackFlagsPatchSchema = object(TrackFlagFields);
15584
+ /**
15585
+ * The resolved flag state after a write. Both fields are REQUIRED here (absent
15586
+ * collapses to `false`) so a caller can drive a toggle's checked state off the
15587
+ * mutation result without a re-fetch.
15588
+ */
15589
+ var TrackFlagsSchema = object({
15590
+ trackId: string(),
15591
+ markForTrain: boolean(),
15592
+ debug: boolean()
15593
+ });
15149
15594
  var TrackSchema = object({
15150
15595
  trackId: string(),
15151
15596
  deviceId: number(),
@@ -15188,7 +15633,8 @@ var TrackSchema = object({
15188
15633
  /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
15189
15634
  * Populated from the persisted envelope columns on historical reads;
15190
15635
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15191
- envelope: TrackEnvelopeSchema.optional()
15636
+ envelope: TrackEnvelopeSchema.optional(),
15637
+ ...TrackFlagFields
15192
15638
  });
15193
15639
  var BaseEventFields = {
15194
15640
  id: string(),
@@ -15401,7 +15847,8 @@ var KeyEventSchema = object({
15401
15847
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15402
15848
  bestEventId: string(),
15403
15849
  /** Track lifetime in ms (lastSeen - firstSeen). */
15404
- windowMs: number().optional()
15850
+ windowMs: number().optional(),
15851
+ ...TrackFlagFields
15405
15852
  });
15406
15853
  object({
15407
15854
  trackId: string(),
@@ -15487,7 +15934,31 @@ var RebuildObjectEmbeddingsInput = object({
15487
15934
  since: number().optional(),
15488
15935
  until: number().optional(),
15489
15936
  /** Stop after this many tracks; the result reports whether more remain. */
15490
- maxTracks: number().int().positive().optional()
15937
+ maxTracks: number().int().positive().optional(),
15938
+ /**
15939
+ * Run every embedding on THIS node instead of round-robining the fleet.
15940
+ *
15941
+ * Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
15942
+ * field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
15943
+ * calling it that would pin the rebuild REQUEST itself to that node — the
15944
+ * rebuild orchestration lives on the hub, and only the per-track step runs
15945
+ * remotely. This field is data; the per-track pin is applied inside.
15946
+ *
15947
+ * Absent ⇒ round-robin over every online node whose runner can serve the
15948
+ * pinned model.
15949
+ */
15950
+ executeOnNodeId: string().optional(),
15951
+ /**
15952
+ * Milliseconds to wait between tracks; omit for the built-in default, `0` to
15953
+ * run flat out.
15954
+ *
15955
+ * A rebuild is bulk maintenance on hub-main's single thread. Measured
15956
+ * 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
15957
+ * pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
15958
+ * force is logged at start and finish so a deliberately slow pass reads
15959
+ * differently from a stalled one.
15960
+ */
15961
+ pacingMs: number().int().nonnegative().optional()
15491
15962
  });
15492
15963
  /**
15493
15964
  * Result of emptying the CLIP index.
@@ -15521,13 +15992,23 @@ var RebuildStatusSchema = object({
15521
15992
  /** Tracks with no usable detection box. */
15522
15993
  missingBbox: number(),
15523
15994
  /**
15524
- * Tracks the pipeline REFUSED rather than broke on: the camera is not
15525
- * attached, or `clip-embedding` is not enabled in its step tree. Separate
15526
- * from `failed` because the remedy is a configuration change, not an engine
15527
- * investigation — and because a pass over decommissioned cameras would
15528
- * otherwise read as a total engine outage.
15995
+ * Tracks an executing node REFUSED rather than broke on — an unreadable key
15996
+ * frame, a step that threw. Separate from `failed` because the remedy is
15997
+ * different, and because a whole camera silently contributing zero vectors
15998
+ * is the shape of failure a rebuild must never hide.
15529
15999
  */
15530
16000
  notRunnable: number(),
16001
+ /**
16002
+ * The pass stopped because NO node could serve the pinned model.
16003
+ *
16004
+ * Distinct from `notRunnable` on purpose: that one says "this track was
16005
+ * refused", this one says "the cluster cannot do this work at all" — every
16006
+ * candidate node either lacks the `clip-embedding` step, lacks a build of the
16007
+ * pinned model for its engine format, or dropped out. The remedy is a model /
16008
+ * engine change, not a per-camera one. Non-zero here always comes with
16009
+ * `complete: false`.
16010
+ */
16011
+ noCapableNode: number(),
15531
16012
  failed: number(),
15532
16013
  /** Set once a pass ends: true only when EVERYTHING was covered. */
15533
16014
  complete: boolean().nullable(),
@@ -15717,6 +16198,31 @@ var pipelineAnalyticsCapability = {
15717
16198
  auth: "admin"
15718
16199
  }),
15719
16200
  /**
16201
+ * Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
16202
+ * The patch is PARTIAL — an omitted key is left untouched — because the
16203
+ * three surfaces that write it (admin Events grid, viewer track detail,
16204
+ * viewer cluster detail) each own one toggle and must not clobber the other.
16205
+ *
16206
+ * Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
16207
+ * live where `label` and `importance` live, not in any per-device settings
16208
+ * store. Updates the in-RAM active track too, so a flag set on a live track
16209
+ * survives its expiry-time persist.
16210
+ *
16211
+ * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
16212
+ * authenticated non-admin surface and two of the three call sites are
16213
+ * there. Revisit if a flag ever gains an effect that costs storage —
16214
+ * `deleteTracks` next door is admin for exactly that reason.
16215
+ *
16216
+ * Returns the RESOLVED state of both flags (absent → `false`) so a caller
16217
+ * can drive its toggle without a re-fetch. Rejects an unknown track.
16218
+ */
16219
+ setTrackFlags: method(object({
16220
+ /** Log/audit scope only — the trackId is globally unique on its own. */
16221
+ deviceId: number(),
16222
+ trackId: string(),
16223
+ flags: TrackFlagsPatchSchema
16224
+ }), TrackFlagsSchema, { kind: "mutation" }),
16225
+ /**
15720
16226
  * Durable event-store footprint for the management UI: event rows
15721
16227
  * (motion + object + audio) counted per camera + total, plus the
15722
16228
  * event-owned media bytes on disk per camera + total. Stat/count-based,
@@ -16443,6 +16949,53 @@ var DetailResultSchema = object({
16443
16949
  nativeFaceShortSidePx: number().optional()
16444
16950
  });
16445
16951
  /**
16952
+ * Why an executing node REFUSED a stateless step run (`runStatelessStep`).
16953
+ *
16954
+ * A refusal is a first-class answer, not an error, because the caller's next
16955
+ * move depends on WHICH one it is — and because "the pass produced nothing"
16956
+ * must never be reachable without a named, counted cause. The two tiers:
16957
+ *
16958
+ * - **node-level** (`unknown-step`, `model-not-servable`) — this node can
16959
+ * never serve this (step, model) pair. The caller drops it from its rotation
16960
+ * and retries the same work elsewhere; nothing about the work changes.
16961
+ * - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
16962
+ * fine, this one request is not. Retrying it on another node would only
16963
+ * spread the same failure.
16964
+ */
16965
+ var StatelessStepRefusalSchema = _enum([
16966
+ "unknown-step",
16967
+ "model-not-servable",
16968
+ "unreadable-frame",
16969
+ "execution-failed"
16970
+ ]);
16971
+ /**
16972
+ * Answer to `runStatelessStep` — a discriminated union rather than a nullable
16973
+ * result, because `null` is exactly what made the camera-bound detail path
16974
+ * unable to tell "refused" from "never asked".
16975
+ */
16976
+ var RunStatelessStepResultSchema = discriminatedUnion("kind", [object({
16977
+ kind: literal("ran"),
16978
+ /** The node that actually executed it — the pin, echoed back for the log. */
16979
+ nodeId: string(),
16980
+ /**
16981
+ * The model the step ran with.
16982
+ *
16983
+ * The node verified this exact id has a build for the format it dispatched
16984
+ * on BEFORE running, so the executor's format resolution returns it
16985
+ * unchanged. A caller that pinned a model must compare this field and
16986
+ * treat a mismatch as a refusal — the whole point of the pin is that a
16987
+ * pass writes one feature space.
16988
+ */
16989
+ modelId: string(),
16990
+ details: array(DetailResultSchema)
16991
+ }), object({
16992
+ kind: literal("refused"),
16993
+ nodeId: string(),
16994
+ reason: StatelessStepRefusalSchema,
16995
+ /** Human-readable specifics — the format tried, the formats shipped, etc. */
16996
+ detail: string()
16997
+ })]);
16998
+ /**
16446
16999
  * Per-camera tunable ranges + defaults. Single source of truth used
16447
17000
  * by both the Zod data schema (validation + default fallback) and
16448
17001
  * the device settings UI (slider min/max/step). Touch one place and
@@ -16792,7 +17345,32 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
16792
17345
  cropJpeg: string().optional(),
16793
17346
  parent: DetailParentSchema,
16794
17347
  steps: array(string()).optional()
16795
- }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
17348
+ }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
17349
+ /** Catalog step id, e.g. `clip-embedding`. */
17350
+ stepId: string(),
17351
+ /**
17352
+ * REQUIRED model pin. The node runs this exact model or refuses with
17353
+ * `model-not-servable` — it never substitutes a format default, because
17354
+ * a fleet pass that round-robins across nodes would then fill one index
17355
+ * from several encoders.
17356
+ */
17357
+ modelId: string(),
17358
+ /** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
17359
+ frameJpeg: string(),
17360
+ /**
17361
+ * The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
17362
+ * purpose: the caller stores boxes against a downscaled analysis frame
17363
+ * while the stored key frame is native-resolution, and the only side
17364
+ * that reliably knows the image's pixel dimensions is the side that
17365
+ * decodes it. Denormalising here removes a second reader of the
17366
+ * dimensions and the class of mismatch that comes with it.
17367
+ */
17368
+ bbox: NativeCropBboxSchema,
17369
+ /** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
17370
+ className: string(),
17371
+ /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
17372
+ sourceDeviceId: number()
17373
+ }), RunStatelessStepResultSchema, { kind: "mutation" });
16796
17374
  var CameraPipelineConfigSchema = object({
16797
17375
  engine: PipelineEngineChoiceSchema.optional(),
16798
17376
  steps: array(PipelineStepInputSchema).readonly(),
@@ -17090,6 +17668,20 @@ var CameraStatusSchema = object({
17090
17668
  detection: CameraDetectionStatusSchema.nullable(),
17091
17669
  audio: CameraAudioStatusSchema.nullable(),
17092
17670
  recording: CameraRecordingStatusSchema.nullable(),
17671
+ /**
17672
+ * Per-camera function switches an OPERATOR has turned off
17673
+ * ([D61](../../../../docs/decisions/adr-0067.md)).
17674
+ *
17675
+ * This is the difference between DISABLED and BROKEN. A camera whose
17676
+ * `detection` block reports zero fps and whose `switchedOff` contains
17677
+ * `'object-detection'` was switched off by a person; the same camera with an
17678
+ * empty list is failing. Every status surface must render the two
17679
+ * differently — a quiet camera that looks identical to a dead one is the
17680
+ * silence-reads-as-never-happened trap this repo keeps paying for.
17681
+ *
17682
+ * Empty when nothing is off. Never contains a switch no provider offers.
17683
+ */
17684
+ switchedOff: array(CameraSwitchIdSchema).readonly(),
17093
17685
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
17094
17686
  fetchedAt: number()
17095
17687
  });
@@ -17258,7 +17850,14 @@ method(object({
17258
17850
  }), method(object({
17259
17851
  deviceId: number(),
17260
17852
  agentNodeId: string().optional()
17261
- }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17853
+ }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraSwitchGroupSchema), method(object({
17854
+ deviceId: number(),
17855
+ switchId: CameraSwitchIdSchema,
17856
+ enabled: boolean()
17857
+ }), CameraSwitchGroupSchema, {
17858
+ kind: "mutation",
17859
+ auth: "admin"
17860
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17262
17861
  name: string(),
17263
17862
  description: string().optional(),
17264
17863
  config: CameraPipelineConfigSchema
@@ -17611,9 +18210,15 @@ DeviceType.Camera, method(object({
17611
18210
  * Bypass the cache freshness check and fetch directly from the
17612
18211
  * native (or stream-broker fallback). Triggered by the UI's
17613
18212
  * "refresh" button so an operator can force a fresh frame
17614
- * even when the cache is well within `snapshotMaxAgeMs`.
17615
- * On battery cams this WILL wake the camera — accept the
17616
- * cost only when the user explicitly asks for it.
18213
+ * even when the cache is well within the device's
18214
+ * `snapshotMaxAgeS` window.
18215
+ *
18216
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
18217
+ * battery camera it is the one thing that walks past the wrapper's
18218
+ * sleep gate and wakes the camera, so a background caller — a poller,
18219
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
18220
+ * gets the cached frame, which on a sleeping battery camera is the
18221
+ * correct answer: stale but honest beats woken.
17617
18222
  */
17618
18223
  force: boolean().optional()
17619
18224
  }), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
@@ -23095,12 +23700,30 @@ var pressureSensorCapability = {
23095
23700
  runtimeState: PressureSensorStatusSchema
23096
23701
  };
23097
23702
  /**
23098
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
23099
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
23100
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
23101
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
23102
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
23103
- * normalized 0..1 (top-left origin).
23703
+ * PRIVACY — what the camera deliberately does not capture. Two planes:
23704
+ *
23705
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
23706
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
23707
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
23708
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
23709
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
23710
+ * normalized 0..1 (top-left origin).
23711
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
23712
+ * camera encoding an audio track at all, so EVERY consumer — live view,
23713
+ * recording, the audio analyzer, an export — sees silent video. There is
23714
+ * no server-side copy of this fact; the camera is the store and every read
23715
+ * is a read-through, which is why a switch over it cannot drift
23716
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
23717
+ *
23718
+ * Both belong here for one reason: they are the two things an operator turns
23719
+ * off when the answer to "what is this camera allowed to record" changes, and
23720
+ * both are applied ON the device, before anything leaves it.
23721
+ *
23722
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
23723
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
23724
+ * by one provider — and it was removed when this landed. A second writer onto
23725
+ * one device register is the shape of every knob this repo has shipped that
23726
+ * disagreed with the one the reader read.
23104
23727
  */
23105
23728
  /** A privacy-mask region's geometry — rectangle or free polygon. */
23106
23729
  var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -23112,21 +23735,45 @@ var PrivacyMaskRegionSchema = object({
23112
23735
  enabled: boolean(),
23113
23736
  shape: PrivacyMaskShapeSchema
23114
23737
  });
23115
- /** Current on-camera privacy-mask state — master enable + zones. */
23738
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
23116
23739
  var PrivacyMaskStatusSchema = object({
23117
23740
  enabled: boolean(),
23118
23741
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
23119
23742
  regions: array(PrivacyMaskRegionSchema),
23743
+ /**
23744
+ * Is the camera capturing sound right now? Read from the camera, never from
23745
+ * a server-side mirror.
23746
+ *
23747
+ * `null` means "no answer" — either this camera exposes no controllable
23748
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
23749
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
23750
+ * "the microphone is off" and "we could not ask" look identical to an
23751
+ * operator only until one of them is wrong.
23752
+ *
23753
+ * On a camera whose profiles carry the flag independently (Reolink writes
23754
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
23755
+ * privacy is only satisfied when every one of them is silent.
23756
+ */
23757
+ audioEnabled: boolean().nullable(),
23120
23758
  lastFetchedAt: number()
23121
23759
  });
23122
- /** Per-camera availability. */
23760
+ /** Per-camera availability. Probed, never assumed from the model name. */
23123
23761
  var PrivacyMaskOptionsSchema = object({
23124
23762
  /** Maximum number of supported zones. */
23125
23763
  maxRegions: number(),
23126
23764
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
23127
23765
  supportedShapes: array(MaskShapeKindSchema),
23128
23766
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
23129
- polygonVertices: MaskPolygonVerticesSchema.optional()
23767
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
23768
+ /**
23769
+ * Does this camera expose a microphone switch we can actually write?
23770
+ *
23771
+ * Camera-probed: `true` only when the firmware answered with an audio flag
23772
+ * we know how to patch. A camera that never answered is `false` — a control
23773
+ * the operator can press that changes nothing is worse than no control, and
23774
+ * the switch group renders "not available" instead.
23775
+ */
23776
+ supportsAudioMute: boolean()
23130
23777
  });
23131
23778
  /** Partial change — every field optional. */
23132
23779
  var PrivacyMaskPatchSchema = object({
@@ -23153,6 +23800,27 @@ var privacyMaskCapability = {
23153
23800
  }), _void(), {
23154
23801
  kind: "mutation",
23155
23802
  auth: "admin"
23803
+ }),
23804
+ /**
23805
+ * Turn the camera's microphone on or off, at the camera.
23806
+ *
23807
+ * Deliberately its OWN mutation rather than a field on
23808
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
23809
+ * mask master switch", and overloading it would make one boolean mean two
23810
+ * unrelated things on the same call. It is also the only method here whose
23811
+ * write leaves the device in a state a later `getStatus` reads back
23812
+ * verbatim, which is what makes it safe as a switch authority.
23813
+ *
23814
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
23815
+ * this rather than silently accept it — a write nothing applies is exactly
23816
+ * what the switch group exists to remove.
23817
+ */
23818
+ setAudioEnabled: method(object({
23819
+ deviceId: number(),
23820
+ enabled: boolean()
23821
+ }), _void(), {
23822
+ kind: "mutation",
23823
+ auth: "admin"
23156
23824
  })
23157
23825
  },
23158
23826
  status: {
@@ -23435,6 +24103,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
23435
24103
  })]);
23436
24104
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
23437
24105
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
24106
+ /**
24107
+ * One GOP of a finalized segment, cut by byte range through the segment's own
24108
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
24109
+ * the single `moof`+`mdat` covering the requested instant — standalone-
24110
+ * demuxable, never the whole file. When the segment's index cannot be parsed
24111
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
24112
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
24113
+ */
24114
+ var ReadGopBytesResultSchema = object({
24115
+ data: _instanceof(Uint8Array),
24116
+ /** Absolute epoch ms of the returned fragment's first sample. */
24117
+ gopStartMs: number(),
24118
+ /** Media ms the returned fragment covers. */
24119
+ gopDurMs: number()
24120
+ });
23438
24121
  method(object({
23439
24122
  deviceId: number(),
23440
24123
  fromMs: number(),
@@ -23477,6 +24160,14 @@ method(object({
23477
24160
  }), ReadSegmentBytesResultSchema, {
23478
24161
  kind: "query",
23479
24162
  auth: "admin"
24163
+ }), method(object({
24164
+ deviceId: number(),
24165
+ profile: string(),
24166
+ startMs: number(),
24167
+ epochMs: number()
24168
+ }), ReadGopBytesResultSchema, {
24169
+ kind: "query",
24170
+ auth: "admin"
23480
24171
  }), method(object({
23481
24172
  deviceId: number(),
23482
24173
  config: RecordingConfigSchema
@@ -24036,6 +24727,16 @@ var StreamProfileConfigSchema = object({
24036
24727
  "baseline"
24037
24728
  ]).optional(),
24038
24729
  gop: number().optional(),
24730
+ /**
24731
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
24732
+ *
24733
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
24734
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
24735
+ * writes every profile at once so "audio off" means silent everywhere. A
24736
+ * per-profile writer beside it would let a camera be half-muted and would be
24737
+ * a second knob onto one device register — the failure D62 exists to
24738
+ * prevent. Absent when the firmware does not report the flag.
24739
+ */
24039
24740
  audio: boolean().optional()
24040
24741
  });
24041
24742
  var StreamParamsStatusSchema = object({
@@ -24076,7 +24777,13 @@ var StreamParamsOptionsSchema = object({
24076
24777
  ext: StreamProfileOptionsSchema.optional()
24077
24778
  });
24078
24779
  /** A partial change to one profile — every field optional; a provider
24079
- * ignores fields it doesn't support. */
24780
+ * ignores fields it doesn't support.
24781
+ *
24782
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
24783
+ * reachable from no form and honoured by exactly one provider, while the
24784
+ * camera's microphone is a whole-device fact. It now has one writer,
24785
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
24786
+ * `privacy-mask.cap.ts`. */
24080
24787
  var StreamProfilePatchSchema = object({
24081
24788
  width: number().optional(),
24082
24789
  height: number().optional(),
@@ -24089,8 +24796,7 @@ var StreamProfilePatchSchema = object({
24089
24796
  "main",
24090
24797
  "baseline"
24091
24798
  ]).optional(),
24092
- gop: number().optional(),
24093
- audio: boolean().optional()
24799
+ gop: number().optional()
24094
24800
  });
24095
24801
  var streamParamsCapability = {
24096
24802
  name: "stream-params",
@@ -28832,6 +29538,12 @@ Object.freeze({
28832
29538
  addonId: null,
28833
29539
  access: "view"
28834
29540
  },
29541
+ "notificationRules.listDeviceMutes": {
29542
+ capName: "notification-rules",
29543
+ capScope: "system",
29544
+ addonId: null,
29545
+ access: "view"
29546
+ },
28835
29547
  "notificationRules.listRules": {
28836
29548
  capName: "notification-rules",
28837
29549
  capScope: "system",
@@ -28850,6 +29562,12 @@ Object.freeze({
28850
29562
  addonId: null,
28851
29563
  access: "create"
28852
29564
  },
29565
+ "notificationRules.setDeviceMuted": {
29566
+ capName: "notification-rules",
29567
+ capScope: "system",
29568
+ addonId: null,
29569
+ access: "create"
29570
+ },
28853
29571
  "notificationRules.setRuleEnabled": {
28854
29572
  capName: "notification-rules",
28855
29573
  capScope: "system",
@@ -29126,6 +29844,12 @@ Object.freeze({
29126
29844
  addonId: null,
29127
29845
  access: "view"
29128
29846
  },
29847
+ "pipelineAnalytics.setTrackFlags": {
29848
+ capName: "pipeline-analytics",
29849
+ capScope: "device",
29850
+ addonId: null,
29851
+ access: "create"
29852
+ },
29129
29853
  "pipelineAnalytics.wipeAllAnalytics": {
29130
29854
  capName: "pipeline-analytics",
29131
29855
  capScope: "device",
@@ -29432,6 +30156,12 @@ Object.freeze({
29432
30156
  addonId: null,
29433
30157
  access: "view"
29434
30158
  },
30159
+ "pipelineOrchestrator.getCameraSwitches": {
30160
+ capName: "pipeline-orchestrator",
30161
+ capScope: "system",
30162
+ addonId: null,
30163
+ access: "view"
30164
+ },
29435
30165
  "pipelineOrchestrator.getCapabilityBindings": {
29436
30166
  capName: "pipeline-orchestrator",
29437
30167
  capScope: "system",
@@ -29564,6 +30294,12 @@ Object.freeze({
29564
30294
  addonId: null,
29565
30295
  access: "create"
29566
30296
  },
30297
+ "pipelineOrchestrator.setCameraSwitch": {
30298
+ capName: "pipeline-orchestrator",
30299
+ capScope: "system",
30300
+ addonId: null,
30301
+ access: "create"
30302
+ },
29567
30303
  "pipelineOrchestrator.setCapabilityBinding": {
29568
30304
  capName: "pipeline-orchestrator",
29569
30305
  capScope: "system",
@@ -29654,6 +30390,12 @@ Object.freeze({
29654
30390
  addonId: null,
29655
30391
  access: "create"
29656
30392
  },
30393
+ "pipelineRunner.runStatelessStep": {
30394
+ capName: "pipeline-runner",
30395
+ capScope: "system",
30396
+ addonId: null,
30397
+ access: "create"
30398
+ },
29657
30399
  "plateGallery.assignPlate": {
29658
30400
  capName: "plate-gallery",
29659
30401
  capScope: "system",
@@ -29786,6 +30528,12 @@ Object.freeze({
29786
30528
  addonId: null,
29787
30529
  access: "view"
29788
30530
  },
30531
+ "privacyMask.setAudioEnabled": {
30532
+ capName: "privacy-mask",
30533
+ capScope: "device",
30534
+ addonId: null,
30535
+ access: "create"
30536
+ },
29789
30537
  "privacyMask.setMask": {
29790
30538
  capName: "privacy-mask",
29791
30539
  capScope: "device",
@@ -29954,6 +30702,12 @@ Object.freeze({
29954
30702
  addonId: null,
29955
30703
  access: "create"
29956
30704
  },
30705
+ "recording.readGopBytes": {
30706
+ capName: "recording",
30707
+ capScope: "system",
30708
+ addonId: null,
30709
+ access: "view"
30710
+ },
29957
30711
  "recording.readSegmentBytes": {
29958
30712
  capName: "recording",
29959
30713
  capScope: "system",
@@ -30470,6 +31224,12 @@ Object.freeze({
30470
31224
  addonId: null,
30471
31225
  access: "create"
30472
31226
  },
31227
+ "streamBroker.acquireEgressTranscode": {
31228
+ capName: "stream-broker",
31229
+ capScope: "system",
31230
+ addonId: null,
31231
+ access: "create"
31232
+ },
30473
31233
  "streamBroker.assignProfile": {
30474
31234
  capName: "stream-broker",
30475
31235
  capScope: "system",
@@ -30578,6 +31338,12 @@ Object.freeze({
30578
31338
  addonId: null,
30579
31339
  access: "create"
30580
31340
  },
31341
+ "streamBroker.releaseEgressTranscode": {
31342
+ capName: "stream-broker",
31343
+ capScope: "system",
31344
+ addonId: null,
31345
+ access: "create"
31346
+ },
30581
31347
  "streamBroker.releaseStreamWithCodec": {
30582
31348
  capName: "stream-broker",
30583
31349
  capScope: "system",
@@ -31408,5 +32174,87 @@ object({
31408
32174
  paddingRatio: .15,
31409
32175
  square: false
31410
32176
  }).paddingRatio;
32177
+ /**
32178
+ * WHICH delivered frames the decode worker retains a native copy of.
32179
+ *
32180
+ * - `all` — every frame the worker delivered to the runner. The shipped
32181
+ * behaviour, and the only correct one if something can ask for a crop of a
32182
+ * frame the runner never sent to inference.
32183
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
32184
+ * A native-crop request always names a `frameId` that rode an inference
32185
+ * result, so that is the only set a request can name. How much it drops is
32186
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
32187
+ * this cluster, not the ~80% the design sketch assumed, because the governor
32188
+ * was not throttling as hard as the sketch supposed. Read
32189
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
32190
+ * of you rather than quoting a number from here. The newest delivered frame is
32191
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
32192
+ * which covers the one-frame race between a mark and the supersede that
32193
+ * consumes it.
32194
+ */
32195
+ var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
32196
+ object({
32197
+ /**
32198
+ * How long a retained native frame is served before it counts as a miss.
32199
+ *
32200
+ * Must cover the FULL late-crop horizon: detection inference + the
32201
+ * cross-process inference-result hop to hub post-analysis + tracking + the
32202
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
32203
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
32204
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
32205
+ */
32206
+ ttlMs: number().int().min(250).max(1e4),
32207
+ /**
32208
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
32209
+ *
32210
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
32211
+ * which one is actually binding before reasoning from that. At the shipped
32212
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
32213
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
32214
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
32215
+ * change that admits fewer frames buys retention WINDOW at constant RAM
32216
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
32217
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
32218
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
32219
+ * to replace).
32220
+ */
32221
+ budgetMb: number().int().min(0).max(4096),
32222
+ /**
32223
+ * Demand window: eager per-frame native retention runs only within this many
32224
+ * ms of the last native-crop request (or of the dial starting).
32225
+ *
32226
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
32227
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
32228
+ * per second on a camera with zero crop demand), so leave it non-zero unless
32229
+ * you are reproducing that.
32230
+ */
32231
+ activityMs: number().int().min(0).max(12e4),
32232
+ /**
32233
+ * Which delivered frames are retained at all — see
32234
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
32235
+ * changes WHAT is kept rather than for how long, so it is also the only one
32236
+ * that can turn a crop that used to hit into a miss. The worker counts every
32237
+ * crop request naming a frame it did NOT see marked
32238
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
32239
+ * there is the signal that some caller names frames outside the inference set
32240
+ * and that this must go back to `all`.
32241
+ */
32242
+ admission: NativeLeaseAdmissionSchema
32243
+ });
32244
+ /**
32245
+ * The values in force when the operator has set nothing — byte-for-byte the
32246
+ * constants the decode worker shipped with as env-var defaults, so making these
32247
+ * settings changed no behaviour on the day it landed.
32248
+ */
32249
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
32250
+ ttlMs: 1200,
32251
+ budgetMb: 1024,
32252
+ activityMs: 15e3,
32253
+ admission: "inferred"
32254
+ };
32255
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
32256
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
32257
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
32258
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
31411
32259
  //#endregion
31412
- export { string as $, kebabToCamel as A, BaseAddon as B, cosineSimilarity as C, encodeVectorBase64 as D, embeddingEncoderCapability as E, subKindsOf as F, nodePin as G, createEvent as H, vectorDimFromBase64 as I, boolean as J, _enum as K, videoclipsCapability as L, pipelineAnalyticsCapability as M, plateGalleryCapability as N, faceGalleryCapability as O, readDeviceStateFrom as P, record as Q, zoneAnalyticsCapability as R, buildEventKindDescriptor as S, defineCustomActions as T, hydrateSchema as U, DeviceType as V, isDeviceScopedCap as W, number as X, literal as Y, object as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, alarmPanelCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, unknown as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, notificationRulesCapability as j, hfModelUrl as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, array as q, EVENT_KIND_BY_CAP as r, NC_TAXONOMY as s, BaseDevice as t, EventCategory as tt, NcRulePatchSchema as u, TimelapseRuleSchema as v, customAction as w, audioMetricsCapability as x, addonWidgetsSourceCapability as y, errMsg as z };
32260
+ export { object as $, hfModelUrl as A, errMsg as B, buildEventKindDescriptor as C, embeddingEncoderCapability as D, defineCustomActions as E, readDeviceStateFrom as F, isDeviceScopedCap as G, DeviceType as H, subKindsOf as I, _enum as J, nodePin as K, vectorDimFromBase64 as L, notificationRulesCapability as M, pipelineAnalyticsCapability as N, encodeVectorBase64 as O, plateGalleryCapability as P, number as Q, videoclipsCapability as R, audioMetricsCapability as S, customAction as T, createEvent as U, BaseAddon as V, hydrateSchema as W, boolean as X, array as Y, literal as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, addonWidgetsSourceCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, record as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, kebabToCamel as j, faceGalleryCapability as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, unknown as nt, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, sleep as q, EVENT_KIND_BY_CAP as r, EventCategory as rt, NC_TAXONOMY as s, BaseDevice as t, string as tt, NcRulePatchSchema as u, TimelapseRuleSchema as v, cosineSimilarity as w, alarmPanelCapability as x, TrackSourceSchema as y, zoneAnalyticsCapability as z };