@camstack/types 1.2.40 → 1.2.42

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 (35) hide show
  1. package/dist/addon.js +1 -1
  2. package/dist/addon.mjs +1 -1
  3. package/dist/canonical-hash-7nfBbEqR.mjs +35 -0
  4. package/dist/canonical-hash-BcZHRHIx.js +40 -0
  5. package/dist/capabilities/index.d.ts +2 -2
  6. package/dist/capabilities/notification-rules.cap.d.ts +41 -0
  7. package/dist/capabilities/pipeline-analytics.cap.d.ts +93 -6
  8. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +123 -0
  9. package/dist/capabilities/pipeline-runner.cap.d.ts +143 -7
  10. package/dist/capabilities/platform-probe.cap.d.ts +3 -3
  11. package/dist/capabilities/recording.cap.d.ts +3 -0
  12. package/dist/capabilities/stream-broker.cap.d.ts +300 -0
  13. package/dist/encode-profile.d.ts +2 -0
  14. package/dist/ffmpeg/encode-defaults.d.ts +89 -0
  15. package/dist/ffmpeg/hwaccel.d.ts +98 -0
  16. package/dist/ffmpeg/invocation.d.ts +250 -0
  17. package/dist/ffmpeg/process.d.ts +135 -0
  18. package/dist/ffmpeg/sharing-key.d.ts +39 -0
  19. package/dist/generated/addon-api.d.ts +56 -0
  20. package/dist/generated/device-proxy.d.ts +1 -1
  21. package/dist/generated/method-access-map.d.ts +1 -1
  22. package/dist/generated/system-proxy.d.ts +2 -2
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +1595 -28
  25. package/dist/index.mjs +1548 -29
  26. package/dist/interfaces/camera-switches.d.ts +217 -0
  27. package/dist/interfaces/ops-log.d.ts +4 -0
  28. package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
  29. package/dist/node.d.ts +2 -0
  30. package/dist/node.js +270 -36
  31. package/dist/node.mjs +269 -36
  32. package/dist/pipeline/detail-crop.d.ts +122 -0
  33. package/dist/{sleep-CXimb854.mjs → sleep-BmNKsY7v.mjs} +5 -0
  34. package/dist/{sleep-DTce7-ch.js → sleep-Cvi1JxZp.js} +5 -0
  35. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +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-DTce7-ch.js");
3
+ const require_sleep = require("./sleep-Cvi1JxZp.js");
4
+ const require_canonical_hash = require("./canonical-hash-BcZHRHIx.js");
4
5
  const require_enums = require("./enums.js");
5
6
  const require_err_msg = require("./err-msg-COpsHMw2.js");
6
7
  let zod = require("zod");
@@ -147,6 +148,14 @@ var VideoEncodeSchema = zod.z.object({
147
148
  "main",
148
149
  "high"
149
150
  ]).optional(),
151
+ /**
152
+ * `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
153
+ * (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
154
+ * it, or it ships a stream that does not match its own advertisement — the
155
+ * defect class that kept HomeKit black for a year and that Alexa carried
156
+ * silently. Optional because a browser negotiates the level itself.
157
+ */
158
+ level: zod.z.string().optional(),
150
159
  width: zod.z.number().int().positive().optional(),
151
160
  height: zod.z.number().int().positive().optional(),
152
161
  fps: zod.z.number().positive().optional(),
@@ -222,6 +231,560 @@ function encodeProfileFromStreamShape(stream) {
222
231
  };
223
232
  }
224
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
+ //#region src/ffmpeg/encode-defaults.ts
564
+ /**
565
+ * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
566
+ * Baseline because it is the one profile every consumer in this repo decodes
567
+ * (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
568
+ */
569
+ var BASE_LIVE_EGRESS_PROFILE = {
570
+ video: {
571
+ codec: "h264",
572
+ profile: "baseline",
573
+ level: "3.1",
574
+ width: 1280,
575
+ height: 720,
576
+ fps: 25,
577
+ bitrateKbps: 2500,
578
+ gopFrames: 25,
579
+ bf: 0,
580
+ preset: "veryfast",
581
+ tune: "zerolatency"
582
+ },
583
+ audio: "passthrough"
584
+ };
585
+ /**
586
+ * The browser WebRTC viewer's transcode profile. Used only when the broker
587
+ * MUST spawn ffmpeg (source codec ∉ the browser-accepted set — an H.265 source
588
+ * on an H.264-only browser). Audio is `passthrough` because werift carries the
589
+ * audio plane out-of-band; putting it in the video pipeline would encode a
590
+ * track nothing reads.
591
+ */
592
+ var WEBRTC_EGRESS_PROFILE = {
593
+ ...BASE_LIVE_EGRESS_PROFILE,
594
+ video: {
595
+ ...BASE_LIVE_EGRESS_PROFILE.video,
596
+ bitrateKbps: 3e3,
597
+ gopFrames: 30,
598
+ preset: "ultrafast"
599
+ },
600
+ audio: "passthrough"
601
+ };
602
+ /**
603
+ * Alexa's egress profile. Alexa's audio ALSO rides the WebRTC session's own
604
+ * audio plane (out-of-band), so this is `passthrough` exactly like the browser
605
+ * — see the ADR for why the previous in-band Opus was encoded and discarded.
606
+ */
607
+ var ALEXA_EGRESS_PROFILE = {
608
+ ...BASE_LIVE_EGRESS_PROFILE,
609
+ audio: "passthrough"
610
+ };
611
+ /** VBV window for a consumer whose budget is enforced per second (HomeKit). */
612
+ var RATE_CONTROL_TIGHT = {
613
+ kind: "cbr",
614
+ vbvSeconds: 1
615
+ };
616
+ /** VBV window for a consumer that tolerates a keyframe spike (browser, Echo). */
617
+ var RATE_CONTROL_RELAXED = {
618
+ kind: "cap",
619
+ vbvSeconds: 2
620
+ };
621
+ /**
622
+ * Transcode-DOWN ceiling: the shape a remote/unhealthy WebRTC session is
623
+ * reduced to. 360p / 400 kbps is the `low` rung of the adaptive ladder — a
624
+ * session steered here is already failing, so the ceiling is chosen to survive
625
+ * a bad link, not to look good.
626
+ */
627
+ var TRANSCODE_DOWN_MAX_HEIGHT = 360;
628
+ var TRANSCODE_DOWN_MAX_BITRATE_KBPS = 400;
629
+ /**
630
+ * HomeKit's two-way audio leg. `lowdelay` + a short frame duration because the
631
+ * leg is interactive; `globalHeader` because HAP owns the SDP and ffmpeg must
632
+ * not put extradata in-band. `sampleRateHz` and `frameDurationMs` are
633
+ * NEGOTIATED per session — the controller picks them — so they are absent here
634
+ * and filled in by the caller.
635
+ */
636
+ var HAP_AUDIO_BITRATE_KBPS = 24;
637
+ var HAP_AUDIO_BASE = {
638
+ kind: "encode",
639
+ codec: "opus",
640
+ bitrateKbps: 24,
641
+ channels: 1,
642
+ application: "lowdelay",
643
+ globalHeader: true,
644
+ filter: "aresample=async=1000:first_pts=0"
645
+ };
646
+ /** HomeKit's `-bufsize` for the audio plane — four seconds of its 24 kbps. */
647
+ var HAP_AUDIO_VBV_KBITS = 96;
648
+ /** Seconds between forced IDRs on a HomeKit transcode. */
649
+ var HAP_KEYFRAME_INTERVAL_SEC = 4;
650
+ //#endregion
651
+ //#region src/ffmpeg/hwaccel.ts
652
+ function createHwAccelCache(options) {
653
+ const now = options.now ?? (() => Date.now());
654
+ let value = null;
655
+ let writtenAt = Number.NEGATIVE_INFINITY;
656
+ return {
657
+ read() {
658
+ return now() - writtenAt < options.ttlMs ? value : void 0;
659
+ },
660
+ write(next) {
661
+ value = next;
662
+ writtenAt = now();
663
+ }
664
+ };
665
+ }
666
+ /** `true` when a value means "decode in software". */
667
+ function meansSoftware(value) {
668
+ return !value || value === "none" || value === "copy";
669
+ }
670
+ /**
671
+ * Resolve the `-hwaccel` value for an egress transcode. Hardware is the
672
+ * DEFAULT — an egress that decodes in software on a hub with working vaapi is
673
+ * paying for nothing — and every software outcome is announced.
674
+ *
675
+ * Returns a concrete backend name, the literal `'auto'`, or `null` for
676
+ * software decode (⇒ {@link buildFfmpegArgs} emits no `-hwaccel` at all).
677
+ */
678
+ async function resolveEgressDecodeHwAccel(deps) {
679
+ const override = deps.override;
680
+ if (override !== void 0 && override !== null && override !== "") return meansSoftware(override) ? null : override;
681
+ const cached = deps.cache?.read();
682
+ if (cached !== void 0) return cached;
683
+ let backend;
684
+ try {
685
+ backend = await deps.readDecoderBackend();
686
+ } catch (err) {
687
+ const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
688
+ deps.onFallback({
689
+ reason: "decoder-unreadable",
690
+ kernelPreferred,
691
+ error: err instanceof Error ? err.message : String(err)
692
+ });
693
+ deps.cache?.write(null);
694
+ return null;
695
+ }
696
+ if (backend === "") {
697
+ const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
698
+ deps.onFallback({
699
+ reason: "decoder-unprobed",
700
+ kernelPreferred
701
+ });
702
+ deps.cache?.write(null);
703
+ return null;
704
+ }
705
+ const resolved = meansSoftware(backend) ? null : backend;
706
+ deps.cache?.write(resolved);
707
+ return resolved;
708
+ }
709
+ //#endregion
710
+ //#region src/ffmpeg/sharing-key.ts
711
+ /**
712
+ * The sharing key for `streamBroker.acquireEgressTranscode`.
713
+ *
714
+ * **Two requesters asking for exactly the same argument set reach the same
715
+ * process.** Exact match, never fuzzy: a quantised ladder that merged
716
+ * NEARLY-identical requests would make the stream a consumer receives depend
717
+ * on who else is watching and in what order they arrived — unpredictable in
718
+ * precisely the way a debugging session cannot tolerate. Same arguments ⇒ same
719
+ * process. Different arguments ⇒ different process, and that is fine; the
720
+ * operator accepted the cost ("it's fine to have several processes").
721
+ *
722
+ * Derived from the STRUCTURED plan, not from a flag array. `pipelineKeyFor`
723
+ * (`transcode-pipeline.ts`) folds `getStreamWithCodec`'s raw `outputArgs` into
724
+ * its key, so that method's extensibility hatch and its sharing key are the
725
+ * same field — a consumer needing one extra flag silently forks the child, and
726
+ * two consumers that want the same thing but spell it differently never share.
727
+ * Here every knob is a named field, and defaults are APPLIED before hashing so
728
+ * an omitted field and its explicit default land on the same key.
729
+ */
730
+ /** Absent optional ⇒ this sentinel, so `undefined` and "not set" agree. */
731
+ var UNSET = "\0unset";
732
+ function canonicalVideo(video) {
733
+ return {
734
+ codec: video.codec,
735
+ profile: video.profile ?? UNSET,
736
+ level: video.level ?? UNSET,
737
+ width: video.width ?? -1,
738
+ height: video.height ?? -1,
739
+ fps: video.fps ?? -1,
740
+ bitrateKbps: video.bitrateKbps ?? -1,
741
+ gopFrames: video.gopFrames ?? -1,
742
+ bf: video.bf ?? -1,
743
+ preset: video.preset ?? UNSET,
744
+ tune: video.tune ?? UNSET
745
+ };
746
+ }
747
+ function canonicalAudio(audio) {
748
+ if (audio === "passthrough") return { codec: "passthrough" };
749
+ return {
750
+ codec: audio.codec,
751
+ bitrateKbps: audio.bitrateKbps ?? -1,
752
+ sampleRateHz: audio.sampleRateHz ?? -1,
753
+ channels: audio.channels ?? -1
754
+ };
755
+ }
756
+ /**
757
+ * The normalised plan a key is computed from. Exported so a test — and a
758
+ * future operator-facing "why are these two not sharing?" surface — can diff
759
+ * two requests without reversing a hash.
760
+ */
761
+ function canonicalEgressPlan(request) {
762
+ return {
763
+ deviceId: request.deviceId,
764
+ source: request.source.kind === "profile" ? `profile:${request.source.profile}` : `cam-stream:${request.source.camStreamId}`,
765
+ video: canonicalVideo(request.encode.video),
766
+ audio: canonicalAudio(request.encode.audio),
767
+ rateControl: request.rateControl ?? "relaxed",
768
+ bitstreamFilter: request.bitstreamFilter ?? UNSET,
769
+ pixelFormat: request.pixelFormat ?? UNSET,
770
+ decodeHwAccel: request.decodeHwAccel ?? UNSET
771
+ };
772
+ }
773
+ /**
774
+ * The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
775
+ * so a request built with a different field order produces the same digest.
776
+ *
777
+ * The handle this keys is IMMUTABLE: there is no `reconfigure`. A consumer
778
+ * whose requirements change RELEASES and re-acquires; the refcount does the
779
+ * rest. That is what stops co-tenants disturbing each other — the co-tenant
780
+ * hazard that made Alexa's shared `derived:alexa-<id>` stream a hazard was
781
+ * exactly a mutable shared object, where one consumer's downgrade dragged
782
+ * every other consumer to 360p.
783
+ */
784
+ function egressTranscodeSharingKey(request) {
785
+ return `egress:${require_canonical_hash.canonicalHash(canonicalEgressPlan(request))}`;
786
+ }
787
+ //#endregion
225
788
  //#region src/health/wiring-health.ts
226
789
  /**
227
790
  * Deep wiring healthcheck — snapshot of active reachability probes across
@@ -331,6 +894,246 @@ var DEFAULT_RETENTION = {
331
894
  snapshotsDays: 14
332
895
  };
333
896
  //#endregion
897
+ //#region src/interfaces/camera-switches.ts
898
+ /**
899
+ * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
900
+ * pipeline functions an operator thinks in terms of.
901
+ *
902
+ * ## This file adds no state
903
+ *
904
+ * Every switch here is a VIEW onto an authority that already existed
905
+ * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
906
+ * group is that there is exactly one place each function is turned off, and
907
+ * the group routes to it:
908
+ *
909
+ * | Switch | Authority | Proven "off stops the work" gate |
910
+ * | --- | --- | --- |
911
+ * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
912
+ * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
913
+ * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
914
+ * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
915
+ * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
916
+ *
917
+ * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
918
+ * migrated the legacy `audioEnabled` / `pipelineEnabled` /
919
+ * `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
920
+ * surface that decision never got.
921
+ *
922
+ * ## Two rules that are load-bearing
923
+ *
924
+ * - **Recording's switch is `enabled`, never the bands.** `bands` is the only
925
+ * authored intent and `mode` is derived from it (`deriveRecordingMode`).
926
+ * Expressing "off" by clearing bands destroys the operator's schedule and
927
+ * turning the camera back on would then silently record nothing.
928
+ * - **A switch that is off must be reported as off**, not merely produce
929
+ * nothing. {@link CameraSwitch.enabled} is what a status surface renders as
930
+ * "disabled by an operator" instead of "broken" — see
931
+ * `CameraStatus.switchedOff`.
932
+ */
933
+ /**
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
936
+ * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
937
+ * editor, not in a five-button safety group.
938
+ */
939
+ var CameraSwitchIdSchema = zod.z.enum([
940
+ "stream-broker",
941
+ "object-detection",
942
+ "audio-analysis",
943
+ "recording",
944
+ "notifications"
945
+ ]);
946
+ /** Stable render order — broadest blast radius first. */
947
+ var CAMERA_SWITCH_ORDER = [
948
+ "stream-broker",
949
+ "object-detection",
950
+ "audio-analysis",
951
+ "recording",
952
+ "notifications"
953
+ ];
954
+ /**
955
+ * WHERE the switch's state actually lives. A discriminated union rather than a
956
+ * string so both the writer (the orchestrator's `setCameraSwitch`) and any
957
+ * reader can exhaustively narrow — and so "the group added a parallel map" is
958
+ * a compile error rather than a review comment.
959
+ */
960
+ var CameraSwitchAuthoritySchema = zod.z.discriminatedUnion("kind", [
961
+ zod.z.object({ kind: zod.z.literal("device-disabled") }),
962
+ zod.z.object({
963
+ kind: zod.z.literal("wrapper-binding"),
964
+ capName: zod.z.string()
965
+ }),
966
+ zod.z.object({ kind: zod.z.literal("recording-config") }),
967
+ zod.z.object({ kind: zod.z.literal("notification-mute") })
968
+ ]);
969
+ /**
970
+ * Why a switch is not offered for this camera. Rendered instead of the
971
+ * control, never as a dead control — an absent function and a broken one must
972
+ * not look the same.
973
+ */
974
+ var CameraSwitchUnavailableReasonSchema = zod.z.enum(["no-provider", "source-unreachable"]);
975
+ /**
976
+ * One switch, resolved for one camera.
977
+ *
978
+ * `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
979
+ * client-side: the viewer is a separate repository that does not import
980
+ * `@camstack/types`, and a cost line duplicated in two clients is a cost line
981
+ * that will disagree with itself. Five rows per camera is nothing.
982
+ */
983
+ var CameraSwitchSchema = zod.z.object({
984
+ id: CameraSwitchIdSchema,
985
+ label: zod.z.string(),
986
+ /**
987
+ * What the operator LOSES while this is off, in one sentence. Required, not
988
+ * optional: a switch that cannot say what it costs should not ship.
989
+ */
990
+ costWhenOff: zod.z.string(),
991
+ /** False = do not render a control. `unavailableReason` says why. */
992
+ available: zod.z.boolean(),
993
+ unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
994
+ /** Current state. Meaningless when `available` is false — read it as `true`. */
995
+ enabled: zod.z.boolean(),
996
+ authority: CameraSwitchAuthoritySchema
997
+ });
998
+ /** The whole group for one camera. */
999
+ var CameraSwitchGroupSchema = zod.z.object({
1000
+ deviceId: zod.z.number().int(),
1001
+ switches: zod.z.array(CameraSwitchSchema).readonly(),
1002
+ /** Unix ms when the group was composed server-side. */
1003
+ fetchedAt: zod.z.number()
1004
+ });
1005
+ /**
1006
+ * The wrapper capability each wrapper-backed switch controls. Named constants
1007
+ * because the same strings appear in `legacy-migrations.ts`, in
1008
+ * `isCapActiveForDevice` call sites and in the fake harness — a typo in any of
1009
+ * them is a switch that silently writes a binding nobody reads.
1010
+ */
1011
+ var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
1012
+ var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
1013
+ /**
1014
+ * THE catalog. One entry per switch; the cost lines are the operator-facing
1015
+ * contract and are written to be true rather than reassuring.
1016
+ */
1017
+ var CAMERA_SWITCH_CATALOG = {
1018
+ "stream-broker": {
1019
+ id: "stream-broker",
1020
+ label: "Camera",
1021
+ 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" }
1023
+ },
1024
+ "object-detection": {
1025
+ id: "object-detection",
1026
+ label: "Object detection & tracking",
1027
+ costWhenOff: "Off: nothing is detected or tracked on this camera, so it produces no events — and with no events there are no object notifications and no event-triggered recording. Live view and continuous recording are unaffected.",
1028
+ authority: {
1029
+ kind: "wrapper-binding",
1030
+ capName: DETECTION_PIPELINE_CAP_NAME
1031
+ }
1032
+ },
1033
+ "audio-analysis": {
1034
+ id: "audio-analysis",
1035
+ label: "Audio detection & classification",
1036
+ costWhenOff: "Off: no audio is decoded or classified for this camera. Tracks carry no audio labels and no audio-triggered rule can fire. Audio recorded alongside video is unaffected.",
1037
+ authority: {
1038
+ kind: "wrapper-binding",
1039
+ capName: AUDIO_ANALYSIS_CAP_NAME
1040
+ }
1041
+ },
1042
+ recording: {
1043
+ id: "recording",
1044
+ label: "Recording",
1045
+ 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" }
1047
+ },
1048
+ notifications: {
1049
+ id: "notifications",
1050
+ label: "Notifications",
1051
+ 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" }
1053
+ }
1054
+ };
1055
+ /** Resolve one switch's `{ available, enabled }` pair. */
1056
+ function resolveState(descriptor, input) {
1057
+ switch (descriptor.authority.kind) {
1058
+ case "device-disabled": return {
1059
+ available: true,
1060
+ enabled: !input.deviceDisabled
1061
+ };
1062
+ case "wrapper-binding": {
1063
+ const capName = descriptor.authority.capName;
1064
+ if (input.bindableCapNames === null || input.activeWrapperCapNames === null) return {
1065
+ available: false,
1066
+ enabled: true,
1067
+ unavailableReason: "source-unreachable"
1068
+ };
1069
+ if (!input.bindableCapNames.includes(capName)) return {
1070
+ available: false,
1071
+ enabled: true,
1072
+ unavailableReason: "no-provider"
1073
+ };
1074
+ return {
1075
+ available: true,
1076
+ enabled: input.activeWrapperCapNames.includes(capName)
1077
+ };
1078
+ }
1079
+ case "recording-config":
1080
+ if (input.recordingEnabled === null) return {
1081
+ available: false,
1082
+ enabled: true,
1083
+ unavailableReason: "source-unreachable"
1084
+ };
1085
+ return {
1086
+ available: true,
1087
+ enabled: input.recordingEnabled
1088
+ };
1089
+ case "notification-mute":
1090
+ if (input.notificationsMuted === null) return {
1091
+ available: false,
1092
+ enabled: true,
1093
+ unavailableReason: "source-unreachable"
1094
+ };
1095
+ return {
1096
+ available: true,
1097
+ enabled: !input.notificationsMuted
1098
+ };
1099
+ }
1100
+ }
1101
+ /**
1102
+ * Pure derivation of the whole group. No I/O — the orchestrator gathers, this
1103
+ * decides, so the decision is testable without a hub.
1104
+ *
1105
+ * Order is {@link CAMERA_SWITCH_ORDER}; unavailable switches are RETURNED
1106
+ * rather than filtered out, so a client can explain the gap instead of
1107
+ * silently rendering four buttons where another camera shows five.
1108
+ */
1109
+ function deriveCameraSwitches(input) {
1110
+ return CAMERA_SWITCH_ORDER.map((id) => {
1111
+ const descriptor = CAMERA_SWITCH_CATALOG[id];
1112
+ const state = resolveState(descriptor, input);
1113
+ return {
1114
+ id: descriptor.id,
1115
+ label: descriptor.label,
1116
+ costWhenOff: descriptor.costWhenOff,
1117
+ available: state.available,
1118
+ ...state.unavailableReason !== void 0 ? { unavailableReason: state.unavailableReason } : {},
1119
+ enabled: state.enabled,
1120
+ authority: descriptor.authority
1121
+ };
1122
+ });
1123
+ }
1124
+ /**
1125
+ * The ids an operator has switched OFF, for a status surface.
1126
+ *
1127
+ * This is the answer to "a disabled function must be visible as DISABLED, not
1128
+ * merely quiet": a camera reporting zero detections with
1129
+ * `switchedOff: ['object-detection']` was turned off; the same camera with an
1130
+ * empty list is broken. Unavailable switches never appear — a function nobody
1131
+ * provides was not switched off by anyone.
1132
+ */
1133
+ function switchedOffIds(switches) {
1134
+ return switches.filter((s) => s.available && !s.enabled).map((s) => s.id);
1135
+ }
1136
+ //#endregion
334
1137
  //#region src/interfaces/device-capabilities/camera.ts
335
1138
  /** Friendly display labels for stream quality IDs. */
336
1139
  var STREAM_QUALITY_LABELS = {
@@ -526,14 +1329,16 @@ var OpsLogOpSchema = zod.z.enum([
526
1329
  "manual-delete",
527
1330
  "rescan",
528
1331
  "retention-run",
529
- "relocate"
1332
+ "relocate",
1333
+ "orphan-audit"
530
1334
  ]);
531
1335
  /** Why the operation ran. */
532
1336
  var OpsLogReasonSchema = zod.z.enum([
533
1337
  "retention",
534
1338
  "quota",
535
1339
  "manual",
536
- "operator"
1340
+ "operator",
1341
+ "maintenance"
537
1342
  ]);
538
1343
  /** One audit row, shared verbatim by both domains. */
539
1344
  var OpsLogEntrySchema = zod.z.object({
@@ -3314,6 +4119,100 @@ var RtpSourceSchema = zod.z.object({
3314
4119
  encoder: zod.z.string(),
3315
4120
  pipelineKey: zod.z.string()
3316
4121
  });
4122
+ /**
4123
+ * The encode request — **structured and serialisable, with NO raw-flag escape
4124
+ * hatch.** This is deliberate and it is the one lesson taken from
4125
+ * `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
4126
+ * its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
4127
+ * adding a flag silently forks the shared child, and two consumers that mean
4128
+ * the same thing but spell it differently never share. Here every knob is a
4129
+ * NAMED field: a new requirement becomes a schema field (and a codegen run),
4130
+ * never an opaque array.
4131
+ *
4132
+ * `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
4133
+ * The operator-facing derived-stream transform editor still has them — that is
4134
+ * a different surface (`publishCameraStream({ kind: 'derived' })`) with a
4135
+ * different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
4136
+ */
4137
+ var EgressEncodeSchema = EncodeProfileSchema.omit({
4138
+ inputArgs: true,
4139
+ outputArgs: true
4140
+ });
4141
+ /**
4142
+ * How the encoder is bounded. `'tight'` is a one-second VBV window for a
4143
+ * consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
4144
+ * seconds, letting a keyframe spike borrow from the next second (a browser,
4145
+ * an Echo). Named rather than numeric so the INTENT survives.
4146
+ */
4147
+ var EgressRateControlSchema = zod.z.enum(["tight", "relaxed"]);
4148
+ var EgressTranscodeRequestSchema = zod.z.object({
4149
+ deviceId: zod.z.number().int().nonnegative(),
4150
+ /** Which published stream to read. */
4151
+ source: zod.z.discriminatedUnion("kind", [zod.z.object({
4152
+ kind: zod.z.literal("profile"),
4153
+ profile: require_sleep.CamProfileSchema
4154
+ }), zod.z.object({
4155
+ kind: zod.z.literal("cam-stream"),
4156
+ camStreamId: zod.z.string().min(1)
4157
+ })]),
4158
+ encode: EgressEncodeSchema,
4159
+ rateControl: EgressRateControlSchema.optional(),
4160
+ /**
4161
+ * `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
4162
+ * out-of-band extradata and needs `dump_extra` on both the copy and encode
4163
+ * branches. Enumerated, not free text.
4164
+ */
4165
+ bitstreamFilter: zod.z.enum([
4166
+ "dump_extra",
4167
+ "h264_mp4toannexb",
4168
+ "hevc_mp4toannexb"
4169
+ ]).optional(),
4170
+ pixelFormat: zod.z.enum(["yuv420p", "nv12"]).optional(),
4171
+ /**
4172
+ * Operator/consumer override for decode hardware. ABSENT is the normal case
4173
+ * and the one that matters: the broker then resolves the backend from the
4174
+ * DECODER ADDON's per-node `probedBestHwaccel` (see
4175
+ * `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
4176
+ * on this hardware — never the raw kernel resolver's qsv-first order.
4177
+ */
4178
+ decodeHwAccel: zod.z.enum([
4179
+ "auto",
4180
+ "none",
4181
+ "videotoolbox",
4182
+ "vaapi",
4183
+ "qsv",
4184
+ "cuda"
4185
+ ]).optional(),
4186
+ /**
4187
+ * Host to embed in the returned restream `url`. The broker mints hub-local
4188
+ * `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
4189
+ * host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
4190
+ * dialable from there. Same contract as `getStreamWithCodec.hostname` —
4191
+ * `substituteRtspHost` rewrites only the dial address, never the restreamer.
4192
+ */
4193
+ hostname: zod.z.string().optional(),
4194
+ /** Attribution for the broker panel. Never part of the sharing key. */
4195
+ tag: zod.z.string().optional()
4196
+ });
4197
+ var EgressTranscodeSchema = zod.z.object({
4198
+ /** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
4199
+ url: zod.z.string(),
4200
+ /** Release handle. Refcounted — the child dies when the last holder releases. */
4201
+ pipelineKey: zod.z.string(),
4202
+ videoCodec: zod.z.enum(["H264", "H265"]),
4203
+ resolution: zod.z.object({
4204
+ width: zod.z.number().int().positive(),
4205
+ height: zod.z.number().int().positive()
4206
+ }),
4207
+ transcoded: zod.z.boolean(),
4208
+ encoder: zod.z.string(),
4209
+ /**
4210
+ * The decode backend the child ACTUALLY ran with — `null` for software.
4211
+ * Returned rather than assumed: a consumer that asked for hardware and got
4212
+ * software needs to be able to see that without reading the broker's logs.
4213
+ */
4214
+ decodeHwAccel: zod.z.string().nullable()
4215
+ });
3317
4216
  var streamBrokerCapability = {
3318
4217
  name: "stream-broker",
3319
4218
  scope: "system",
@@ -3485,6 +4384,40 @@ var streamBrokerCapability = {
3485
4384
  auth: "admin"
3486
4385
  }),
3487
4386
  /**
4387
+ * THE ffmpeg primitive. Acquire an encoded stream matching a structured
4388
+ * encode plan; the broker builds the argv through the ONE builder
4389
+ * (`@camstack/types` `ffmpeg/invocation.ts`), resolves decode hardware from
4390
+ * the DECODER ADDON's per-node ranking, and returns a dialable RTSP url.
4391
+ *
4392
+ * **Refcounted and deduplicated on EXACT match.** Two requesters whose
4393
+ * requests produce the same `egressTranscodeSharingKey` receive the SAME
4394
+ * `pipelineKey` and the same child process. Nearly-identical requests are
4395
+ * NOT merged.
4396
+ *
4397
+ * **The handle is immutable.** There is deliberately no `reconfigure`
4398
+ * method and this one never accepts a `pipelineKey` alongside encode
4399
+ * parameters: a consumer whose requirements change releases and
4400
+ * re-acquires. A mutable shared handle is exactly what made Alexa's old
4401
+ * `derived:alexa-<id>` stream a co-tenant hazard — one consumer's
4402
+ * downgrade dragged every other consumer down with it.
4403
+ *
4404
+ * Placement: unpinned, `classifyCapRoute` serves this hub-in-process
4405
+ * (Priority 1). A caller that wants the transcode elsewhere passes
4406
+ * `nodePin(nodeId)` and a `hostname` it can dial.
4407
+ */
4408
+ acquireEgressTranscode: require_sleep.method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
4409
+ kind: "mutation",
4410
+ auth: "admin"
4411
+ }),
4412
+ /** Drop one reference. The child dies when the last holder releases. */
4413
+ releaseEgressTranscode: require_sleep.method(zod.z.object({ pipelineKey: zod.z.string() }), zod.z.object({
4414
+ released: zod.z.boolean(),
4415
+ refcount: zod.z.number().int().nonnegative()
4416
+ }), {
4417
+ kind: "mutation",
4418
+ auth: "admin"
4419
+ }),
4420
+ /**
3488
4421
  * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
3489
4422
  *
3490
4423
  * The serialisable replacement for the live-object `IStreamBroker.
@@ -10029,12 +10962,13 @@ var NcConditionsSchema = zod.z.object({
10029
10962
  * source; otherwise the subject's source must equal it. Legacy records
10030
10963
  * with no stamped source are treated as `pipeline`. The union spans both
10031
10964
  * record kinds — object events carry `pipeline` | `onboard`, synthetic
10032
- * tracks carry `sensor`.
10965
+ * tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
10033
10966
  */
10034
10967
  source: zod.z.enum([
10035
10968
  "pipeline",
10036
10969
  "onboard",
10037
10970
  "sensor",
10971
+ "audio",
10038
10972
  "any"
10039
10973
  ]).optional(),
10040
10974
  /**
@@ -10588,6 +11522,10 @@ var NC_CONDITION_CATALOG = [
10588
11522
  {
10589
11523
  value: "sensor",
10590
11524
  label: "Sensor"
11525
+ },
11526
+ {
11527
+ value: "audio",
11528
+ label: "Audio marker"
10591
11529
  }
10592
11530
  ],
10593
11531
  operator: "in",
@@ -10598,7 +11536,7 @@ var NC_CONDITION_CATALOG = [
10598
11536
  "package-event"
10599
11537
  ],
10600
11538
  phase: "P1",
10601
- description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
11539
+ description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
10602
11540
  },
10603
11541
  {
10604
11542
  id: "deviceState",
@@ -10950,6 +11888,35 @@ var notificationRulesCapability = {
10950
11888
  auth: "admin"
10951
11889
  }),
10952
11890
  /**
11891
+ * PERMANENT per-camera mute — the notifications half of the per-camera
11892
+ * function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
11893
+ *
11894
+ * Deliberately NOT a snooze. A snooze is bounded at
11895
+ * {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
11896
+ * expire would be an outage the operator asked for once and forgot" — and
11897
+ * widening it to express "this camera never notifies" would destroy that
11898
+ * property for every snooze. A mute is the other thing: an explicit,
11899
+ * indefinite, admin-only decision, visible in the switch group next to the
11900
+ * other four, and reported on `CameraStatus.switchedOff` so a silent
11901
+ * camera never reads as a working one.
11902
+ *
11903
+ * Returned as ONE list rather than a per-camera query: the group's reader
11904
+ * needs every camera's state, and a per-camera fan-out over the viewer's
11905
+ * single WebSocket is N frames serialised on one socket.
11906
+ */
11907
+ listDeviceMutes: require_sleep.method(zod.z.object({}), zod.z.object({ mutedDeviceIds: zod.z.array(zod.z.number().int()).readonly() }), { auth: "admin" }),
11908
+ /**
11909
+ * Mute or unmute one camera. Idempotent; an unmute of a camera that was
11910
+ * never muted succeeds.
11911
+ */
11912
+ setDeviceMuted: require_sleep.method(zod.z.object({
11913
+ deviceId: zod.z.number().int(),
11914
+ muted: zod.z.boolean()
11915
+ }), zod.z.object({ success: zod.z.literal(true) }), {
11916
+ kind: "mutation",
11917
+ auth: "admin"
11918
+ }),
11919
+ /**
10953
11920
  * Dry-run a rule against recently persisted records (object events for
10954
11921
  * `immediate`, closed tracks for `track-end`). Mutation kind only to
10955
11922
  * carry the full rule object safely; no side effects.
@@ -11367,12 +12334,60 @@ var TrackAudioLabelSchema = zod.z.object({
11367
12334
  });
11368
12335
  /**
11369
12336
  * How a track was produced. `pipeline` (default / absent) = the spatial
11370
- * detection+tracking pipeline. `sensor` = a SYNTHETIC track projected from a
11371
- * linked sensor/control state change (no positions; carries a snapshot). The
11372
- * spatial subsystems (tracker association, occupancy count, re-id/embedding,
11373
- * resurrection) MUST skip `sensor` tracks they have no bbox trajectory.
12337
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection
12338
+ * no positions, a single snapshot, and no bbox trajectory at all:
12339
+ *
12340
+ * - `sensor` — a linked sensor/control device state change.
12341
+ * - `audio` — an audio event on the camera itself that was anomalous for
12342
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
12343
+ *
12344
+ * The spatial subsystems (tracker association, occupancy count, re-id /
12345
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
12346
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
12347
+ * check silently readmits every source added after it was written.
11374
12348
  */
11375
- var TrackSourceSchema = zod.z.enum(["pipeline", "sensor"]);
12349
+ var TrackSourceSchema = zod.z.enum([
12350
+ "pipeline",
12351
+ "sensor",
12352
+ "audio"
12353
+ ]);
12354
+ /**
12355
+ * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
12356
+ * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
12357
+ * so the two surfaces cannot drift.
12358
+ *
12359
+ * **Absent ≠ false.** A track that has never been touched omits the field; an
12360
+ * explicitly un-flagged track carries `false`. Legacy rows written before the
12361
+ * columns existed read as absent, and a consumer that needs a boolean should say
12362
+ * `flag === true`, not `flag !== false`.
12363
+ *
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.
12368
+ */
12369
+ var TrackFlagFields = {
12370
+ /** Operator marked this track as training material. */
12371
+ markForTrain: zod.z.boolean().optional(),
12372
+ /** Operator marked this track for diagnostic attention. */
12373
+ debug: zod.z.boolean().optional()
12374
+ };
12375
+ /**
12376
+ * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
12377
+ * one flag can never clear the other — the toggles are independent and are
12378
+ * driven from three surfaces that do not know about each other.
12379
+ */
12380
+ var TrackFlagsPatchSchema = zod.z.object(TrackFlagFields);
12381
+ /**
12382
+ * The resolved flag state after a write. Both fields are REQUIRED here (absent
12383
+ * collapses to `false`) so a caller can drive a toggle's checked state off the
12384
+ * mutation result without a re-fetch.
12385
+ */
12386
+ var TrackFlagsSchema = zod.z.object({
12387
+ trackId: zod.z.string(),
12388
+ markForTrain: zod.z.boolean(),
12389
+ debug: zod.z.boolean()
12390
+ });
11376
12391
  var TrackSchema = zod.z.object({
11377
12392
  trackId: zod.z.string(),
11378
12393
  deviceId: zod.z.number(),
@@ -11415,7 +12430,8 @@ var TrackSchema = zod.z.object({
11415
12430
  /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
11416
12431
  * Populated from the persisted envelope columns on historical reads;
11417
12432
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
11418
- envelope: TrackEnvelopeSchema.optional()
12433
+ envelope: TrackEnvelopeSchema.optional(),
12434
+ ...TrackFlagFields
11419
12435
  });
11420
12436
  var BaseEventFields = {
11421
12437
  id: zod.z.string(),
@@ -11636,7 +12652,8 @@ var KeyEventSchema = zod.z.object({
11636
12652
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
11637
12653
  bestEventId: zod.z.string(),
11638
12654
  /** Track lifetime in ms (lastSeen - firstSeen). */
11639
- windowMs: zod.z.number().optional()
12655
+ windowMs: zod.z.number().optional(),
12656
+ ...TrackFlagFields
11640
12657
  });
11641
12658
  var TrackedDetectionSchema = zod.z.object({
11642
12659
  trackId: zod.z.string(),
@@ -11721,16 +12738,32 @@ var RebuildObjectEmbeddingsInput = zod.z.object({
11721
12738
  deviceId: zod.z.number().optional(),
11722
12739
  since: zod.z.number().optional(),
11723
12740
  until: zod.z.number().optional(),
12741
+ /** Stop after this many tracks; the result reports whether more remain. */
12742
+ maxTracks: zod.z.number().int().positive().optional(),
12743
+ /**
12744
+ * Run every embedding on THIS node instead of round-robining the fleet.
12745
+ *
12746
+ * Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
12747
+ * field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
12748
+ * calling it that would pin the rebuild REQUEST itself to that node — the
12749
+ * rebuild orchestration lives on the hub, and only the per-track step runs
12750
+ * remotely. This field is data; the per-track pin is applied inside.
12751
+ *
12752
+ * Absent ⇒ round-robin over every online node whose runner can serve the
12753
+ * pinned model.
12754
+ */
12755
+ executeOnNodeId: zod.z.string().optional(),
11724
12756
  /**
11725
- * Fraction added on EACH side of the detection box before cropping.
11726
- * ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
11727
- * the pixel-tight crop the first implementation used.
12757
+ * Milliseconds to wait between tracks; omit for the built-in default, `0` to
12758
+ * run flat out.
12759
+ *
12760
+ * A rebuild is bulk maintenance on hub-main's single thread. Measured
12761
+ * 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
12762
+ * pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
12763
+ * force is logged at start and finish so a deliberately slow pass reads
12764
+ * differently from a stalled one.
11728
12765
  */
11729
- cropMargin: zod.z.number().min(0).max(4).optional(),
11730
- /** Square the window before extracting, so a tall subject is not squashed. */
11731
- square: zod.z.boolean().optional(),
11732
- /** Stop after this many tracks; the result reports whether more remain. */
11733
- maxTracks: zod.z.number().int().positive().optional()
12766
+ pacingMs: zod.z.number().int().nonnegative().optional()
11734
12767
  });
11735
12768
  /**
11736
12769
  * Result of emptying the CLIP index.
@@ -11763,6 +12796,24 @@ var RebuildStatusSchema = zod.z.object({
11763
12796
  missingKeyFrame: zod.z.number(),
11764
12797
  /** Tracks with no usable detection box. */
11765
12798
  missingBbox: zod.z.number(),
12799
+ /**
12800
+ * Tracks an executing node REFUSED rather than broke on — an unreadable key
12801
+ * frame, a step that threw. Separate from `failed` because the remedy is
12802
+ * different, and because a whole camera silently contributing zero vectors
12803
+ * is the shape of failure a rebuild must never hide.
12804
+ */
12805
+ notRunnable: zod.z.number(),
12806
+ /**
12807
+ * The pass stopped because NO node could serve the pinned model.
12808
+ *
12809
+ * Distinct from `notRunnable` on purpose: that one says "this track was
12810
+ * refused", this one says "the cluster cannot do this work at all" — every
12811
+ * candidate node either lacks the `clip-embedding` step, lacks a build of the
12812
+ * pinned model for its engine format, or dropped out. The remedy is a model /
12813
+ * engine change, not a per-camera one. Non-zero here always comes with
12814
+ * `complete: false`.
12815
+ */
12816
+ noCapableNode: zod.z.number(),
11766
12817
  failed: zod.z.number(),
11767
12818
  /** Set once a pass ends: true only when EVERYTHING was covered. */
11768
12819
  complete: zod.z.boolean().nullable(),
@@ -11952,6 +13003,31 @@ var pipelineAnalyticsCapability = {
11952
13003
  auth: "admin"
11953
13004
  }),
11954
13005
  /**
13006
+ * Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
13007
+ * The patch is PARTIAL — an omitted key is left untouched — because the
13008
+ * three surfaces that write it (admin Events grid, viewer track detail,
13009
+ * viewer cluster detail) each own one toggle and must not clobber the other.
13010
+ *
13011
+ * Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
13012
+ * live where `label` and `importance` live, not in any per-device settings
13013
+ * store. Updates the in-RAM active track too, so a flag set on a live track
13014
+ * survives its expiry-time persist.
13015
+ *
13016
+ * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
13017
+ * 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.
13020
+ *
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.
13023
+ */
13024
+ setTrackFlags: require_sleep.method(zod.z.object({
13025
+ /** Log/audit scope only — the trackId is globally unique on its own. */
13026
+ deviceId: zod.z.number(),
13027
+ trackId: zod.z.string(),
13028
+ flags: TrackFlagsPatchSchema
13029
+ }), TrackFlagsSchema, { kind: "mutation" }),
13030
+ /**
11955
13031
  * Durable event-store footprint for the management UI: event rows
11956
13032
  * (motion + object + audio) counted per camera + total, plus the
11957
13033
  * event-owned media bytes on disk per camera + total. Stat/count-based,
@@ -12840,6 +13916,53 @@ var DetailResultSchema = zod.z.object({
12840
13916
  nativeFaceShortSidePx: zod.z.number().optional()
12841
13917
  });
12842
13918
  /**
13919
+ * Why an executing node REFUSED a stateless step run (`runStatelessStep`).
13920
+ *
13921
+ * A refusal is a first-class answer, not an error, because the caller's next
13922
+ * move depends on WHICH one it is — and because "the pass produced nothing"
13923
+ * must never be reachable without a named, counted cause. The two tiers:
13924
+ *
13925
+ * - **node-level** (`unknown-step`, `model-not-servable`) — this node can
13926
+ * never serve this (step, model) pair. The caller drops it from its rotation
13927
+ * and retries the same work elsewhere; nothing about the work changes.
13928
+ * - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
13929
+ * fine, this one request is not. Retrying it on another node would only
13930
+ * spread the same failure.
13931
+ */
13932
+ var StatelessStepRefusalSchema = zod.z.enum([
13933
+ "unknown-step",
13934
+ "model-not-servable",
13935
+ "unreadable-frame",
13936
+ "execution-failed"
13937
+ ]);
13938
+ /**
13939
+ * Answer to `runStatelessStep` — a discriminated union rather than a nullable
13940
+ * result, because `null` is exactly what made the camera-bound detail path
13941
+ * unable to tell "refused" from "never asked".
13942
+ */
13943
+ var RunStatelessStepResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
13944
+ kind: zod.z.literal("ran"),
13945
+ /** The node that actually executed it — the pin, echoed back for the log. */
13946
+ nodeId: zod.z.string(),
13947
+ /**
13948
+ * The model the step ran with.
13949
+ *
13950
+ * The node verified this exact id has a build for the format it dispatched
13951
+ * on BEFORE running, so the executor's format resolution returns it
13952
+ * unchanged. A caller that pinned a model must compare this field and
13953
+ * treat a mismatch as a refusal — the whole point of the pin is that a
13954
+ * pass writes one feature space.
13955
+ */
13956
+ modelId: zod.z.string(),
13957
+ details: zod.z.array(DetailResultSchema)
13958
+ }), zod.z.object({
13959
+ kind: zod.z.literal("refused"),
13960
+ nodeId: zod.z.string(),
13961
+ reason: StatelessStepRefusalSchema,
13962
+ /** Human-readable specifics — the format tried, the formats shipped, etc. */
13963
+ detail: zod.z.string()
13964
+ })]);
13965
+ /**
12843
13966
  * Per-camera tunable ranges + defaults. Single source of truth used
12844
13967
  * by both the Zod data schema (validation + default fallback) and
12845
13968
  * the device settings UI (slider min/max/step). Touch one place and
@@ -13382,20 +14505,118 @@ var pipelineRunnerCapability = {
13382
14505
  * for a single tracked detection. The per-frame plane (`runPipeline`
13383
14506
  * with `plane: 'frame'`) skips crop children entirely; a track-level
13384
14507
  * caller invokes this per-track, on its own cadence, instead of on
13385
- * every frame. Takes either a `frameHandle` (shm lease/session
13386
- * preferred, zero-copy) or a `cropJpeg` fallback when the lease/
13387
- * session backing the frame is already gone. `steps` narrows which
13388
- * configured children to run (default: all configured children for
13389
- * `parent.className`). Returns `null` when neither frame source is
13390
- * resolvable (handle evicted and no cropJpeg fallback supplied).
14508
+ * every frame. `steps` narrows which configured children to run
14509
+ * (default: all configured children for `parent.className`).
14510
+ *
14511
+ * ## Three pixel sources, and WHO decides the rectangle
14512
+ *
14513
+ * Tried in this order; the difference between them is not the pixels but
14514
+ * which side derives the crop, and confusing the two is how the CLIP index
14515
+ * ended up holding two incomparable feature spaces:
14516
+ *
14517
+ * 1. `frameHandle` — shm lease/session, preferred and zero-copy. **The
14518
+ * runner cuts**, applying the cluster crop convention.
14519
+ * 2. `frameJpeg` — a FULL FRAME supplied by the caller (the embedding
14520
+ * rebuild's stored key frame). **The runner cuts**, applying the same
14521
+ * convention to the same function, so a rebuilt vector lands in the same
14522
+ * feature space as a live one.
14523
+ * 3. `cropJpeg` — an ALREADY-CUT tile. **The caller decided the
14524
+ * rectangle**; the runner applies NO padding and no squaring and feeds
14525
+ * it to the model verbatim. Only for the lease-miss retry, where the
14526
+ * caller holds pixels the runner can no longer reach.
14527
+ *
14528
+ * Returns `null` when no source resolves (handle evicted and no fallback
14529
+ * supplied), when the camera is not attached, or when no enabled step
14530
+ * matches — every one of those is logged on the runner with the deviceId.
13391
14531
  */
13392
14532
  runDetailSubtree: require_sleep.method(zod.z.object({
13393
14533
  deviceId: zod.z.number(),
13394
14534
  frameHandle: require_sleep.FrameHandleSchema.optional(),
14535
+ /**
14536
+ * FULL FRAME (base64 JPEG). The runner derives the crop rectangle from
14537
+ * `parent.bbox` with the cluster crop convention and cuts it itself —
14538
+ * do NOT pre-crop for this field, that is what `cropJpeg` is.
14539
+ */
14540
+ frameJpeg: zod.z.string().optional(),
14541
+ /**
14542
+ * PRE-CUT tile (base64 JPEG), used verbatim — NO padding is applied.
14543
+ * The fallback when the lease/session backing the frame is gone and the
14544
+ * caller already holds a crop.
14545
+ */
13395
14546
  cropJpeg: zod.z.string().optional(),
13396
14547
  parent: DetailParentSchema,
13397
14548
  steps: zod.z.array(zod.z.string()).optional()
13398
- }), zod.z.object({ details: zod.z.array(DetailResultSchema) }).nullable(), { kind: "mutation" })
14549
+ }), zod.z.object({ details: zod.z.array(DetailResultSchema) }).nullable(), { kind: "mutation" }),
14550
+ /**
14551
+ * Run ONE enrichment step against caller-supplied pixels, with NO camera
14552
+ * session — no attach, no frame handle, no device affinity.
14553
+ *
14554
+ * ## Why this exists next to `runDetailSubtree` and not inside it
14555
+ *
14556
+ * `runDetailSubtree` resolves its step tree from the LIVE ATTACH STATE. That
14557
+ * is correct for it: its reason to exist is a frame HANDLE, and a handle is
14558
+ * only resolvable while a decode session is open. But it makes the method
14559
+ * useless to anything walking history — cameras run `detectionMode:
14560
+ * 'on-motion'` and detach ~30 s after motion stops, so a fleet-wide
14561
+ * embedding rebuild found most cameras detached and rebuilt only whatever
14562
+ * happened to be attached at that second (measured 2026-08-06: 1125 tracks
14563
+ * scanned, 121 rebuilt, 601 refused as "camera is not attached").
14564
+ *
14565
+ * A caller that ships its own pixels needs none of that machinery. So this
14566
+ * method takes the three things a step actually needs — an image, a box, a
14567
+ * step id — resolves the step from the CATALOG rather than from an
14568
+ * attachment, and runs it on whichever node the caller pinned.
14569
+ *
14570
+ * ## What it deliberately keeps
14571
+ *
14572
+ * The crop rectangle is derived by the SAME `deriveDetailCropRect` call the
14573
+ * live detail plane uses, from the same cluster-wide convention
14574
+ * ([D52](../../../docs/decisions/adr-0052.md)). `frameJpeg` is a FULL frame,
14575
+ * never a pre-cut tile, for exactly the reason `runDetailSubtree` documents:
14576
+ * a caller that cuts is a second feature space.
14577
+ *
14578
+ * ## What it deliberately drops
14579
+ *
14580
+ * No device jump and no `deviceKey`: the jump roster comes from the attach
14581
+ * config, which does not exist here. The step runs on the node's default
14582
+ * engine. This is a maintenance path, not a latency-sensitive one.
14583
+ *
14584
+ * ## Routing
14585
+ *
14586
+ * NOT device-bound. `sourceDeviceId` is DIAGNOSTIC — the camera whose track
14587
+ * these pixels came from, so every log line on the executing node can carry
14588
+ * `tags: { deviceId }`. It is deliberately not named `deviceId`: that field
14589
+ * is a routing hint that would send the call to the camera's owning node,
14590
+ * which is the placement constraint this method exists to remove. Callers
14591
+ * choose the node with `nodePin(nodeId)`; unpinned, an unowned call is
14592
+ * served hub-in-process like any other singleton.
14593
+ */
14594
+ runStatelessStep: require_sleep.method(zod.z.object({
14595
+ /** Catalog step id, e.g. `clip-embedding`. */
14596
+ stepId: zod.z.string(),
14597
+ /**
14598
+ * REQUIRED model pin. The node runs this exact model or refuses with
14599
+ * `model-not-servable` — it never substitutes a format default, because
14600
+ * a fleet pass that round-robins across nodes would then fill one index
14601
+ * from several encoders.
14602
+ */
14603
+ modelId: zod.z.string(),
14604
+ /** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
14605
+ frameJpeg: zod.z.string(),
14606
+ /**
14607
+ * The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
14608
+ * purpose: the caller stores boxes against a downscaled analysis frame
14609
+ * while the stored key frame is native-resolution, and the only side
14610
+ * that reliably knows the image's pixel dimensions is the side that
14611
+ * decodes it. Denormalising here removes a second reader of the
14612
+ * dimensions and the class of mismatch that comes with it.
14613
+ */
14614
+ bbox: NativeCropBboxSchema,
14615
+ /** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
14616
+ className: zod.z.string(),
14617
+ /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
14618
+ sourceDeviceId: zod.z.number()
14619
+ }), RunStatelessStepResultSchema, { kind: "mutation" })
13399
14620
  }
13400
14621
  };
13401
14622
  //#endregion
@@ -13705,6 +14926,20 @@ var CameraStatusSchema = zod.z.object({
13705
14926
  detection: CameraDetectionStatusSchema.nullable(),
13706
14927
  audio: CameraAudioStatusSchema.nullable(),
13707
14928
  recording: CameraRecordingStatusSchema.nullable(),
14929
+ /**
14930
+ * Per-camera function switches an OPERATOR has turned off
14931
+ * ([D61](../../../../docs/decisions/adr-0067.md)).
14932
+ *
14933
+ * This is the difference between DISABLED and BROKEN. A camera whose
14934
+ * `detection` block reports zero fps and whose `switchedOff` contains
14935
+ * `'object-detection'` was switched off by a person; the same camera with an
14936
+ * empty list is failing. Every status surface must render the two
14937
+ * differently — a quiet camera that looks identical to a dead one is the
14938
+ * silence-reads-as-never-happened trap this repo keeps paying for.
14939
+ *
14940
+ * Empty when nothing is off. Never contains a switch no provider offers.
14941
+ */
14942
+ switchedOff: zod.z.array(CameraSwitchIdSchema).readonly(),
13708
14943
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
13709
14944
  fetchedAt: zod.z.number()
13710
14945
  });
@@ -14104,6 +15339,43 @@ var pipelineOrchestratorCapability = {
14104
15339
  agentNodeId: zod.z.string().optional()
14105
15340
  }), CameraPipelineConfigSchema),
14106
15341
  /**
15342
+ * The whole per-camera function switch group, DERIVED — never a stored
15343
+ * list ([D61](../../../../docs/decisions/adr-0067.md)).
15344
+ *
15345
+ * The group adds no state. Each switch is a view onto the authority that
15346
+ * already owned it (`deviceManager.setDisabled`,
15347
+ * `deviceManager.setWrapperActive`, `RecordingConfig.enabled`,
15348
+ * `notificationRules.setDeviceMuted`), and `switch.authority` says which.
15349
+ * Availability comes from `deviceManager.listBindableCapsForDeviceType`,
15350
+ * so a deployment with no audio analyzer renders no audio switch.
15351
+ *
15352
+ * `auth: 'view'` deliberately — a NON-admin must be able to see that a
15353
+ * camera is quiet because somebody switched it off. Only the mutation is
15354
+ * admin-gated.
15355
+ */
15356
+ getCameraSwitches: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), CameraSwitchGroupSchema),
15357
+ /**
15358
+ * Flip ONE switch, routed to its existing authority.
15359
+ *
15360
+ * Never writes a parallel map: `recording` patches `RecordingConfig.enabled`
15361
+ * and leaves `bands` byte-identical (clearing bands to express "off"
15362
+ * destroys the operator's authored schedule and turning the camera back on
15363
+ * would silently record nothing), and the two pipeline switches write the
15364
+ * SAME wrapper binding the legacy `pipelineEnabled` / `audioEnabled`
15365
+ * booleans were migrated onto.
15366
+ *
15367
+ * Rejects a switch this camera does not offer rather than persisting a
15368
+ * write nothing reads.
15369
+ */
15370
+ setCameraSwitch: require_sleep.method(zod.z.object({
15371
+ deviceId: zod.z.number(),
15372
+ switchId: CameraSwitchIdSchema,
15373
+ enabled: zod.z.boolean()
15374
+ }), CameraSwitchGroupSchema, {
15375
+ kind: "mutation",
15376
+ auth: "admin"
15377
+ }),
15378
+ /**
14107
15379
  * Server-composed aggregated status for a single camera.
14108
15380
  *
14109
15381
  * Fans out in parallel (bounded, per-stage graceful degradation) to
@@ -30124,6 +31396,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30124
31396
  addonId: null,
30125
31397
  access: "view"
30126
31398
  },
31399
+ "notificationRules.listDeviceMutes": {
31400
+ capName: "notification-rules",
31401
+ capScope: "system",
31402
+ addonId: null,
31403
+ access: "view"
31404
+ },
30127
31405
  "notificationRules.listRules": {
30128
31406
  capName: "notification-rules",
30129
31407
  capScope: "system",
@@ -30142,6 +31420,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30142
31420
  addonId: null,
30143
31421
  access: "create"
30144
31422
  },
31423
+ "notificationRules.setDeviceMuted": {
31424
+ capName: "notification-rules",
31425
+ capScope: "system",
31426
+ addonId: null,
31427
+ access: "create"
31428
+ },
30145
31429
  "notificationRules.setRuleEnabled": {
30146
31430
  capName: "notification-rules",
30147
31431
  capScope: "system",
@@ -30418,6 +31702,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30418
31702
  addonId: null,
30419
31703
  access: "view"
30420
31704
  },
31705
+ "pipelineAnalytics.setTrackFlags": {
31706
+ capName: "pipeline-analytics",
31707
+ capScope: "device",
31708
+ addonId: null,
31709
+ access: "create"
31710
+ },
30421
31711
  "pipelineAnalytics.wipeAllAnalytics": {
30422
31712
  capName: "pipeline-analytics",
30423
31713
  capScope: "device",
@@ -30724,6 +32014,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30724
32014
  addonId: null,
30725
32015
  access: "view"
30726
32016
  },
32017
+ "pipelineOrchestrator.getCameraSwitches": {
32018
+ capName: "pipeline-orchestrator",
32019
+ capScope: "system",
32020
+ addonId: null,
32021
+ access: "view"
32022
+ },
30727
32023
  "pipelineOrchestrator.getCapabilityBindings": {
30728
32024
  capName: "pipeline-orchestrator",
30729
32025
  capScope: "system",
@@ -30856,6 +32152,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30856
32152
  addonId: null,
30857
32153
  access: "create"
30858
32154
  },
32155
+ "pipelineOrchestrator.setCameraSwitch": {
32156
+ capName: "pipeline-orchestrator",
32157
+ capScope: "system",
32158
+ addonId: null,
32159
+ access: "create"
32160
+ },
30859
32161
  "pipelineOrchestrator.setCapabilityBinding": {
30860
32162
  capName: "pipeline-orchestrator",
30861
32163
  capScope: "system",
@@ -30946,6 +32248,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30946
32248
  addonId: null,
30947
32249
  access: "create"
30948
32250
  },
32251
+ "pipelineRunner.runStatelessStep": {
32252
+ capName: "pipeline-runner",
32253
+ capScope: "system",
32254
+ addonId: null,
32255
+ access: "create"
32256
+ },
30949
32257
  "plateGallery.assignPlate": {
30950
32258
  capName: "plate-gallery",
30951
32259
  capScope: "system",
@@ -31762,6 +33070,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
31762
33070
  addonId: null,
31763
33071
  access: "create"
31764
33072
  },
33073
+ "streamBroker.acquireEgressTranscode": {
33074
+ capName: "stream-broker",
33075
+ capScope: "system",
33076
+ addonId: null,
33077
+ access: "create"
33078
+ },
31765
33079
  "streamBroker.assignProfile": {
31766
33080
  capName: "stream-broker",
31767
33081
  capScope: "system",
@@ -31870,6 +33184,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
31870
33184
  addonId: null,
31871
33185
  access: "create"
31872
33186
  },
33187
+ "streamBroker.releaseEgressTranscode": {
33188
+ capName: "stream-broker",
33189
+ capScope: "system",
33190
+ addonId: null,
33191
+ access: "create"
33192
+ },
31873
33193
  "streamBroker.releaseStreamWithCodec": {
31874
33194
  capName: "stream-broker",
31875
33195
  capScope: "system",
@@ -33282,7 +34602,8 @@ function createSystemProxy(api) {
33282
34602
  getLocalMetrics: (input) => dispatch("pipelineRunner", "getLocalMetrics", "query", input),
33283
34603
  getAllCameraMetrics: (input) => dispatch("pipelineRunner", "getAllCameraMetrics", "query", input),
33284
34604
  getLocalCameras: (input) => dispatch("pipelineRunner", "getLocalCameras", "query", input),
33285
- getNativeCrop: (input) => dispatch("pipelineRunner", "getNativeCrop", "query", input)
34605
+ getNativeCrop: (input) => dispatch("pipelineRunner", "getNativeCrop", "query", input),
34606
+ runStatelessStep: (input) => dispatch("pipelineRunner", "runStatelessStep", "mutation", input)
33286
34607
  },
33287
34608
  plateGallery: {
33288
34609
  getPlateMedia: (input) => dispatch("plateGallery", "getPlateMedia", "query", input),
@@ -33369,6 +34690,8 @@ function createSystemProxy(api) {
33369
34690
  getStreamUrl: (input) => dispatch("streamBroker", "getStreamUrl", "query", input),
33370
34691
  getStreamWithCodec: (input) => dispatch("streamBroker", "getStreamWithCodec", "mutation", input),
33371
34692
  releaseStreamWithCodec: (input) => dispatch("streamBroker", "releaseStreamWithCodec", "mutation", input),
34693
+ acquireEgressTranscode: (input) => dispatch("streamBroker", "acquireEgressTranscode", "mutation", input),
34694
+ releaseEgressTranscode: (input) => dispatch("streamBroker", "releaseEgressTranscode", "mutation", input),
33372
34695
  subscribeAudioChunks: (input) => dispatch("streamBroker", "subscribeAudioChunks", "mutation", input),
33373
34696
  pullAudioChunks: (input) => dispatch("streamBroker", "pullAudioChunks", "query", input),
33374
34697
  unsubscribeAudioChunks: (input) => dispatch("streamBroker", "unsubscribeAudioChunks", "mutation", input),
@@ -34881,6 +36204,202 @@ function pointInPolygon(point, polygon) {
34881
36204
  return inside;
34882
36205
  }
34883
36206
  //#endregion
36207
+ //#region src/pipeline/detail-crop.ts
36208
+ /**
36209
+ * THE detail-crop convention — the single derivation of the rectangle a
36210
+ * detail/enrichment step (clip-embedding, face-detection, plate-detection…)
36211
+ * is fed.
36212
+ *
36213
+ * ## Why this is one module and not two constants
36214
+ *
36215
+ * `object-clip` is ONE vector index, and cosine similarity is only meaningful
36216
+ * between vectors produced from the same crop convention. Two encode paths
36217
+ * write into it — the live detail plane and the embedding rebuild — and they
36218
+ * used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
36219
+ * with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
36220
+ * by default on the other. Every rebuild therefore poured a second, silently
36221
+ * incomparable feature space into the index it exists to keep consistent.
36222
+ *
36223
+ * So the rectangle is derived HERE, once, from ONE convention value. Both
36224
+ * paths now reach this function through `pipelineRunner.runDetailSubtree` —
36225
+ * the runner is the only process that cuts (see `detail-subtree.ts`), and the
36226
+ * convention is a cluster-global `pipeline-orchestrator` setting. There is
36227
+ * deliberately no per-node or per-device scope: a per-accelerator crop margin
36228
+ * would reintroduce the same split, merely relocated.
36229
+ *
36230
+ * ## The default IS the live convention
36231
+ *
36232
+ * {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
36233
+ * been storing (0.15, no squaring). Anything else would invalidate every
36234
+ * vector already in the index on the day it shipped. Changing the convention
36235
+ * is legitimate — that is what the operator knob is for — but it must be
36236
+ * followed by a rebuild, which is now guaranteed to produce crops from this
36237
+ * same function.
36238
+ */
36239
+ /**
36240
+ * Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
36241
+ * (cluster-wide) settings.
36242
+ *
36243
+ * These live here rather than in the orchestrator because the reader is a
36244
+ * different addon — the pipeline runner, over the hub-routed `addon-settings`
36245
+ * cap. Addons never import each other, so a key owned by the writer would have
36246
+ * to be hand-copied by the reader, and a hand-copied key is how a setting
36247
+ * silently stops arriving while both sides still look correct.
36248
+ */
36249
+ var DETAIL_CROP_SECTION_ID = "detail-crop";
36250
+ var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
36251
+ var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
36252
+ /**
36253
+ * Operator-tunable crop convention. Single-valued and cluster-wide — see the
36254
+ * module docblock for why it cannot be scoped per node or per device.
36255
+ */
36256
+ var DetailCropConventionSchema = zod.z.object({
36257
+ /**
36258
+ * Fraction of the box's own size added on EACH side before cutting.
36259
+ *
36260
+ * CLIP is trained on natural images WITH surroundings; a pixel-tight crop
36261
+ * removes exactly the context it is strongest on (a dog cut to its outline
36262
+ * is a dark blob). The right value is an empirical question, which is why it
36263
+ * is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
36264
+ */
36265
+ paddingRatio: zod.z.number().min(0).max(4),
36266
+ /**
36267
+ * Square the window (in PIXELS) before cutting.
36268
+ *
36269
+ * CLIP's input is square, so a tall bbox resized straight to NxN is squashed
36270
+ * — a standing person becomes a shape the model never saw. Squaring costs
36271
+ * extra background, which is context the model wants anyway. Off by default
36272
+ * because the live path has never squared and the stored index reflects that.
36273
+ */
36274
+ square: zod.z.boolean()
36275
+ });
36276
+ /**
36277
+ * The convention in force when nobody has configured one — byte-for-byte the
36278
+ * behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
36279
+ */
36280
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
36281
+ paddingRatio: .15,
36282
+ square: false
36283
+ };
36284
+ /**
36285
+ * Narrow a FLAT settings record to the convention.
36286
+ *
36287
+ * Per-FIELD fallback, deliberately: a junk padding must not also discard a
36288
+ * valid squaring choice. An absent or invalid value resolves to
36289
+ * {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
36290
+ * rather than to a clamped number nobody chose, so a bad read can never
36291
+ * quietly change what the stored vectors mean.
36292
+ */
36293
+ function readDetailCropConvention(config) {
36294
+ const paddingRatio = DetailCropConventionSchema.shape.paddingRatio.safeParse(config[DETAIL_CROP_PADDING_KEY]);
36295
+ const square = DetailCropConventionSchema.shape.square.safeParse(config[DETAIL_CROP_SQUARE_KEY]);
36296
+ return {
36297
+ paddingRatio: paddingRatio.success ? paddingRatio.data : DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio,
36298
+ square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
36299
+ };
36300
+ }
36301
+ function isHydratedField(entry) {
36302
+ return typeof entry === "object" && entry !== null && "key" in entry;
36303
+ }
36304
+ /**
36305
+ * Extract the convention from an `addon-settings.getGlobalSettings` payload.
36306
+ *
36307
+ * Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
36308
+ * alone: the keys are unique across the addon's schema, and a section rename
36309
+ * must not silently revert the whole cluster to the default. A `null` payload
36310
+ * (addon mid-boot) is the default convention.
36311
+ */
36312
+ function pickDetailCropConvention(view) {
36313
+ if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
36314
+ const flat = {};
36315
+ for (const section of view.sections) for (const entry of section.fields) {
36316
+ if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
36317
+ if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
36318
+ }
36319
+ return readDetailCropConvention(flat);
36320
+ }
36321
+ /** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
36322
+ var DETAIL_CROP_PADDING_FIELD = {
36323
+ min: 0,
36324
+ max: 1,
36325
+ step: .05,
36326
+ default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
36327
+ };
36328
+ /**
36329
+ * Derive the crop rectangle for one parent detection.
36330
+ *
36331
+ * Order: pad by `paddingRatio` of the box's own size → optionally square in
36332
+ * pixel space around the padded centre → keep it inside the frame. Pure:
36333
+ * always returns a new rect and never mutates `bbox`.
36334
+ *
36335
+ * Edge handling differs by mode, on purpose:
36336
+ *
36337
+ * - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
36338
+ * path has always done (`padAndClampFrameBbox`). A subject against the edge
36339
+ * gets a slightly smaller window. Changing this would silently reinterpret
36340
+ * every edge-touching vector already in the index.
36341
+ * - **squared** — SLID inward instead, because a truncated square is not
36342
+ * square and squaring exists precisely to preserve the aspect the model
36343
+ * sees. It only shrinks when the square is larger than the frame itself.
36344
+ */
36345
+ function deriveDetailCropRect(bbox, frameWidth, frameHeight, convention) {
36346
+ const padX = convention.paddingRatio * bbox.w;
36347
+ const padY = convention.paddingRatio * bbox.h;
36348
+ const padded = {
36349
+ x: bbox.x - padX,
36350
+ y: bbox.y - padY,
36351
+ w: bbox.w + 2 * padX,
36352
+ h: bbox.h + 2 * padY
36353
+ };
36354
+ return convention.square ? slideInsideFrame(squareInPixels(padded, frameWidth, frameHeight), frameWidth, frameHeight) : truncateToFrame(padded, frameWidth, frameHeight);
36355
+ }
36356
+ /**
36357
+ * Grow the shorter side to the longer one around the window's centre, bounded
36358
+ * by the frame's shorter side — a square larger than the frame cannot exist,
36359
+ * and collapsing to the frame's short side is the most that does.
36360
+ */
36361
+ function squareInPixels(rect, frameWidth, frameHeight) {
36362
+ const side = Math.min(Math.max(rect.w, rect.h), Math.min(frameWidth, frameHeight));
36363
+ const cx = rect.x + rect.w / 2;
36364
+ const cy = rect.y + rect.h / 2;
36365
+ return {
36366
+ x: cx - side / 2,
36367
+ y: cy - side / 2,
36368
+ w: side,
36369
+ h: side
36370
+ };
36371
+ }
36372
+ /**
36373
+ * Cut the window at the frame border — the pre-unification live behaviour,
36374
+ * preserved exactly so unsquared crops keep matching the stored index.
36375
+ */
36376
+ function truncateToFrame(rect, frameWidth, frameHeight) {
36377
+ const x1 = Math.max(0, rect.x);
36378
+ const y1 = Math.max(0, rect.y);
36379
+ const x2 = Math.min(frameWidth, rect.x + rect.w);
36380
+ const y2 = Math.min(frameHeight, rect.y + rect.h);
36381
+ return {
36382
+ x: x1,
36383
+ y: y1,
36384
+ w: Math.max(0, x2 - x1),
36385
+ h: Math.max(0, y2 - y1)
36386
+ };
36387
+ }
36388
+ /**
36389
+ * Move the window inside the frame keeping its extent — used only for squared
36390
+ * windows, where truncating would destroy the squareness that is the point.
36391
+ */
36392
+ function slideInsideFrame(rect, frameWidth, frameHeight) {
36393
+ const w = Math.max(0, Math.min(rect.w, frameWidth));
36394
+ const h = Math.max(0, Math.min(rect.h, frameHeight));
36395
+ return {
36396
+ x: Math.min(Math.max(0, rect.x), Math.max(0, frameWidth - w)),
36397
+ y: Math.min(Math.max(0, rect.y), Math.max(0, frameHeight - h)),
36398
+ w,
36399
+ h
36400
+ };
36401
+ }
36402
+ //#endregion
34884
36403
  //#region src/helpers/bind-addon-actions.ts
34885
36404
  /**
34886
36405
  * Bind an addon's custom-action catalog to its tRPC surface, returning a
@@ -35133,10 +36652,13 @@ function enumerateInferenceDevices(hw) {
35133
36652
  }
35134
36653
  //#endregion
35135
36654
  exports.ACCESSORY_LABEL = ACCESSORY_LABEL;
36655
+ exports.ALEXA_EGRESS_PROFILE = ALEXA_EGRESS_PROFILE;
35136
36656
  exports.ALL_CAPABILITY_DEFINITIONS = ALL_CAPABILITY_DEFINITIONS;
35137
36657
  exports.APPLE_SA_TO_MACRO = APPLE_SA_TO_MACRO;
36658
+ exports.AUDIO_ANALYSIS_CAP_NAME = AUDIO_ANALYSIS_CAP_NAME;
35138
36659
  exports.AUDIO_BACKEND_CHOICES = AUDIO_BACKEND_CHOICES;
35139
36660
  exports.AUDIO_MACRO_LABELS = AUDIO_MACRO_LABELS;
36661
+ exports.AUDIO_PRESETS = AUDIO_PRESETS;
35140
36662
  exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
35141
36663
  exports.AccessoryKind = AccessoryKind;
35142
36664
  exports.AddBrokerInputSchema = AddBrokerInputSchema;
@@ -35190,6 +36712,7 @@ exports.AutoUpdateSettingsSchema = AutoUpdateSettingsSchema;
35190
36712
  exports.AutomationControlStatusSchema = AutomationControlStatusSchema;
35191
36713
  exports.AvailableIntegrationTypeSchema = AvailableIntegrationTypeSchema;
35192
36714
  exports.BACKEND_TO_FORMAT = BACKEND_TO_FORMAT;
36715
+ exports.BASE_LIVE_EGRESS_PROFILE = BASE_LIVE_EGRESS_PROFILE;
35193
36716
  exports.BATTERY_DEVICE_PROFILE = BATTERY_DEVICE_PROFILE;
35194
36717
  exports.BacklightModeSchema = BacklightModeSchema;
35195
36718
  exports.BackupDestinationInfoSchema = BackupDestinationInfoSchema;
@@ -35222,6 +36745,8 @@ exports.BrokerSubscribeInputSchema = SubscribeInputSchema;
35222
36745
  exports.BrokerSubscribeResultSchema = SubscribeResultSchema;
35223
36746
  exports.BrokerTestConnectionResultSchema = TestConnectionResultSchema;
35224
36747
  exports.BrokerUnsubscribeInputSchema = UnsubscribeInputSchema;
36748
+ exports.CAMERA_SWITCH_CATALOG = CAMERA_SWITCH_CATALOG;
36749
+ exports.CAMERA_SWITCH_ORDER = CAMERA_SWITCH_ORDER;
35225
36750
  exports.CAM_PROFILE_ORDER = require_sleep.CAM_PROFILE_ORDER;
35226
36751
  exports.CAPABILITY_NAMES = CAPABILITY_NAMES;
35227
36752
  exports.CAPABILITY_ROUTER_KEYS = CAPABILITY_ROUTER_KEYS;
@@ -35255,6 +36780,11 @@ exports.CameraSourceStatusSchema = CameraSourceStatusSchema;
35255
36780
  exports.CameraSourceStreamSchema = CameraSourceStreamSchema;
35256
36781
  exports.CameraStatusSchema = CameraStatusSchema;
35257
36782
  exports.CameraStreamSchema = require_sleep.CameraStreamSchema;
36783
+ exports.CameraSwitchAuthoritySchema = CameraSwitchAuthoritySchema;
36784
+ exports.CameraSwitchGroupSchema = CameraSwitchGroupSchema;
36785
+ exports.CameraSwitchIdSchema = CameraSwitchIdSchema;
36786
+ exports.CameraSwitchSchema = CameraSwitchSchema;
36787
+ exports.CameraSwitchUnavailableReasonSchema = CameraSwitchUnavailableReasonSchema;
35258
36788
  exports.CandidateQueryFilterSchema = CandidateQueryFilterSchema;
35259
36789
  exports.CapScopeSchema = CapScopeSchema;
35260
36790
  exports.CapabilityBindingsSchema = CapabilityBindingsSchema;
@@ -35300,10 +36830,16 @@ exports.DATAPLANE_SECRET_HEADER = require_sleep.DATAPLANE_SECRET_HEADER;
35300
36830
  exports.DEFAULT_ADDON_PLACEMENT = DEFAULT_ADDON_PLACEMENT;
35301
36831
  exports.DEFAULT_AUDIO_ANALYZER_CONFIG = DEFAULT_AUDIO_ANALYZER_CONFIG;
35302
36832
  exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
36833
+ exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
35303
36834
  exports.DEFAULT_EVENT_COLOR = DEFAULT_EVENT_COLOR;
35304
36835
  exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
35305
36836
  exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
35306
36837
  exports.DEFAULT_SCRUB_THUMBNAIL_PRESET = DEFAULT_SCRUB_THUMBNAIL_PRESET;
36838
+ exports.DETAIL_CROP_PADDING_FIELD = DETAIL_CROP_PADDING_FIELD;
36839
+ exports.DETAIL_CROP_PADDING_KEY = DETAIL_CROP_PADDING_KEY;
36840
+ exports.DETAIL_CROP_SECTION_ID = DETAIL_CROP_SECTION_ID;
36841
+ exports.DETAIL_CROP_SQUARE_KEY = DETAIL_CROP_SQUARE_KEY;
36842
+ exports.DETECTION_PIPELINE_CAP_NAME = DETECTION_PIPELINE_CAP_NAME;
35307
36843
  exports.DEVICE_BACKEND_TO_FORMAT = DEVICE_BACKEND_TO_FORMAT;
35308
36844
  exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
35309
36845
  exports.DEVICE_PROFILES = DEVICE_PROFILES;
@@ -35323,6 +36859,7 @@ exports.DecodedFrameSchema = require_sleep.DecodedFrameSchema;
35323
36859
  exports.DecoderSessionConfigSchema = DecoderSessionConfigSchema;
35324
36860
  exports.DecoderStatsSchema = DecoderStatsSchema;
35325
36861
  exports.DeleteIntegrationResultSchema = DeleteIntegrationResultSchema;
36862
+ exports.DetailCropConventionSchema = DetailCropConventionSchema;
35326
36863
  exports.DetectionSourceSchema = DetectionSourceSchema;
35327
36864
  exports.DeviceCodeSeveritySchema = DeviceCodeSeveritySchema;
35328
36865
  exports.DeviceConfig = DeviceConfig;
@@ -35395,6 +36932,10 @@ exports.FrameInputSchema = FrameInputSchema;
35395
36932
  exports.GasStatusSchema = GasStatusSchema;
35396
36933
  exports.GetStreamWithCodecInputSchema = GetStreamWithCodecInputSchema;
35397
36934
  exports.GlobalMetricsSchema = GlobalMetricsSchema;
36935
+ exports.HAP_AUDIO_BASE = HAP_AUDIO_BASE;
36936
+ exports.HAP_AUDIO_BITRATE_KBPS = HAP_AUDIO_BITRATE_KBPS;
36937
+ exports.HAP_AUDIO_VBV_KBITS = HAP_AUDIO_VBV_KBITS;
36938
+ exports.HAP_KEYFRAME_INTERVAL_SEC = HAP_KEYFRAME_INTERVAL_SEC;
35398
36939
  exports.HF_BASE_URL = HF_BASE_URL;
35399
36940
  exports.HF_REPO = HF_REPO;
35400
36941
  exports.HWACCEL_OPTIONS = HWACCEL_OPTIONS;
@@ -35628,6 +37169,8 @@ exports.PtzPositionSchema = PtzPositionSchema;
35628
37169
  exports.PtzPresetSchema = PtzPresetSchema;
35629
37170
  exports.PtzStatusSchema = PtzStatusSchema;
35630
37171
  exports.QueryFilterSchema = QueryFilterSchema;
37172
+ exports.RATE_CONTROL_RELAXED = RATE_CONTROL_RELAXED;
37173
+ exports.RATE_CONTROL_TIGHT = RATE_CONTROL_TIGHT;
35631
37174
  exports.REACHABILITY_FAILURES_TO_OFFLINE = REACHABILITY_FAILURES_TO_OFFLINE;
35632
37175
  exports.REACHABILITY_POLL_INTERVAL_MS = REACHABILITY_POLL_INTERVAL_MS;
35633
37176
  exports.REACHABILITY_PROBE_TIMEOUT_MS = REACHABILITY_PROBE_TIMEOUT_MS;
@@ -35754,6 +37297,8 @@ exports.SystemMetricsSchema = SystemMetricsSchema;
35754
37297
  exports.SystemMirror = SystemMirror;
35755
37298
  exports.TAXONOMY_COLORS = TAXONOMY_COLORS;
35756
37299
  exports.TIMEZONES = TIMEZONES;
37300
+ exports.TRANSCODE_DOWN_MAX_BITRATE_KBPS = TRANSCODE_DOWN_MAX_BITRATE_KBPS;
37301
+ exports.TRANSCODE_DOWN_MAX_HEIGHT = TRANSCODE_DOWN_MAX_HEIGHT;
35757
37302
  exports.TamperStatusSchema = TamperStatusSchema;
35758
37303
  exports.TankStatusSchema = TankStatusSchema;
35759
37304
  exports.TargetKindCapsSchema = TargetKindCapsSchema;
@@ -35776,8 +37321,11 @@ exports.TopologyProcessSchema = TopologyProcessSchema;
35776
37321
  exports.TopologyServiceSchema = TopologyServiceSchema;
35777
37322
  exports.TrackCascadeCountsSchema = TrackCascadeCountsSchema;
35778
37323
  exports.TrackEnvelopeSchema = TrackEnvelopeSchema;
37324
+ exports.TrackFlagsPatchSchema = TrackFlagsPatchSchema;
37325
+ exports.TrackFlagsSchema = TrackFlagsSchema;
35779
37326
  exports.TrackProjectionSchema = TrackProjectionSchema;
35780
37327
  exports.TrackSchema = TrackSchema;
37328
+ exports.TrackSourceSchema = TrackSourceSchema;
35781
37329
  exports.TrackStateSchema = TrackStateSchema;
35782
37330
  exports.TrackZoneFilterSchema = TrackZoneFilterSchema;
35783
37331
  exports.TrackedDetectionSchema = TrackedDetectionSchema;
@@ -35813,6 +37361,7 @@ exports.VectorUpsertInputSchema = VectorUpsertInputSchema;
35813
37361
  exports.VectorUpsertResultSchema = VectorUpsertResultSchema;
35814
37362
  exports.VibrationStatusSchema = VibrationStatusSchema;
35815
37363
  exports.VideoEncodeSchema = VideoEncodeSchema;
37364
+ exports.WEBRTC_EGRESS_PROFILE = WEBRTC_EGRESS_PROFILE;
35816
37365
  exports.WELL_KNOWN_TABS = require_sleep.WELL_KNOWN_TABS;
35817
37366
  exports.WELL_KNOWN_TAB_MAP = require_sleep.WELL_KNOWN_TAB_MAP;
35818
37367
  exports.WaterHeaterStatusSchema = WaterHeaterStatusSchema;
@@ -35859,6 +37408,7 @@ exports.audioAnalysisCapability = audioAnalysisCapability;
35859
37408
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
35860
37409
  exports.audioCodecCapability = audioCodecCapability;
35861
37410
  exports.audioMetricsCapability = audioMetricsCapability;
37411
+ exports.audioPlanFromEncodeProfile = audioPlanFromEncodeProfile;
35862
37412
  exports.authProviderCapability = authProviderCapability;
35863
37413
  exports.autoAssignProfiles = autoAssignProfiles;
35864
37414
  exports.automationControlCapability = automationControlCapability;
@@ -35870,15 +37420,20 @@ exports.bindAddonActions = bindAddonActions;
35870
37420
  exports.brightnessCapability = brightnessCapability;
35871
37421
  exports.brokerCapability = brokerCapability;
35872
37422
  exports.buildAddonRouteProvider = buildAddonRouteProvider;
37423
+ exports.buildAudioArgs = buildAudioArgs;
35873
37424
  exports.buildEventKindDescriptor = buildEventKindDescriptor;
37425
+ exports.buildFfmpegArgs = buildFfmpegArgs;
37426
+ exports.buildInputArgs = buildInputArgs;
35874
37427
  exports.buildModelVariantGroups = buildModelVariantGroups;
35875
37428
  exports.buildNcTaxonomy = buildNcTaxonomy;
35876
37429
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
37430
+ exports.buildVideoArgs = buildVideoArgs;
35877
37431
  exports.buttonCapability = buttonCapability;
35878
37432
  exports.cameraCredentialsCapability = cameraCredentialsCapability;
35879
37433
  exports.cameraPipelineConfigCapability = cameraPipelineConfigCapability;
35880
37434
  exports.cameraStreamsCapability = cameraStreamsCapability;
35881
37435
  exports.canConvertUnit = canConvertUnit;
37436
+ exports.canonicalEgressPlan = canonicalEgressPlan;
35882
37437
  exports.carbonMonoxideCapability = carbonMonoxideCapability;
35883
37438
  exports.cellsToRects = cellsToRects;
35884
37439
  exports.classifyStream = classifyStream;
@@ -35902,6 +37457,7 @@ exports.createDeviceProxy = require_sleep.createDeviceProxy;
35902
37457
  exports.createDurableState = require_sleep.createDurableState;
35903
37458
  exports.createEvent = require_sleep.createEvent;
35904
37459
  exports.createExpressionScope = createExpressionScope;
37460
+ exports.createHwAccelCache = createHwAccelCache;
35905
37461
  exports.createLazyTrpcSource = require_sleep.createLazyTrpcSource;
35906
37462
  exports.createMirrorSource = require_sleep.createMirrorSource;
35907
37463
  exports.createRuntimeStateBridge = createRuntimeStateBridge;
@@ -35915,6 +37471,8 @@ exports.decodeVectorBase64 = decodeVectorBase64;
35915
37471
  exports.decoderCapability = decoderCapability;
35916
37472
  exports.defaultDeviceFor = defaultDeviceFor;
35917
37473
  exports.defineCustomActions = defineCustomActions;
37474
+ exports.deriveCameraSwitches = deriveCameraSwitches;
37475
+ exports.deriveDetailCropRect = deriveDetailCropRect;
35918
37476
  exports.deriveRecordingMode = deriveRecordingMode;
35919
37477
  exports.describeModelVariant = describeModelVariant;
35920
37478
  exports.detectionPipelineCapability = detectionPipelineCapability;
@@ -35930,6 +37488,7 @@ exports.deviceProviderCapability = deviceProviderCapability;
35930
37488
  exports.deviceStateCapability = deviceStateCapability;
35931
37489
  exports.deviceStatusCapability = deviceStatusCapability;
35932
37490
  exports.doorbellCapability = doorbellCapability;
37491
+ exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
35933
37492
  exports.embeddingEncoderCapability = embeddingEncoderCapability;
35934
37493
  exports.emitDownForOwnedCaps = require_sleep.emitDownForOwnedCaps;
35935
37494
  exports.emitReadiness = require_sleep.emitReadiness;
@@ -35972,6 +37531,7 @@ exports.imageCapability = imageCapability;
35972
37531
  exports.imageSettingsCapability = imageSettingsCapability;
35973
37532
  exports.integrationsCapability = integrationsCapability;
35974
37533
  exports.intercomCapability = intercomCapability;
37534
+ exports.invocationFromEncodeProfile = invocationFromEncodeProfile;
35975
37535
  exports.isAgentOnlyPlacement = isAgentOnlyPlacement;
35976
37536
  exports.isArrayOutputSchema = isArrayOutputSchema;
35977
37537
  exports.isBaseConditionKey = isBaseConditionKey;
@@ -35982,6 +37542,7 @@ exports.isDeviceScopedCap = require_sleep.isDeviceScopedCap;
35982
37542
  exports.isEvent = require_sleep.isEvent;
35983
37543
  exports.isNode = isNode;
35984
37544
  exports.isObjectInput = isObjectInput;
37545
+ exports.isSoftwareDecode = isSoftwareDecode;
35985
37546
  exports.isVoidInput = isVoidInput;
35986
37547
  exports.jobKindSchema = jobKindSchema;
35987
37548
  exports.kebabToCamel = kebabToCamel;
@@ -35996,6 +37557,7 @@ exports.llmRuntimeCapability = llmRuntimeCapability;
35996
37557
  exports.localNetworkCapability = localNetworkCapability;
35997
37558
  exports.locationSimilarity = locationSimilarity;
35998
37559
  exports.lockControlCapability = lockControlCapability;
37560
+ exports.logBannerArgs = logBannerArgs;
35999
37561
  exports.logDestinationCapability = logDestinationCapability;
36000
37562
  exports.logLevelAtMost = logLevelAtMost;
36001
37563
  exports.loginMethodCapability = loginMethodCapability;
@@ -36042,7 +37604,9 @@ exports.parseProfileBrokerId = require_sleep.parseProfileBrokerId;
36042
37604
  exports.parseStreamParamsFormPatch = parseStreamParamsFormPatch;
36043
37605
  exports.petFeederCapability = petFeederCapability;
36044
37606
  exports.pickAccessoryControl = pickAccessoryControl;
37607
+ exports.pickDetailCropConvention = pickDetailCropConvention;
36045
37608
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
37609
+ exports.pickVideoEncoder = pickVideoEncoder;
36046
37610
  exports.pickerForCondition = pickerForCondition;
36047
37611
  exports.pipelineAnalyticsCapability = pipelineAnalyticsCapability;
36048
37612
  exports.pipelineExecutorCapability = pipelineExecutorCapability;
@@ -36059,6 +37623,7 @@ exports.procedureAuthKey = procedureAuthKey;
36059
37623
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
36060
37624
  exports.ptzCapability = ptzCapability;
36061
37625
  exports.pythonScriptForBackend = pythonScriptForBackend;
37626
+ exports.readDetailCropConvention = readDetailCropConvention;
36062
37627
  exports.readDeviceStateFrom = readDeviceStateFrom;
36063
37628
  exports.readNodePin = require_sleep.readNodePin;
36064
37629
  exports.readinessKey = require_sleep.readinessKey;
@@ -36075,6 +37640,7 @@ exports.resolveCapMount = require_sleep.resolveCapMount;
36075
37640
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
36076
37641
  exports.resolveDeviceControlKind = resolveDeviceControlKind;
36077
37642
  exports.resolveDeviceProfile = resolveDeviceProfile;
37643
+ exports.resolveEgressDecodeHwAccel = resolveEgressDecodeHwAccel;
36078
37644
  exports.resolveFormat = resolveFormat;
36079
37645
  exports.resolveHydratedFieldValue = require_sleep.resolveHydratedFieldValue;
36080
37646
  exports.resolveModelFormat = resolveModelFormat;
@@ -36111,6 +37677,7 @@ exports.streamQualityLabel = streamQualityLabel;
36111
37677
  exports.subKindsOf = subKindsOf;
36112
37678
  exports.supportedRuntimes = supportedRuntimes;
36113
37679
  exports.switchCapability = switchCapability;
37680
+ exports.switchedOffIds = switchedOffIds;
36114
37681
  exports.synthesizeSourceInfo = synthesizeSourceInfo;
36115
37682
  exports.systemCapability = systemCapability;
36116
37683
  exports.tamperCapability = tamperCapability;