@camstack/types 1.2.43 → 1.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/addon.js +8 -2
  2. package/dist/addon.mjs +8 -2
  3. package/dist/capabilities/index.d.ts +5 -2
  4. package/dist/capabilities/osd-manager.cap.d.ts +900 -0
  5. package/dist/capabilities/pipeline-analytics.cap.d.ts +171 -4
  6. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +10 -0
  7. package/dist/capabilities/schemas/streaming-shared.d.ts +2 -0
  8. package/dist/capabilities/server-management.cap.d.ts +3 -3
  9. package/dist/capabilities/stream-broker.cap.d.ts +32 -0
  10. package/dist/ffmpeg/fmp4-box-splitter.d.ts +113 -0
  11. package/dist/ffmpeg/fmp4-fragment-child.d.ts +85 -0
  12. package/dist/ffmpeg/fmp4-fragment-plane.d.ts +142 -0
  13. package/dist/ffmpeg/invocation.d.ts +4 -2
  14. package/dist/fmp4-box-splitter-B53u9-Nu.mjs +615 -0
  15. package/dist/fmp4-box-splitter-BkWH7O3L.js +686 -0
  16. package/dist/generated/addon-api.d.ts +85 -0
  17. package/dist/generated/cap-input-defaults.d.ts +1 -1
  18. package/dist/generated/capability-router-map.d.ts +5 -2
  19. package/dist/generated/device-proxy.d.ts +3 -1
  20. package/dist/generated/method-access-map.d.ts +1 -1
  21. package/dist/generated/system-proxy.d.ts +2 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +581 -404
  24. package/dist/index.mjs +558 -394
  25. package/dist/interfaces/camera-switches.d.ts +48 -0
  26. package/dist/interfaces/stream-broker.d.ts +18 -0
  27. package/dist/node.d.ts +4 -0
  28. package/dist/node.js +509 -3
  29. package/dist/node.mjs +507 -3
  30. package/dist/notification/schedule.d.ts +20 -0
  31. package/dist/{sleep-DtstvzWm.mjs → sleep-cC4Fuup8.mjs} +26 -1
  32. package/dist/{sleep-Bx9IIoT0.js → sleep-eiC10_cX.js} +26 -1
  33. package/dist/types/pipeline-step.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/dist/canonical-hash-7nfBbEqR.mjs +0 -35
  36. package/dist/canonical-hash-BcZHRHIx.js +0 -40
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_event_category = require("./event-category-BE4PDZ_3.js");
3
- const require_sleep = require("./sleep-Bx9IIoT0.js");
4
- const require_canonical_hash = require("./canonical-hash-BcZHRHIx.js");
3
+ const require_sleep = require("./sleep-eiC10_cX.js");
4
+ const require_fmp4_box_splitter = require("./fmp4-box-splitter-BkWH7O3L.js");
5
5
  const require_enums = require("./enums.js");
6
6
  const require_err_msg = require("./err-msg-COpsHMw2.js");
7
7
  let zod = require("zod");
@@ -231,383 +231,6 @@ function encodeProfileFromStreamShape(stream) {
231
231
  };
232
232
  }
233
233
  //#endregion
234
- //#region src/ffmpeg/invocation.ts
235
- var AUDIO_ENCODER_BY_CODEC = {
236
- opus: "libopus",
237
- aac: "aac",
238
- pcmu: "pcm_mulaw",
239
- pcma: "pcm_alaw"
240
- };
241
- /**
242
- * Camera-microphone audio, per codec. Lives HERE rather than in
243
- * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
244
- * on these types); it is re-exported from there, which is where to read it.
245
- *
246
- * Every source in this repo is a mono camera mic. The former broker preset
247
- * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
248
- * that is the value this consolidation changed.
249
- */
250
- var AUDIO_PRESETS = {
251
- aac: {
252
- kind: "encode",
253
- codec: "aac",
254
- bitrateKbps: 128,
255
- sampleRateHz: 48e3,
256
- channels: 1
257
- },
258
- opus: {
259
- kind: "encode",
260
- codec: "opus",
261
- bitrateKbps: 64,
262
- sampleRateHz: 48e3,
263
- channels: 1
264
- },
265
- pcmu: {
266
- kind: "encode",
267
- codec: "pcmu",
268
- sampleRateHz: 8e3,
269
- channels: 1
270
- }
271
- };
272
- /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
273
- function logBannerArgs(level) {
274
- return [
275
- "-hide_banner",
276
- "-loglevel",
277
- level
278
- ];
279
- }
280
- /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
281
- function isSoftwareDecode(decodeHwAccel) {
282
- return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
283
- }
284
- /**
285
- * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
286
- * appended to this list by a caller — that is the whole point of the function.
287
- */
288
- function buildInputArgs(input, decodeHwAccel) {
289
- const args = [];
290
- if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
291
- if (input.extraArgs?.length) args.push(...input.extraArgs);
292
- if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
293
- if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
294
- if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
295
- if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
296
- args.push("-i", input.url);
297
- return args;
298
- }
299
- /** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
300
- function buildVideoFilterArgs(scale, outputArgs) {
301
- if (!scale) return [];
302
- if (outputArgs.some((a) => a === "-vf")) return [];
303
- if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}`];
304
- return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2`];
305
- }
306
- /** Rate-control args for an encode plan. */
307
- function buildRateControlArgs(video) {
308
- const kbps = video.bitrateKbps;
309
- if (kbps === void 0) return [];
310
- const rc = video.rateControl ?? {
311
- kind: "cap",
312
- vbvSeconds: 2
313
- };
314
- const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
315
- return [
316
- ...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
317
- "-maxrate",
318
- `${kbps}k`,
319
- "-bufsize",
320
- `${bufsize}k`
321
- ];
322
- }
323
- /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
324
- function buildVideoArgs(video, outputArgs) {
325
- if (video.kind === "copy") return [
326
- "-c:v",
327
- "copy",
328
- ...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
329
- ];
330
- const args = [
331
- ...buildVideoFilterArgs(video.scale, outputArgs),
332
- "-c:v",
333
- video.encoder
334
- ];
335
- if (video.preset !== void 0) args.push("-preset", video.preset);
336
- if (video.tune !== void 0) args.push("-tune", video.tune);
337
- if (video.profile !== void 0) args.push("-profile:v", video.profile);
338
- if (video.level !== void 0) args.push("-level", video.level);
339
- if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
340
- if (video.fps !== void 0) args.push("-r", String(video.fps));
341
- if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
342
- if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
343
- if (video.bf !== void 0) args.push("-bf", String(video.bf));
344
- args.push(...buildRateControlArgs(video));
345
- if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
346
- return args;
347
- }
348
- /** The whole audio block, after `-i`. */
349
- function buildAudioArgs(audio) {
350
- if (audio.kind === "none") return ["-an"];
351
- if (audio.kind === "copy") return ["-c:a", "copy"];
352
- const args = [];
353
- if (audio.filter !== void 0) args.push("-af", audio.filter);
354
- args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
355
- if (audio.application !== void 0) args.push("-application", audio.application);
356
- if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
357
- if (audio.globalHeader === true) args.push("-flags", "+global_header");
358
- if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
359
- if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
360
- if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
361
- if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
362
- return args;
363
- }
364
- /** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
365
- function buildRtpOutputArgs(out) {
366
- const args = [];
367
- if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
368
- if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
369
- if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
370
- args.push("-f", "rtp", out.url);
371
- return args;
372
- }
373
- /** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
374
- function isElementaryVideoSink(sink) {
375
- return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
376
- }
377
- /**
378
- * The fragmented-MP4 muxer flags, in the order the recorder has proven them
379
- * (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
380
- * `-segment_format_options`, across every vendor in the fleet):
381
- *
382
- * - `frag_keyframe` — cut a fragment at each key frame, so every fragment
383
- * opens on a sync sample. HKSV's whole requirement.
384
- * - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
385
- * is what makes the head a standalone INITIALISATION segment.
386
- * - `default_base_moof` — fragment offsets are self-relative, so a fragment is
387
- * demuxable without the bytes that preceded it. D31's byte-range read path
388
- * depends on exactly this property of the recorder's segments.
389
- */
390
- var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
391
- /**
392
- * The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
393
- * union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
394
- * which is what a plain `container` read would have done for `mp4` — a valid
395
- * argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
396
- * unusable byte stream.
397
- */
398
- function buildStdoutOrRtspSinkArgs(sink) {
399
- if (sink.kind === "rtsp-listen") return [
400
- "-f",
401
- "rtsp",
402
- "-rtsp_transport",
403
- "tcp",
404
- "-rtsp_flags",
405
- "listen",
406
- sink.url
407
- ];
408
- if (sink.kind === "rtp-outputs") return [];
409
- return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
410
- "-f",
411
- sink.container,
412
- "pipe:1"
413
- ];
414
- }
415
- /** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
416
- function buildFmp4SinkArgs(sink) {
417
- return [
418
- "-movflags",
419
- FMP4_MOVFLAGS,
420
- "-min_frag_duration",
421
- String(Math.max(0, Math.round(sink.fragmentMs * 1e3))),
422
- "-f",
423
- "mp4",
424
- "pipe:1"
425
- ];
426
- }
427
- /**
428
- * A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
429
- * audio optional so a source with no audio skips it instead of failing the
430
- * whole invocation.
431
- */
432
- function buildAudioSidecarArgs(sidecar) {
433
- return [
434
- "-map",
435
- "0:a:0?",
436
- ...buildAudioArgs(sidecar.codec === "pcma" ? {
437
- kind: "encode",
438
- codec: "pcma",
439
- sampleRateHz: 8e3,
440
- channels: 1
441
- } : AUDIO_PRESETS[sidecar.codec]),
442
- ...buildRtpOutputArgs({
443
- url: sidecar.rtpUrl,
444
- sdpFile: sidecar.sdpFile
445
- })
446
- ];
447
- }
448
- /**
449
- * Assemble the full ffmpeg argument list. Layout:
450
- *
451
- * -hide_banner -loglevel <level>
452
- * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
453
- * [<input.extraArgs>] │
454
- * [-fflags <flag>…] │
455
- * [-rtsp_transport tcp] │
456
- * -i <url> ─┘
457
- * <video block> <threads> <audio block> ─┐ OUTPUT options.
458
- * <consumer outputArgs verbatim> │
459
- * <sink> ─┘ terminal
460
- */
461
- function buildFfmpegArgs(inv) {
462
- const head = [...logBannerArgs(inv.logLevel), ...buildInputArgs(inv.input, inv.decodeHwAccel)];
463
- const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
464
- if (inv.sink.kind === "rtp-outputs") {
465
- const videoLeg = inv.sink.video ? [
466
- "-an",
467
- "-map",
468
- "0:v:0",
469
- ...buildVideoArgs(inv.video, inv.outputArgs),
470
- ...threadArgs,
471
- ...inv.outputArgs,
472
- ...buildRtpOutputArgs(inv.sink.video)
473
- ] : [];
474
- const audioLeg = inv.sink.audio ? [
475
- "-vn",
476
- "-map",
477
- "0:a:0?",
478
- ...buildAudioArgs(inv.audio),
479
- ...buildRtpOutputArgs(inv.sink.audio)
480
- ] : [];
481
- return [
482
- ...head,
483
- ...videoLeg,
484
- ...audioLeg
485
- ];
486
- }
487
- const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
488
- const sinkArgs = buildStdoutOrRtspSinkArgs(inv.sink);
489
- return [
490
- ...head,
491
- ...buildVideoArgs(inv.video, inv.outputArgs),
492
- ...threadArgs,
493
- ...audioArgs,
494
- ...inv.outputArgs,
495
- ...sinkArgs,
496
- ...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
497
- ];
498
- }
499
- /**
500
- * Hardware ENCODER ids per decode-hwaccel backend — a static, deterministic
501
- * map (the same shape the reference NVR uses: platform → encoder, no probe).
502
- */
503
- var ENCODER_IDS_BY_BACKEND = {
504
- videotoolbox: {
505
- h264: "h264_videotoolbox",
506
- h265: "hevc_videotoolbox"
507
- },
508
- vaapi: {
509
- h264: "h264_vaapi",
510
- h265: "hevc_vaapi"
511
- },
512
- qsv: {
513
- h264: "h264_qsv",
514
- h265: "hevc_qsv"
515
- },
516
- cuda: {
517
- h264: "h264_nvenc",
518
- h265: "hevc_nvenc"
519
- },
520
- nvdec: {
521
- h264: "h264_nvenc",
522
- h265: "hevc_nvenc"
523
- },
524
- amf: {
525
- h264: "h264_amf",
526
- h265: "hevc_amf"
527
- }
528
- };
529
- /**
530
- * The hardware encoder for a target codec on `backend`, or the software one.
531
- * `'auto'` is NOT a backend identity (it is an instruction to ffmpeg), so it
532
- * maps to software encoding.
533
- */
534
- function pickVideoEncoder(target, backend, useHardware) {
535
- const software = target === "h264" ? "libx264" : "libx265";
536
- if (!useHardware || backend === null || isSoftwareDecode(backend) || backend === "auto") return software;
537
- const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
538
- if (!ids) return software;
539
- return target === "h264" ? ids.h264 : ids.h265;
540
- }
541
- /** Map an `EncodeProfile.audio` to an audio plan. */
542
- function audioPlanFromEncodeProfile(audio) {
543
- if (audio === "passthrough") return { kind: "none" };
544
- if (audio.codec === "copy") return { kind: "copy" };
545
- return {
546
- kind: "encode",
547
- codec: audio.codec,
548
- ...audio.bitrateKbps !== void 0 ? { bitrateKbps: audio.bitrateKbps } : {},
549
- ...audio.sampleRateHz !== void 0 ? { sampleRateHz: audio.sampleRateHz } : {},
550
- ...audio.channels !== void 0 ? { channels: audio.channels } : {}
551
- };
552
- }
553
- /**
554
- * Adapt an `EncodeProfile` (the operator/consumer-facing shape) into an
555
- * {@link FfmpegInvocation}. This is the ONLY bridge between the two models —
556
- * a second one is how the repo grew two argv builders that disagreed about
557
- * hardware.
558
- *
559
- * Smart video copy: when the source already speaks the requested codec the
560
- * encode block is elided entirely and ffmpeg runs as a re-muxer on the video
561
- * plane. Width / height / fps / bitrate in the profile are a downstream BUDGET,
562
- * not a forced rescale.
563
- */
564
- function invocationFromEncodeProfile(input) {
565
- const v = input.profile.video;
566
- const shouldCopy = v.codec === "copy" || input.forceReencode !== true && v.codec === input.sourceCodec;
567
- const scale = v.width !== void 0 && v.height !== void 0 ? {
568
- mode: "fit",
569
- width: v.width,
570
- height: v.height
571
- } : null;
572
- const target = v.codec === "h265" ? "h265" : "h264";
573
- const video = shouldCopy ? {
574
- kind: "copy",
575
- ...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
576
- } : {
577
- kind: "encode",
578
- encoder: pickVideoEncoder(target, input.decodeHwAccel, input.hardwareEncoders === true),
579
- scale,
580
- ...v.preset !== void 0 ? { preset: v.preset } : {},
581
- ...v.tune !== void 0 ? { tune: v.tune } : {},
582
- ...v.profile !== void 0 ? { profile: v.profile } : {},
583
- ...v.level !== void 0 ? { level: v.level } : {},
584
- ...input.pixelFormat !== void 0 ? { pixelFormat: input.pixelFormat } : {},
585
- ...v.fps !== void 0 ? { fps: v.fps } : {},
586
- ...v.gopFrames !== void 0 ? { gopFrames: v.gopFrames } : {},
587
- ...input.forceKeyFramesSeconds !== void 0 ? { forceKeyFramesSeconds: input.forceKeyFramesSeconds } : {},
588
- ...v.bf !== void 0 ? { bf: v.bf } : {},
589
- ...v.bitrateKbps !== void 0 ? { bitrateKbps: v.bitrateKbps } : {},
590
- ...input.rateControl !== void 0 ? { rateControl: input.rateControl } : {},
591
- ...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
592
- };
593
- return {
594
- logLevel: input.logLevel ?? "error",
595
- decodeHwAccel: input.decodeHwAccel,
596
- input: {
597
- url: input.sourceUrl,
598
- rtspTransport: "tcp",
599
- fflags: ["+discardcorrupt"],
600
- ...input.profile.inputArgs?.length ? { extraArgs: input.profile.inputArgs } : {}
601
- },
602
- video,
603
- audio: audioPlanFromEncodeProfile(input.profile.audio),
604
- threadCount: input.threadCount ?? 0,
605
- outputArgs: input.profile.outputArgs ?? [],
606
- sink: input.sink,
607
- ...input.audioSidecar !== void 0 ? { audioSidecar: input.audioSidecar } : {}
608
- };
609
- }
610
- //#endregion
611
234
  //#region src/ffmpeg/encode-defaults.ts
612
235
  /**
613
236
  * The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
@@ -858,7 +481,7 @@ function canonicalEgressPlan(request, delivery = egressTransportFromRequest(requ
858
481
  * every other consumer to 360p.
859
482
  */
860
483
  function egressTranscodeSharingKey(request, delivery = egressTransportFromRequest(request)) {
861
- return `egress:${require_canonical_hash.canonicalHash(canonicalEgressPlan(request, delivery))}`;
484
+ return `egress:${require_fmp4_box_splitter.canonicalHash(canonicalEgressPlan(request, delivery))}`;
862
485
  }
863
486
  //#endregion
864
487
  //#region src/health/wiring-health.ts
@@ -991,6 +614,19 @@ var DEFAULT_RETENTION = {
991
614
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
992
615
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
993
616
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
617
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
618
+ *
619
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
620
+ *
621
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
622
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
623
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
624
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
625
+ * instant, vendor-independent and reversible without touching the camera, and
626
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
627
+ * D62 forbids a second switch that *disagrees* with the first; these two
628
+ * cannot disagree, because neither reads the other's store — the camera holds
629
+ * one, the broker holds the other, and each reports its own fact.
994
630
  *
995
631
  * ## The two switches whose authority is not on this server
996
632
  *
@@ -1052,6 +688,7 @@ var CameraSwitchIdSchema = zod.z.enum([
1052
688
  "object-detection",
1053
689
  "privacy-mask",
1054
690
  "device-audio",
691
+ "broker-audio",
1055
692
  "audio-analysis",
1056
693
  "recording",
1057
694
  "notifications"
@@ -1067,12 +704,17 @@ var CameraSwitchIdSchema = zod.z.enum([
1067
704
  * `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
1068
705
  * radius is partial, and the "broadest first" rule does not rank a partial
1069
706
  * control above a whole-function one.
707
+ *
708
+ * `broker-audio` sits directly BELOW `device-audio` by the same source-first
709
+ * rule: the camera's microphone feeds the broker, so silencing the camera
710
+ * leaves the broker's mute with nothing to suppress; the reverse is not true.
1070
711
  */
1071
712
  var CAMERA_SWITCH_ORDER = [
1072
713
  "stream-broker",
1073
714
  "object-detection",
1074
715
  "privacy-mask",
1075
716
  "device-audio",
717
+ "broker-audio",
1076
718
  "audio-analysis",
1077
719
  "recording",
1078
720
  "notifications"
@@ -1098,7 +740,8 @@ var CameraSwitchAuthoritySchema = zod.z.discriminatedUnion("kind", [
1098
740
  zod.z.object({
1099
741
  kind: zod.z.literal("camera-mask"),
1100
742
  capName: zod.z.string()
1101
- })
743
+ }),
744
+ zod.z.object({ kind: zod.z.literal("broker-audio-mute") })
1102
745
  ]);
1103
746
  /**
1104
747
  * Why a switch is not offered for this camera. Rendered instead of the
@@ -1197,6 +840,13 @@ var CAMERA_SWITCH_CATALOG = {
1197
840
  },
1198
841
  countsAsSwitchedOff: true
1199
842
  },
843
+ "broker-audio": {
844
+ id: "broker-audio",
845
+ label: "Audio distribution",
846
+ costWhenOff: "Off: this server distributes no sound for this camera — live view is silent and everything recorded while it is off is silent FOREVER, even after you turn it back on. Audio detection and classification also have nothing to analyse. The camera keeps capturing sound, so nothing about the camera changes and turning this back on is instant, with no interruption to the picture.",
847
+ authority: { kind: "broker-audio-mute" },
848
+ countsAsSwitchedOff: true
849
+ },
1200
850
  "audio-analysis": {
1201
851
  id: "audio-analysis",
1202
852
  label: "Audio detection & classification",
@@ -1315,6 +965,18 @@ function resolveState(descriptor, input) {
1315
965
  enabled: mask.enabled
1316
966
  };
1317
967
  }
968
+ case "broker-audio-mute": {
969
+ const broker = input.brokerAudio;
970
+ if (broker === null) return {
971
+ available: false,
972
+ enabled: true,
973
+ unavailableReason: "source-unreachable"
974
+ };
975
+ return {
976
+ available: true,
977
+ enabled: !broker.muted
978
+ };
979
+ }
1318
980
  }
1319
981
  }
1320
982
  /**
@@ -4756,7 +4418,44 @@ var streamBrokerCapability = {
4756
4418
  kind: "mutation",
4757
4419
  auth: "admin"
4758
4420
  }),
4759
- isRtspEnabled: require_sleep.method(zod.z.object({ brokerId: zod.z.string() }), zod.z.boolean())
4421
+ isRtspEnabled: require_sleep.method(zod.z.object({ brokerId: zod.z.string() }), zod.z.boolean()),
4422
+ /**
4423
+ * ── Per-device audio-plane policy (D83) ───────────────────────────
4424
+ *
4425
+ * The BROKER-side mute: while `muted`, this node distributes none of
4426
+ * the device's audio, on any plane it serves — live (WebRTC / encoded
4427
+ * subscribers) AND recording (the RTSP restreamer the recorder pulls,
4428
+ * which additionally serves the video-only SDP so the recorder's ffmpeg
4429
+ * never declares an audio stream it will not receive).
4430
+ *
4431
+ * Keyed by `deviceId`, not `brokerId`: every one of a camera's streams
4432
+ * carries the same microphone, and a per-stream answer would let main
4433
+ * and sub disagree about whether the camera is silent.
4434
+ *
4435
+ * The state lives in the broker's existing `DeviceOverride` blob — no
4436
+ * new store — and the mute is applied to a fresh broker at creation, so
4437
+ * a restart, a re-dial or a catalog republish never un-mutes a camera.
4438
+ */
4439
+ getDeviceAudioMute: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({
4440
+ muted: zod.z.boolean(),
4441
+ /**
4442
+ * How many live non-derived brokers currently hold the mute. Purely
4443
+ * diagnostic: `muted` is the policy and is authoritative on its own
4444
+ * (it applies to brokers that do not exist yet), while this says
4445
+ * whether anything is presently being silenced.
4446
+ */
4447
+ appliedBrokers: zod.z.number().int().nonnegative()
4448
+ })),
4449
+ setDeviceAudioMute: require_sleep.method(zod.z.object({
4450
+ deviceId: zod.z.number().int(),
4451
+ muted: zod.z.boolean()
4452
+ }), zod.z.object({
4453
+ muted: zod.z.boolean(),
4454
+ appliedBrokers: zod.z.number().int().nonnegative()
4455
+ }), {
4456
+ kind: "mutation",
4457
+ auth: "admin"
4458
+ })
4760
4459
  },
4761
4460
  events: {
4762
4461
  onCamStreamDemand: require_sleep.event(zod.z.object({
@@ -12603,6 +12302,30 @@ var TrackSourceSchema = zod.z.enum([
12603
12302
  "audio"
12604
12303
  ]);
12605
12304
  /**
12305
+ * Where a track sits in the RETRAIN lifecycle (D81).
12306
+ *
12307
+ * - `none` — never marked, or un-marked. Evictable.
12308
+ * - `staging` — the operator wants this track as training material and has not
12309
+ * finished with it. **This is the only state retention holds**: the track and
12310
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
12311
+ * the device's age window.
12312
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
12313
+ * were COPIED into the retrain dataset at selection time, so the dataset no
12314
+ * longer depends on the track's media and the track becomes EVICTABLE again.
12315
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
12316
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
12317
+ *
12318
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
12319
+ * the store's filter language has only positive equality and `whereIn` — no
12320
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
12321
+ * would make the entire pre-column history immortal in one deploy.
12322
+ */
12323
+ var RetrainStatusSchema = zod.z.enum([
12324
+ "none",
12325
+ "staging",
12326
+ "trained"
12327
+ ]);
12328
+ /**
12606
12329
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
12607
12330
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
12608
12331
  * so the two surfaces cannot drift.
@@ -12612,18 +12335,31 @@ var TrackSourceSchema = zod.z.enum([
12612
12335
  * columns existed read as absent, and a consumer that needs a boolean should say
12613
12336
  * `flag === true`, not `flag !== false`.
12614
12337
  *
12615
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
12616
- * operator curation, and the behaviour they drive will be specified separately.
12617
- * In particular a `markForTrain` track is NOT pinned against retention — see
12618
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
12338
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
12339
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
12340
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
12341
+ * `trained` track reports `false` while refusing both writes. The boolean is
12342
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
12343
+ * "never marked" from "already trained" must read `retrainStatus`.
12344
+ *
12345
+ * `debug` does NOT pin; it is attention, not durability.
12619
12346
  */
12620
12347
  var TrackFlagFields = {
12621
- /** Operator marked this track as training material. */
12348
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
12349
+ * `'staging'`. */
12622
12350
  markForTrain: zod.z.boolean().optional(),
12623
12351
  /** Operator marked this track for diagnostic attention. */
12624
12352
  debug: zod.z.boolean().optional()
12625
12353
  };
12626
12354
  /**
12355
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
12356
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
12357
+ * write patch, and the status is not something the toggle sets — it is what the
12358
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
12359
+ * always present on a persisted row (the column default materialises `'none'`).
12360
+ */
12361
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
12362
+ /**
12627
12363
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
12628
12364
  * one flag can never clear the other — the toggles are independent and are
12629
12365
  * driven from three surfaces that do not know about each other.
@@ -12637,7 +12373,32 @@ var TrackFlagsPatchSchema = zod.z.object(TrackFlagFields);
12637
12373
  var TrackFlagsSchema = zod.z.object({
12638
12374
  trackId: zod.z.string(),
12639
12375
  markForTrain: zod.z.boolean(),
12640
- debug: zod.z.boolean()
12376
+ debug: zod.z.boolean(),
12377
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
12378
+ * a track row) because this shape is only ever produced by the write body,
12379
+ * which always knows it — and a surface that has just written needs to render
12380
+ * `trained` without a re-fetch. */
12381
+ retrainStatus: RetrainStatusSchema
12382
+ });
12383
+ /** Per-camera slice of a training-export estimate. */
12384
+ var TrainingExportDeviceTotalsSchema = zod.z.object({
12385
+ deviceId: zod.z.number(),
12386
+ tracks: zod.z.number().int(),
12387
+ files: zod.z.number().int(),
12388
+ bytes: zod.z.number().int()
12389
+ });
12390
+ /**
12391
+ * What a training export WOULD contain. Computed from media index rows only —
12392
+ * no blob is read to produce this.
12393
+ */
12394
+ var TrainingExportSummarySchema = zod.z.object({
12395
+ generatedAt: zod.z.number(),
12396
+ trackCount: zod.z.number().int(),
12397
+ fileCount: zod.z.number().int(),
12398
+ byteCount: zod.z.number().int(),
12399
+ /** More marked tracks exist than a single pass carries. */
12400
+ truncated: zod.z.boolean(),
12401
+ devices: zod.z.array(TrainingExportDeviceTotalsSchema).readonly()
12641
12402
  });
12642
12403
  var TrackSchema = zod.z.object({
12643
12404
  trackId: zod.z.string(),
@@ -12682,7 +12443,8 @@ var TrackSchema = zod.z.object({
12682
12443
  * Populated from the persisted envelope columns on historical reads;
12683
12444
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
12684
12445
  envelope: TrackEnvelopeSchema.optional(),
12685
- ...TrackFlagFields
12446
+ ...TrackFlagFields,
12447
+ ...TrackRetrainFields
12686
12448
  });
12687
12449
  var BaseEventFields = {
12688
12450
  id: zod.z.string(),
@@ -12904,7 +12666,8 @@ var KeyEventSchema = zod.z.object({
12904
12666
  bestEventId: zod.z.string(),
12905
12667
  /** Track lifetime in ms (lastSeen - firstSeen). */
12906
12668
  windowMs: zod.z.number().optional(),
12907
- ...TrackFlagFields
12669
+ ...TrackFlagFields,
12670
+ ...TrackRetrainFields
12908
12671
  });
12909
12672
  var TrackedDetectionSchema = zod.z.object({
12910
12673
  trackId: zod.z.string(),
@@ -13266,11 +13029,29 @@ var pipelineAnalyticsCapability = {
13266
13029
  *
13267
13030
  * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
13268
13031
  * authenticated non-admin surface and two of the three call sites are
13269
- * there. Revisit if a flag ever gains an effect that costs storage
13270
- * `deleteTracks` next door is admin for exactly that reason.
13032
+ * there. The note that used to sit here said to revisit this the day a flag
13033
+ * gained an effect that costs storage, and D81 is that day — `markForTrain`
13034
+ * now pins. It STAYS protected, and the reason is that the alternative
13035
+ * makes the feature pointless: marking a track is something you do while
13036
+ * looking at it, on the surface you were already looking at it on, and that
13037
+ * surface is the viewer. What the storage cost gets instead is a BOUND — a
13038
+ * per-device pin budget enforced in the body, refusing a new pin past the
13039
+ * limit while always allowing un-marking. `deleteTracks` next door is still
13040
+ * admin, because destroying evidence and preserving it are not symmetric.
13041
+ *
13042
+ * `markForTrain` writes the retrain LIFECYCLE, not a boolean column: `true`
13043
+ * is `none → staging`, `false` is `staging → none`. A track already
13044
+ * `trained` refuses BOTH — its frames are copies inside the retrain dataset
13045
+ * and re-staging it from a generic toggle is how the same material gets
13046
+ * annotated twice under two ground truths. Returning a trained track to
13047
+ * staging is a deliberate action of the retrain page, which is also the only
13048
+ * thing that produces `trained` in the first place.
13271
13049
  *
13272
- * Returns the RESOLVED state of both flags (absent → `false`) so a caller
13273
- * can drive its toggle without a re-fetch. Rejects an unknown track.
13050
+ * Returns the RESOLVED state of both flags (absent → `false`) plus the
13051
+ * `retrainStatus` they were derived from, so a caller can drive its toggle
13052
+ * — and render a `trained` badge — without a re-fetch. Rejects an unknown
13053
+ * track, a new staging mark on a device already holding its full budget, and
13054
+ * any `markForTrain` write against a trained track.
13274
13055
  */
13275
13056
  setTrackFlags: require_sleep.method(zod.z.object({
13276
13057
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -13334,6 +13115,42 @@ var pipelineAnalyticsCapability = {
13334
13115
  kind: "query",
13335
13116
  auth: "admin"
13336
13117
  }),
13118
+ /**
13119
+ * The CHEAP QUESTION, asked before any media moves: how big is the dataset
13120
+ * the marked (`markForTrain`) tracks would produce?
13121
+ *
13122
+ * Answered from media INDEX rows only — key, kind, size, timestamp — so it
13123
+ * costs ~2 KB of reads per track and no blob reads at all. The measured harm
13124
+ * behind D56 was a bulk pass that read and base64'd every blob a track owned
13125
+ * before deciding anything, taking hub-main to 82 s busy out of 120; an
13126
+ * export is that same I/O shape, so it inherits the same discipline: know
13127
+ * the size, then decide.
13128
+ *
13129
+ * `truncated` reports that more marked tracks exist than one pass carries.
13130
+ * Empty `deviceIds` ⇒ every device that has marked tracks.
13131
+ */
13132
+ getTrainingExportSummary: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), TrainingExportSummarySchema, {
13133
+ kind: "query",
13134
+ auth: "admin"
13135
+ }),
13136
+ /**
13137
+ * Where to download the dataset archive.
13138
+ *
13139
+ * The BYTES do not come back through this cap — they come from the returned
13140
+ * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
13141
+ * archive base64'd through a unary RPC envelope would be held whole in
13142
+ * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
13143
+ * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
13144
+ * precedent, and this follows it deliberately.
13145
+ *
13146
+ * The archive contains a `manifest.json` FIRST, then the stored media
13147
+ * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
13148
+ * model is run: a training set's pixels must be the pixels the pipeline saw.
13149
+ */
13150
+ getTrainingExportUrl: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), zod.z.object({ url: zod.z.string() }), {
13151
+ kind: "query",
13152
+ auth: "admin"
13153
+ }),
13337
13154
  getEventMedia: require_sleep.method(zod.z.object({
13338
13155
  eventId: zod.z.string(),
13339
13156
  kind: MediaFileKindEnum.optional()
@@ -22098,6 +21915,216 @@ setOverlay: require_sleep.method(zod.z.object({
22098
21915
  }] }
22099
21916
  };
22100
21917
  //#endregion
21918
+ //#region src/capabilities/osd-manager.cap.ts
21919
+ /**
21920
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
21921
+ *
21922
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
21923
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
21924
+ * comes from, and it must not — a driver that grew a "show the temperature
21925
+ * here" feature would grow it once per vendor.
21926
+ *
21927
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
21928
+ * which value feeds the slot, how it is formatted, and under which
21929
+ * conditions it is shown at all. One addon renders every binding on every
21930
+ * camera, so a new source costs zero driver code.
21931
+ *
21932
+ * Three deliberate choices, each with a rejected alternative:
21933
+ *
21934
+ * 1. A source is `(capName, valuePath)` over the kernel's device
21935
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
21936
+ * cap-keyed slice a device publishes is bindable the day the cap
21937
+ * ships. The rejected alternative (one enum member per source, with
21938
+ * a resolver branch each) is what makes "add the humidity too" a
21939
+ * code change.
21940
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
21941
+ * notification centre's condition vocabulary — rather than a parallel
21942
+ * model. An operator who has learned one condition editor has learned
21943
+ * both.
21944
+ * 3. Because the renderer's facts are device STATE and not a detection
21945
+ * record, only a SUBSET of that vocabulary can be answered here.
21946
+ * `setSlotBinding` REJECTS the rest at write time (see
21947
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
21948
+ * condition that can never be true renders a permanently blank
21949
+ * overlay, and a blank overlay looks exactly like a broken camera.
21950
+ */
21951
+ /** Where a slot's value comes from. */
21952
+ var OsdSourceSchema = zod.z.discriminatedUnion("kind", [
21953
+ zod.z.object({
21954
+ kind: zod.z.literal("static"),
21955
+ text: zod.z.string().max(64)
21956
+ }),
21957
+ zod.z.object({
21958
+ kind: zod.z.literal("clock"),
21959
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
21960
+ pattern: zod.z.string().min(1).max(32).default("HH:mm"),
21961
+ /** IANA zone. Omitted = the server's zone. */
21962
+ timezone: zod.z.string().min(1).max(64).optional()
21963
+ }),
21964
+ zod.z.object({
21965
+ kind: zod.z.literal("device-state"),
21966
+ deviceId: zod.z.number().int().optional(),
21967
+ capName: zod.z.string().min(1).max(64),
21968
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
21969
+ valuePath: zod.z.string().min(1).max(64)
21970
+ })
21971
+ ]);
21972
+ var OsdSlotBindingSchema = zod.z.object({
21973
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
21974
+ enabled: zod.z.boolean().default(true),
21975
+ source: OsdSourceSchema,
21976
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
21977
+ template: zod.z.string().max(96).default("${value}"),
21978
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
21979
+ maxCharacters: zod.z.number().int().min(4).max(64).optional(),
21980
+ /**
21981
+ * Decimal places for a numeric value. `0` yields an integer — the
21982
+ * documented workaround for firmwares that reject `.` in overlay text.
21983
+ */
21984
+ maxDecimals: zod.z.number().int().min(0).max(4).default(1),
21985
+ /** Appended via `${unit}`. The state mirror does not carry units. */
21986
+ unitLabel: zod.z.string().max(8).optional(),
21987
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
21988
+ valueMap: zod.z.record(zod.z.string(), zod.z.string()).optional(),
21989
+ /** Time windows in which the slot is shown. Absent = always. */
21990
+ schedule: NcScheduleSchema.optional(),
21991
+ /**
21992
+ * Display gate, in the notification centre's condition vocabulary.
21993
+ * Only the keys reported by `getConditionSupport` are accepted.
21994
+ */
21995
+ conditions: NcConditionsSchema.optional(),
21996
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
21997
+ fallbackText: zod.z.string().max(64).default("")
21998
+ });
21999
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
22000
+ var OsdSlotViewSchema = zod.z.object({
22001
+ slotId: zod.z.string(),
22002
+ kind: OsdOverlayKindEnum,
22003
+ /** Firmware refuses text edits (a timestamp, the channel name). */
22004
+ readOnly: zod.z.boolean(),
22005
+ cameraEnabled: zod.z.boolean(),
22006
+ cameraText: zod.z.string().optional(),
22007
+ binding: OsdSlotBindingSchema.nullable()
22008
+ });
22009
+ /**
22010
+ * What happened to one slot on one render pass. `unchanged` exists so the
22011
+ * operator can tell "we are driving this and the value is steady" from
22012
+ * "we never got there" — and so the loop can prove it is not rewriting
22013
+ * identical text to the camera every tick.
22014
+ */
22015
+ var OsdRenderOutcomeEnum = zod.z.enum([
22016
+ "written",
22017
+ "unchanged",
22018
+ "gated",
22019
+ "unreadable",
22020
+ "disabled",
22021
+ "unbound",
22022
+ "failed"
22023
+ ]);
22024
+ var OsdRenderResultSchema = zod.z.object({
22025
+ slotId: zod.z.string(),
22026
+ outcome: OsdRenderOutcomeEnum,
22027
+ /** The text the slot should carry. Empty = the slot is switched off. */
22028
+ text: zod.z.string(),
22029
+ /** Why, whenever the outcome is not a plain write. Never silent. */
22030
+ reason: zod.z.string().optional()
22031
+ });
22032
+ var OsdSourceValueTypeEnum = zod.z.enum([
22033
+ "number",
22034
+ "boolean",
22035
+ "string",
22036
+ "enum"
22037
+ ]);
22038
+ /**
22039
+ * One bindable value, derived from a cap's `runtimeState` schema — never
22040
+ * hand-listed. The editor renders from this, so a cap that ships a new
22041
+ * state field becomes bindable with no UI change.
22042
+ */
22043
+ var OsdSourceOptionSchema = zod.z.object({
22044
+ deviceId: zod.z.number().int(),
22045
+ deviceName: zod.z.string(),
22046
+ capName: zod.z.string(),
22047
+ valuePath: zod.z.string(),
22048
+ label: zod.z.string(),
22049
+ valueType: OsdSourceValueTypeEnum,
22050
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
22051
+ enumValues: zod.z.array(zod.z.string()).readonly().optional()
22052
+ });
22053
+ var osdManagerCapability = {
22054
+ name: "osd-manager",
22055
+ scope: "system",
22056
+ mode: "singleton",
22057
+ methods: {
22058
+ /**
22059
+ * The camera's slots, each with its binding. `supported: false` when
22060
+ * the camera publishes no `osd` cap — the page then says so instead
22061
+ * of rendering an empty list that reads as "no overlays configured".
22062
+ */
22063
+ getDeviceOsd: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({
22064
+ supported: zod.z.boolean(),
22065
+ slots: zod.z.array(OsdSlotViewSchema)
22066
+ }), { auth: "admin" }),
22067
+ /**
22068
+ * Every value bindable on this camera: its own state leaves first,
22069
+ * then every other device's. Derived from the cap definitions, so it
22070
+ * cannot drift from what the resolver can actually read.
22071
+ */
22072
+ getSourceCatalog: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({ sources: zod.z.array(OsdSourceOptionSchema) }), { auth: "admin" }),
22073
+ /**
22074
+ * The condition ids this gate can answer, alongside the FULL notification
22075
+ * condition catalog. Both, in one call, on purpose: an editor that showed
22076
+ * the catalog without the supported set would offer conditions that
22077
+ * `setSlotBinding` then rejects.
22078
+ */
22079
+ getConditionSupport: require_sleep.method(zod.z.object({}), zod.z.object({
22080
+ supported: zod.z.array(zod.z.string()),
22081
+ catalog: zod.z.array(NcConditionDescriptorSchema)
22082
+ }), { auth: "admin" }),
22083
+ /**
22084
+ * Persist a binding and render it immediately, returning the slot as
22085
+ * it now stands. Throws when the binding names an unsupported
22086
+ * condition, an unknown cap, or a value path that cap does not have.
22087
+ */
22088
+ setSlotBinding: require_sleep.method(zod.z.object({
22089
+ deviceId: zod.z.number().int(),
22090
+ slotId: zod.z.string().min(1),
22091
+ binding: OsdSlotBindingSchema
22092
+ }), zod.z.object({
22093
+ slot: OsdSlotViewSchema,
22094
+ render: OsdRenderResultSchema
22095
+ }), {
22096
+ kind: "mutation",
22097
+ auth: "admin"
22098
+ }),
22099
+ /** Forget the binding. The slot keeps whatever text it last carried. */
22100
+ clearSlotBinding: require_sleep.method(zod.z.object({
22101
+ deviceId: zod.z.number().int(),
22102
+ slotId: zod.z.string().min(1)
22103
+ }), zod.z.object({ success: zod.z.literal(true) }), {
22104
+ kind: "mutation",
22105
+ auth: "admin"
22106
+ }),
22107
+ /**
22108
+ * Render without writing. `binding` overrides the stored one so an
22109
+ * editor can preview an unsaved change. Mutation kind only to carry
22110
+ * the binding object safely; no side effects.
22111
+ */
22112
+ previewSlot: require_sleep.method(zod.z.object({
22113
+ deviceId: zod.z.number().int(),
22114
+ slotId: zod.z.string().min(1),
22115
+ binding: OsdSlotBindingSchema.optional()
22116
+ }), OsdRenderResultSchema, {
22117
+ kind: "mutation",
22118
+ auth: "admin"
22119
+ }),
22120
+ /** Force a render pass over one camera now, instead of at the next tick. */
22121
+ renderDevice: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int() }), zod.z.object({ results: zod.z.array(OsdRenderResultSchema) }), {
22122
+ kind: "mutation",
22123
+ auth: "admin"
22124
+ })
22125
+ }
22126
+ };
22127
+ //#endregion
22101
22128
  //#region src/capabilities/pet-feeder.cap.ts
22102
22129
  /**
22103
22130
  * PetKit pet-feeder cap. Models the control + telemetry surface of a
@@ -28185,6 +28212,7 @@ var CAPABILITY_NAMES = {
28185
28212
  numericSensor: "numeric-sensor",
28186
28213
  oauthIntegration: "oauth-integration",
28187
28214
  osd: "osd",
28215
+ osdManager: "osd-manager",
28188
28216
  petFeeder: "pet-feeder",
28189
28217
  pipelineAnalytics: "pipeline-analytics",
28190
28218
  pipelineExecutor: "pipeline-executor",
@@ -28616,6 +28644,10 @@ var CAPABILITY_ROUTER_KEYS = [
28616
28644
  key: "osd",
28617
28645
  name: "osd"
28618
28646
  },
28647
+ {
28648
+ key: "osdManager",
28649
+ name: "osd-manager"
28650
+ },
28619
28651
  {
28620
28652
  key: "petFeeder",
28621
28653
  name: "pet-feeder"
@@ -28929,6 +28961,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
28929
28961
  numericSensorCapability,
28930
28962
  oauthIntegrationCapability,
28931
28963
  osdCapability,
28964
+ osdManagerCapability,
28932
28965
  petFeederCapability,
28933
28966
  pipelineAnalyticsCapability,
28934
28967
  pipelineExecutorCapability,
@@ -31847,6 +31880,48 @@ var METHOD_ACCESS_MAP = Object.freeze({
31847
31880
  addonId: null,
31848
31881
  access: "create"
31849
31882
  },
31883
+ "osdManager.clearSlotBinding": {
31884
+ capName: "osd-manager",
31885
+ capScope: "system",
31886
+ addonId: null,
31887
+ access: "delete"
31888
+ },
31889
+ "osdManager.getConditionSupport": {
31890
+ capName: "osd-manager",
31891
+ capScope: "system",
31892
+ addonId: null,
31893
+ access: "view"
31894
+ },
31895
+ "osdManager.getDeviceOsd": {
31896
+ capName: "osd-manager",
31897
+ capScope: "system",
31898
+ addonId: null,
31899
+ access: "view"
31900
+ },
31901
+ "osdManager.getSourceCatalog": {
31902
+ capName: "osd-manager",
31903
+ capScope: "system",
31904
+ addonId: null,
31905
+ access: "view"
31906
+ },
31907
+ "osdManager.previewSlot": {
31908
+ capName: "osd-manager",
31909
+ capScope: "system",
31910
+ addonId: null,
31911
+ access: "create"
31912
+ },
31913
+ "osdManager.renderDevice": {
31914
+ capName: "osd-manager",
31915
+ capScope: "system",
31916
+ addonId: null,
31917
+ access: "create"
31918
+ },
31919
+ "osdManager.setSlotBinding": {
31920
+ capName: "osd-manager",
31921
+ capScope: "system",
31922
+ addonId: null,
31923
+ access: "create"
31924
+ },
31850
31925
  "petFeeder.callPet": {
31851
31926
  capName: "pet-feeder",
31852
31927
  capScope: "device",
@@ -32009,6 +32084,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
32009
32084
  addonId: null,
32010
32085
  access: "view"
32011
32086
  },
32087
+ "pipelineAnalytics.getTrainingExportSummary": {
32088
+ capName: "pipeline-analytics",
32089
+ capScope: "device",
32090
+ addonId: null,
32091
+ access: "view"
32092
+ },
32093
+ "pipelineAnalytics.getTrainingExportUrl": {
32094
+ capName: "pipeline-analytics",
32095
+ capScope: "device",
32096
+ addonId: null,
32097
+ access: "view"
32098
+ },
32012
32099
  "pipelineAnalytics.listEventKinds": {
32013
32100
  capName: "pipeline-analytics",
32014
32101
  capScope: "device",
@@ -33485,6 +33572,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33485
33572
  addonId: null,
33486
33573
  access: "view"
33487
33574
  },
33575
+ "streamBroker.getDeviceAudioMute": {
33576
+ capName: "stream-broker",
33577
+ capScope: "system",
33578
+ addonId: null,
33579
+ access: "view"
33580
+ },
33488
33581
  "streamBroker.getPreBufferInfo": {
33489
33582
  capName: "stream-broker",
33490
33583
  capScope: "system",
@@ -33605,6 +33698,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33605
33698
  addonId: null,
33606
33699
  access: "create"
33607
33700
  },
33701
+ "streamBroker.setDeviceAudioMute": {
33702
+ capName: "stream-broker",
33703
+ capScope: "system",
33704
+ addonId: null,
33705
+ access: "create"
33706
+ },
33608
33707
  "streamBroker.setPreBufferDuration": {
33609
33708
  capName: "stream-broker",
33610
33709
  capScope: "system",
@@ -34320,6 +34419,7 @@ var KNOWN_CAP_NAMES = [
34320
34419
  "notifier",
34321
34420
  "oauth-integration",
34322
34421
  "osd",
34422
+ "osd-manager",
34323
34423
  "pet-feeder",
34324
34424
  "pipeline-analytics",
34325
34425
  "pipeline-executor",
@@ -34467,6 +34567,7 @@ var SYSTEM_CAP_NAMES = [
34467
34567
  "notification-output",
34468
34568
  "notification-rules",
34469
34569
  "oauth-integration",
34570
+ "osd-manager",
34470
34571
  "pipeline-executor",
34471
34572
  "pipeline-orchestrator",
34472
34573
  "pipeline-runner",
@@ -34925,6 +35026,7 @@ function createSystemProxy(api) {
34925
35026
  deleteTarget: (input) => dispatch("notificationOutput", "deleteTarget", "mutation", input),
34926
35027
  setTargetEnabled: (input) => dispatch("notificationOutput", "setTargetEnabled", "mutation", input)
34927
35028
  },
35029
+ osdManager: { getConditionSupport: (input) => dispatch("osdManager", "getConditionSupport", "query", input) },
34928
35030
  pipelineExecutor: {
34929
35031
  getAvailableEngines: (input) => dispatch("pipelineExecutor", "getAvailableEngines", "query", input),
34930
35032
  getSelectedEngine: (input) => dispatch("pipelineExecutor", "getSelectedEngine", "query", input),
@@ -35466,6 +35568,68 @@ function prepareNotification(caps, n) {
35466
35568
  };
35467
35569
  }
35468
35570
  //#endregion
35571
+ //#region src/notification/schedule.ts
35572
+ var WEEKDAY_TO_DAY = {
35573
+ Sun: 0,
35574
+ Mon: 1,
35575
+ Tue: 2,
35576
+ Wed: 3,
35577
+ Thu: 4,
35578
+ Fri: 5,
35579
+ Sat: 6
35580
+ };
35581
+ /** Resolve (weekday, minute-of-day) of `atMs` in the schedule's timezone.
35582
+ * An invalid/unknown IANA name falls back to the host timezone. */
35583
+ function localDayMinute(atMs, timezone) {
35584
+ const d = new Date(atMs);
35585
+ if (timezone !== void 0) try {
35586
+ const parts = new Intl.DateTimeFormat("en-US", {
35587
+ timeZone: timezone,
35588
+ weekday: "short",
35589
+ hour: "numeric",
35590
+ minute: "numeric",
35591
+ hourCycle: "h23"
35592
+ }).formatToParts(d);
35593
+ let weekday;
35594
+ let hour;
35595
+ let minute;
35596
+ for (const p of parts) if (p.type === "weekday") weekday = p.value;
35597
+ else if (p.type === "hour") hour = Number(p.value);
35598
+ else if (p.type === "minute") minute = Number(p.value);
35599
+ const day = weekday !== void 0 ? WEEKDAY_TO_DAY[weekday] : void 0;
35600
+ if (day !== void 0 && hour !== void 0 && minute !== void 0) return {
35601
+ day,
35602
+ minute: hour * 60 + minute
35603
+ };
35604
+ } catch {}
35605
+ return {
35606
+ day: d.getDay(),
35607
+ minute: d.getHours() * 60 + d.getMinutes()
35608
+ };
35609
+ }
35610
+ /**
35611
+ * Is the schedule active at `atMs`? No schedule = always active. Windows
35612
+ * are OR'd; a window with `startMinute > endMinute` crosses midnight (it
35613
+ * starts on a listed day and spills into the next). `invert` flips the
35614
+ * result (active OUTSIDE the windows).
35615
+ */
35616
+ function isScheduleActive(schedule, atMs) {
35617
+ if (schedule === void 0) return true;
35618
+ const { day, minute } = localDayMinute(atMs, schedule.timezone);
35619
+ const prevDay = (day + 6) % 7;
35620
+ let inside = false;
35621
+ for (const w of schedule.windows) if (w.startMinute <= w.endMinute) {
35622
+ if (w.days.includes(day) && minute >= w.startMinute && minute < w.endMinute) {
35623
+ inside = true;
35624
+ break;
35625
+ }
35626
+ } else if (w.days.includes(day) && minute >= w.startMinute || w.days.includes(prevDay) && minute < w.endMinute) {
35627
+ inside = true;
35628
+ break;
35629
+ }
35630
+ return schedule.invert === true ? !inside : inside;
35631
+ }
35632
+ //#endregion
35469
35633
  //#region src/notification/timelapse-rule.ts
35470
35634
  /**
35471
35635
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
@@ -37326,7 +37490,7 @@ exports.APPLE_SA_TO_MACRO = APPLE_SA_TO_MACRO;
37326
37490
  exports.AUDIO_ANALYSIS_CAP_NAME = AUDIO_ANALYSIS_CAP_NAME;
37327
37491
  exports.AUDIO_BACKEND_CHOICES = AUDIO_BACKEND_CHOICES;
37328
37492
  exports.AUDIO_MACRO_LABELS = AUDIO_MACRO_LABELS;
37329
- exports.AUDIO_PRESETS = AUDIO_PRESETS;
37493
+ exports.AUDIO_PRESETS = require_fmp4_box_splitter.AUDIO_PRESETS;
37330
37494
  exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
37331
37495
  exports.AccessoryKind = AccessoryKind;
37332
37496
  exports.AddBrokerInputSchema = AddBrokerInputSchema;
@@ -37599,6 +37763,7 @@ exports.FanDirectionSchema = FanDirectionSchema;
37599
37763
  exports.FeatureManifestSchema = FeatureManifestSchema;
37600
37764
  exports.FeatureProbeStatusSchema = FeatureProbeStatusSchema;
37601
37765
  exports.FloodStatusSchema = FloodStatusSchema;
37766
+ exports.Fmp4BoxSplitter = require_fmp4_box_splitter.Fmp4BoxSplitter;
37602
37767
  exports.FrameHandleFormatSchema = require_sleep.FrameHandleFormatSchema;
37603
37768
  exports.FrameHandleSchema = require_sleep.FrameHandleSchema;
37604
37769
  exports.FrameInputSchema = FrameInputSchema;
@@ -37805,6 +37970,13 @@ exports.OsdOverlayKindEnum = OsdOverlayKindEnum;
37805
37970
  exports.OsdOverlayPatchSchema = OsdOverlayPatchSchema;
37806
37971
  exports.OsdOverlaySchema = OsdOverlaySchema;
37807
37972
  exports.OsdPositionEnum = OsdPositionEnum;
37973
+ exports.OsdRenderOutcomeEnum = OsdRenderOutcomeEnum;
37974
+ exports.OsdRenderResultSchema = OsdRenderResultSchema;
37975
+ exports.OsdSlotBindingSchema = OsdSlotBindingSchema;
37976
+ exports.OsdSlotViewSchema = OsdSlotViewSchema;
37977
+ exports.OsdSourceOptionSchema = OsdSourceOptionSchema;
37978
+ exports.OsdSourceSchema = OsdSourceSchema;
37979
+ exports.OsdSourceValueTypeEnum = OsdSourceValueTypeEnum;
37808
37980
  exports.OsdStatusSchema = OsdStatusSchema;
37809
37981
  exports.PET_FEEDER_MANUAL_FEED_MAX = PET_FEEDER_MANUAL_FEED_MAX;
37810
37982
  exports.PET_FEEDER_MANUAL_FEED_MIN = PET_FEEDER_MANUAL_FEED_MIN;
@@ -37893,6 +38065,7 @@ exports.RelocateJobStateSchema = RelocateJobStateSchema;
37893
38065
  exports.RelocateMediaInputSchema = RelocateMediaInputSchema;
37894
38066
  exports.RenderedAsSchema = RenderedAsSchema;
37895
38067
  exports.ReportMotionInputSchema = ReportMotionInputSchema;
38068
+ exports.RetrainStatusSchema = RetrainStatusSchema;
37896
38069
  exports.RingBuffer = RingBuffer;
37897
38070
  exports.RtpSourceSchema = RtpSourceSchema;
37898
38071
  exports.RtspRestreamEntrySchema = RtspRestreamEntrySchema;
@@ -38015,6 +38188,8 @@ exports.TrackSourceSchema = TrackSourceSchema;
38015
38188
  exports.TrackStateSchema = TrackStateSchema;
38016
38189
  exports.TrackZoneFilterSchema = TrackZoneFilterSchema;
38017
38190
  exports.TrackedDetectionSchema = TrackedDetectionSchema;
38191
+ exports.TrainingExportDeviceTotalsSchema = TrainingExportDeviceTotalsSchema;
38192
+ exports.TrainingExportSummarySchema = TrainingExportSummarySchema;
38018
38193
  exports.TurnServerSchema = TurnServerSchema;
38019
38194
  exports.UNIT_TABLE = UNIT_TABLE;
38020
38195
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema$1;
@@ -38094,7 +38269,7 @@ exports.audioAnalysisCapability = audioAnalysisCapability;
38094
38269
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
38095
38270
  exports.audioCodecCapability = audioCodecCapability;
38096
38271
  exports.audioMetricsCapability = audioMetricsCapability;
38097
- exports.audioPlanFromEncodeProfile = audioPlanFromEncodeProfile;
38272
+ exports.audioPlanFromEncodeProfile = require_fmp4_box_splitter.audioPlanFromEncodeProfile;
38098
38273
  exports.authProviderCapability = authProviderCapability;
38099
38274
  exports.autoAssignProfiles = autoAssignProfiles;
38100
38275
  exports.automationControlCapability = automationControlCapability;
@@ -38107,14 +38282,14 @@ exports.bindAddonActions = bindAddonActions;
38107
38282
  exports.brightnessCapability = brightnessCapability;
38108
38283
  exports.brokerCapability = brokerCapability;
38109
38284
  exports.buildAddonRouteProvider = buildAddonRouteProvider;
38110
- exports.buildAudioArgs = buildAudioArgs;
38285
+ exports.buildAudioArgs = require_fmp4_box_splitter.buildAudioArgs;
38111
38286
  exports.buildEventKindDescriptor = buildEventKindDescriptor;
38112
- exports.buildFfmpegArgs = buildFfmpegArgs;
38113
- exports.buildInputArgs = buildInputArgs;
38287
+ exports.buildFfmpegArgs = require_fmp4_box_splitter.buildFfmpegArgs;
38288
+ exports.buildInputArgs = require_fmp4_box_splitter.buildInputArgs;
38114
38289
  exports.buildModelVariantGroups = buildModelVariantGroups;
38115
38290
  exports.buildNcTaxonomy = buildNcTaxonomy;
38116
38291
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
38117
- exports.buildVideoArgs = buildVideoArgs;
38292
+ exports.buildVideoArgs = require_fmp4_box_splitter.buildVideoArgs;
38118
38293
  exports.buttonCapability = buttonCapability;
38119
38294
  exports.cameraCredentialsCapability = cameraCredentialsCapability;
38120
38295
  exports.cameraPipelineConfigCapability = cameraPipelineConfigCapability;
@@ -38219,7 +38394,7 @@ exports.imageCapability = imageCapability;
38219
38394
  exports.imageSettingsCapability = imageSettingsCapability;
38220
38395
  exports.integrationsCapability = integrationsCapability;
38221
38396
  exports.intercomCapability = intercomCapability;
38222
- exports.invocationFromEncodeProfile = invocationFromEncodeProfile;
38397
+ exports.invocationFromEncodeProfile = require_fmp4_box_splitter.invocationFromEncodeProfile;
38223
38398
  exports.isAgentOnlyPlacement = isAgentOnlyPlacement;
38224
38399
  exports.isArrayOutputSchema = isArrayOutputSchema;
38225
38400
  exports.isBaseConditionKey = isBaseConditionKey;
@@ -38231,7 +38406,8 @@ exports.isEvent = require_sleep.isEvent;
38231
38406
  exports.isNode = isNode;
38232
38407
  exports.isObjectInput = isObjectInput;
38233
38408
  exports.isSameAddonId = isSameAddonId;
38234
- exports.isSoftwareDecode = isSoftwareDecode;
38409
+ exports.isScheduleActive = isScheduleActive;
38410
+ exports.isSoftwareDecode = require_fmp4_box_splitter.isSoftwareDecode;
38235
38411
  exports.isVoidInput = isVoidInput;
38236
38412
  exports.jobKindSchema = jobKindSchema;
38237
38413
  exports.kebabToCamel = kebabToCamel;
@@ -38246,7 +38422,7 @@ exports.llmRuntimeCapability = llmRuntimeCapability;
38246
38422
  exports.localNetworkCapability = localNetworkCapability;
38247
38423
  exports.locationSimilarity = locationSimilarity;
38248
38424
  exports.lockControlCapability = lockControlCapability;
38249
- exports.logBannerArgs = logBannerArgs;
38425
+ exports.logBannerArgs = require_fmp4_box_splitter.logBannerArgs;
38250
38426
  exports.logDestinationCapability = logDestinationCapability;
38251
38427
  exports.logLevelAtMost = logLevelAtMost;
38252
38428
  exports.loginMethodCapability = loginMethodCapability;
@@ -38284,6 +38460,7 @@ exports.numericSensorCapability = numericSensorCapability;
38284
38460
  exports.oauthIntegrationCapability = oauthIntegrationCapability;
38285
38461
  exports.objectInputDeclaresAddonId = objectInputDeclaresAddonId;
38286
38462
  exports.osdCapability = osdCapability;
38463
+ exports.osdManagerCapability = osdManagerCapability;
38287
38464
  exports.parseCameraStreamConfig = parseCameraStreamConfig;
38288
38465
  exports.parseExpression = parseExpression;
38289
38466
  exports.parseJsonArray = require_sleep.parseJsonArray;
@@ -38296,7 +38473,7 @@ exports.pickAccessoryControl = pickAccessoryControl;
38296
38473
  exports.pickDetailCropConvention = pickDetailCropConvention;
38297
38474
  exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
38298
38475
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
38299
- exports.pickVideoEncoder = pickVideoEncoder;
38476
+ exports.pickVideoEncoder = require_fmp4_box_splitter.pickVideoEncoder;
38300
38477
  exports.pickerForCondition = pickerForCondition;
38301
38478
  exports.pipelineAnalyticsCapability = pipelineAnalyticsCapability;
38302
38479
  exports.pipelineExecutorCapability = pipelineExecutorCapability;