@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.
@@ -6531,9 +6531,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
6531
6531
  /**
6532
6532
  * Build the tRPC request options that pin a single capability call to `nodeId`.
6533
6533
  * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
6534
+ *
6535
+ * ## The id is normalised here, and it has to be
6536
+ *
6537
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
6538
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
6539
+ * compares a pin against real node ids, so such a pin matches nothing and the
6540
+ * call fails with `no provider registered for cap "…"`. The local-first
6541
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
6542
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
6543
+ * the thing you reach for when you specifically need THIS node — silently
6544
+ * addressed a node that does not exist.
6545
+ *
6546
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
6547
+ * own node to read the host's hardware-decode backend. It never once answered,
6548
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
6549
+ * while D67's whole premise was that the decoder addon is the authority on
6550
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
6551
+ * hardware", which was false.
6552
+ *
6553
+ * Normalising in the ONE constructor fixes every caller at once, which is why
6554
+ * it is here and not at the call sites.
6534
6555
  */
6535
6556
  function nodePin(nodeId) {
6536
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
6557
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
6558
+ }
6559
+ /**
6560
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
6561
+ * is idempotent, so passing an already-clean id costs nothing.
6562
+ */
6563
+ function toNodeId(idOrRunnerId) {
6564
+ const head = idOrRunnerId.split("/")[0];
6565
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
6537
6566
  }
6538
6567
  /**
6539
6568
  * Output schema shared by the contribution + live methods.
@@ -7090,6 +7119,30 @@ var DEVICE_SCOPED_CAPS = new Set([
7090
7119
  function isDeviceScopedCap(capName) {
7091
7120
  return DEVICE_SCOPED_CAPS.has(capName);
7092
7121
  }
7122
+ /**
7123
+ * Promise-based timer helpers — used everywhere the codebase needs to
7124
+ * wait, back off, or schedule a retry. Before these helpers landed, each
7125
+ * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
7126
+ * with subtle variations (some swallowing cancellation, some not). Two
7127
+ * shapes cover every observed use case:
7128
+ *
7129
+ * - {@link sleep} for a plain, uncancellable wait — the default choice.
7130
+ * - {@link sleepCancellable} for a wait that wakes early when an
7131
+ * abort signal trips, used by long-running pollers whose teardown
7132
+ * must stop a pending backoff promptly.
7133
+ */
7134
+ /**
7135
+ * Resolve after `ms` milliseconds. Never rejects, never cancels. The
7136
+ * sleep cannot be interrupted; for a wakeable variant use
7137
+ * {@link sleepCancellable}.
7138
+ *
7139
+ * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
7140
+ * still gives the event loop a chance to drain — useful for breaking
7141
+ * up tight async loops without changing call-site semantics.
7142
+ */
7143
+ function sleep(ms) {
7144
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7145
+ }
7093
7146
  //#endregion
7094
7147
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7095
7148
  /**
@@ -7114,6 +7167,14 @@ var EncodeProfileSchema = object({
7114
7167
  "main",
7115
7168
  "high"
7116
7169
  ]).optional(),
7170
+ /**
7171
+ * `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
7172
+ * (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
7173
+ * it, or it ships a stream that does not match its own advertisement — the
7174
+ * defect class that kept HomeKit black for a year and that Alexa carried
7175
+ * silently. Optional because a browser negotiates the level itself.
7176
+ */
7177
+ level: string().optional(),
7117
7178
  width: number().int().positive().optional(),
7118
7179
  height: number().int().positive().optional(),
7119
7180
  fps: number().positive().optional(),
@@ -7161,6 +7222,29 @@ var EncodeProfileSchema = object({
7161
7222
  outputArgs: array(string()).optional()
7162
7223
  });
7163
7224
  /**
7225
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
7226
+ * Baseline because it is the one profile every consumer in this repo decodes
7227
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
7228
+ */
7229
+ var BASE_LIVE_EGRESS_PROFILE = {
7230
+ video: {
7231
+ codec: "h264",
7232
+ profile: "baseline",
7233
+ level: "3.1",
7234
+ width: 1280,
7235
+ height: 720,
7236
+ fps: 25,
7237
+ bitrateKbps: 2500,
7238
+ gopFrames: 25,
7239
+ bf: 0,
7240
+ preset: "veryfast",
7241
+ tune: "zerolatency"
7242
+ },
7243
+ audio: "passthrough"
7244
+ };
7245
+ ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
7246
+ ({ ...BASE_LIVE_EGRESS_PROFILE });
7247
+ /**
7164
7248
  * Deep wiring healthcheck — snapshot of active reachability probes across
7165
7249
  * every declared capability + widget of every installed plugin, on every
7166
7250
  * node. Produced by the backend `WiringHealthService` and surfaced via
@@ -7210,6 +7294,154 @@ object({
7210
7294
  })
7211
7295
  });
7212
7296
  /**
7297
+ * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7298
+ * pipeline functions an operator thinks in terms of.
7299
+ *
7300
+ * ## This file adds no state
7301
+ *
7302
+ * Every switch here is a VIEW onto an authority that already existed
7303
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
7304
+ * group is that there is exactly one place each function is turned off, and
7305
+ * the group routes to it:
7306
+ *
7307
+ * | Switch | Authority | Proven "off stops the work" gate |
7308
+ * | --- | --- | --- |
7309
+ * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7310
+ * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7311
+ * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7312
+ * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7313
+ * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7314
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7315
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7316
+ *
7317
+ * ## The two switches whose authority is not on this server
7318
+ *
7319
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
7320
+ * in "the group stores nothing" — it is the purest form of it: the camera
7321
+ * holds the fact, every read is a read-through, and there is no server-side
7322
+ * copy that could drift. Their availability therefore cannot come from
7323
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
7324
+ * is filtered out there); it comes from the cap's own camera-probed
7325
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
7326
+ * THIS camera rather than for the device type
7327
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
7328
+ *
7329
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
7330
+ *
7331
+ * Every other switch means *this camera's function is doing its job*, so
7332
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
7333
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
7334
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
7335
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
7336
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
7337
+ * worse than either surface not having one.
7338
+ *
7339
+ * Two consequences follow and both are load-bearing:
7340
+ *
7341
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
7342
+ * exactly this row. With the polarity above, every camera that has NOT drawn
7343
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
7344
+ * normal, healthy state of most cameras rendered as an operator disablement.
7345
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
7346
+ * unconditionally by both clients, so for this row it has to read correctly
7347
+ * whichever way the switch is sitting.
7348
+ *
7349
+ * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7350
+ * migrated the legacy `audioEnabled` / `pipelineEnabled` /
7351
+ * `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
7352
+ * surface that decision never got.
7353
+ *
7354
+ * ## Two rules that are load-bearing
7355
+ *
7356
+ * - **Recording's switch is `enabled`, never the bands.** `bands` is the only
7357
+ * authored intent and `mode` is derived from it (`deriveRecordingMode`).
7358
+ * Expressing "off" by clearing bands destroys the operator's schedule and
7359
+ * turning the camera back on would then silently record nothing.
7360
+ * - **A switch that is off must be reported as off**, not merely produce
7361
+ * nothing. {@link CameraSwitch.enabled} is what a status surface renders as
7362
+ * "disabled by an operator" instead of "broken" — see
7363
+ * `CameraStatus.switchedOff`.
7364
+ */
7365
+ /**
7366
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
7367
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
7368
+ * recognition and plate/LPR are per-step toggles on
7369
+ * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7370
+ * editor, not in a safety group.
7371
+ */
7372
+ var CameraSwitchIdSchema = _enum([
7373
+ "stream-broker",
7374
+ "object-detection",
7375
+ "privacy-mask",
7376
+ "device-audio",
7377
+ "audio-analysis",
7378
+ "recording",
7379
+ "notifications"
7380
+ ]);
7381
+ /**
7382
+ * WHERE the switch's state actually lives. A discriminated union rather than a
7383
+ * string so both the writer (the orchestrator's `setCameraSwitch`) and any
7384
+ * reader can exhaustively narrow — and so "the group added a parallel map" is
7385
+ * a compile error rather than a review comment.
7386
+ */
7387
+ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7388
+ object({ kind: literal("device-disabled") }),
7389
+ object({
7390
+ kind: literal("wrapper-binding"),
7391
+ capName: string()
7392
+ }),
7393
+ object({ kind: literal("recording-config") }),
7394
+ object({ kind: literal("notification-mute") }),
7395
+ object({
7396
+ kind: literal("camera-audio"),
7397
+ capName: string()
7398
+ }),
7399
+ object({
7400
+ kind: literal("camera-mask"),
7401
+ capName: string()
7402
+ })
7403
+ ]);
7404
+ /**
7405
+ * Why a switch is not offered for this camera. Rendered instead of the
7406
+ * control, never as a dead control — an absent function and a broken one must
7407
+ * not look the same.
7408
+ */
7409
+ var CameraSwitchUnavailableReasonSchema = _enum([
7410
+ "no-provider",
7411
+ "source-unreachable",
7412
+ "not-configured"
7413
+ ]);
7414
+ /**
7415
+ * One switch, resolved for one camera.
7416
+ *
7417
+ * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
7418
+ * client-side: the viewer is a separate repository that does not import
7419
+ * `@camstack/types`, and a cost line duplicated in two clients is a cost line
7420
+ * that will disagree with itself. Five rows per camera is nothing.
7421
+ */
7422
+ var CameraSwitchSchema = object({
7423
+ id: CameraSwitchIdSchema,
7424
+ label: string(),
7425
+ /**
7426
+ * What the operator LOSES while this is off, in one sentence. Required, not
7427
+ * optional: a switch that cannot say what it costs should not ship.
7428
+ */
7429
+ costWhenOff: string(),
7430
+ /** False = do not render a control. `unavailableReason` says why. */
7431
+ available: boolean(),
7432
+ unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
7433
+ /** Current state. Meaningless when `available` is false — read it as `true`. */
7434
+ enabled: boolean(),
7435
+ authority: CameraSwitchAuthoritySchema
7436
+ });
7437
+ /** The whole group for one camera. */
7438
+ var CameraSwitchGroupSchema = object({
7439
+ deviceId: number().int(),
7440
+ switches: array(CameraSwitchSchema).readonly(),
7441
+ /** Unix ms when the group was composed server-side. */
7442
+ fetchedAt: number()
7443
+ });
7444
+ /**
7213
7445
  * Ops-log — the durable, append-only operations audit shared by the
7214
7446
  * recordings and events management surfaces.
7215
7447
  *
@@ -7228,14 +7460,16 @@ var OpsLogOpSchema = _enum([
7228
7460
  "manual-delete",
7229
7461
  "rescan",
7230
7462
  "retention-run",
7231
- "relocate"
7463
+ "relocate",
7464
+ "orphan-audit"
7232
7465
  ]);
7233
7466
  /** Why the operation ran. */
7234
7467
  var OpsLogReasonSchema = _enum([
7235
7468
  "retention",
7236
7469
  "quota",
7237
7470
  "manual",
7238
- "operator"
7471
+ "operator",
7472
+ "maintenance"
7239
7473
  ]);
7240
7474
  /** One audit row, shared verbatim by both domains. */
7241
7475
  var OpsLogEntrySchema = object({
@@ -9121,6 +9355,126 @@ var RtpSourceSchema = object({
9121
9355
  encoder: string(),
9122
9356
  pipelineKey: string()
9123
9357
  });
9358
+ /**
9359
+ * The encode request — **structured and serialisable, with NO raw-flag escape
9360
+ * hatch.** This is deliberate and it is the one lesson taken from
9361
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
9362
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
9363
+ * adding a flag silently forks the shared child, and two consumers that mean
9364
+ * the same thing but spell it differently never share. Here every knob is a
9365
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
9366
+ * never an opaque array.
9367
+ *
9368
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
9369
+ * The operator-facing derived-stream transform editor still has them — that is
9370
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
9371
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
9372
+ */
9373
+ var EgressEncodeSchema = EncodeProfileSchema.omit({
9374
+ inputArgs: true,
9375
+ outputArgs: true
9376
+ });
9377
+ /**
9378
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
9379
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
9380
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
9381
+ * an Echo). Named rather than numeric so the INTENT survives.
9382
+ */
9383
+ var EgressRateControlSchema = _enum(["tight", "relaxed"]);
9384
+ var EgressTranscodeRequestSchema = object({
9385
+ deviceId: number().int().nonnegative(),
9386
+ /** Which published stream to read. */
9387
+ source: discriminatedUnion("kind", [object({
9388
+ kind: literal("profile"),
9389
+ profile: CamProfileSchema
9390
+ }), object({
9391
+ kind: literal("cam-stream"),
9392
+ camStreamId: string().min(1)
9393
+ })]),
9394
+ encode: EgressEncodeSchema,
9395
+ rateControl: EgressRateControlSchema.optional(),
9396
+ /**
9397
+ * `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
9398
+ * out-of-band extradata and needs `dump_extra` on both the copy and encode
9399
+ * branches. Enumerated, not free text.
9400
+ */
9401
+ bitstreamFilter: _enum([
9402
+ "dump_extra",
9403
+ "h264_mp4toannexb",
9404
+ "hevc_mp4toannexb"
9405
+ ]).optional(),
9406
+ /**
9407
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
9408
+ * consumer to dial the returned url. The broker picks the id and returns it
9409
+ * as `camStreamId` — a caller-supplied one would be circular, since the
9410
+ * sharing key is computed FROM this request.
9411
+ *
9412
+ * The url is still returned and still the contract for a transcode pinned to
9413
+ * another node. But dialling it locally costs an RTSP round trip that changes
9414
+ * the transport underneath the consumer: a dialled stream is an RTP source,
9415
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
9416
+ * repacketizer branch. The push branch — the one the derived mechanism has
9417
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
9418
+ * arriving, key frame arriving, black screen, on a chain healthy at every
9419
+ * other point.
9420
+ *
9421
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
9422
+ * gives priority to `hub-in-process` so a local call never leaves the node.
9423
+ * This is that rule for media.
9424
+ */
9425
+ publishLocally: boolean().optional(),
9426
+ pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9427
+ /**
9428
+ * Operator/consumer override for decode hardware. ABSENT is the normal case
9429
+ * and the one that matters: the broker then resolves the backend from the
9430
+ * DECODER ADDON's per-node `probedBestHwaccel` (see
9431
+ * `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
9432
+ * on this hardware — never the raw kernel resolver's qsv-first order.
9433
+ */
9434
+ decodeHwAccel: _enum([
9435
+ "auto",
9436
+ "none",
9437
+ "videotoolbox",
9438
+ "vaapi",
9439
+ "qsv",
9440
+ "cuda"
9441
+ ]).optional(),
9442
+ /**
9443
+ * Host to embed in the returned restream `url`. The broker mints hub-local
9444
+ * `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
9445
+ * host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
9446
+ * dialable from there. Same contract as `getStreamWithCodec.hostname` —
9447
+ * `substituteRtspHost` rewrites only the dial address, never the restreamer.
9448
+ */
9449
+ hostname: string().optional(),
9450
+ /** Attribution for the broker panel. Never part of the sharing key. */
9451
+ tag: string().optional()
9452
+ });
9453
+ var EgressTranscodeSchema = object({
9454
+ /** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
9455
+ url: string(),
9456
+ /** Release handle. Refcounted — the child dies when the last holder releases. */
9457
+ pipelineKey: string(),
9458
+ videoCodec: _enum(["H264", "H265"]),
9459
+ resolution: object({
9460
+ width: number().int().positive(),
9461
+ height: number().int().positive()
9462
+ }),
9463
+ transcoded: boolean(),
9464
+ encoder: string(),
9465
+ /**
9466
+ * The decode backend the child ACTUALLY ran with — `null` for software.
9467
+ * Returned rather than assumed: a consumer that asked for hardware and got
9468
+ * software needs to be able to see that without reading the broker's logs.
9469
+ */
9470
+ decodeHwAccel: string().nullable(),
9471
+ /**
9472
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
9473
+ * `url`, and the session takes the push/deframe transport rather than the
9474
+ * RTP-passthrough one. `null` means the consumer must dial.
9475
+ */
9476
+ camStreamId: string().nullable()
9477
+ });
9124
9478
  method(object({
9125
9479
  deviceId: number().int().nonnegative(),
9126
9480
  camStreamId: string().min(1),
@@ -9230,6 +9584,15 @@ method(object({
9230
9584
  }), {
9231
9585
  kind: "mutation",
9232
9586
  auth: "admin"
9587
+ }), method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
9588
+ kind: "mutation",
9589
+ auth: "admin"
9590
+ }), method(object({ pipelineKey: string() }), object({
9591
+ released: boolean(),
9592
+ refcount: number().int().nonnegative()
9593
+ }), {
9594
+ kind: "mutation",
9595
+ auth: "admin"
9233
9596
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
9234
9597
  subscriptionId: string(),
9235
9598
  maxCount: number().int().positive().default(8)
@@ -13842,12 +14205,13 @@ var NcConditionsSchema = object({
13842
14205
  * source; otherwise the subject's source must equal it. Legacy records
13843
14206
  * with no stamped source are treated as `pipeline`. The union spans both
13844
14207
  * record kinds — object events carry `pipeline` | `onboard`, synthetic
13845
- * tracks carry `sensor`.
14208
+ * tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
13846
14209
  */
13847
14210
  source: _enum([
13848
14211
  "pipeline",
13849
14212
  "onboard",
13850
14213
  "sensor",
14214
+ "audio",
13851
14215
  "any"
13852
14216
  ]).optional(),
13853
14217
  /**
@@ -14401,6 +14765,10 @@ var NC_CONDITION_CATALOG = [
14401
14765
  {
14402
14766
  value: "sensor",
14403
14767
  label: "Sensor"
14768
+ },
14769
+ {
14770
+ value: "audio",
14771
+ label: "Audio marker"
14404
14772
  }
14405
14773
  ],
14406
14774
  operator: "in",
@@ -14411,7 +14779,7 @@ var NC_CONDITION_CATALOG = [
14411
14779
  "package-event"
14412
14780
  ],
14413
14781
  phase: "P1",
14414
- description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
14782
+ description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
14415
14783
  },
14416
14784
  {
14417
14785
  id: "deviceState",
@@ -14757,6 +15125,35 @@ var notificationRulesCapability = {
14757
15125
  auth: "admin"
14758
15126
  }),
14759
15127
  /**
15128
+ * PERMANENT per-camera mute — the notifications half of the per-camera
15129
+ * function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
15130
+ *
15131
+ * Deliberately NOT a snooze. A snooze is bounded at
15132
+ * {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
15133
+ * expire would be an outage the operator asked for once and forgot" — and
15134
+ * widening it to express "this camera never notifies" would destroy that
15135
+ * property for every snooze. A mute is the other thing: an explicit,
15136
+ * indefinite, admin-only decision, visible in the switch group next to the
15137
+ * other four, and reported on `CameraStatus.switchedOff` so a silent
15138
+ * camera never reads as a working one.
15139
+ *
15140
+ * Returned as ONE list rather than a per-camera query: the group's reader
15141
+ * needs every camera's state, and a per-camera fan-out over the viewer's
15142
+ * single WebSocket is N frames serialised on one socket.
15143
+ */
15144
+ listDeviceMutes: method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }),
15145
+ /**
15146
+ * Mute or unmute one camera. Idempotent; an unmute of a camera that was
15147
+ * never muted succeeds.
15148
+ */
15149
+ setDeviceMuted: method(object({
15150
+ deviceId: number().int(),
15151
+ muted: boolean()
15152
+ }), object({ success: literal(true) }), {
15153
+ kind: "mutation",
15154
+ auth: "admin"
15155
+ }),
15156
+ /**
14760
15157
  * Dry-run a rule against recently persisted records (object events for
14761
15158
  * `immediate`, closed tracks for `track-end`). Mutation kind only to
14762
15159
  * carry the full rule object safely; no side effects.
@@ -15162,12 +15559,60 @@ var TrackAudioLabelSchema = object({
15162
15559
  });
15163
15560
  /**
15164
15561
  * How a track was produced. `pipeline` (default / absent) = the spatial
15165
- * detection+tracking pipeline. `sensor` = a SYNTHETIC track projected from a
15166
- * linked sensor/control state change (no positions; carries a snapshot). The
15167
- * spatial subsystems (tracker association, occupancy count, re-id/embedding,
15168
- * resurrection) MUST skip `sensor` tracks — they have no bbox trajectory.
15562
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
15563
+ * no positions, a single snapshot, and no bbox trajectory at all:
15564
+ *
15565
+ * - `sensor` — a linked sensor/control device state change.
15566
+ * - `audio` — an audio event on the camera itself that was anomalous for
15567
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
15568
+ *
15569
+ * The spatial subsystems (tracker association, occupancy count, re-id /
15570
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
15571
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
15572
+ * check silently readmits every source added after it was written.
15573
+ */
15574
+ var TrackSourceSchema = _enum([
15575
+ "pipeline",
15576
+ "sensor",
15577
+ "audio"
15578
+ ]);
15579
+ /**
15580
+ * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15581
+ * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15582
+ * so the two surfaces cannot drift.
15583
+ *
15584
+ * **Absent ≠ false.** A track that has never been touched omits the field; an
15585
+ * explicitly un-flagged track carries `false`. Legacy rows written before the
15586
+ * columns existed read as absent, and a consumer that needs a boolean should say
15587
+ * `flag === true`, not `flag !== false`.
15588
+ *
15589
+ * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15590
+ * operator curation, and the behaviour they drive will be specified separately.
15591
+ * In particular a `markForTrain` track is NOT pinned against retention — see
15592
+ * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
15593
+ */
15594
+ var TrackFlagFields = {
15595
+ /** Operator marked this track as training material. */
15596
+ markForTrain: boolean().optional(),
15597
+ /** Operator marked this track for diagnostic attention. */
15598
+ debug: boolean().optional()
15599
+ };
15600
+ /**
15601
+ * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15602
+ * one flag can never clear the other — the toggles are independent and are
15603
+ * driven from three surfaces that do not know about each other.
15169
15604
  */
15170
- var TrackSourceSchema = _enum(["pipeline", "sensor"]);
15605
+ var TrackFlagsPatchSchema = object(TrackFlagFields);
15606
+ /**
15607
+ * The resolved flag state after a write. Both fields are REQUIRED here (absent
15608
+ * collapses to `false`) so a caller can drive a toggle's checked state off the
15609
+ * mutation result without a re-fetch.
15610
+ */
15611
+ var TrackFlagsSchema = object({
15612
+ trackId: string(),
15613
+ markForTrain: boolean(),
15614
+ debug: boolean()
15615
+ });
15171
15616
  var TrackSchema = object({
15172
15617
  trackId: string(),
15173
15618
  deviceId: number(),
@@ -15210,7 +15655,8 @@ var TrackSchema = object({
15210
15655
  /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
15211
15656
  * Populated from the persisted envelope columns on historical reads;
15212
15657
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15213
- envelope: TrackEnvelopeSchema.optional()
15658
+ envelope: TrackEnvelopeSchema.optional(),
15659
+ ...TrackFlagFields
15214
15660
  });
15215
15661
  var BaseEventFields = {
15216
15662
  id: string(),
@@ -15423,7 +15869,8 @@ var KeyEventSchema = object({
15423
15869
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15424
15870
  bestEventId: string(),
15425
15871
  /** Track lifetime in ms (lastSeen - firstSeen). */
15426
- windowMs: number().optional()
15872
+ windowMs: number().optional(),
15873
+ ...TrackFlagFields
15427
15874
  });
15428
15875
  object({
15429
15876
  trackId: string(),
@@ -15509,7 +15956,31 @@ var RebuildObjectEmbeddingsInput = object({
15509
15956
  since: number().optional(),
15510
15957
  until: number().optional(),
15511
15958
  /** Stop after this many tracks; the result reports whether more remain. */
15512
- maxTracks: number().int().positive().optional()
15959
+ maxTracks: number().int().positive().optional(),
15960
+ /**
15961
+ * Run every embedding on THIS node instead of round-robining the fleet.
15962
+ *
15963
+ * Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
15964
+ * field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
15965
+ * calling it that would pin the rebuild REQUEST itself to that node — the
15966
+ * rebuild orchestration lives on the hub, and only the per-track step runs
15967
+ * remotely. This field is data; the per-track pin is applied inside.
15968
+ *
15969
+ * Absent ⇒ round-robin over every online node whose runner can serve the
15970
+ * pinned model.
15971
+ */
15972
+ executeOnNodeId: string().optional(),
15973
+ /**
15974
+ * Milliseconds to wait between tracks; omit for the built-in default, `0` to
15975
+ * run flat out.
15976
+ *
15977
+ * A rebuild is bulk maintenance on hub-main's single thread. Measured
15978
+ * 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
15979
+ * pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
15980
+ * force is logged at start and finish so a deliberately slow pass reads
15981
+ * differently from a stalled one.
15982
+ */
15983
+ pacingMs: number().int().nonnegative().optional()
15513
15984
  });
15514
15985
  /**
15515
15986
  * Result of emptying the CLIP index.
@@ -15543,13 +16014,23 @@ var RebuildStatusSchema = object({
15543
16014
  /** Tracks with no usable detection box. */
15544
16015
  missingBbox: number(),
15545
16016
  /**
15546
- * Tracks the pipeline REFUSED rather than broke on: the camera is not
15547
- * attached, or `clip-embedding` is not enabled in its step tree. Separate
15548
- * from `failed` because the remedy is a configuration change, not an engine
15549
- * investigation — and because a pass over decommissioned cameras would
15550
- * otherwise read as a total engine outage.
16017
+ * Tracks an executing node REFUSED rather than broke on — an unreadable key
16018
+ * frame, a step that threw. Separate from `failed` because the remedy is
16019
+ * different, and because a whole camera silently contributing zero vectors
16020
+ * is the shape of failure a rebuild must never hide.
15551
16021
  */
15552
16022
  notRunnable: number(),
16023
+ /**
16024
+ * The pass stopped because NO node could serve the pinned model.
16025
+ *
16026
+ * Distinct from `notRunnable` on purpose: that one says "this track was
16027
+ * refused", this one says "the cluster cannot do this work at all" — every
16028
+ * candidate node either lacks the `clip-embedding` step, lacks a build of the
16029
+ * pinned model for its engine format, or dropped out. The remedy is a model /
16030
+ * engine change, not a per-camera one. Non-zero here always comes with
16031
+ * `complete: false`.
16032
+ */
16033
+ noCapableNode: number(),
15553
16034
  failed: number(),
15554
16035
  /** Set once a pass ends: true only when EVERYTHING was covered. */
15555
16036
  complete: boolean().nullable(),
@@ -15739,6 +16220,31 @@ var pipelineAnalyticsCapability = {
15739
16220
  auth: "admin"
15740
16221
  }),
15741
16222
  /**
16223
+ * Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
16224
+ * The patch is PARTIAL — an omitted key is left untouched — because the
16225
+ * three surfaces that write it (admin Events grid, viewer track detail,
16226
+ * viewer cluster detail) each own one toggle and must not clobber the other.
16227
+ *
16228
+ * Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
16229
+ * live where `label` and `importance` live, not in any per-device settings
16230
+ * store. Updates the in-RAM active track too, so a flag set on a live track
16231
+ * survives its expiry-time persist.
16232
+ *
16233
+ * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
16234
+ * authenticated non-admin surface and two of the three call sites are
16235
+ * there. Revisit if a flag ever gains an effect that costs storage —
16236
+ * `deleteTracks` next door is admin for exactly that reason.
16237
+ *
16238
+ * Returns the RESOLVED state of both flags (absent → `false`) so a caller
16239
+ * can drive its toggle without a re-fetch. Rejects an unknown track.
16240
+ */
16241
+ setTrackFlags: method(object({
16242
+ /** Log/audit scope only — the trackId is globally unique on its own. */
16243
+ deviceId: number(),
16244
+ trackId: string(),
16245
+ flags: TrackFlagsPatchSchema
16246
+ }), TrackFlagsSchema, { kind: "mutation" }),
16247
+ /**
15742
16248
  * Durable event-store footprint for the management UI: event rows
15743
16249
  * (motion + object + audio) counted per camera + total, plus the
15744
16250
  * event-owned media bytes on disk per camera + total. Stat/count-based,
@@ -16465,6 +16971,53 @@ var DetailResultSchema = object({
16465
16971
  nativeFaceShortSidePx: number().optional()
16466
16972
  });
16467
16973
  /**
16974
+ * Why an executing node REFUSED a stateless step run (`runStatelessStep`).
16975
+ *
16976
+ * A refusal is a first-class answer, not an error, because the caller's next
16977
+ * move depends on WHICH one it is — and because "the pass produced nothing"
16978
+ * must never be reachable without a named, counted cause. The two tiers:
16979
+ *
16980
+ * - **node-level** (`unknown-step`, `model-not-servable`) — this node can
16981
+ * never serve this (step, model) pair. The caller drops it from its rotation
16982
+ * and retries the same work elsewhere; nothing about the work changes.
16983
+ * - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
16984
+ * fine, this one request is not. Retrying it on another node would only
16985
+ * spread the same failure.
16986
+ */
16987
+ var StatelessStepRefusalSchema = _enum([
16988
+ "unknown-step",
16989
+ "model-not-servable",
16990
+ "unreadable-frame",
16991
+ "execution-failed"
16992
+ ]);
16993
+ /**
16994
+ * Answer to `runStatelessStep` — a discriminated union rather than a nullable
16995
+ * result, because `null` is exactly what made the camera-bound detail path
16996
+ * unable to tell "refused" from "never asked".
16997
+ */
16998
+ var RunStatelessStepResultSchema = discriminatedUnion("kind", [object({
16999
+ kind: literal("ran"),
17000
+ /** The node that actually executed it — the pin, echoed back for the log. */
17001
+ nodeId: string(),
17002
+ /**
17003
+ * The model the step ran with.
17004
+ *
17005
+ * The node verified this exact id has a build for the format it dispatched
17006
+ * on BEFORE running, so the executor's format resolution returns it
17007
+ * unchanged. A caller that pinned a model must compare this field and
17008
+ * treat a mismatch as a refusal — the whole point of the pin is that a
17009
+ * pass writes one feature space.
17010
+ */
17011
+ modelId: string(),
17012
+ details: array(DetailResultSchema)
17013
+ }), object({
17014
+ kind: literal("refused"),
17015
+ nodeId: string(),
17016
+ reason: StatelessStepRefusalSchema,
17017
+ /** Human-readable specifics — the format tried, the formats shipped, etc. */
17018
+ detail: string()
17019
+ })]);
17020
+ /**
16468
17021
  * Per-camera tunable ranges + defaults. Single source of truth used
16469
17022
  * by both the Zod data schema (validation + default fallback) and
16470
17023
  * the device settings UI (slider min/max/step). Touch one place and
@@ -16814,7 +17367,32 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
16814
17367
  cropJpeg: string().optional(),
16815
17368
  parent: DetailParentSchema,
16816
17369
  steps: array(string()).optional()
16817
- }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
17370
+ }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
17371
+ /** Catalog step id, e.g. `clip-embedding`. */
17372
+ stepId: string(),
17373
+ /**
17374
+ * REQUIRED model pin. The node runs this exact model or refuses with
17375
+ * `model-not-servable` — it never substitutes a format default, because
17376
+ * a fleet pass that round-robins across nodes would then fill one index
17377
+ * from several encoders.
17378
+ */
17379
+ modelId: string(),
17380
+ /** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
17381
+ frameJpeg: string(),
17382
+ /**
17383
+ * The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
17384
+ * purpose: the caller stores boxes against a downscaled analysis frame
17385
+ * while the stored key frame is native-resolution, and the only side
17386
+ * that reliably knows the image's pixel dimensions is the side that
17387
+ * decodes it. Denormalising here removes a second reader of the
17388
+ * dimensions and the class of mismatch that comes with it.
17389
+ */
17390
+ bbox: NativeCropBboxSchema,
17391
+ /** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
17392
+ className: string(),
17393
+ /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
17394
+ sourceDeviceId: number()
17395
+ }), RunStatelessStepResultSchema, { kind: "mutation" });
16818
17396
  var CameraPipelineConfigSchema = object({
16819
17397
  engine: PipelineEngineChoiceSchema.optional(),
16820
17398
  steps: array(PipelineStepInputSchema).readonly(),
@@ -17112,6 +17690,20 @@ var CameraStatusSchema = object({
17112
17690
  detection: CameraDetectionStatusSchema.nullable(),
17113
17691
  audio: CameraAudioStatusSchema.nullable(),
17114
17692
  recording: CameraRecordingStatusSchema.nullable(),
17693
+ /**
17694
+ * Per-camera function switches an OPERATOR has turned off
17695
+ * ([D61](../../../../docs/decisions/adr-0067.md)).
17696
+ *
17697
+ * This is the difference between DISABLED and BROKEN. A camera whose
17698
+ * `detection` block reports zero fps and whose `switchedOff` contains
17699
+ * `'object-detection'` was switched off by a person; the same camera with an
17700
+ * empty list is failing. Every status surface must render the two
17701
+ * differently — a quiet camera that looks identical to a dead one is the
17702
+ * silence-reads-as-never-happened trap this repo keeps paying for.
17703
+ *
17704
+ * Empty when nothing is off. Never contains a switch no provider offers.
17705
+ */
17706
+ switchedOff: array(CameraSwitchIdSchema).readonly(),
17115
17707
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
17116
17708
  fetchedAt: number()
17117
17709
  });
@@ -17280,7 +17872,14 @@ method(object({
17280
17872
  }), method(object({
17281
17873
  deviceId: number(),
17282
17874
  agentNodeId: string().optional()
17283
- }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17875
+ }), CameraPipelineConfigSchema), method(object({ deviceId: number() }), CameraSwitchGroupSchema), method(object({
17876
+ deviceId: number(),
17877
+ switchId: CameraSwitchIdSchema,
17878
+ enabled: boolean()
17879
+ }), CameraSwitchGroupSchema, {
17880
+ kind: "mutation",
17881
+ auth: "admin"
17882
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
17284
17883
  name: string(),
17285
17884
  description: string().optional(),
17286
17885
  config: CameraPipelineConfigSchema
@@ -17633,9 +18232,15 @@ DeviceType.Camera, method(object({
17633
18232
  * Bypass the cache freshness check and fetch directly from the
17634
18233
  * native (or stream-broker fallback). Triggered by the UI's
17635
18234
  * "refresh" button so an operator can force a fresh frame
17636
- * even when the cache is well within `snapshotMaxAgeMs`.
17637
- * On battery cams this WILL wake the camera — accept the
17638
- * cost only when the user explicitly asks for it.
18235
+ * even when the cache is well within the device's
18236
+ * `snapshotMaxAgeS` window.
18237
+ *
18238
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
18239
+ * battery camera it is the one thing that walks past the wrapper's
18240
+ * sleep gate and wakes the camera, so a background caller — a poller,
18241
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
18242
+ * gets the cached frame, which on a sleeping battery camera is the
18243
+ * correct answer: stale but honest beats woken.
17639
18244
  */
17640
18245
  force: boolean().optional()
17641
18246
  }), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
@@ -23117,12 +23722,30 @@ var pressureSensorCapability = {
23117
23722
  runtimeState: PressureSensorStatusSchema
23118
23723
  };
23119
23724
  /**
23120
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
23121
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
23122
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
23123
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
23124
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
23125
- * normalized 0..1 (top-left origin).
23725
+ * PRIVACY — what the camera deliberately does not capture. Two planes:
23726
+ *
23727
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
23728
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
23729
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
23730
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
23731
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
23732
+ * normalized 0..1 (top-left origin).
23733
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
23734
+ * camera encoding an audio track at all, so EVERY consumer — live view,
23735
+ * recording, the audio analyzer, an export — sees silent video. There is
23736
+ * no server-side copy of this fact; the camera is the store and every read
23737
+ * is a read-through, which is why a switch over it cannot drift
23738
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
23739
+ *
23740
+ * Both belong here for one reason: they are the two things an operator turns
23741
+ * off when the answer to "what is this camera allowed to record" changes, and
23742
+ * both are applied ON the device, before anything leaves it.
23743
+ *
23744
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
23745
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
23746
+ * by one provider — and it was removed when this landed. A second writer onto
23747
+ * one device register is the shape of every knob this repo has shipped that
23748
+ * disagreed with the one the reader read.
23126
23749
  */
23127
23750
  /** A privacy-mask region's geometry — rectangle or free polygon. */
23128
23751
  var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -23134,21 +23757,45 @@ var PrivacyMaskRegionSchema = object({
23134
23757
  enabled: boolean(),
23135
23758
  shape: PrivacyMaskShapeSchema
23136
23759
  });
23137
- /** Current on-camera privacy-mask state — master enable + zones. */
23760
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
23138
23761
  var PrivacyMaskStatusSchema = object({
23139
23762
  enabled: boolean(),
23140
23763
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
23141
23764
  regions: array(PrivacyMaskRegionSchema),
23765
+ /**
23766
+ * Is the camera capturing sound right now? Read from the camera, never from
23767
+ * a server-side mirror.
23768
+ *
23769
+ * `null` means "no answer" — either this camera exposes no controllable
23770
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
23771
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
23772
+ * "the microphone is off" and "we could not ask" look identical to an
23773
+ * operator only until one of them is wrong.
23774
+ *
23775
+ * On a camera whose profiles carry the flag independently (Reolink writes
23776
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
23777
+ * privacy is only satisfied when every one of them is silent.
23778
+ */
23779
+ audioEnabled: boolean().nullable(),
23142
23780
  lastFetchedAt: number()
23143
23781
  });
23144
- /** Per-camera availability. */
23782
+ /** Per-camera availability. Probed, never assumed from the model name. */
23145
23783
  var PrivacyMaskOptionsSchema = object({
23146
23784
  /** Maximum number of supported zones. */
23147
23785
  maxRegions: number(),
23148
23786
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
23149
23787
  supportedShapes: array(MaskShapeKindSchema),
23150
23788
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
23151
- polygonVertices: MaskPolygonVerticesSchema.optional()
23789
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
23790
+ /**
23791
+ * Does this camera expose a microphone switch we can actually write?
23792
+ *
23793
+ * Camera-probed: `true` only when the firmware answered with an audio flag
23794
+ * we know how to patch. A camera that never answered is `false` — a control
23795
+ * the operator can press that changes nothing is worse than no control, and
23796
+ * the switch group renders "not available" instead.
23797
+ */
23798
+ supportsAudioMute: boolean()
23152
23799
  });
23153
23800
  /** Partial change — every field optional. */
23154
23801
  var PrivacyMaskPatchSchema = object({
@@ -23175,6 +23822,27 @@ var privacyMaskCapability = {
23175
23822
  }), _void(), {
23176
23823
  kind: "mutation",
23177
23824
  auth: "admin"
23825
+ }),
23826
+ /**
23827
+ * Turn the camera's microphone on or off, at the camera.
23828
+ *
23829
+ * Deliberately its OWN mutation rather than a field on
23830
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
23831
+ * mask master switch", and overloading it would make one boolean mean two
23832
+ * unrelated things on the same call. It is also the only method here whose
23833
+ * write leaves the device in a state a later `getStatus` reads back
23834
+ * verbatim, which is what makes it safe as a switch authority.
23835
+ *
23836
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
23837
+ * this rather than silently accept it — a write nothing applies is exactly
23838
+ * what the switch group exists to remove.
23839
+ */
23840
+ setAudioEnabled: method(object({
23841
+ deviceId: number(),
23842
+ enabled: boolean()
23843
+ }), _void(), {
23844
+ kind: "mutation",
23845
+ auth: "admin"
23178
23846
  })
23179
23847
  },
23180
23848
  status: {
@@ -23457,6 +24125,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
23457
24125
  })]);
23458
24126
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
23459
24127
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
24128
+ /**
24129
+ * One GOP of a finalized segment, cut by byte range through the segment's own
24130
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
24131
+ * the single `moof`+`mdat` covering the requested instant — standalone-
24132
+ * demuxable, never the whole file. When the segment's index cannot be parsed
24133
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
24134
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
24135
+ */
24136
+ var ReadGopBytesResultSchema = object({
24137
+ data: _instanceof(Uint8Array),
24138
+ /** Absolute epoch ms of the returned fragment's first sample. */
24139
+ gopStartMs: number(),
24140
+ /** Media ms the returned fragment covers. */
24141
+ gopDurMs: number()
24142
+ });
23460
24143
  method(object({
23461
24144
  deviceId: number(),
23462
24145
  fromMs: number(),
@@ -23499,6 +24182,14 @@ method(object({
23499
24182
  }), ReadSegmentBytesResultSchema, {
23500
24183
  kind: "query",
23501
24184
  auth: "admin"
24185
+ }), method(object({
24186
+ deviceId: number(),
24187
+ profile: string(),
24188
+ startMs: number(),
24189
+ epochMs: number()
24190
+ }), ReadGopBytesResultSchema, {
24191
+ kind: "query",
24192
+ auth: "admin"
23502
24193
  }), method(object({
23503
24194
  deviceId: number(),
23504
24195
  config: RecordingConfigSchema
@@ -24058,6 +24749,16 @@ var StreamProfileConfigSchema = object({
24058
24749
  "baseline"
24059
24750
  ]).optional(),
24060
24751
  gop: number().optional(),
24752
+ /**
24753
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
24754
+ *
24755
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
24756
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
24757
+ * writes every profile at once so "audio off" means silent everywhere. A
24758
+ * per-profile writer beside it would let a camera be half-muted and would be
24759
+ * a second knob onto one device register — the failure D62 exists to
24760
+ * prevent. Absent when the firmware does not report the flag.
24761
+ */
24061
24762
  audio: boolean().optional()
24062
24763
  });
24063
24764
  var StreamParamsStatusSchema = object({
@@ -24098,7 +24799,13 @@ var StreamParamsOptionsSchema = object({
24098
24799
  ext: StreamProfileOptionsSchema.optional()
24099
24800
  });
24100
24801
  /** A partial change to one profile — every field optional; a provider
24101
- * ignores fields it doesn't support. */
24802
+ * ignores fields it doesn't support.
24803
+ *
24804
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
24805
+ * reachable from no form and honoured by exactly one provider, while the
24806
+ * camera's microphone is a whole-device fact. It now has one writer,
24807
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
24808
+ * `privacy-mask.cap.ts`. */
24102
24809
  var StreamProfilePatchSchema = object({
24103
24810
  width: number().optional(),
24104
24811
  height: number().optional(),
@@ -24111,8 +24818,7 @@ var StreamProfilePatchSchema = object({
24111
24818
  "main",
24112
24819
  "baseline"
24113
24820
  ]).optional(),
24114
- gop: number().optional(),
24115
- audio: boolean().optional()
24821
+ gop: number().optional()
24116
24822
  });
24117
24823
  var streamParamsCapability = {
24118
24824
  name: "stream-params",
@@ -28854,6 +29560,12 @@ Object.freeze({
28854
29560
  addonId: null,
28855
29561
  access: "view"
28856
29562
  },
29563
+ "notificationRules.listDeviceMutes": {
29564
+ capName: "notification-rules",
29565
+ capScope: "system",
29566
+ addonId: null,
29567
+ access: "view"
29568
+ },
28857
29569
  "notificationRules.listRules": {
28858
29570
  capName: "notification-rules",
28859
29571
  capScope: "system",
@@ -28872,6 +29584,12 @@ Object.freeze({
28872
29584
  addonId: null,
28873
29585
  access: "create"
28874
29586
  },
29587
+ "notificationRules.setDeviceMuted": {
29588
+ capName: "notification-rules",
29589
+ capScope: "system",
29590
+ addonId: null,
29591
+ access: "create"
29592
+ },
28875
29593
  "notificationRules.setRuleEnabled": {
28876
29594
  capName: "notification-rules",
28877
29595
  capScope: "system",
@@ -29148,6 +29866,12 @@ Object.freeze({
29148
29866
  addonId: null,
29149
29867
  access: "view"
29150
29868
  },
29869
+ "pipelineAnalytics.setTrackFlags": {
29870
+ capName: "pipeline-analytics",
29871
+ capScope: "device",
29872
+ addonId: null,
29873
+ access: "create"
29874
+ },
29151
29875
  "pipelineAnalytics.wipeAllAnalytics": {
29152
29876
  capName: "pipeline-analytics",
29153
29877
  capScope: "device",
@@ -29454,6 +30178,12 @@ Object.freeze({
29454
30178
  addonId: null,
29455
30179
  access: "view"
29456
30180
  },
30181
+ "pipelineOrchestrator.getCameraSwitches": {
30182
+ capName: "pipeline-orchestrator",
30183
+ capScope: "system",
30184
+ addonId: null,
30185
+ access: "view"
30186
+ },
29457
30187
  "pipelineOrchestrator.getCapabilityBindings": {
29458
30188
  capName: "pipeline-orchestrator",
29459
30189
  capScope: "system",
@@ -29586,6 +30316,12 @@ Object.freeze({
29586
30316
  addonId: null,
29587
30317
  access: "create"
29588
30318
  },
30319
+ "pipelineOrchestrator.setCameraSwitch": {
30320
+ capName: "pipeline-orchestrator",
30321
+ capScope: "system",
30322
+ addonId: null,
30323
+ access: "create"
30324
+ },
29589
30325
  "pipelineOrchestrator.setCapabilityBinding": {
29590
30326
  capName: "pipeline-orchestrator",
29591
30327
  capScope: "system",
@@ -29676,6 +30412,12 @@ Object.freeze({
29676
30412
  addonId: null,
29677
30413
  access: "create"
29678
30414
  },
30415
+ "pipelineRunner.runStatelessStep": {
30416
+ capName: "pipeline-runner",
30417
+ capScope: "system",
30418
+ addonId: null,
30419
+ access: "create"
30420
+ },
29679
30421
  "plateGallery.assignPlate": {
29680
30422
  capName: "plate-gallery",
29681
30423
  capScope: "system",
@@ -29808,6 +30550,12 @@ Object.freeze({
29808
30550
  addonId: null,
29809
30551
  access: "view"
29810
30552
  },
30553
+ "privacyMask.setAudioEnabled": {
30554
+ capName: "privacy-mask",
30555
+ capScope: "device",
30556
+ addonId: null,
30557
+ access: "create"
30558
+ },
29811
30559
  "privacyMask.setMask": {
29812
30560
  capName: "privacy-mask",
29813
30561
  capScope: "device",
@@ -29976,6 +30724,12 @@ Object.freeze({
29976
30724
  addonId: null,
29977
30725
  access: "create"
29978
30726
  },
30727
+ "recording.readGopBytes": {
30728
+ capName: "recording",
30729
+ capScope: "system",
30730
+ addonId: null,
30731
+ access: "view"
30732
+ },
29979
30733
  "recording.readSegmentBytes": {
29980
30734
  capName: "recording",
29981
30735
  capScope: "system",
@@ -30492,6 +31246,12 @@ Object.freeze({
30492
31246
  addonId: null,
30493
31247
  access: "create"
30494
31248
  },
31249
+ "streamBroker.acquireEgressTranscode": {
31250
+ capName: "stream-broker",
31251
+ capScope: "system",
31252
+ addonId: null,
31253
+ access: "create"
31254
+ },
30495
31255
  "streamBroker.assignProfile": {
30496
31256
  capName: "stream-broker",
30497
31257
  capScope: "system",
@@ -30600,6 +31360,12 @@ Object.freeze({
30600
31360
  addonId: null,
30601
31361
  access: "create"
30602
31362
  },
31363
+ "streamBroker.releaseEgressTranscode": {
31364
+ capName: "stream-broker",
31365
+ capScope: "system",
31366
+ addonId: null,
31367
+ access: "create"
31368
+ },
30603
31369
  "streamBroker.releaseStreamWithCodec": {
30604
31370
  capName: "stream-broker",
30605
31371
  capScope: "system",
@@ -31430,6 +32196,88 @@ object({
31430
32196
  paddingRatio: .15,
31431
32197
  square: false
31432
32198
  }).paddingRatio;
32199
+ /**
32200
+ * WHICH delivered frames the decode worker retains a native copy of.
32201
+ *
32202
+ * - `all` — every frame the worker delivered to the runner. The shipped
32203
+ * behaviour, and the only correct one if something can ask for a crop of a
32204
+ * frame the runner never sent to inference.
32205
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
32206
+ * A native-crop request always names a `frameId` that rode an inference
32207
+ * result, so that is the only set a request can name. How much it drops is
32208
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
32209
+ * this cluster, not the ~80% the design sketch assumed, because the governor
32210
+ * was not throttling as hard as the sketch supposed. Read
32211
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
32212
+ * of you rather than quoting a number from here. The newest delivered frame is
32213
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
32214
+ * which covers the one-frame race between a mark and the supersede that
32215
+ * consumes it.
32216
+ */
32217
+ var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
32218
+ object({
32219
+ /**
32220
+ * How long a retained native frame is served before it counts as a miss.
32221
+ *
32222
+ * Must cover the FULL late-crop horizon: detection inference + the
32223
+ * cross-process inference-result hop to hub post-analysis + tracking + the
32224
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
32225
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
32226
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
32227
+ */
32228
+ ttlMs: number().int().min(250).max(1e4),
32229
+ /**
32230
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
32231
+ *
32232
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
32233
+ * which one is actually binding before reasoning from that. At the shipped
32234
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
32235
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
32236
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
32237
+ * change that admits fewer frames buys retention WINDOW at constant RAM
32238
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
32239
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
32240
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
32241
+ * to replace).
32242
+ */
32243
+ budgetMb: number().int().min(0).max(4096),
32244
+ /**
32245
+ * Demand window: eager per-frame native retention runs only within this many
32246
+ * ms of the last native-crop request (or of the dial starting).
32247
+ *
32248
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
32249
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
32250
+ * per second on a camera with zero crop demand), so leave it non-zero unless
32251
+ * you are reproducing that.
32252
+ */
32253
+ activityMs: number().int().min(0).max(12e4),
32254
+ /**
32255
+ * Which delivered frames are retained at all — see
32256
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
32257
+ * changes WHAT is kept rather than for how long, so it is also the only one
32258
+ * that can turn a crop that used to hit into a miss. The worker counts every
32259
+ * crop request naming a frame it did NOT see marked
32260
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
32261
+ * there is the signal that some caller names frames outside the inference set
32262
+ * and that this must go back to `all`.
32263
+ */
32264
+ admission: NativeLeaseAdmissionSchema
32265
+ });
32266
+ /**
32267
+ * The values in force when the operator has set nothing — byte-for-byte the
32268
+ * constants the decode worker shipped with as env-var defaults, so making these
32269
+ * settings changed no behaviour on the day it landed.
32270
+ */
32271
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
32272
+ ttlMs: 1200,
32273
+ budgetMb: 1024,
32274
+ activityMs: 15e3,
32275
+ admission: "inferred"
32276
+ };
32277
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
32278
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
32279
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
32280
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
31433
32281
  //#endregion
31434
32282
  Object.defineProperty(exports, "BaseAddon", {
31435
32283
  enumerable: true,
@@ -31557,6 +32405,12 @@ Object.defineProperty(exports, "TimelapseRuleSchema", {
31557
32405
  return TimelapseRuleSchema;
31558
32406
  }
31559
32407
  });
32408
+ Object.defineProperty(exports, "TrackSourceSchema", {
32409
+ enumerable: true,
32410
+ get: function() {
32411
+ return TrackSourceSchema;
32412
+ }
32413
+ });
31560
32414
  Object.defineProperty(exports, "__toESM", {
31561
32415
  enumerable: true,
31562
32416
  get: function() {
@@ -31731,6 +32585,12 @@ Object.defineProperty(exports, "record", {
31731
32585
  return record;
31732
32586
  }
31733
32587
  });
32588
+ Object.defineProperty(exports, "sleep", {
32589
+ enumerable: true,
32590
+ get: function() {
32591
+ return sleep;
32592
+ }
32593
+ });
31734
32594
  Object.defineProperty(exports, "string", {
31735
32595
  enumerable: true,
31736
32596
  get: function() {