@camstack/types 1.2.42 → 1.2.44

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.
Files changed (48) hide show
  1. package/dist/addon.js +8 -2
  2. package/dist/addon.mjs +8 -2
  3. package/dist/cap-call-context.d.ts +26 -0
  4. package/dist/capabilities/index.d.ts +8 -5
  5. package/dist/capabilities/osd-manager.cap.d.ts +900 -0
  6. package/dist/capabilities/pipeline-analytics.cap.d.ts +171 -4
  7. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +36 -0
  8. package/dist/capabilities/privacy-mask.cap.d.ts +69 -9
  9. package/dist/capabilities/recording.cap.d.ts +27 -0
  10. package/dist/capabilities/schemas/streaming-shared.d.ts +2 -0
  11. package/dist/capabilities/server-management.cap.d.ts +3 -3
  12. package/dist/capabilities/snapshot.cap.d.ts +1 -1
  13. package/dist/capabilities/stream-broker.cap.d.ts +36 -0
  14. package/dist/capabilities/stream-params.cap.d.ts +8 -4
  15. package/dist/device/device-profile.d.ts +12 -4
  16. package/dist/device/system-mirror.d.ts +11 -0
  17. package/dist/ffmpeg/encode-defaults.d.ts +18 -0
  18. package/dist/ffmpeg/fmp4-box-splitter.d.ts +113 -0
  19. package/dist/ffmpeg/fmp4-fragment-child.d.ts +85 -0
  20. package/dist/ffmpeg/fmp4-fragment-plane.d.ts +142 -0
  21. package/dist/ffmpeg/invocation.d.ts +102 -2
  22. package/dist/ffmpeg/sharing-key.d.ts +54 -2
  23. package/dist/fmp4-box-splitter-B53u9-Nu.mjs +615 -0
  24. package/dist/fmp4-box-splitter-BkWH7O3L.js +686 -0
  25. package/dist/generated/addon-api.d.ts +117 -0
  26. package/dist/generated/cap-input-defaults.d.ts +1 -1
  27. package/dist/generated/capability-router-map.d.ts +5 -2
  28. package/dist/generated/device-proxy.d.ts +4 -2
  29. package/dist/generated/method-access-map.d.ts +1 -1
  30. package/dist/generated/system-proxy.d.ts +2 -0
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.js +1264 -394
  33. package/dist/index.mjs +1217 -385
  34. package/dist/interfaces/camera-switches.d.ts +211 -5
  35. package/dist/interfaces/inference-engine.d.ts +24 -3
  36. package/dist/interfaces/stream-broker.d.ts +18 -0
  37. package/dist/node.d.ts +4 -0
  38. package/dist/node.js +509 -3
  39. package/dist/node.mjs +507 -3
  40. package/dist/notification/schedule.d.ts +20 -0
  41. package/dist/pipeline/native-lease.d.ts +150 -0
  42. package/dist/{sleep-BmNKsY7v.mjs → sleep-cC4Fuup8.mjs} +59 -3
  43. package/dist/{sleep-Cvi1JxZp.js → sleep-eiC10_cX.js} +64 -2
  44. package/dist/types/pipeline-step.d.ts +1 -1
  45. package/dist/utils/addon-id.d.ts +30 -0
  46. package/package.json +1 -1
  47. package/dist/canonical-hash-7nfBbEqR.mjs +0 -35
  48. package/dist/canonical-hash-BcZHRHIx.js +0 -40
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_event_category = require("./event-category-BE4PDZ_3.js");
3
- const require_sleep = require("./sleep-Cvi1JxZp.js");
4
- const require_canonical_hash = require("./canonical-hash-BcZHRHIx.js");
3
+ const require_sleep = require("./sleep-eiC10_cX.js");
4
+ const require_fmp4_box_splitter = require("./fmp4-box-splitter-BkWH7O3L.js");
5
5
  const require_enums = require("./enums.js");
6
6
  const require_err_msg = require("./err-msg-COpsHMw2.js");
7
7
  let zod = require("zod");
@@ -231,335 +231,6 @@ function encodeProfileFromStreamShape(stream) {
231
231
  };
232
232
  }
233
233
  //#endregion
234
- //#region src/ffmpeg/invocation.ts
235
- var AUDIO_ENCODER_BY_CODEC = {
236
- opus: "libopus",
237
- aac: "aac",
238
- pcmu: "pcm_mulaw",
239
- pcma: "pcm_alaw"
240
- };
241
- /**
242
- * Camera-microphone audio, per codec. Lives HERE rather than in
243
- * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
244
- * on these types); it is re-exported from there, which is where to read it.
245
- *
246
- * Every source in this repo is a mono camera mic. The former broker preset
247
- * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
248
- * that is the value this consolidation changed.
249
- */
250
- var AUDIO_PRESETS = {
251
- aac: {
252
- kind: "encode",
253
- codec: "aac",
254
- bitrateKbps: 128,
255
- sampleRateHz: 48e3,
256
- channels: 1
257
- },
258
- opus: {
259
- kind: "encode",
260
- codec: "opus",
261
- bitrateKbps: 64,
262
- sampleRateHz: 48e3,
263
- channels: 1
264
- },
265
- pcmu: {
266
- kind: "encode",
267
- codec: "pcmu",
268
- sampleRateHz: 8e3,
269
- channels: 1
270
- }
271
- };
272
- /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
273
- function logBannerArgs(level) {
274
- return [
275
- "-hide_banner",
276
- "-loglevel",
277
- level
278
- ];
279
- }
280
- /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
281
- function isSoftwareDecode(decodeHwAccel) {
282
- return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
283
- }
284
- /**
285
- * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
286
- * appended to this list by a caller — that is the whole point of the function.
287
- */
288
- function buildInputArgs(input, decodeHwAccel) {
289
- const args = [];
290
- if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
291
- if (input.extraArgs?.length) args.push(...input.extraArgs);
292
- if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
293
- if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
294
- args.push("-i", input.url);
295
- return args;
296
- }
297
- /** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
298
- function buildVideoFilterArgs(scale, outputArgs) {
299
- if (!scale) return [];
300
- if (outputArgs.some((a) => a === "-vf")) return [];
301
- if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}`];
302
- return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2`];
303
- }
304
- /** Rate-control args for an encode plan. */
305
- function buildRateControlArgs(video) {
306
- const kbps = video.bitrateKbps;
307
- if (kbps === void 0) return [];
308
- const rc = video.rateControl ?? {
309
- kind: "cap",
310
- vbvSeconds: 2
311
- };
312
- const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
313
- return [
314
- ...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
315
- "-maxrate",
316
- `${kbps}k`,
317
- "-bufsize",
318
- `${bufsize}k`
319
- ];
320
- }
321
- /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
322
- function buildVideoArgs(video, outputArgs) {
323
- if (video.kind === "copy") return [
324
- "-c:v",
325
- "copy",
326
- ...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
327
- ];
328
- const args = [
329
- ...buildVideoFilterArgs(video.scale, outputArgs),
330
- "-c:v",
331
- video.encoder
332
- ];
333
- if (video.preset !== void 0) args.push("-preset", video.preset);
334
- if (video.tune !== void 0) args.push("-tune", video.tune);
335
- if (video.profile !== void 0) args.push("-profile:v", video.profile);
336
- if (video.level !== void 0) args.push("-level", video.level);
337
- if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
338
- if (video.fps !== void 0) args.push("-r", String(video.fps));
339
- if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
340
- if (video.bf !== void 0) args.push("-bf", String(video.bf));
341
- args.push(...buildRateControlArgs(video));
342
- if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
343
- return args;
344
- }
345
- /** The whole audio block, after `-i`. */
346
- function buildAudioArgs(audio) {
347
- if (audio.kind === "none") return ["-an"];
348
- if (audio.kind === "copy") return ["-c:a", "copy"];
349
- const args = [];
350
- if (audio.filter !== void 0) args.push("-af", audio.filter);
351
- args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
352
- if (audio.application !== void 0) args.push("-application", audio.application);
353
- if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
354
- if (audio.globalHeader === true) args.push("-flags", "+global_header");
355
- if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
356
- if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
357
- if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
358
- if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
359
- return args;
360
- }
361
- /** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
362
- function buildRtpOutputArgs(out) {
363
- const args = [];
364
- if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
365
- if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
366
- if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
367
- args.push("-f", "rtp", out.url);
368
- return args;
369
- }
370
- /** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
371
- function isElementaryVideoSink(sink) {
372
- return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
373
- }
374
- /**
375
- * A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
376
- * audio optional so a source with no audio skips it instead of failing the
377
- * whole invocation.
378
- */
379
- function buildAudioSidecarArgs(sidecar) {
380
- return [
381
- "-map",
382
- "0:a:0?",
383
- ...buildAudioArgs(sidecar.codec === "pcma" ? {
384
- kind: "encode",
385
- codec: "pcma",
386
- sampleRateHz: 8e3,
387
- channels: 1
388
- } : AUDIO_PRESETS[sidecar.codec]),
389
- ...buildRtpOutputArgs({
390
- url: sidecar.rtpUrl,
391
- sdpFile: sidecar.sdpFile
392
- })
393
- ];
394
- }
395
- /**
396
- * Assemble the full ffmpeg argument list. Layout:
397
- *
398
- * -hide_banner -loglevel <level>
399
- * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
400
- * [<input.extraArgs>] │
401
- * [-fflags <flag>…] │
402
- * [-rtsp_transport tcp] │
403
- * -i <url> ─┘
404
- * <video block> <threads> <audio block> ─┐ OUTPUT options.
405
- * <consumer outputArgs verbatim> │
406
- * <sink> ─┘ terminal
407
- */
408
- function buildFfmpegArgs(inv) {
409
- const head = [...logBannerArgs(inv.logLevel), ...buildInputArgs(inv.input, inv.decodeHwAccel)];
410
- const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
411
- if (inv.sink.kind === "rtp-outputs") {
412
- const videoLeg = inv.sink.video ? [
413
- "-an",
414
- "-map",
415
- "0:v:0",
416
- ...buildVideoArgs(inv.video, inv.outputArgs),
417
- ...threadArgs,
418
- ...inv.outputArgs,
419
- ...buildRtpOutputArgs(inv.sink.video)
420
- ] : [];
421
- const audioLeg = inv.sink.audio ? [
422
- "-vn",
423
- "-map",
424
- "0:a:0?",
425
- ...buildAudioArgs(inv.audio),
426
- ...buildRtpOutputArgs(inv.sink.audio)
427
- ] : [];
428
- return [
429
- ...head,
430
- ...videoLeg,
431
- ...audioLeg
432
- ];
433
- }
434
- const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
435
- const sinkArgs = inv.sink.kind === "stdout" ? [
436
- "-f",
437
- inv.sink.container,
438
- "pipe:1"
439
- ] : [
440
- "-f",
441
- "rtsp",
442
- "-rtsp_transport",
443
- "tcp",
444
- "-rtsp_flags",
445
- "listen",
446
- inv.sink.url
447
- ];
448
- return [
449
- ...head,
450
- ...buildVideoArgs(inv.video, inv.outputArgs),
451
- ...threadArgs,
452
- ...audioArgs,
453
- ...inv.outputArgs,
454
- ...sinkArgs,
455
- ...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
456
- ];
457
- }
458
- /**
459
- * Hardware ENCODER ids per decode-hwaccel backend — a static, deterministic
460
- * map (the same shape the reference NVR uses: platform → encoder, no probe).
461
- */
462
- var ENCODER_IDS_BY_BACKEND = {
463
- videotoolbox: {
464
- h264: "h264_videotoolbox",
465
- h265: "hevc_videotoolbox"
466
- },
467
- vaapi: {
468
- h264: "h264_vaapi",
469
- h265: "hevc_vaapi"
470
- },
471
- qsv: {
472
- h264: "h264_qsv",
473
- h265: "hevc_qsv"
474
- },
475
- cuda: {
476
- h264: "h264_nvenc",
477
- h265: "hevc_nvenc"
478
- },
479
- nvdec: {
480
- h264: "h264_nvenc",
481
- h265: "hevc_nvenc"
482
- },
483
- amf: {
484
- h264: "h264_amf",
485
- h265: "hevc_amf"
486
- }
487
- };
488
- /**
489
- * The hardware encoder for a target codec on `backend`, or the software one.
490
- * `'auto'` is NOT a backend identity (it is an instruction to ffmpeg), so it
491
- * maps to software encoding.
492
- */
493
- function pickVideoEncoder(target, backend, useHardware) {
494
- const software = target === "h264" ? "libx264" : "libx265";
495
- if (!useHardware || backend === null || isSoftwareDecode(backend) || backend === "auto") return software;
496
- const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
497
- if (!ids) return software;
498
- return target === "h264" ? ids.h264 : ids.h265;
499
- }
500
- /** Map an `EncodeProfile.audio` to an audio plan. */
501
- function audioPlanFromEncodeProfile(audio) {
502
- if (audio === "passthrough") return { kind: "none" };
503
- if (audio.codec === "copy") return { kind: "copy" };
504
- return {
505
- kind: "encode",
506
- codec: audio.codec,
507
- ...audio.bitrateKbps !== void 0 ? { bitrateKbps: audio.bitrateKbps } : {},
508
- ...audio.sampleRateHz !== void 0 ? { sampleRateHz: audio.sampleRateHz } : {},
509
- ...audio.channels !== void 0 ? { channels: audio.channels } : {}
510
- };
511
- }
512
- /**
513
- * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
514
- * {@link FfmpegInvocation}. This is the ONLY bridge between the two models —
515
- * a second one is how the repo grew two argv builders that disagreed about
516
- * hardware.
517
- *
518
- * Smart video copy: when the source already speaks the requested codec the
519
- * encode block is elided entirely and ffmpeg runs as a re-muxer on the video
520
- * plane. Width / height / fps / bitrate in the profile are a downstream BUDGET,
521
- * not a forced rescale.
522
- */
523
- function invocationFromEncodeProfile(input) {
524
- const v = input.profile.video;
525
- const shouldCopy = v.codec === "copy" || input.forceReencode !== true && v.codec === input.sourceCodec;
526
- const scale = v.width !== void 0 && v.height !== void 0 ? {
527
- mode: "fit",
528
- width: v.width,
529
- height: v.height
530
- } : null;
531
- const target = v.codec === "h265" ? "h265" : "h264";
532
- const video = shouldCopy ? { kind: "copy" } : {
533
- kind: "encode",
534
- encoder: pickVideoEncoder(target, input.decodeHwAccel, input.hardwareEncoders === true),
535
- scale,
536
- ...v.preset !== void 0 ? { preset: v.preset } : {},
537
- ...v.tune !== void 0 ? { tune: v.tune } : {},
538
- ...v.profile !== void 0 ? { profile: v.profile } : {},
539
- ...v.level !== void 0 ? { level: v.level } : {},
540
- ...v.fps !== void 0 ? { fps: v.fps } : {},
541
- ...v.gopFrames !== void 0 ? { gopFrames: v.gopFrames } : {},
542
- ...v.bf !== void 0 ? { bf: v.bf } : {},
543
- ...v.bitrateKbps !== void 0 ? { bitrateKbps: v.bitrateKbps } : {}
544
- };
545
- return {
546
- logLevel: input.logLevel ?? "error",
547
- decodeHwAccel: input.decodeHwAccel,
548
- input: {
549
- url: input.sourceUrl,
550
- rtspTransport: "tcp",
551
- fflags: ["+discardcorrupt"],
552
- ...input.profile.inputArgs?.length ? { extraArgs: input.profile.inputArgs } : {}
553
- },
554
- video,
555
- audio: audioPlanFromEncodeProfile(input.profile.audio),
556
- threadCount: input.threadCount ?? 0,
557
- outputArgs: input.profile.outputArgs ?? [],
558
- sink: input.sink,
559
- ...input.audioSidecar !== void 0 ? { audioSidecar: input.audioSidecar } : {}
560
- };
561
- }
562
- //#endregion
563
234
  //#region src/ffmpeg/encode-defaults.ts
564
235
  /**
565
236
  * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
@@ -604,9 +275,27 @@ var WEBRTC_EGRESS_PROFILE = {
604
275
  * audio plane (out-of-band), so this is `passthrough` exactly like the browser
605
276
  * — see the ADR for why the previous in-band Opus was encoded and discarded.
606
277
  */
278
+ /**
279
+ * Alexa's egress asks for OPUS, and that is a change with a history.
280
+ *
281
+ * The previous profile encoded Opus IN-BAND into an MPEG-TS, where it was
282
+ * discarded: Opus is `stream_type 0x06` and the broker's demuxer maps only
283
+ * `0x0f` (aac), `0x03`/`0x04` (mp2) and `0x81` (ac3). Every frame it produced
284
+ * died at the demuxer, so the encode was replaced with `passthrough` — correct,
285
+ * because paying libopus for nothing is worse than silence.
286
+ *
287
+ * But `passthrough` means the egress emits NO audio at all, and Alexa's audio
288
+ * does not arrive by magic: it rides the published stream, which is what the
289
+ * WebRTC session dials. So the Echo had video and silence either way.
290
+ *
291
+ * Opus here now reaches the egress AUDIO SIDECAR — a separate RTP leg the
292
+ * restreamer grafts onto its SDP — which never touches the MPEG-TS demuxer that
293
+ * killed the in-band attempt. Same codec, different plane, and this one the
294
+ * consumer can actually negotiate.
295
+ */
607
296
  var ALEXA_EGRESS_PROFILE = {
608
297
  ...BASE_LIVE_EGRESS_PROFILE,
609
- audio: "passthrough"
298
+ audio: { codec: "opus" }
610
299
  };
611
300
  /** VBV window for a consumer whose budget is enforced per second (HomeKit). */
612
301
  var RATE_CONTROL_TIGHT = {
@@ -727,6 +416,14 @@ async function resolveEgressDecodeHwAccel(deps) {
727
416
  * Here every knob is a named field, and defaults are APPLIED before hashing so
728
417
  * an omitted field and its explicit default land on the same key.
729
418
  */
419
+ /**
420
+ * The transport a CAP request describes. `fragments` is deliberately
421
+ * unreachable from here — the request schema has no way to ask for it, so the
422
+ * cap path can never be handed a fragment child by accident.
423
+ */
424
+ function egressTransportFromRequest(request) {
425
+ return { transport: request.publishLocally === true ? "push" : "dial" };
426
+ }
730
427
  /** Absent optional ⇒ this sentinel, so `undefined` and "not set" agree. */
731
428
  var UNSET = "\0unset";
732
429
  function canonicalVideo(video) {
@@ -758,10 +455,12 @@ function canonicalAudio(audio) {
758
455
  * future operator-facing "why are these two not sharing?" surface — can diff
759
456
  * two requests without reversing a hash.
760
457
  */
761
- function canonicalEgressPlan(request) {
458
+ function canonicalEgressPlan(request, delivery = egressTransportFromRequest(request)) {
762
459
  return {
763
460
  deviceId: request.deviceId,
764
461
  source: request.source.kind === "profile" ? `profile:${request.source.profile}` : `cam-stream:${request.source.camStreamId}`,
462
+ transport: delivery.transport,
463
+ fragmentMs: delivery.fragmentMs ?? -1,
765
464
  video: canonicalVideo(request.encode.video),
766
465
  audio: canonicalAudio(request.encode.audio),
767
466
  rateControl: request.rateControl ?? "relaxed",
@@ -781,8 +480,8 @@ function canonicalEgressPlan(request) {
781
480
  * exactly a mutable shared object, where one consumer's downgrade dragged
782
481
  * every other consumer to 360p.
783
482
  */
784
- function egressTranscodeSharingKey(request) {
785
- return `egress:${require_canonical_hash.canonicalHash(canonicalEgressPlan(request))}`;
483
+ function egressTranscodeSharingKey(request, delivery = egressTransportFromRequest(request)) {
484
+ return `egress:${require_fmp4_box_splitter.canonicalHash(canonicalEgressPlan(request, delivery))}`;
786
485
  }
787
486
  //#endregion
788
487
  //#region src/health/wiring-health.ts
@@ -902,7 +601,7 @@ var DEFAULT_RETENTION = {
902
601
  * ## This file adds no state
903
602
  *
904
603
  * Every switch here is a VIEW onto an authority that already existed
905
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
604
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
906
605
  * group is that there is exactly one place each function is turned off, and
907
606
  * the group routes to it:
908
607
  *
@@ -913,6 +612,53 @@ var DEFAULT_RETENTION = {
913
612
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
914
613
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
915
614
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
615
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
616
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
617
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
618
+ *
619
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
620
+ *
621
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
622
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
623
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
624
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
625
+ * instant, vendor-independent and reversible without touching the camera, and
626
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
627
+ * D62 forbids a second switch that *disagrees* with the first; these two
628
+ * cannot disagree, because neither reads the other's store — the camera holds
629
+ * one, the broker holds the other, and each reports its own fact.
630
+ *
631
+ * ## The two switches whose authority is not on this server
632
+ *
633
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
634
+ * in "the group stores nothing" — it is the purest form of it: the camera
635
+ * holds the fact, every read is a read-through, and there is no server-side
636
+ * copy that could drift. Their availability therefore cannot come from
637
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
638
+ * is filtered out there); it comes from the cap's own camera-probed
639
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
640
+ * THIS camera rather than for the device type
641
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
642
+ *
643
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
644
+ *
645
+ * Every other switch means *this camera's function is doing its job*, so
646
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
647
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
648
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
649
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
650
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
651
+ * worse than either surface not having one.
652
+ *
653
+ * Two consequences follow and both are load-bearing:
654
+ *
655
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
656
+ * exactly this row. With the polarity above, every camera that has NOT drawn
657
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
658
+ * normal, healthy state of most cameras rendered as an operator disablement.
659
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
660
+ * unconditionally by both clients, so for this row it has to read correctly
661
+ * whichever way the switch is sitting.
916
662
  *
917
663
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
918
664
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -931,22 +677,44 @@ var DEFAULT_RETENTION = {
931
677
  * `CameraStatus.switchedOff`.
932
678
  */
933
679
  /**
934
- * The five functions the operator named (2026-08-05). Deliberately NOT one id
935
- * per pipeline step: face recognition and plate/LPR are per-step toggles on
680
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
681
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
682
+ * recognition and plate/LPR are per-step toggles on
936
683
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
937
- * editor, not in a five-button safety group.
684
+ * editor, not in a safety group.
938
685
  */
939
686
  var CameraSwitchIdSchema = zod.z.enum([
940
687
  "stream-broker",
941
688
  "object-detection",
689
+ "privacy-mask",
690
+ "device-audio",
691
+ "broker-audio",
942
692
  "audio-analysis",
943
693
  "recording",
944
694
  "notifications"
945
695
  ]);
946
- /** Stable render order — broadest blast radius first. */
696
+ /**
697
+ * Stable render order — broadest blast radius first, and a source before the
698
+ * thing that consumes it. `device-audio` sits ABOVE `audio-analysis` because
699
+ * turning the microphone off leaves the analyzer with nothing to analyse; the
700
+ * reverse is not true.
701
+ *
702
+ * `privacy-mask` sits directly ABOVE `device-audio` because they are literal
703
+ * siblings — one cap, one device plane, video then audio — and NOT above
704
+ * `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
705
+ * radius is partial, and the "broadest first" rule does not rank a partial
706
+ * control above a whole-function one.
707
+ *
708
+ * `broker-audio` sits directly BELOW `device-audio` by the same source-first
709
+ * rule: the camera's microphone feeds the broker, so silencing the camera
710
+ * leaves the broker's mute with nothing to suppress; the reverse is not true.
711
+ */
947
712
  var CAMERA_SWITCH_ORDER = [
948
713
  "stream-broker",
949
714
  "object-detection",
715
+ "privacy-mask",
716
+ "device-audio",
717
+ "broker-audio",
950
718
  "audio-analysis",
951
719
  "recording",
952
720
  "notifications"
@@ -964,14 +732,27 @@ var CameraSwitchAuthoritySchema = zod.z.discriminatedUnion("kind", [
964
732
  capName: zod.z.string()
965
733
  }),
966
734
  zod.z.object({ kind: zod.z.literal("recording-config") }),
967
- zod.z.object({ kind: zod.z.literal("notification-mute") })
735
+ zod.z.object({ kind: zod.z.literal("notification-mute") }),
736
+ zod.z.object({
737
+ kind: zod.z.literal("camera-audio"),
738
+ capName: zod.z.string()
739
+ }),
740
+ zod.z.object({
741
+ kind: zod.z.literal("camera-mask"),
742
+ capName: zod.z.string()
743
+ }),
744
+ zod.z.object({ kind: zod.z.literal("broker-audio-mute") })
968
745
  ]);
969
746
  /**
970
747
  * Why a switch is not offered for this camera. Rendered instead of the
971
748
  * control, never as a dead control — an absent function and a broken one must
972
749
  * not look the same.
973
750
  */
974
- var CameraSwitchUnavailableReasonSchema = zod.z.enum(["no-provider", "source-unreachable"]);
751
+ var CameraSwitchUnavailableReasonSchema = zod.z.enum([
752
+ "no-provider",
753
+ "source-unreachable",
754
+ "not-configured"
755
+ ]);
975
756
  /**
976
757
  * One switch, resolved for one camera.
977
758
  *
@@ -1011,6 +792,13 @@ var CameraSwitchGroupSchema = zod.z.object({
1011
792
  var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
1012
793
  var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
1013
794
  /**
795
+ * The device-NATIVE cap that owns what the camera does not capture — masked
796
+ * video regions and, since 2026-08-07, the microphone. Named here because the
797
+ * `camera-audio` authority, the orchestrator's gather and the fake harness all
798
+ * have to agree on it.
799
+ */
800
+ var PRIVACY_MASK_CAP_NAME = "privacy-mask";
801
+ /**
1014
802
  * THE catalog. One entry per switch; the cost lines are the operator-facing
1015
803
  * contract and are written to be true rather than reassuring.
1016
804
  */
@@ -1019,7 +807,8 @@ var CAMERA_SWITCH_CATALOG = {
1019
807
  id: "stream-broker",
1020
808
  label: "Camera",
1021
809
  costWhenOff: "Off: the whole camera stops. No live view, no recording, no detection and no notifications — its streams are released and nothing dials it again until you turn it back on.",
1022
- authority: { kind: "device-disabled" }
810
+ authority: { kind: "device-disabled" },
811
+ countsAsSwitchedOff: true
1023
812
  },
1024
813
  "object-detection": {
1025
814
  id: "object-detection",
@@ -1028,7 +817,35 @@ var CAMERA_SWITCH_CATALOG = {
1028
817
  authority: {
1029
818
  kind: "wrapper-binding",
1030
819
  capName: DETECTION_PIPELINE_CAP_NAME
1031
- }
820
+ },
821
+ countsAsSwitchedOff: true
822
+ },
823
+ "privacy-mask": {
824
+ id: "privacy-mask",
825
+ label: "Privacy mask",
826
+ costWhenOff: "On: the zones you drew on this camera are blacked out by the camera itself — live view, playback, exports and detection all see the black boxes, and nothing behind them was ever recorded. Off: the camera captures the whole frame, and your zones are kept for when you turn it back on.",
827
+ authority: {
828
+ kind: "camera-mask",
829
+ capName: PRIVACY_MASK_CAP_NAME
830
+ },
831
+ countsAsSwitchedOff: false
832
+ },
833
+ "device-audio": {
834
+ id: "device-audio",
835
+ label: "Camera microphone",
836
+ costWhenOff: "Off: the camera captures no sound at all. Live view and recordings become silent video, and audio detection and classification have nothing left to analyse — turning them back on will not recover it. The picture, motion and object detection are unaffected, and two-way talk still works. Applies at the camera, so every consumer sees the same silence.",
837
+ authority: {
838
+ kind: "camera-audio",
839
+ capName: PRIVACY_MASK_CAP_NAME
840
+ },
841
+ countsAsSwitchedOff: true
842
+ },
843
+ "broker-audio": {
844
+ id: "broker-audio",
845
+ label: "Audio distribution",
846
+ costWhenOff: "Off: this server distributes no sound for this camera — live view is silent and everything recorded while it is off is silent FOREVER, even after you turn it back on. Audio detection and classification also have nothing to analyse. The camera keeps capturing sound, so nothing about the camera changes and turning this back on is instant, with no interruption to the picture.",
847
+ authority: { kind: "broker-audio-mute" },
848
+ countsAsSwitchedOff: true
1032
849
  },
1033
850
  "audio-analysis": {
1034
851
  id: "audio-analysis",
@@ -1037,19 +854,22 @@ var CAMERA_SWITCH_CATALOG = {
1037
854
  authority: {
1038
855
  kind: "wrapper-binding",
1039
856
  capName: AUDIO_ANALYSIS_CAP_NAME
1040
- }
857
+ },
858
+ countsAsSwitchedOff: true
1041
859
  },
1042
860
  recording: {
1043
861
  id: "recording",
1044
862
  label: "Recording",
1045
863
  costWhenOff: "Off: nothing new is written to disk. Footage already recorded stays, but retention keeps deleting it — so this camera’s history shrinks and is not replaced. Your recording schedule is kept and resumes when you turn it back on.",
1046
- authority: { kind: "recording-config" }
864
+ authority: { kind: "recording-config" },
865
+ countsAsSwitchedOff: true
1047
866
  },
1048
867
  notifications: {
1049
868
  id: "notifications",
1050
869
  label: "Notifications",
1051
870
  costWhenOff: "Off: this camera never notifies anyone, on any rule, with no expiry. Detection, events and recording carry on exactly as before — you simply stop being told about them.",
1052
- authority: { kind: "notification-mute" }
871
+ authority: { kind: "notification-mute" },
872
+ countsAsSwitchedOff: true
1053
873
  }
1054
874
  };
1055
875
  /** Resolve one switch's `{ available, enabled }` pair. */
@@ -1096,6 +916,67 @@ function resolveState(descriptor, input) {
1096
916
  available: true,
1097
917
  enabled: !input.notificationsMuted
1098
918
  };
919
+ case "camera-audio": {
920
+ const audio = input.deviceAudio;
921
+ if (audio === null) return {
922
+ available: false,
923
+ enabled: true,
924
+ unavailableReason: "source-unreachable"
925
+ };
926
+ if (!audio.supported) return {
927
+ available: false,
928
+ enabled: true,
929
+ unavailableReason: "no-provider"
930
+ };
931
+ if (audio.enabled === null) return {
932
+ available: false,
933
+ enabled: true,
934
+ unavailableReason: "source-unreachable"
935
+ };
936
+ return {
937
+ available: true,
938
+ enabled: audio.enabled
939
+ };
940
+ }
941
+ case "camera-mask": {
942
+ const mask = input.privacyMask;
943
+ if (mask === null) return {
944
+ available: false,
945
+ enabled: false,
946
+ unavailableReason: "source-unreachable"
947
+ };
948
+ if (!mask.supported) return {
949
+ available: false,
950
+ enabled: false,
951
+ unavailableReason: "no-provider"
952
+ };
953
+ if (mask.configuredRegions === null || mask.enabled === null) return {
954
+ available: false,
955
+ enabled: false,
956
+ unavailableReason: "source-unreachable"
957
+ };
958
+ if (mask.configuredRegions === 0) return {
959
+ available: false,
960
+ enabled: false,
961
+ unavailableReason: "not-configured"
962
+ };
963
+ return {
964
+ available: true,
965
+ enabled: mask.enabled
966
+ };
967
+ }
968
+ case "broker-audio-mute": {
969
+ const broker = input.brokerAudio;
970
+ if (broker === null) return {
971
+ available: false,
972
+ enabled: true,
973
+ unavailableReason: "source-unreachable"
974
+ };
975
+ return {
976
+ available: true,
977
+ enabled: !broker.muted
978
+ };
979
+ }
1099
980
  }
1100
981
  }
1101
982
  /**
@@ -1129,9 +1010,15 @@ function deriveCameraSwitches(input) {
1129
1010
  * `switchedOff: ['object-detection']` was turned off; the same camera with an
1130
1011
  * empty list is broken. Unavailable switches never appear — a function nobody
1131
1012
  * provides was not switched off by anyone.
1013
+ *
1014
+ * `privacy-mask` never appears either, whichever way it is sitting, because its
1015
+ * ON means "the mask is active" rather than "the function works"
1016
+ * ({@link CameraSwitchDescriptor.countsAsSwitchedOff}). Without that filter the
1017
+ * ordinary state of every camera nobody has masked would carry a
1018
+ * "switched off" badge, and the badge that matters would be lost in it.
1132
1019
  */
1133
1020
  function switchedOffIds(switches) {
1134
- return switches.filter((s) => s.available && !s.enabled).map((s) => s.id);
1021
+ return switches.filter((s) => CAMERA_SWITCH_CATALOG[s.id].countsAsSwitchedOff && s.available && !s.enabled).map((s) => s.id);
1135
1022
  }
1136
1023
  //#endregion
1137
1024
  //#region src/interfaces/device-capabilities/camera.ts
@@ -4167,6 +4054,26 @@ var EgressTranscodeRequestSchema = zod.z.object({
4167
4054
  "h264_mp4toannexb",
4168
4055
  "hevc_mp4toannexb"
4169
4056
  ]).optional(),
4057
+ /**
4058
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
4059
+ * consumer to dial the returned url. The broker picks the id and returns it
4060
+ * as `camStreamId` — a caller-supplied one would be circular, since the
4061
+ * sharing key is computed FROM this request.
4062
+ *
4063
+ * The url is still returned and still the contract for a transcode pinned to
4064
+ * another node. But dialling it locally costs an RTSP round trip that changes
4065
+ * the transport underneath the consumer: a dialled stream is an RTP source,
4066
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
4067
+ * repacketizer branch. The push branch — the one the derived mechanism has
4068
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
4069
+ * arriving, key frame arriving, black screen, on a chain healthy at every
4070
+ * other point.
4071
+ *
4072
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
4073
+ * gives priority to `hub-in-process` so a local call never leaves the node.
4074
+ * This is that rule for media.
4075
+ */
4076
+ publishLocally: zod.z.boolean().optional(),
4170
4077
  pixelFormat: zod.z.enum(["yuv420p", "nv12"]).optional(),
4171
4078
  /**
4172
4079
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -4211,7 +4118,13 @@ var EgressTranscodeSchema = zod.z.object({
4211
4118
  * Returned rather than assumed: a consumer that asked for hardware and got
4212
4119
  * software needs to be able to see that without reading the broker's logs.
4213
4120
  */
4214
- decodeHwAccel: zod.z.string().nullable()
4121
+ decodeHwAccel: zod.z.string().nullable(),
4122
+ /**
4123
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
4124
+ * `url`, and the session takes the push/deframe transport rather than the
4125
+ * RTP-passthrough one. `null` means the consumer must dial.
4126
+ */
4127
+ camStreamId: zod.z.string().nullable()
4215
4128
  });
4216
4129
  var streamBrokerCapability = {
4217
4130
  name: "stream-broker",
@@ -4505,7 +4418,44 @@ var streamBrokerCapability = {
4505
4418
  kind: "mutation",
4506
4419
  auth: "admin"
4507
4420
  }),
4508
- isRtspEnabled: require_sleep.method(zod.z.object({ brokerId: zod.z.string() }), zod.z.boolean())
4421
+ isRtspEnabled: require_sleep.method(zod.z.object({ brokerId: zod.z.string() }), zod.z.boolean()),
4422
+ /**
4423
+ * ── Per-device audio-plane policy (D83) ───────────────────────────
4424
+ *
4425
+ * The BROKER-side mute: while `muted`, this node distributes none of
4426
+ * the device's audio, on any plane it serves — live (WebRTC / encoded
4427
+ * subscribers) AND recording (the RTSP restreamer the recorder pulls,
4428
+ * which additionally serves the video-only SDP so the recorder's ffmpeg
4429
+ * never declares an audio stream it will not receive).
4430
+ *
4431
+ * Keyed by `deviceId`, not `brokerId`: every one of a camera's streams
4432
+ * carries the same microphone, and a per-stream answer would let main
4433
+ * and sub disagree about whether the camera is silent.
4434
+ *
4435
+ * The state lives in the broker's existing `DeviceOverride` blob — no
4436
+ * new store — and the mute is applied to a fresh broker at creation, so
4437
+ * a restart, a re-dial or a catalog republish never un-mutes a camera.
4438
+ */
4439
+ getDeviceAudioMute: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({
4440
+ muted: zod.z.boolean(),
4441
+ /**
4442
+ * How many live non-derived brokers currently hold the mute. Purely
4443
+ * diagnostic: `muted` is the policy and is authoritative on its own
4444
+ * (it applies to brokers that do not exist yet), while this says
4445
+ * whether anything is presently being silenced.
4446
+ */
4447
+ appliedBrokers: zod.z.number().int().nonnegative()
4448
+ })),
4449
+ setDeviceAudioMute: require_sleep.method(zod.z.object({
4450
+ deviceId: zod.z.number().int(),
4451
+ muted: zod.z.boolean()
4452
+ }), zod.z.object({
4453
+ muted: zod.z.boolean(),
4454
+ appliedBrokers: zod.z.number().int().nonnegative()
4455
+ }), {
4456
+ kind: "mutation",
4457
+ auth: "admin"
4458
+ })
4509
4459
  },
4510
4460
  events: {
4511
4461
  onCamStreamDemand: require_sleep.event(zod.z.object({
@@ -12352,6 +12302,30 @@ var TrackSourceSchema = zod.z.enum([
12352
12302
  "audio"
12353
12303
  ]);
12354
12304
  /**
12305
+ * Where a track sits in the RETRAIN lifecycle (D81).
12306
+ *
12307
+ * - `none` — never marked, or un-marked. Evictable.
12308
+ * - `staging` — the operator wants this track as training material and has not
12309
+ * finished with it. **This is the only state retention holds**: the track and
12310
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
12311
+ * the device's age window.
12312
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
12313
+ * were COPIED into the retrain dataset at selection time, so the dataset no
12314
+ * longer depends on the track's media and the track becomes EVICTABLE again.
12315
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
12316
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
12317
+ *
12318
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
12319
+ * the store's filter language has only positive equality and `whereIn` — no
12320
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
12321
+ * would make the entire pre-column history immortal in one deploy.
12322
+ */
12323
+ var RetrainStatusSchema = zod.z.enum([
12324
+ "none",
12325
+ "staging",
12326
+ "trained"
12327
+ ]);
12328
+ /**
12355
12329
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
12356
12330
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
12357
12331
  * so the two surfaces cannot drift.
@@ -12361,18 +12335,31 @@ var TrackSourceSchema = zod.z.enum([
12361
12335
  * columns existed read as absent, and a consumer that needs a boolean should say
12362
12336
  * `flag === true`, not `flag !== false`.
12363
12337
  *
12364
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
12365
- * operator curation, and the behaviour they drive will be specified separately.
12366
- * In particular a `markForTrain` track is NOT pinned against retention — see
12367
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
12338
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
12339
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
12340
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
12341
+ * `trained` track reports `false` while refusing both writes. The boolean is
12342
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
12343
+ * "never marked" from "already trained" must read `retrainStatus`.
12344
+ *
12345
+ * `debug` does NOT pin; it is attention, not durability.
12368
12346
  */
12369
12347
  var TrackFlagFields = {
12370
- /** Operator marked this track as training material. */
12348
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
12349
+ * `'staging'`. */
12371
12350
  markForTrain: zod.z.boolean().optional(),
12372
12351
  /** Operator marked this track for diagnostic attention. */
12373
12352
  debug: zod.z.boolean().optional()
12374
12353
  };
12375
12354
  /**
12355
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
12356
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
12357
+ * write patch, and the status is not something the toggle sets — it is what the
12358
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
12359
+ * always present on a persisted row (the column default materialises `'none'`).
12360
+ */
12361
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
12362
+ /**
12376
12363
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
12377
12364
  * one flag can never clear the other — the toggles are independent and are
12378
12365
  * driven from three surfaces that do not know about each other.
@@ -12386,7 +12373,32 @@ var TrackFlagsPatchSchema = zod.z.object(TrackFlagFields);
12386
12373
  var TrackFlagsSchema = zod.z.object({
12387
12374
  trackId: zod.z.string(),
12388
12375
  markForTrain: zod.z.boolean(),
12389
- debug: zod.z.boolean()
12376
+ debug: zod.z.boolean(),
12377
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
12378
+ * a track row) because this shape is only ever produced by the write body,
12379
+ * which always knows it — and a surface that has just written needs to render
12380
+ * `trained` without a re-fetch. */
12381
+ retrainStatus: RetrainStatusSchema
12382
+ });
12383
+ /** Per-camera slice of a training-export estimate. */
12384
+ var TrainingExportDeviceTotalsSchema = zod.z.object({
12385
+ deviceId: zod.z.number(),
12386
+ tracks: zod.z.number().int(),
12387
+ files: zod.z.number().int(),
12388
+ bytes: zod.z.number().int()
12389
+ });
12390
+ /**
12391
+ * What a training export WOULD contain. Computed from media index rows only —
12392
+ * no blob is read to produce this.
12393
+ */
12394
+ var TrainingExportSummarySchema = zod.z.object({
12395
+ generatedAt: zod.z.number(),
12396
+ trackCount: zod.z.number().int(),
12397
+ fileCount: zod.z.number().int(),
12398
+ byteCount: zod.z.number().int(),
12399
+ /** More marked tracks exist than a single pass carries. */
12400
+ truncated: zod.z.boolean(),
12401
+ devices: zod.z.array(TrainingExportDeviceTotalsSchema).readonly()
12390
12402
  });
12391
12403
  var TrackSchema = zod.z.object({
12392
12404
  trackId: zod.z.string(),
@@ -12431,7 +12443,8 @@ var TrackSchema = zod.z.object({
12431
12443
  * Populated from the persisted envelope columns on historical reads;
12432
12444
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
12433
12445
  envelope: TrackEnvelopeSchema.optional(),
12434
- ...TrackFlagFields
12446
+ ...TrackFlagFields,
12447
+ ...TrackRetrainFields
12435
12448
  });
12436
12449
  var BaseEventFields = {
12437
12450
  id: zod.z.string(),
@@ -12653,7 +12666,8 @@ var KeyEventSchema = zod.z.object({
12653
12666
  bestEventId: zod.z.string(),
12654
12667
  /** Track lifetime in ms (lastSeen - firstSeen). */
12655
12668
  windowMs: zod.z.number().optional(),
12656
- ...TrackFlagFields
12669
+ ...TrackFlagFields,
12670
+ ...TrackRetrainFields
12657
12671
  });
12658
12672
  var TrackedDetectionSchema = zod.z.object({
12659
12673
  trackId: zod.z.string(),
@@ -13015,11 +13029,29 @@ var pipelineAnalyticsCapability = {
13015
13029
  *
13016
13030
  * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
13017
13031
  * authenticated non-admin surface and two of the three call sites are
13018
- * there. Revisit if a flag ever gains an effect that costs storage
13019
- * `deleteTracks` next door is admin for exactly that reason.
13032
+ * there. The note that used to sit here said to revisit this the day a flag
13033
+ * gained an effect that costs storage, and D81 is that day — `markForTrain`
13034
+ * now pins. It STAYS protected, and the reason is that the alternative
13035
+ * makes the feature pointless: marking a track is something you do while
13036
+ * looking at it, on the surface you were already looking at it on, and that
13037
+ * surface is the viewer. What the storage cost gets instead is a BOUND — a
13038
+ * per-device pin budget enforced in the body, refusing a new pin past the
13039
+ * limit while always allowing un-marking. `deleteTracks` next door is still
13040
+ * admin, because destroying evidence and preserving it are not symmetric.
13041
+ *
13042
+ * `markForTrain` writes the retrain LIFECYCLE, not a boolean column: `true`
13043
+ * is `none → staging`, `false` is `staging → none`. A track already
13044
+ * `trained` refuses BOTH — its frames are copies inside the retrain dataset
13045
+ * and re-staging it from a generic toggle is how the same material gets
13046
+ * annotated twice under two ground truths. Returning a trained track to
13047
+ * staging is a deliberate action of the retrain page, which is also the only
13048
+ * thing that produces `trained` in the first place.
13020
13049
  *
13021
- * Returns the RESOLVED state of both flags (absent → `false`) so a caller
13022
- * can drive its toggle without a re-fetch. Rejects an unknown track.
13050
+ * Returns the RESOLVED state of both flags (absent → `false`) plus the
13051
+ * `retrainStatus` they were derived from, so a caller can drive its toggle
13052
+ * — and render a `trained` badge — without a re-fetch. Rejects an unknown
13053
+ * track, a new staging mark on a device already holding its full budget, and
13054
+ * any `markForTrain` write against a trained track.
13023
13055
  */
13024
13056
  setTrackFlags: require_sleep.method(zod.z.object({
13025
13057
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -13083,6 +13115,42 @@ var pipelineAnalyticsCapability = {
13083
13115
  kind: "query",
13084
13116
  auth: "admin"
13085
13117
  }),
13118
+ /**
13119
+ * The CHEAP QUESTION, asked before any media moves: how big is the dataset
13120
+ * the marked (`markForTrain`) tracks would produce?
13121
+ *
13122
+ * Answered from media INDEX rows only — key, kind, size, timestamp — so it
13123
+ * costs ~2 KB of reads per track and no blob reads at all. The measured harm
13124
+ * behind D56 was a bulk pass that read and base64'd every blob a track owned
13125
+ * before deciding anything, taking hub-main to 82 s busy out of 120; an
13126
+ * export is that same I/O shape, so it inherits the same discipline: know
13127
+ * the size, then decide.
13128
+ *
13129
+ * `truncated` reports that more marked tracks exist than one pass carries.
13130
+ * Empty `deviceIds` ⇒ every device that has marked tracks.
13131
+ */
13132
+ getTrainingExportSummary: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), TrainingExportSummarySchema, {
13133
+ kind: "query",
13134
+ auth: "admin"
13135
+ }),
13136
+ /**
13137
+ * Where to download the dataset archive.
13138
+ *
13139
+ * The BYTES do not come back through this cap — they come from the returned
13140
+ * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
13141
+ * archive base64'd through a unary RPC envelope would be held whole in
13142
+ * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
13143
+ * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
13144
+ * precedent, and this follows it deliberately.
13145
+ *
13146
+ * The archive contains a `manifest.json` FIRST, then the stored media
13147
+ * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
13148
+ * model is run: a training set's pixels must be the pixels the pipeline saw.
13149
+ */
13150
+ getTrainingExportUrl: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), zod.z.object({ url: zod.z.string() }), {
13151
+ kind: "query",
13152
+ auth: "admin"
13153
+ }),
13086
13154
  getEventMedia: require_sleep.method(zod.z.object({
13087
13155
  eventId: zod.z.string(),
13088
13156
  kind: MediaFileKindEnum.optional()
@@ -15863,9 +15931,15 @@ var snapshotCapability = {
15863
15931
  * Bypass the cache freshness check and fetch directly from the
15864
15932
  * native (or stream-broker fallback). Triggered by the UI's
15865
15933
  * "refresh" button so an operator can force a fresh frame
15866
- * even when the cache is well within `snapshotMaxAgeMs`.
15867
- * On battery cams this WILL wake the camera — accept the
15868
- * cost only when the user explicitly asks for it.
15934
+ * even when the cache is well within the device's
15935
+ * `snapshotMaxAgeS` window.
15936
+ *
15937
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
15938
+ * battery camera it is the one thing that walks past the wrapper's
15939
+ * sleep gate and wakes the camera, so a background caller — a poller,
15940
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
15941
+ * gets the cached frame, which on a sleeping battery camera is the
15942
+ * correct answer: stale but honest beats woken.
15869
15943
  */
15870
15944
  force: zod.z.boolean().optional()
15871
15945
  }), SnapshotImageSchema.nullable()),
@@ -21841,6 +21915,216 @@ setOverlay: require_sleep.method(zod.z.object({
21841
21915
  }] }
21842
21916
  };
21843
21917
  //#endregion
21918
+ //#region src/capabilities/osd-manager.cap.ts
21919
+ /**
21920
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
21921
+ *
21922
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
21923
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
21924
+ * comes from, and it must not — a driver that grew a "show the temperature
21925
+ * here" feature would grow it once per vendor.
21926
+ *
21927
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
21928
+ * which value feeds the slot, how it is formatted, and under which
21929
+ * conditions it is shown at all. One addon renders every binding on every
21930
+ * camera, so a new source costs zero driver code.
21931
+ *
21932
+ * Three deliberate choices, each with a rejected alternative:
21933
+ *
21934
+ * 1. A source is `(capName, valuePath)` over the kernel's device
21935
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
21936
+ * cap-keyed slice a device publishes is bindable the day the cap
21937
+ * ships. The rejected alternative (one enum member per source, with
21938
+ * a resolver branch each) is what makes "add the humidity too" a
21939
+ * code change.
21940
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
21941
+ * notification centre's condition vocabulary — rather than a parallel
21942
+ * model. An operator who has learned one condition editor has learned
21943
+ * both.
21944
+ * 3. Because the renderer's facts are device STATE and not a detection
21945
+ * record, only a SUBSET of that vocabulary can be answered here.
21946
+ * `setSlotBinding` REJECTS the rest at write time (see
21947
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
21948
+ * condition that can never be true renders a permanently blank
21949
+ * overlay, and a blank overlay looks exactly like a broken camera.
21950
+ */
21951
+ /** Where a slot's value comes from. */
21952
+ var OsdSourceSchema = zod.z.discriminatedUnion("kind", [
21953
+ zod.z.object({
21954
+ kind: zod.z.literal("static"),
21955
+ text: zod.z.string().max(64)
21956
+ }),
21957
+ zod.z.object({
21958
+ kind: zod.z.literal("clock"),
21959
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
21960
+ pattern: zod.z.string().min(1).max(32).default("HH:mm"),
21961
+ /** IANA zone. Omitted = the server's zone. */
21962
+ timezone: zod.z.string().min(1).max(64).optional()
21963
+ }),
21964
+ zod.z.object({
21965
+ kind: zod.z.literal("device-state"),
21966
+ deviceId: zod.z.number().int().optional(),
21967
+ capName: zod.z.string().min(1).max(64),
21968
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
21969
+ valuePath: zod.z.string().min(1).max(64)
21970
+ })
21971
+ ]);
21972
+ var OsdSlotBindingSchema = zod.z.object({
21973
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
21974
+ enabled: zod.z.boolean().default(true),
21975
+ source: OsdSourceSchema,
21976
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
21977
+ template: zod.z.string().max(96).default("${value}"),
21978
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
21979
+ maxCharacters: zod.z.number().int().min(4).max(64).optional(),
21980
+ /**
21981
+ * Decimal places for a numeric value. `0` yields an integer — the
21982
+ * documented workaround for firmwares that reject `.` in overlay text.
21983
+ */
21984
+ maxDecimals: zod.z.number().int().min(0).max(4).default(1),
21985
+ /** Appended via `${unit}`. The state mirror does not carry units. */
21986
+ unitLabel: zod.z.string().max(8).optional(),
21987
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
21988
+ valueMap: zod.z.record(zod.z.string(), zod.z.string()).optional(),
21989
+ /** Time windows in which the slot is shown. Absent = always. */
21990
+ schedule: NcScheduleSchema.optional(),
21991
+ /**
21992
+ * Display gate, in the notification centre's condition vocabulary.
21993
+ * Only the keys reported by `getConditionSupport` are accepted.
21994
+ */
21995
+ conditions: NcConditionsSchema.optional(),
21996
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
21997
+ fallbackText: zod.z.string().max(64).default("")
21998
+ });
21999
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
22000
+ var OsdSlotViewSchema = zod.z.object({
22001
+ slotId: zod.z.string(),
22002
+ kind: OsdOverlayKindEnum,
22003
+ /** Firmware refuses text edits (a timestamp, the channel name). */
22004
+ readOnly: zod.z.boolean(),
22005
+ cameraEnabled: zod.z.boolean(),
22006
+ cameraText: zod.z.string().optional(),
22007
+ binding: OsdSlotBindingSchema.nullable()
22008
+ });
22009
+ /**
22010
+ * What happened to one slot on one render pass. `unchanged` exists so the
22011
+ * operator can tell "we are driving this and the value is steady" from
22012
+ * "we never got there" — and so the loop can prove it is not rewriting
22013
+ * identical text to the camera every tick.
22014
+ */
22015
+ var OsdRenderOutcomeEnum = zod.z.enum([
22016
+ "written",
22017
+ "unchanged",
22018
+ "gated",
22019
+ "unreadable",
22020
+ "disabled",
22021
+ "unbound",
22022
+ "failed"
22023
+ ]);
22024
+ var OsdRenderResultSchema = zod.z.object({
22025
+ slotId: zod.z.string(),
22026
+ outcome: OsdRenderOutcomeEnum,
22027
+ /** The text the slot should carry. Empty = the slot is switched off. */
22028
+ text: zod.z.string(),
22029
+ /** Why, whenever the outcome is not a plain write. Never silent. */
22030
+ reason: zod.z.string().optional()
22031
+ });
22032
+ var OsdSourceValueTypeEnum = zod.z.enum([
22033
+ "number",
22034
+ "boolean",
22035
+ "string",
22036
+ "enum"
22037
+ ]);
22038
+ /**
22039
+ * One bindable value, derived from a cap's `runtimeState` schema — never
22040
+ * hand-listed. The editor renders from this, so a cap that ships a new
22041
+ * state field becomes bindable with no UI change.
22042
+ */
22043
+ var OsdSourceOptionSchema = zod.z.object({
22044
+ deviceId: zod.z.number().int(),
22045
+ deviceName: zod.z.string(),
22046
+ capName: zod.z.string(),
22047
+ valuePath: zod.z.string(),
22048
+ label: zod.z.string(),
22049
+ valueType: OsdSourceValueTypeEnum,
22050
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
22051
+ enumValues: zod.z.array(zod.z.string()).readonly().optional()
22052
+ });
22053
+ var osdManagerCapability = {
22054
+ name: "osd-manager",
22055
+ scope: "system",
22056
+ mode: "singleton",
22057
+ methods: {
22058
+ /**
22059
+ * The camera's slots, each with its binding. `supported: false` when
22060
+ * the camera publishes no `osd` cap — the page then says so instead
22061
+ * of rendering an empty list that reads as "no overlays configured".
22062
+ */
22063
+ getDeviceOsd: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({
22064
+ supported: zod.z.boolean(),
22065
+ slots: zod.z.array(OsdSlotViewSchema)
22066
+ }), { auth: "admin" }),
22067
+ /**
22068
+ * Every value bindable on this camera: its own state leaves first,
22069
+ * then every other device's. Derived from the cap definitions, so it
22070
+ * cannot drift from what the resolver can actually read.
22071
+ */
22072
+ getSourceCatalog: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({ sources: zod.z.array(OsdSourceOptionSchema) }), { auth: "admin" }),
22073
+ /**
22074
+ * The condition ids this gate can answer, alongside the FULL notification
22075
+ * condition catalog. Both, in one call, on purpose: an editor that showed
22076
+ * the catalog without the supported set would offer conditions that
22077
+ * `setSlotBinding` then rejects.
22078
+ */
22079
+ getConditionSupport: require_sleep.method(zod.z.object({}), zod.z.object({
22080
+ supported: zod.z.array(zod.z.string()),
22081
+ catalog: zod.z.array(NcConditionDescriptorSchema)
22082
+ }), { auth: "admin" }),
22083
+ /**
22084
+ * Persist a binding and render it immediately, returning the slot as
22085
+ * it now stands. Throws when the binding names an unsupported
22086
+ * condition, an unknown cap, or a value path that cap does not have.
22087
+ */
22088
+ setSlotBinding: require_sleep.method(zod.z.object({
22089
+ deviceId: zod.z.number().int(),
22090
+ slotId: zod.z.string().min(1),
22091
+ binding: OsdSlotBindingSchema
22092
+ }), zod.z.object({
22093
+ slot: OsdSlotViewSchema,
22094
+ render: OsdRenderResultSchema
22095
+ }), {
22096
+ kind: "mutation",
22097
+ auth: "admin"
22098
+ }),
22099
+ /** Forget the binding. The slot keeps whatever text it last carried. */
22100
+ clearSlotBinding: require_sleep.method(zod.z.object({
22101
+ deviceId: zod.z.number().int(),
22102
+ slotId: zod.z.string().min(1)
22103
+ }), zod.z.object({ success: zod.z.literal(true) }), {
22104
+ kind: "mutation",
22105
+ auth: "admin"
22106
+ }),
22107
+ /**
22108
+ * Render without writing. `binding` overrides the stored one so an
22109
+ * editor can preview an unsaved change. Mutation kind only to carry
22110
+ * the binding object safely; no side effects.
22111
+ */
22112
+ previewSlot: require_sleep.method(zod.z.object({
22113
+ deviceId: zod.z.number().int(),
22114
+ slotId: zod.z.string().min(1),
22115
+ binding: OsdSlotBindingSchema.optional()
22116
+ }), OsdRenderResultSchema, {
22117
+ kind: "mutation",
22118
+ auth: "admin"
22119
+ }),
22120
+ /** Force a render pass over one camera now, instead of at the next tick. */
22121
+ renderDevice: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({ results: zod.z.array(OsdRenderResultSchema) }), {
22122
+ kind: "mutation",
22123
+ auth: "admin"
22124
+ })
22125
+ }
22126
+ };
22127
+ //#endregion
21844
22128
  //#region src/capabilities/pet-feeder.cap.ts
21845
22129
  /**
21846
22130
  * PetKit pet-feeder cap. Models the control + telemetry surface of a
@@ -22488,12 +22772,30 @@ var pressureSensorCapability = {
22488
22772
  //#endregion
22489
22773
  //#region src/capabilities/privacy-mask.cap.ts
22490
22774
  /**
22491
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
22492
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
22493
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
22494
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
22495
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
22496
- * normalized 0..1 (top-left origin).
22775
+ * PRIVACY what the camera deliberately does not capture. Two planes:
22776
+ *
22777
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
22778
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
22779
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
22780
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
22781
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
22782
+ * normalized 0..1 (top-left origin).
22783
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
22784
+ * camera encoding an audio track at all, so EVERY consumer — live view,
22785
+ * recording, the audio analyzer, an export — sees silent video. There is
22786
+ * no server-side copy of this fact; the camera is the store and every read
22787
+ * is a read-through, which is why a switch over it cannot drift
22788
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
22789
+ *
22790
+ * Both belong here for one reason: they are the two things an operator turns
22791
+ * off when the answer to "what is this camera allowed to record" changes, and
22792
+ * both are applied ON the device, before anything leaves it.
22793
+ *
22794
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
22795
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
22796
+ * by one provider — and it was removed when this landed. A second writer onto
22797
+ * one device register is the shape of every knob this repo has shipped that
22798
+ * disagreed with the one the reader read.
22497
22799
  */
22498
22800
  /** A privacy-mask region's geometry — rectangle or free polygon. */
22499
22801
  var PrivacyMaskShapeSchema = zod.z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -22505,21 +22807,45 @@ var PrivacyMaskRegionSchema = zod.z.object({
22505
22807
  enabled: zod.z.boolean(),
22506
22808
  shape: PrivacyMaskShapeSchema
22507
22809
  });
22508
- /** Current on-camera privacy-mask state — master enable + zones. */
22810
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
22509
22811
  var PrivacyMaskStatusSchema = zod.z.object({
22510
22812
  enabled: zod.z.boolean(),
22511
22813
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
22512
22814
  regions: zod.z.array(PrivacyMaskRegionSchema),
22815
+ /**
22816
+ * Is the camera capturing sound right now? Read from the camera, never from
22817
+ * a server-side mirror.
22818
+ *
22819
+ * `null` means "no answer" — either this camera exposes no controllable
22820
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
22821
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
22822
+ * "the microphone is off" and "we could not ask" look identical to an
22823
+ * operator only until one of them is wrong.
22824
+ *
22825
+ * On a camera whose profiles carry the flag independently (Reolink writes
22826
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
22827
+ * privacy is only satisfied when every one of them is silent.
22828
+ */
22829
+ audioEnabled: zod.z.boolean().nullable(),
22513
22830
  lastFetchedAt: zod.z.number()
22514
22831
  });
22515
- /** Per-camera availability. */
22832
+ /** Per-camera availability. Probed, never assumed from the model name. */
22516
22833
  var PrivacyMaskOptionsSchema = zod.z.object({
22517
22834
  /** Maximum number of supported zones. */
22518
22835
  maxRegions: zod.z.number(),
22519
22836
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
22520
22837
  supportedShapes: zod.z.array(MaskShapeKindSchema),
22521
22838
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
22522
- polygonVertices: MaskPolygonVerticesSchema.optional()
22839
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
22840
+ /**
22841
+ * Does this camera expose a microphone switch we can actually write?
22842
+ *
22843
+ * Camera-probed: `true` only when the firmware answered with an audio flag
22844
+ * we know how to patch. A camera that never answered is `false` — a control
22845
+ * the operator can press that changes nothing is worse than no control, and
22846
+ * the switch group renders "not available" instead.
22847
+ */
22848
+ supportsAudioMute: zod.z.boolean()
22523
22849
  });
22524
22850
  /** Partial change — every field optional. */
22525
22851
  var PrivacyMaskPatchSchema = zod.z.object({
@@ -22546,6 +22872,27 @@ var privacyMaskCapability = {
22546
22872
  }), zod.z.void(), {
22547
22873
  kind: "mutation",
22548
22874
  auth: "admin"
22875
+ }),
22876
+ /**
22877
+ * Turn the camera's microphone on or off, at the camera.
22878
+ *
22879
+ * Deliberately its OWN mutation rather than a field on
22880
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
22881
+ * mask master switch", and overloading it would make one boolean mean two
22882
+ * unrelated things on the same call. It is also the only method here whose
22883
+ * write leaves the device in a state a later `getStatus` reads back
22884
+ * verbatim, which is what makes it safe as a switch authority.
22885
+ *
22886
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
22887
+ * this rather than silently accept it — a write nothing applies is exactly
22888
+ * what the switch group exists to remove.
22889
+ */
22890
+ setAudioEnabled: require_sleep.method(zod.z.object({
22891
+ deviceId: zod.z.number(),
22892
+ enabled: zod.z.boolean()
22893
+ }), zod.z.void(), {
22894
+ kind: "mutation",
22895
+ auth: "admin"
22549
22896
  })
22550
22897
  },
22551
22898
  status: {
@@ -22554,6 +22901,26 @@ var privacyMaskCapability = {
22554
22901
  },
22555
22902
  runtimeState: PrivacyMaskStatusSchema
22556
22903
  };
22904
+ /**
22905
+ * Collapse a camera's PER-PROFILE audio flags into the one answer
22906
+ * {@link PrivacyMaskStatusSchema.shape.audioEnabled} promises.
22907
+ *
22908
+ * Both firmwares this cap talks to store the flag per stream profile, and
22909
+ * both let those profiles disagree. The rule is `some`, not `every`: privacy
22910
+ * is only satisfied when NOTHING is carrying sound, so a camera whose sub
22911
+ * stream is still audible must read as `true` and be switchable off — not as
22912
+ * `false` because the main stream happens to be muted already.
22913
+ *
22914
+ * An empty list is `null` ("this camera reported no audio flag at all"),
22915
+ * never `false`.
22916
+ *
22917
+ * Lives here rather than in each provider so the rule the schema documents
22918
+ * and the rule the providers apply cannot drift apart.
22919
+ */
22920
+ function summarisePrivacyAudio(profiles) {
22921
+ if (profiles.length === 0) return null;
22922
+ return profiles.some((p) => p.audioEnabled);
22923
+ }
22557
22924
  //#endregion
22558
22925
  //#region src/capabilities/ptz.cap.ts
22559
22926
  var PtzPresetSchema = zod.z.object({
@@ -22938,6 +23305,21 @@ var LocateSegmentResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
22938
23305
  })]);
22939
23306
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22940
23307
  var ReadSegmentBytesResultSchema = zod.z.object({ data: zod.z.instanceof(Uint8Array) });
23308
+ /**
23309
+ * One GOP of a finalized segment, cut by byte range through the segment's own
23310
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
23311
+ * the single `moof`+`mdat` covering the requested instant — standalone-
23312
+ * demuxable, never the whole file. When the segment's index cannot be parsed
23313
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
23314
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
23315
+ */
23316
+ var ReadGopBytesResultSchema = zod.z.object({
23317
+ data: zod.z.instanceof(Uint8Array),
23318
+ /** Absolute epoch ms of the returned fragment's first sample. */
23319
+ gopStartMs: zod.z.number(),
23320
+ /** Media ms the returned fragment covers. */
23321
+ gopDurMs: zod.z.number()
23322
+ });
22941
23323
  var recordingCapability = {
22942
23324
  name: "recording",
22943
23325
  scope: "system",
@@ -23005,6 +23387,18 @@ var recordingCapability = {
23005
23387
  kind: "query",
23006
23388
  auth: "admin"
23007
23389
  }),
23390
+ /** Read the single GOP of segment `startMs` covering `epochMs`, by mfra
23391
+ * byte range — the scrub-granular read (D31 letter on the feeder path).
23392
+ * See {@link ReadGopBytesResultSchema} for the degradation contract. */
23393
+ readGopBytes: require_sleep.method(zod.z.object({
23394
+ deviceId: zod.z.number(),
23395
+ profile: zod.z.string(),
23396
+ startMs: zod.z.number(),
23397
+ epochMs: zod.z.number()
23398
+ }), ReadGopBytesResultSchema, {
23399
+ kind: "query",
23400
+ auth: "admin"
23401
+ }),
23008
23402
  setDeviceConfig: require_sleep.method(zod.z.object({
23009
23403
  deviceId: zod.z.number(),
23010
23404
  config: RecordingConfigSchema
@@ -23666,6 +24060,16 @@ var StreamProfileConfigSchema = zod.z.object({
23666
24060
  "baseline"
23667
24061
  ]).optional(),
23668
24062
  gop: zod.z.number().optional(),
24063
+ /**
24064
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
24065
+ *
24066
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
24067
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
24068
+ * writes every profile at once so "audio off" means silent everywhere. A
24069
+ * per-profile writer beside it would let a camera be half-muted and would be
24070
+ * a second knob onto one device register — the failure D62 exists to
24071
+ * prevent. Absent when the firmware does not report the flag.
24072
+ */
23669
24073
  audio: zod.z.boolean().optional()
23670
24074
  });
23671
24075
  var StreamParamsStatusSchema = zod.z.object({
@@ -23706,7 +24110,13 @@ var StreamParamsOptionsSchema = zod.z.object({
23706
24110
  ext: StreamProfileOptionsSchema.optional()
23707
24111
  });
23708
24112
  /** A partial change to one profile — every field optional; a provider
23709
- * ignores fields it doesn't support. */
24113
+ * ignores fields it doesn't support.
24114
+ *
24115
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
24116
+ * reachable from no form and honoured by exactly one provider, while the
24117
+ * camera's microphone is a whole-device fact. It now has one writer,
24118
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
24119
+ * `privacy-mask.cap.ts`. */
23710
24120
  var StreamProfilePatchSchema = zod.z.object({
23711
24121
  width: zod.z.number().optional(),
23712
24122
  height: zod.z.number().optional(),
@@ -23719,8 +24129,7 @@ var StreamProfilePatchSchema = zod.z.object({
23719
24129
  "main",
23720
24130
  "baseline"
23721
24131
  ]).optional(),
23722
- gop: zod.z.number().optional(),
23723
- audio: zod.z.boolean().optional()
24132
+ gop: zod.z.number().optional()
23724
24133
  });
23725
24134
  var streamParamsCapability = {
23726
24135
  name: "stream-params",
@@ -26639,10 +27048,7 @@ var BATTERY_DEVICE_PROFILE = {
26639
27048
  audioMode: "disabled",
26640
27049
  detectionMode: "on-motion"
26641
27050
  },
26642
- settings: {
26643
- "snapshot.minRefreshIntervalSec": 3600,
26644
- "streamBroker.preBufferEnabled": false
26645
- }
27051
+ settings: {}
26646
27052
  };
26647
27053
  /**
26648
27054
  * Profile registry — order matters when multiple profiles match the
@@ -27494,7 +27900,7 @@ var SystemMirror = class {
27494
27900
  }
27495
27901
  async refreshDeviceMetadata(deviceId, kind) {
27496
27902
  try {
27497
- const info = (await this.api.deviceManager.listAll.query({})).find((d) => d.id === deviceId);
27903
+ const info = await this.api.deviceManager.getDevice.query({ deviceId });
27498
27904
  if (!info) return;
27499
27905
  const wasNew = !this.devices.has(deviceId);
27500
27906
  this.devices.set(deviceId, info);
@@ -27806,6 +28212,7 @@ var CAPABILITY_NAMES = {
27806
28212
  numericSensor: "numeric-sensor",
27807
28213
  oauthIntegration: "oauth-integration",
27808
28214
  osd: "osd",
28215
+ osdManager: "osd-manager",
27809
28216
  petFeeder: "pet-feeder",
27810
28217
  pipelineAnalytics: "pipeline-analytics",
27811
28218
  pipelineExecutor: "pipeline-executor",
@@ -28237,6 +28644,10 @@ var CAPABILITY_ROUTER_KEYS = [
28237
28644
  key: "osd",
28238
28645
  name: "osd"
28239
28646
  },
28647
+ {
28648
+ key: "osdManager",
28649
+ name: "osd-manager"
28650
+ },
28240
28651
  {
28241
28652
  key: "petFeeder",
28242
28653
  name: "pet-feeder"
@@ -28550,6 +28961,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
28550
28961
  numericSensorCapability,
28551
28962
  oauthIntegrationCapability,
28552
28963
  osdCapability,
28964
+ osdManagerCapability,
28553
28965
  petFeederCapability,
28554
28966
  pipelineAnalyticsCapability,
28555
28967
  pipelineExecutorCapability,
@@ -31468,6 +31880,48 @@ var METHOD_ACCESS_MAP = Object.freeze({
31468
31880
  addonId: null,
31469
31881
  access: "create"
31470
31882
  },
31883
+ "osdManager.clearSlotBinding": {
31884
+ capName: "osd-manager",
31885
+ capScope: "system",
31886
+ addonId: null,
31887
+ access: "delete"
31888
+ },
31889
+ "osdManager.getConditionSupport": {
31890
+ capName: "osd-manager",
31891
+ capScope: "system",
31892
+ addonId: null,
31893
+ access: "view"
31894
+ },
31895
+ "osdManager.getDeviceOsd": {
31896
+ capName: "osd-manager",
31897
+ capScope: "system",
31898
+ addonId: null,
31899
+ access: "view"
31900
+ },
31901
+ "osdManager.getSourceCatalog": {
31902
+ capName: "osd-manager",
31903
+ capScope: "system",
31904
+ addonId: null,
31905
+ access: "view"
31906
+ },
31907
+ "osdManager.previewSlot": {
31908
+ capName: "osd-manager",
31909
+ capScope: "system",
31910
+ addonId: null,
31911
+ access: "create"
31912
+ },
31913
+ "osdManager.renderDevice": {
31914
+ capName: "osd-manager",
31915
+ capScope: "system",
31916
+ addonId: null,
31917
+ access: "create"
31918
+ },
31919
+ "osdManager.setSlotBinding": {
31920
+ capName: "osd-manager",
31921
+ capScope: "system",
31922
+ addonId: null,
31923
+ access: "create"
31924
+ },
31471
31925
  "petFeeder.callPet": {
31472
31926
  capName: "pet-feeder",
31473
31927
  capScope: "device",
@@ -31630,6 +32084,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
31630
32084
  addonId: null,
31631
32085
  access: "view"
31632
32086
  },
32087
+ "pipelineAnalytics.getTrainingExportSummary": {
32088
+ capName: "pipeline-analytics",
32089
+ capScope: "device",
32090
+ addonId: null,
32091
+ access: "view"
32092
+ },
32093
+ "pipelineAnalytics.getTrainingExportUrl": {
32094
+ capName: "pipeline-analytics",
32095
+ capScope: "device",
32096
+ addonId: null,
32097
+ access: "view"
32098
+ },
31633
32099
  "pipelineAnalytics.listEventKinds": {
31634
32100
  capName: "pipeline-analytics",
31635
32101
  capScope: "device",
@@ -32386,6 +32852,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
32386
32852
  addonId: null,
32387
32853
  access: "view"
32388
32854
  },
32855
+ "privacyMask.setAudioEnabled": {
32856
+ capName: "privacy-mask",
32857
+ capScope: "device",
32858
+ addonId: null,
32859
+ access: "create"
32860
+ },
32389
32861
  "privacyMask.setMask": {
32390
32862
  capName: "privacy-mask",
32391
32863
  capScope: "device",
@@ -32554,6 +33026,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
32554
33026
  addonId: null,
32555
33027
  access: "create"
32556
33028
  },
33029
+ "recording.readGopBytes": {
33030
+ capName: "recording",
33031
+ capScope: "system",
33032
+ addonId: null,
33033
+ access: "view"
33034
+ },
32557
33035
  "recording.readSegmentBytes": {
32558
33036
  capName: "recording",
32559
33037
  capScope: "system",
@@ -33094,6 +33572,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33094
33572
  addonId: null,
33095
33573
  access: "view"
33096
33574
  },
33575
+ "streamBroker.getDeviceAudioMute": {
33576
+ capName: "stream-broker",
33577
+ capScope: "system",
33578
+ addonId: null,
33579
+ access: "view"
33580
+ },
33097
33581
  "streamBroker.getPreBufferInfo": {
33098
33582
  capName: "stream-broker",
33099
33583
  capScope: "system",
@@ -33214,6 +33698,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33214
33698
  addonId: null,
33215
33699
  access: "create"
33216
33700
  },
33701
+ "streamBroker.setDeviceAudioMute": {
33702
+ capName: "stream-broker",
33703
+ capScope: "system",
33704
+ addonId: null,
33705
+ access: "create"
33706
+ },
33217
33707
  "streamBroker.setPreBufferDuration": {
33218
33708
  capName: "stream-broker",
33219
33709
  capScope: "system",
@@ -33929,6 +34419,7 @@ var KNOWN_CAP_NAMES = [
33929
34419
  "notifier",
33930
34420
  "oauth-integration",
33931
34421
  "osd",
34422
+ "osd-manager",
33932
34423
  "pet-feeder",
33933
34424
  "pipeline-analytics",
33934
34425
  "pipeline-executor",
@@ -34076,6 +34567,7 @@ var SYSTEM_CAP_NAMES = [
34076
34567
  "notification-output",
34077
34568
  "notification-rules",
34078
34569
  "oauth-integration",
34570
+ "osd-manager",
34079
34571
  "pipeline-executor",
34080
34572
  "pipeline-orchestrator",
34081
34573
  "pipeline-runner",
@@ -34534,6 +35026,7 @@ function createSystemProxy(api) {
34534
35026
  deleteTarget: (input) => dispatch("notificationOutput", "deleteTarget", "mutation", input),
34535
35027
  setTargetEnabled: (input) => dispatch("notificationOutput", "setTargetEnabled", "mutation", input)
34536
35028
  },
35029
+ osdManager: { getConditionSupport: (input) => dispatch("osdManager", "getConditionSupport", "query", input) },
34537
35030
  pipelineExecutor: {
34538
35031
  getAvailableEngines: (input) => dispatch("pipelineExecutor", "getAvailableEngines", "query", input),
34539
35032
  getSelectedEngine: (input) => dispatch("pipelineExecutor", "getSelectedEngine", "query", input),
@@ -35075,6 +35568,68 @@ function prepareNotification(caps, n) {
35075
35568
  };
35076
35569
  }
35077
35570
  //#endregion
35571
+ //#region src/notification/schedule.ts
35572
+ var WEEKDAY_TO_DAY = {
35573
+ Sun: 0,
35574
+ Mon: 1,
35575
+ Tue: 2,
35576
+ Wed: 3,
35577
+ Thu: 4,
35578
+ Fri: 5,
35579
+ Sat: 6
35580
+ };
35581
+ /** Resolve (weekday, minute-of-day) of `atMs` in the schedule's timezone.
35582
+ * An invalid/unknown IANA name falls back to the host timezone. */
35583
+ function localDayMinute(atMs, timezone) {
35584
+ const d = new Date(atMs);
35585
+ if (timezone !== void 0) try {
35586
+ const parts = new Intl.DateTimeFormat("en-US", {
35587
+ timeZone: timezone,
35588
+ weekday: "short",
35589
+ hour: "numeric",
35590
+ minute: "numeric",
35591
+ hourCycle: "h23"
35592
+ }).formatToParts(d);
35593
+ let weekday;
35594
+ let hour;
35595
+ let minute;
35596
+ for (const p of parts) if (p.type === "weekday") weekday = p.value;
35597
+ else if (p.type === "hour") hour = Number(p.value);
35598
+ else if (p.type === "minute") minute = Number(p.value);
35599
+ const day = weekday !== void 0 ? WEEKDAY_TO_DAY[weekday] : void 0;
35600
+ if (day !== void 0 && hour !== void 0 && minute !== void 0) return {
35601
+ day,
35602
+ minute: hour * 60 + minute
35603
+ };
35604
+ } catch {}
35605
+ return {
35606
+ day: d.getDay(),
35607
+ minute: d.getHours() * 60 + d.getMinutes()
35608
+ };
35609
+ }
35610
+ /**
35611
+ * Is the schedule active at `atMs`? No schedule = always active. Windows
35612
+ * are OR'd; a window with `startMinute > endMinute` crosses midnight (it
35613
+ * starts on a listed day and spills into the next). `invert` flips the
35614
+ * result (active OUTSIDE the windows).
35615
+ */
35616
+ function isScheduleActive(schedule, atMs) {
35617
+ if (schedule === void 0) return true;
35618
+ const { day, minute } = localDayMinute(atMs, schedule.timezone);
35619
+ const prevDay = (day + 6) % 7;
35620
+ let inside = false;
35621
+ for (const w of schedule.windows) if (w.startMinute <= w.endMinute) {
35622
+ if (w.days.includes(day) && minute >= w.startMinute && minute < w.endMinute) {
35623
+ inside = true;
35624
+ break;
35625
+ }
35626
+ } else if (w.days.includes(day) && minute >= w.startMinute || w.days.includes(prevDay) && minute < w.endMinute) {
35627
+ inside = true;
35628
+ break;
35629
+ }
35630
+ return schedule.invert === true ? !inside : inside;
35631
+ }
35632
+ //#endregion
35078
35633
  //#region src/notification/timelapse-rule.ts
35079
35634
  /**
35080
35635
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
@@ -35572,6 +36127,44 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
35572
36127
  return best;
35573
36128
  }
35574
36129
  //#endregion
36130
+ //#region src/utils/addon-id.ts
36131
+ /**
36132
+ * The addon id as the REGISTRY, the wire and the durable stores spell it.
36133
+ *
36134
+ * `AddonContext.id` does not agree with itself across the two context
36135
+ * factories:
36136
+ *
36137
+ * - a FORKED addon gets the bare manifest id
36138
+ * (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
36139
+ * - an addon co-located in hub-main gets it PREFIXED
36140
+ * (`server/backend/src/core/addon/addon-registry.service.ts` →
36141
+ * ``id: `addon:${addonId}` ``).
36142
+ *
36143
+ * Everything an addon might compare `ctx.id` AGAINST carries the bare form:
36144
+ * `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
36145
+ * `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
36146
+ *
36147
+ * So `entry.providerAddonId === this.ctx.id` is silently, permanently false
36148
+ * for a builtin — and only for a builtin, which is why it survives a green
36149
+ * suite whose fake supplies the bare id. That is exactly how camera 615's
36150
+ * virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
36151
+ * binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
36152
+ *
36153
+ * Route every comparison between `ctx.id` and a registry/store addon id
36154
+ * through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
36155
+ * enforces it in the processes where the prefix exists.
36156
+ */
36157
+ /** The prefix the hub-main context factory prepends to the manifest id. */
36158
+ var ADDON_ID_PREFIX = "addon:";
36159
+ /** The manifest id, whichever spelling of `ctx.id` you were handed. */
36160
+ function bareAddonId(id) {
36161
+ return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
36162
+ }
36163
+ /** True when both ids name the same addon, prefixed or not. */
36164
+ function isSameAddonId(a, b) {
36165
+ return bareAddonId(a) === bareAddonId(b);
36166
+ }
36167
+ //#endregion
35575
36168
  //#region src/utils/cosine-similarity.ts
35576
36169
  /** Cosine similarity between two embedding vectors */
35577
36170
  function cosineSimilarity(a, b) {
@@ -36298,7 +36891,7 @@ function readDetailCropConvention(config) {
36298
36891
  square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
36299
36892
  };
36300
36893
  }
36301
- function isHydratedField(entry) {
36894
+ function isHydratedField$1(entry) {
36302
36895
  return typeof entry === "object" && entry !== null && "key" in entry;
36303
36896
  }
36304
36897
  /**
@@ -36313,7 +36906,7 @@ function pickDetailCropConvention(view) {
36313
36906
  if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
36314
36907
  const flat = {};
36315
36908
  for (const section of view.sections) for (const entry of section.fields) {
36316
- if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
36909
+ if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
36317
36910
  if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
36318
36911
  }
36319
36912
  return readDetailCropConvention(flat);
@@ -36400,6 +36993,245 @@ function slideInsideFrame(rect, frameWidth, frameHeight) {
36400
36993
  };
36401
36994
  }
36402
36995
  //#endregion
36996
+ //#region src/pipeline/native-lease.ts
36997
+ /**
36998
+ * THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
36999
+ * decode worker's native-resolution frame retention.
37000
+ *
37001
+ * ## Why they live here and not in the addon that reads them
37002
+ *
37003
+ * The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
37004
+ * the READER is a private child process of `addon-pipeline`'s pipeline-runner.
37005
+ * Addons never import each other, so a key owned by either side would have to
37006
+ * be hand-copied by the other — and a hand-copied key is how a setting silently
37007
+ * stops arriving while both sides still look correct. Same reasoning, same
37008
+ * placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
37009
+ *
37010
+ * ## Why cluster-wide and not per-node
37011
+ *
37012
+ * The lease is a per-decode-worker RAM window. Its purpose — the late
37013
+ * cross-process native crop landing on a full-resolution frame rather than the
37014
+ * ≤640 detection fallback — is a property of the PIPELINE, not of a node's
37015
+ * hardware: a per-node TTL would mean the same camera produces different crop
37016
+ * quality depending on which node the balancer placed it on, and nobody could
37017
+ * tell that from the stored media. Node-level RAM pressure is already handled
37018
+ * by the per-session budget ceiling, which is itself one of these knobs.
37019
+ *
37020
+ * ## What each knob costs
37021
+ *
37022
+ * A retained frame is a full NATIVE-resolution copy in system RAM. With the
37023
+ * default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
37024
+ * 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
37025
+ * for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
37026
+ * resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
37027
+ * clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
37028
+ * admission" for what actually gets admitted.
37029
+ */
37030
+ /**
37031
+ * Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
37032
+ * (cluster-wide) settings. Keys are unique across that addon's whole schema, so
37033
+ * the reader can walk every section instead of trusting the section id.
37034
+ */
37035
+ var NATIVE_LEASE_SECTION_ID = "native-lease";
37036
+ var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
37037
+ var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
37038
+ var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
37039
+ var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
37040
+ /**
37041
+ * WHICH delivered frames the decode worker retains a native copy of.
37042
+ *
37043
+ * - `all` — every frame the worker delivered to the runner. The shipped
37044
+ * behaviour, and the only correct one if something can ask for a crop of a
37045
+ * frame the runner never sent to inference.
37046
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
37047
+ * A native-crop request always names a `frameId` that rode an inference
37048
+ * result, so that is the only set a request can name. How much it drops is
37049
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
37050
+ * this cluster, not the ~80% the design sketch assumed, because the governor
37051
+ * was not throttling as hard as the sketch supposed. Read
37052
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
37053
+ * of you rather than quoting a number from here. The newest delivered frame is
37054
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
37055
+ * which covers the one-frame race between a mark and the supersede that
37056
+ * consumes it.
37057
+ */
37058
+ var NativeLeaseAdmissionSchema = zod.z.enum(["all", "inferred"]);
37059
+ /**
37060
+ * Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
37061
+ * the slider) because the value also travels to a forked child process, where a
37062
+ * junk number would silently become a 0-length or unbounded retention window.
37063
+ */
37064
+ var NativeLeaseSettingsSchema = zod.z.object({
37065
+ /**
37066
+ * How long a retained native frame is served before it counts as a miss.
37067
+ *
37068
+ * Must cover the FULL late-crop horizon: detection inference + the
37069
+ * cross-process inference-result hop to hub post-analysis + tracking + the
37070
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
37071
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
37072
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
37073
+ */
37074
+ ttlMs: zod.z.number().int().min(250).max(1e4),
37075
+ /**
37076
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
37077
+ *
37078
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
37079
+ * which one is actually binding before reasoning from that. At the shipped
37080
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
37081
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
37082
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
37083
+ * change that admits fewer frames buys retention WINDOW at constant RAM
37084
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
37085
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
37086
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
37087
+ * to replace).
37088
+ */
37089
+ budgetMb: zod.z.number().int().min(0).max(4096),
37090
+ /**
37091
+ * Demand window: eager per-frame native retention runs only within this many
37092
+ * ms of the last native-crop request (or of the dial starting).
37093
+ *
37094
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
37095
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
37096
+ * per second on a camera with zero crop demand), so leave it non-zero unless
37097
+ * you are reproducing that.
37098
+ */
37099
+ activityMs: zod.z.number().int().min(0).max(12e4),
37100
+ /**
37101
+ * Which delivered frames are retained at all — see
37102
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
37103
+ * changes WHAT is kept rather than for how long, so it is also the only one
37104
+ * that can turn a crop that used to hit into a miss. The worker counts every
37105
+ * crop request naming a frame it did NOT see marked
37106
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
37107
+ * there is the signal that some caller names frames outside the inference set
37108
+ * and that this must go back to `all`.
37109
+ */
37110
+ admission: NativeLeaseAdmissionSchema
37111
+ });
37112
+ /**
37113
+ * The values in force when the operator has set nothing — byte-for-byte the
37114
+ * constants the decode worker shipped with as env-var defaults, so making these
37115
+ * settings changed no behaviour on the day it landed.
37116
+ */
37117
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
37118
+ ttlMs: 1200,
37119
+ budgetMb: 1024,
37120
+ activityMs: 15e3,
37121
+ admission: "inferred"
37122
+ };
37123
+ /** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
37124
+ var NATIVE_LEASE_TTL_FIELD = {
37125
+ min: 250,
37126
+ max: 1e4,
37127
+ step: 50,
37128
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
37129
+ };
37130
+ var NATIVE_LEASE_BUDGET_FIELD = {
37131
+ min: 0,
37132
+ max: 4096,
37133
+ step: 64,
37134
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
37135
+ };
37136
+ var NATIVE_LEASE_ACTIVITY_FIELD = {
37137
+ min: 0,
37138
+ max: 12e4,
37139
+ step: 1e3,
37140
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
37141
+ };
37142
+ /** Select options for the admission knob (orchestrator settings UI). */
37143
+ var NATIVE_LEASE_ADMISSION_FIELD = {
37144
+ options: [{
37145
+ value: "all",
37146
+ label: "Every delivered frame"
37147
+ }, {
37148
+ value: "inferred",
37149
+ label: "Only frames sent to inference"
37150
+ }],
37151
+ default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
37152
+ };
37153
+ /**
37154
+ * Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
37155
+ * value equal to the shipped default.
37156
+ *
37157
+ * That last rule is not tidiness, it is the difference between the documented
37158
+ * precedence being true and being a lie. `addon-settings.getGlobalSettings`
37159
+ * returns a HYDRATED payload, and `hydrateField` fills an unstored field with
37160
+ * the schema's own `default` (verified live on the hub: a cluster that has never
37161
+ * opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
37162
+ * that at face value would report all three knobs as "set" on every cluster on
37163
+ * the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
37164
+ * emergency override that the precedence promises. There is no raw-store read on
37165
+ * this cap to distinguish the two, so the default value itself is treated as
37166
+ * "the operator has expressed no preference" — which is also what leaving a
37167
+ * slider untouched means.
37168
+ *
37169
+ * The cost is one honest edge: an operator who deliberately selects the default
37170
+ * value in order to overrule an env var does not get it. Clear the env var
37171
+ * instead; the worker's spawn line names the source, so this is visible rather
37172
+ * than mysterious.
37173
+ */
37174
+ function readKnob(knob, raw) {
37175
+ const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
37176
+ if (!parsed.success) return null;
37177
+ return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
37178
+ }
37179
+ /** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
37180
+ function readAdmissionKnob(raw) {
37181
+ const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
37182
+ if (!parsed.success) return null;
37183
+ return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
37184
+ }
37185
+ /**
37186
+ * Narrow a FLAT settings record to the knobs the operator set.
37187
+ *
37188
+ * Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
37189
+ * budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
37190
+ * clamped, not defaulted) so the caller can still fall through to the env
37191
+ * override — clamping here would turn a typo into a value nobody chose. See
37192
+ * {@link readKnob} for why the default counts as unset.
37193
+ */
37194
+ function readNativeLeaseOverride(config) {
37195
+ const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
37196
+ const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
37197
+ const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
37198
+ const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
37199
+ return {
37200
+ ...ttlMs === null ? {} : { ttlMs },
37201
+ ...budgetMb === null ? {} : { budgetMb },
37202
+ ...activityMs === null ? {} : { activityMs },
37203
+ ...admission === null ? {} : { admission }
37204
+ };
37205
+ }
37206
+ function isHydratedField(entry) {
37207
+ return typeof entry === "object" && entry !== null && "key" in entry;
37208
+ }
37209
+ var LEASE_KEYS = [
37210
+ NATIVE_LEASE_TTL_KEY,
37211
+ NATIVE_LEASE_BUDGET_KEY,
37212
+ NATIVE_LEASE_ACTIVITY_KEY,
37213
+ NATIVE_LEASE_ADMISSION_KEY
37214
+ ];
37215
+ /**
37216
+ * Extract the operator's lease overrides from an
37217
+ * `addon-settings.getGlobalSettings` payload.
37218
+ *
37219
+ * Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
37220
+ * alone: the keys are unique across the addon's schema, and a section rename
37221
+ * must not silently revert the whole cluster to the defaults. A `null` payload
37222
+ * (addon mid-boot) means "operator set nothing" — the env/default fallback then
37223
+ * applies, which is the correct read of "I could not ask".
37224
+ */
37225
+ function pickNativeLeaseOverride(view) {
37226
+ if (view === null) return {};
37227
+ const flat = {};
37228
+ for (const section of view.sections) for (const entry of section.fields) {
37229
+ if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
37230
+ if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
37231
+ }
37232
+ return readNativeLeaseOverride(flat);
37233
+ }
37234
+ //#endregion
36403
37235
  //#region src/helpers/bind-addon-actions.ts
36404
37236
  /**
36405
37237
  * Bind an addon's custom-action catalog to its tRPC surface, returning a
@@ -36658,7 +37490,7 @@ exports.APPLE_SA_TO_MACRO = APPLE_SA_TO_MACRO;
36658
37490
  exports.AUDIO_ANALYSIS_CAP_NAME = AUDIO_ANALYSIS_CAP_NAME;
36659
37491
  exports.AUDIO_BACKEND_CHOICES = AUDIO_BACKEND_CHOICES;
36660
37492
  exports.AUDIO_MACRO_LABELS = AUDIO_MACRO_LABELS;
36661
- exports.AUDIO_PRESETS = AUDIO_PRESETS;
37493
+ exports.AUDIO_PRESETS = require_fmp4_box_splitter.AUDIO_PRESETS;
36662
37494
  exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
36663
37495
  exports.AccessoryKind = AccessoryKind;
36664
37496
  exports.AddBrokerInputSchema = AddBrokerInputSchema;
@@ -36833,6 +37665,7 @@ exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
36833
37665
  exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
36834
37666
  exports.DEFAULT_EVENT_COLOR = DEFAULT_EVENT_COLOR;
36835
37667
  exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
37668
+ exports.DEFAULT_NATIVE_LEASE_SETTINGS = DEFAULT_NATIVE_LEASE_SETTINGS;
36836
37669
  exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
36837
37670
  exports.DEFAULT_SCRUB_THUMBNAIL_PRESET = DEFAULT_SCRUB_THUMBNAIL_PRESET;
36838
37671
  exports.DETAIL_CROP_PADDING_FIELD = DETAIL_CROP_PADDING_FIELD;
@@ -36891,6 +37724,10 @@ exports.EXPRESSION_BUILTIN_NAMES = EXPRESSION_BUILTIN_NAMES;
36891
37724
  exports.EXPRESSION_COMPILE_CACHE_CAPACITY = EXPRESSION_COMPILE_CACHE_CAPACITY;
36892
37725
  exports.EXPRESSION_IDENTIFIER_RE = EXPRESSION_IDENTIFIER_RE;
36893
37726
  exports.EXPRESSION_INJECTED_NOW = EXPRESSION_INJECTED_NOW;
37727
+ exports.EgressEncodeSchema = EgressEncodeSchema;
37728
+ exports.EgressRateControlSchema = EgressRateControlSchema;
37729
+ exports.EgressTranscodeRequestSchema = EgressTranscodeRequestSchema;
37730
+ exports.EgressTranscodeSchema = EgressTranscodeSchema;
36894
37731
  exports.ElementConfigStore = ElementConfigStore;
36895
37732
  exports.EmbeddingInfoSchema = EmbeddingInfoSchema;
36896
37733
  exports.EmbeddingResultSchema = EmbeddingResultSchema;
@@ -36926,6 +37763,7 @@ exports.FanDirectionSchema = FanDirectionSchema;
36926
37763
  exports.FeatureManifestSchema = FeatureManifestSchema;
36927
37764
  exports.FeatureProbeStatusSchema = FeatureProbeStatusSchema;
36928
37765
  exports.FloodStatusSchema = FloodStatusSchema;
37766
+ exports.Fmp4BoxSplitter = require_fmp4_box_splitter.Fmp4BoxSplitter;
36929
37767
  exports.FrameHandleFormatSchema = require_sleep.FrameHandleFormatSchema;
36930
37768
  exports.FrameHandleSchema = require_sleep.FrameHandleSchema;
36931
37769
  exports.FrameInputSchema = FrameInputSchema;
@@ -37046,6 +37884,15 @@ exports.MotionZoneRegionSchema = MotionZoneRegionSchema;
37046
37884
  exports.MotionZoneStatusSchema = MotionZoneStatusSchema;
37047
37885
  exports.MqttBrokerStatusSchema = StatusSchema;
37048
37886
  exports.MutationFilterSchema = MutationFilterSchema;
37887
+ exports.NATIVE_LEASE_ACTIVITY_FIELD = NATIVE_LEASE_ACTIVITY_FIELD;
37888
+ exports.NATIVE_LEASE_ACTIVITY_KEY = NATIVE_LEASE_ACTIVITY_KEY;
37889
+ exports.NATIVE_LEASE_ADMISSION_FIELD = NATIVE_LEASE_ADMISSION_FIELD;
37890
+ exports.NATIVE_LEASE_ADMISSION_KEY = NATIVE_LEASE_ADMISSION_KEY;
37891
+ exports.NATIVE_LEASE_BUDGET_FIELD = NATIVE_LEASE_BUDGET_FIELD;
37892
+ exports.NATIVE_LEASE_BUDGET_KEY = NATIVE_LEASE_BUDGET_KEY;
37893
+ exports.NATIVE_LEASE_SECTION_ID = NATIVE_LEASE_SECTION_ID;
37894
+ exports.NATIVE_LEASE_TTL_FIELD = NATIVE_LEASE_TTL_FIELD;
37895
+ exports.NATIVE_LEASE_TTL_KEY = NATIVE_LEASE_TTL_KEY;
37049
37896
  exports.NC_BASE_CONDITION_KEYS = NC_BASE_CONDITION_KEYS;
37050
37897
  exports.NC_CONDITION_CATALOG = NC_CONDITION_CATALOG;
37051
37898
  exports.NC_HISTORY_LIMIT_DEFAULT = NC_HISTORY_LIMIT_DEFAULT;
@@ -37057,6 +37904,8 @@ exports.NativeCropBboxSchema = NativeCropBboxSchema;
37057
37904
  exports.NativeCropRefSchema = NativeCropRefSchema;
37058
37905
  exports.NativeCropResultSchema = NativeCropResultSchema;
37059
37906
  exports.NativeDetectionSchema = NativeDetectionSchema;
37907
+ exports.NativeLeaseAdmissionSchema = NativeLeaseAdmissionSchema;
37908
+ exports.NativeLeaseSettingsSchema = NativeLeaseSettingsSchema;
37060
37909
  exports.NativeObjectClassEnum = NativeObjectClassEnum;
37061
37910
  exports.NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionRuntimeStateSchema;
37062
37911
  exports.NativeObjectDetectionStatusSchema = NativeObjectDetectionStatusSchema;
@@ -37121,11 +37970,19 @@ exports.OsdOverlayKindEnum = OsdOverlayKindEnum;
37121
37970
  exports.OsdOverlayPatchSchema = OsdOverlayPatchSchema;
37122
37971
  exports.OsdOverlaySchema = OsdOverlaySchema;
37123
37972
  exports.OsdPositionEnum = OsdPositionEnum;
37973
+ exports.OsdRenderOutcomeEnum = OsdRenderOutcomeEnum;
37974
+ exports.OsdRenderResultSchema = OsdRenderResultSchema;
37975
+ exports.OsdSlotBindingSchema = OsdSlotBindingSchema;
37976
+ exports.OsdSlotViewSchema = OsdSlotViewSchema;
37977
+ exports.OsdSourceOptionSchema = OsdSourceOptionSchema;
37978
+ exports.OsdSourceSchema = OsdSourceSchema;
37979
+ exports.OsdSourceValueTypeEnum = OsdSourceValueTypeEnum;
37124
37980
  exports.OsdStatusSchema = OsdStatusSchema;
37125
37981
  exports.PET_FEEDER_MANUAL_FEED_MAX = PET_FEEDER_MANUAL_FEED_MAX;
37126
37982
  exports.PET_FEEDER_MANUAL_FEED_MIN = PET_FEEDER_MANUAL_FEED_MIN;
37127
37983
  exports.PIPELINE_FLOW_CAPABILITY_NAMES = PIPELINE_FLOW_CAPABILITY_NAMES;
37128
37984
  exports.PIPELINE_OWNER_CAPABILITY_NAMES = PIPELINE_OWNER_CAPABILITY_NAMES;
37985
+ exports.PRIVACY_MASK_CAP_NAME = PRIVACY_MASK_CAP_NAME;
37129
37986
  exports.PROVIDER_KIND_CAP_NAMES = PROVIDER_KIND_CAP_NAMES;
37130
37987
  exports.PYTHON_SCRIPT = PYTHON_SCRIPT;
37131
37988
  exports.PackageUpdateSchema = PackageUpdateSchema;
@@ -37179,6 +38036,7 @@ exports.RESERVED_BINDING_NAMES = RESERVED_BINDING_NAMES;
37179
38036
  exports.RUNTIME_DEFAULTS = RUNTIME_DEFAULTS;
37180
38037
  exports.RUNTIME_TO_FORMAT = RUNTIME_TO_FORMAT;
37181
38038
  exports.RawStateResultSchema = require_sleep.RawStateResultSchema;
38039
+ exports.ReadGopBytesResultSchema = ReadGopBytesResultSchema;
37182
38040
  exports.ReadSegmentBytesResultSchema = ReadSegmentBytesResultSchema;
37183
38041
  exports.ReadinessRegistry = require_sleep.ReadinessRegistry;
37184
38042
  exports.ReadinessTimeoutError = require_sleep.ReadinessTimeoutError;
@@ -37207,6 +38065,7 @@ exports.RelocateJobStateSchema = RelocateJobStateSchema;
37207
38065
  exports.RelocateMediaInputSchema = RelocateMediaInputSchema;
37208
38066
  exports.RenderedAsSchema = RenderedAsSchema;
37209
38067
  exports.ReportMotionInputSchema = ReportMotionInputSchema;
38068
+ exports.RetrainStatusSchema = RetrainStatusSchema;
37210
38069
  exports.RingBuffer = RingBuffer;
37211
38070
  exports.RtpSourceSchema = RtpSourceSchema;
37212
38071
  exports.RtspRestreamEntrySchema = RtspRestreamEntrySchema;
@@ -37329,6 +38188,8 @@ exports.TrackSourceSchema = TrackSourceSchema;
37329
38188
  exports.TrackStateSchema = TrackStateSchema;
37330
38189
  exports.TrackZoneFilterSchema = TrackZoneFilterSchema;
37331
38190
  exports.TrackedDetectionSchema = TrackedDetectionSchema;
38191
+ exports.TrainingExportDeviceTotalsSchema = TrainingExportDeviceTotalsSchema;
38192
+ exports.TrainingExportSummarySchema = TrainingExportSummarySchema;
37332
38193
  exports.TurnServerSchema = TurnServerSchema;
37333
38194
  exports.UNIT_TABLE = UNIT_TABLE;
37334
38195
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema$1;
@@ -37408,11 +38269,12 @@ exports.audioAnalysisCapability = audioAnalysisCapability;
37408
38269
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
37409
38270
  exports.audioCodecCapability = audioCodecCapability;
37410
38271
  exports.audioMetricsCapability = audioMetricsCapability;
37411
- exports.audioPlanFromEncodeProfile = audioPlanFromEncodeProfile;
38272
+ exports.audioPlanFromEncodeProfile = require_fmp4_box_splitter.audioPlanFromEncodeProfile;
37412
38273
  exports.authProviderCapability = authProviderCapability;
37413
38274
  exports.autoAssignProfiles = autoAssignProfiles;
37414
38275
  exports.automationControlCapability = automationControlCapability;
37415
38276
  exports.backupCapability = backupCapability;
38277
+ exports.bareAddonId = bareAddonId;
37416
38278
  exports.batteryCapability = batteryCapability;
37417
38279
  exports.bestLocationMatch = bestLocationMatch;
37418
38280
  exports.binaryCapability = binaryCapability;
@@ -37420,14 +38282,14 @@ exports.bindAddonActions = bindAddonActions;
37420
38282
  exports.brightnessCapability = brightnessCapability;
37421
38283
  exports.brokerCapability = brokerCapability;
37422
38284
  exports.buildAddonRouteProvider = buildAddonRouteProvider;
37423
- exports.buildAudioArgs = buildAudioArgs;
38285
+ exports.buildAudioArgs = require_fmp4_box_splitter.buildAudioArgs;
37424
38286
  exports.buildEventKindDescriptor = buildEventKindDescriptor;
37425
- exports.buildFfmpegArgs = buildFfmpegArgs;
37426
- exports.buildInputArgs = buildInputArgs;
38287
+ exports.buildFfmpegArgs = require_fmp4_box_splitter.buildFfmpegArgs;
38288
+ exports.buildInputArgs = require_fmp4_box_splitter.buildInputArgs;
37427
38289
  exports.buildModelVariantGroups = buildModelVariantGroups;
37428
38290
  exports.buildNcTaxonomy = buildNcTaxonomy;
37429
38291
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
37430
- exports.buildVideoArgs = buildVideoArgs;
38292
+ exports.buildVideoArgs = require_fmp4_box_splitter.buildVideoArgs;
37431
38293
  exports.buttonCapability = buttonCapability;
37432
38294
  exports.cameraCredentialsCapability = cameraCredentialsCapability;
37433
38295
  exports.cameraPipelineConfigCapability = cameraPipelineConfigCapability;
@@ -37489,6 +38351,7 @@ exports.deviceStateCapability = deviceStateCapability;
37489
38351
  exports.deviceStatusCapability = deviceStatusCapability;
37490
38352
  exports.doorbellCapability = doorbellCapability;
37491
38353
  exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
38354
+ exports.egressTransportFromRequest = egressTransportFromRequest;
37492
38355
  exports.embeddingEncoderCapability = embeddingEncoderCapability;
37493
38356
  exports.emitDownForOwnedCaps = require_sleep.emitDownForOwnedCaps;
37494
38357
  exports.emitReadiness = require_sleep.emitReadiness;
@@ -37531,7 +38394,7 @@ exports.imageCapability = imageCapability;
37531
38394
  exports.imageSettingsCapability = imageSettingsCapability;
37532
38395
  exports.integrationsCapability = integrationsCapability;
37533
38396
  exports.intercomCapability = intercomCapability;
37534
- exports.invocationFromEncodeProfile = invocationFromEncodeProfile;
38397
+ exports.invocationFromEncodeProfile = require_fmp4_box_splitter.invocationFromEncodeProfile;
37535
38398
  exports.isAgentOnlyPlacement = isAgentOnlyPlacement;
37536
38399
  exports.isArrayOutputSchema = isArrayOutputSchema;
37537
38400
  exports.isBaseConditionKey = isBaseConditionKey;
@@ -37542,7 +38405,9 @@ exports.isDeviceScopedCap = require_sleep.isDeviceScopedCap;
37542
38405
  exports.isEvent = require_sleep.isEvent;
37543
38406
  exports.isNode = isNode;
37544
38407
  exports.isObjectInput = isObjectInput;
37545
- exports.isSoftwareDecode = isSoftwareDecode;
38408
+ exports.isSameAddonId = isSameAddonId;
38409
+ exports.isScheduleActive = isScheduleActive;
38410
+ exports.isSoftwareDecode = require_fmp4_box_splitter.isSoftwareDecode;
37546
38411
  exports.isVoidInput = isVoidInput;
37547
38412
  exports.jobKindSchema = jobKindSchema;
37548
38413
  exports.kebabToCamel = kebabToCamel;
@@ -37557,7 +38422,7 @@ exports.llmRuntimeCapability = llmRuntimeCapability;
37557
38422
  exports.localNetworkCapability = localNetworkCapability;
37558
38423
  exports.locationSimilarity = locationSimilarity;
37559
38424
  exports.lockControlCapability = lockControlCapability;
37560
- exports.logBannerArgs = logBannerArgs;
38425
+ exports.logBannerArgs = require_fmp4_box_splitter.logBannerArgs;
37561
38426
  exports.logDestinationCapability = logDestinationCapability;
37562
38427
  exports.logLevelAtMost = logLevelAtMost;
37563
38428
  exports.loginMethodCapability = loginMethodCapability;
@@ -37595,6 +38460,7 @@ exports.numericSensorCapability = numericSensorCapability;
37595
38460
  exports.oauthIntegrationCapability = oauthIntegrationCapability;
37596
38461
  exports.objectInputDeclaresAddonId = objectInputDeclaresAddonId;
37597
38462
  exports.osdCapability = osdCapability;
38463
+ exports.osdManagerCapability = osdManagerCapability;
37598
38464
  exports.parseCameraStreamConfig = parseCameraStreamConfig;
37599
38465
  exports.parseExpression = parseExpression;
37600
38466
  exports.parseJsonArray = require_sleep.parseJsonArray;
@@ -37605,8 +38471,9 @@ exports.parseStreamParamsFormPatch = parseStreamParamsFormPatch;
37605
38471
  exports.petFeederCapability = petFeederCapability;
37606
38472
  exports.pickAccessoryControl = pickAccessoryControl;
37607
38473
  exports.pickDetailCropConvention = pickDetailCropConvention;
38474
+ exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
37608
38475
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
37609
- exports.pickVideoEncoder = pickVideoEncoder;
38476
+ exports.pickVideoEncoder = require_fmp4_box_splitter.pickVideoEncoder;
37610
38477
  exports.pickerForCondition = pickerForCondition;
37611
38478
  exports.pipelineAnalyticsCapability = pipelineAnalyticsCapability;
37612
38479
  exports.pipelineExecutorCapability = pipelineExecutorCapability;
@@ -37625,6 +38492,7 @@ exports.ptzCapability = ptzCapability;
37625
38492
  exports.pythonScriptForBackend = pythonScriptForBackend;
37626
38493
  exports.readDetailCropConvention = readDetailCropConvention;
37627
38494
  exports.readDeviceStateFrom = readDeviceStateFrom;
38495
+ exports.readNativeLeaseOverride = readNativeLeaseOverride;
37628
38496
  exports.readNodePin = require_sleep.readNodePin;
37629
38497
  exports.readinessKey = require_sleep.readinessKey;
37630
38498
  exports.rebootCapability = rebootCapability;
@@ -37675,6 +38543,7 @@ exports.streamParamsCapability = streamParamsCapability;
37675
38543
  exports.streamPixels = streamPixels;
37676
38544
  exports.streamQualityLabel = streamQualityLabel;
37677
38545
  exports.subKindsOf = subKindsOf;
38546
+ exports.summarisePrivacyAudio = summarisePrivacyAudio;
37678
38547
  exports.supportedRuntimes = supportedRuntimes;
37679
38548
  exports.switchCapability = switchCapability;
37680
38549
  exports.switchedOffIds = switchedOffIds;
@@ -37689,6 +38558,7 @@ exports.terminalSessionCapability = terminalSessionCapability;
37689
38558
  exports.textToHtml = textToHtml;
37690
38559
  exports.toDeviceSummary = toDeviceSummary;
37691
38560
  exports.toExpressionValue = toExpressionValue;
38561
+ exports.toNodeId = require_sleep.toNodeId;
37692
38562
  exports.toStreamSourceEntry = toStreamSourceEntry;
37693
38563
  exports.toastCapability = toastCapability;
37694
38564
  exports.tokenize = tokenize;