@camstack/types 1.2.41 → 1.2.43
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.
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/canonical-hash-7nfBbEqR.mjs +35 -0
- package/dist/canonical-hash-BcZHRHIx.js +40 -0
- package/dist/cap-call-context.d.ts +26 -0
- package/dist/capabilities/index.d.ts +5 -5
- package/dist/capabilities/notification-rules.cap.d.ts +41 -0
- package/dist/capabilities/pipeline-analytics.cap.d.ts +92 -4
- package/dist/capabilities/pipeline-orchestrator.cap.d.ts +149 -0
- package/dist/capabilities/pipeline-runner.cap.d.ts +119 -1
- package/dist/capabilities/platform-probe.cap.d.ts +3 -3
- package/dist/capabilities/privacy-mask.cap.d.ts +69 -9
- package/dist/capabilities/recording.cap.d.ts +30 -0
- package/dist/capabilities/snapshot.cap.d.ts +1 -1
- package/dist/capabilities/stream-broker.cap.d.ts +304 -0
- package/dist/capabilities/stream-params.cap.d.ts +8 -4
- package/dist/device/device-profile.d.ts +12 -4
- package/dist/device/system-mirror.d.ts +11 -0
- package/dist/encode-profile.d.ts +2 -0
- package/dist/ffmpeg/encode-defaults.d.ts +107 -0
- package/dist/ffmpeg/hwaccel.d.ts +98 -0
- package/dist/ffmpeg/invocation.d.ts +348 -0
- package/dist/ffmpeg/process.d.ts +135 -0
- package/dist/ffmpeg/sharing-key.d.ts +91 -0
- package/dist/generated/addon-api.d.ts +92 -4
- package/dist/generated/device-proxy.d.ts +2 -2
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +2 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2069 -42
- package/dist/index.mjs +2006 -43
- package/dist/interfaces/camera-switches.d.ts +375 -0
- package/dist/interfaces/inference-engine.d.ts +24 -3
- package/dist/interfaces/ops-log.d.ts +4 -0
- package/dist/interfaces/pipeline-runner-capability.d.ts +9 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +270 -36
- package/dist/node.mjs +269 -36
- package/dist/pipeline/native-lease.d.ts +150 -0
- package/dist/{sleep-DTce7-ch.js → sleep-Bx9IIoT0.js} +43 -1
- package/dist/{sleep-CXimb854.mjs → sleep-DtstvzWm.mjs} +38 -2
- package/dist/utils/addon-id.d.ts +30 -0
- 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-
|
|
3
|
+
const require_sleep = require("./sleep-Bx9IIoT0.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,636 @@ 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.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
|
+
//#region src/ffmpeg/encode-defaults.ts
|
|
612
|
+
/**
|
|
613
|
+
* The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
|
|
614
|
+
* Baseline because it is the one profile every consumer in this repo decodes
|
|
615
|
+
* (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
|
|
616
|
+
*/
|
|
617
|
+
var BASE_LIVE_EGRESS_PROFILE = {
|
|
618
|
+
video: {
|
|
619
|
+
codec: "h264",
|
|
620
|
+
profile: "baseline",
|
|
621
|
+
level: "3.1",
|
|
622
|
+
width: 1280,
|
|
623
|
+
height: 720,
|
|
624
|
+
fps: 25,
|
|
625
|
+
bitrateKbps: 2500,
|
|
626
|
+
gopFrames: 25,
|
|
627
|
+
bf: 0,
|
|
628
|
+
preset: "veryfast",
|
|
629
|
+
tune: "zerolatency"
|
|
630
|
+
},
|
|
631
|
+
audio: "passthrough"
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* The browser WebRTC viewer's transcode profile. Used only when the broker
|
|
635
|
+
* MUST spawn ffmpeg (source codec ∉ the browser-accepted set — an H.265 source
|
|
636
|
+
* on an H.264-only browser). Audio is `passthrough` because werift carries the
|
|
637
|
+
* audio plane out-of-band; putting it in the video pipeline would encode a
|
|
638
|
+
* track nothing reads.
|
|
639
|
+
*/
|
|
640
|
+
var WEBRTC_EGRESS_PROFILE = {
|
|
641
|
+
...BASE_LIVE_EGRESS_PROFILE,
|
|
642
|
+
video: {
|
|
643
|
+
...BASE_LIVE_EGRESS_PROFILE.video,
|
|
644
|
+
bitrateKbps: 3e3,
|
|
645
|
+
gopFrames: 30,
|
|
646
|
+
preset: "ultrafast"
|
|
647
|
+
},
|
|
648
|
+
audio: "passthrough"
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Alexa's egress profile. Alexa's audio ALSO rides the WebRTC session's own
|
|
652
|
+
* audio plane (out-of-band), so this is `passthrough` exactly like the browser
|
|
653
|
+
* — see the ADR for why the previous in-band Opus was encoded and discarded.
|
|
654
|
+
*/
|
|
655
|
+
/**
|
|
656
|
+
* Alexa's egress asks for OPUS, and that is a change with a history.
|
|
657
|
+
*
|
|
658
|
+
* The previous profile encoded Opus IN-BAND into an MPEG-TS, where it was
|
|
659
|
+
* discarded: Opus is `stream_type 0x06` and the broker's demuxer maps only
|
|
660
|
+
* `0x0f` (aac), `0x03`/`0x04` (mp2) and `0x81` (ac3). Every frame it produced
|
|
661
|
+
* died at the demuxer, so the encode was replaced with `passthrough` — correct,
|
|
662
|
+
* because paying libopus for nothing is worse than silence.
|
|
663
|
+
*
|
|
664
|
+
* But `passthrough` means the egress emits NO audio at all, and Alexa's audio
|
|
665
|
+
* does not arrive by magic: it rides the published stream, which is what the
|
|
666
|
+
* WebRTC session dials. So the Echo had video and silence either way.
|
|
667
|
+
*
|
|
668
|
+
* Opus here now reaches the egress AUDIO SIDECAR — a separate RTP leg the
|
|
669
|
+
* restreamer grafts onto its SDP — which never touches the MPEG-TS demuxer that
|
|
670
|
+
* killed the in-band attempt. Same codec, different plane, and this one the
|
|
671
|
+
* consumer can actually negotiate.
|
|
672
|
+
*/
|
|
673
|
+
var ALEXA_EGRESS_PROFILE = {
|
|
674
|
+
...BASE_LIVE_EGRESS_PROFILE,
|
|
675
|
+
audio: { codec: "opus" }
|
|
676
|
+
};
|
|
677
|
+
/** VBV window for a consumer whose budget is enforced per second (HomeKit). */
|
|
678
|
+
var RATE_CONTROL_TIGHT = {
|
|
679
|
+
kind: "cbr",
|
|
680
|
+
vbvSeconds: 1
|
|
681
|
+
};
|
|
682
|
+
/** VBV window for a consumer that tolerates a keyframe spike (browser, Echo). */
|
|
683
|
+
var RATE_CONTROL_RELAXED = {
|
|
684
|
+
kind: "cap",
|
|
685
|
+
vbvSeconds: 2
|
|
686
|
+
};
|
|
687
|
+
/**
|
|
688
|
+
* Transcode-DOWN ceiling: the shape a remote/unhealthy WebRTC session is
|
|
689
|
+
* reduced to. 360p / 400 kbps is the `low` rung of the adaptive ladder — a
|
|
690
|
+
* session steered here is already failing, so the ceiling is chosen to survive
|
|
691
|
+
* a bad link, not to look good.
|
|
692
|
+
*/
|
|
693
|
+
var TRANSCODE_DOWN_MAX_HEIGHT = 360;
|
|
694
|
+
var TRANSCODE_DOWN_MAX_BITRATE_KBPS = 400;
|
|
695
|
+
/**
|
|
696
|
+
* HomeKit's two-way audio leg. `lowdelay` + a short frame duration because the
|
|
697
|
+
* leg is interactive; `globalHeader` because HAP owns the SDP and ffmpeg must
|
|
698
|
+
* not put extradata in-band. `sampleRateHz` and `frameDurationMs` are
|
|
699
|
+
* NEGOTIATED per session — the controller picks them — so they are absent here
|
|
700
|
+
* and filled in by the caller.
|
|
701
|
+
*/
|
|
702
|
+
var HAP_AUDIO_BITRATE_KBPS = 24;
|
|
703
|
+
var HAP_AUDIO_BASE = {
|
|
704
|
+
kind: "encode",
|
|
705
|
+
codec: "opus",
|
|
706
|
+
bitrateKbps: 24,
|
|
707
|
+
channels: 1,
|
|
708
|
+
application: "lowdelay",
|
|
709
|
+
globalHeader: true,
|
|
710
|
+
filter: "aresample=async=1000:first_pts=0"
|
|
711
|
+
};
|
|
712
|
+
/** HomeKit's `-bufsize` for the audio plane — four seconds of its 24 kbps. */
|
|
713
|
+
var HAP_AUDIO_VBV_KBITS = 96;
|
|
714
|
+
/** Seconds between forced IDRs on a HomeKit transcode. */
|
|
715
|
+
var HAP_KEYFRAME_INTERVAL_SEC = 4;
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/ffmpeg/hwaccel.ts
|
|
718
|
+
function createHwAccelCache(options) {
|
|
719
|
+
const now = options.now ?? (() => Date.now());
|
|
720
|
+
let value = null;
|
|
721
|
+
let writtenAt = Number.NEGATIVE_INFINITY;
|
|
722
|
+
return {
|
|
723
|
+
read() {
|
|
724
|
+
return now() - writtenAt < options.ttlMs ? value : void 0;
|
|
725
|
+
},
|
|
726
|
+
write(next) {
|
|
727
|
+
value = next;
|
|
728
|
+
writtenAt = now();
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** `true` when a value means "decode in software". */
|
|
733
|
+
function meansSoftware(value) {
|
|
734
|
+
return !value || value === "none" || value === "copy";
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Resolve the `-hwaccel` value for an egress transcode. Hardware is the
|
|
738
|
+
* DEFAULT — an egress that decodes in software on a hub with working vaapi is
|
|
739
|
+
* paying for nothing — and every software outcome is announced.
|
|
740
|
+
*
|
|
741
|
+
* Returns a concrete backend name, the literal `'auto'`, or `null` for
|
|
742
|
+
* software decode (⇒ {@link buildFfmpegArgs} emits no `-hwaccel` at all).
|
|
743
|
+
*/
|
|
744
|
+
async function resolveEgressDecodeHwAccel(deps) {
|
|
745
|
+
const override = deps.override;
|
|
746
|
+
if (override !== void 0 && override !== null && override !== "") return meansSoftware(override) ? null : override;
|
|
747
|
+
const cached = deps.cache?.read();
|
|
748
|
+
if (cached !== void 0) return cached;
|
|
749
|
+
let backend;
|
|
750
|
+
try {
|
|
751
|
+
backend = await deps.readDecoderBackend();
|
|
752
|
+
} catch (err) {
|
|
753
|
+
const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
|
|
754
|
+
deps.onFallback({
|
|
755
|
+
reason: "decoder-unreadable",
|
|
756
|
+
kernelPreferred,
|
|
757
|
+
error: err instanceof Error ? err.message : String(err)
|
|
758
|
+
});
|
|
759
|
+
deps.cache?.write(null);
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
if (backend === "") {
|
|
763
|
+
const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
|
|
764
|
+
deps.onFallback({
|
|
765
|
+
reason: "decoder-unprobed",
|
|
766
|
+
kernelPreferred
|
|
767
|
+
});
|
|
768
|
+
deps.cache?.write(null);
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const resolved = meansSoftware(backend) ? null : backend;
|
|
772
|
+
deps.cache?.write(resolved);
|
|
773
|
+
return resolved;
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/ffmpeg/sharing-key.ts
|
|
777
|
+
/**
|
|
778
|
+
* The sharing key for `streamBroker.acquireEgressTranscode`.
|
|
779
|
+
*
|
|
780
|
+
* **Two requesters asking for exactly the same argument set reach the same
|
|
781
|
+
* process.** Exact match, never fuzzy: a quantised ladder that merged
|
|
782
|
+
* NEARLY-identical requests would make the stream a consumer receives depend
|
|
783
|
+
* on who else is watching and in what order they arrived — unpredictable in
|
|
784
|
+
* precisely the way a debugging session cannot tolerate. Same arguments ⇒ same
|
|
785
|
+
* process. Different arguments ⇒ different process, and that is fine; the
|
|
786
|
+
* operator accepted the cost ("it's fine to have several processes").
|
|
787
|
+
*
|
|
788
|
+
* Derived from the STRUCTURED plan, not from a flag array. `pipelineKeyFor`
|
|
789
|
+
* (`transcode-pipeline.ts`) folds `getStreamWithCodec`'s raw `outputArgs` into
|
|
790
|
+
* its key, so that method's extensibility hatch and its sharing key are the
|
|
791
|
+
* same field — a consumer needing one extra flag silently forks the child, and
|
|
792
|
+
* two consumers that want the same thing but spell it differently never share.
|
|
793
|
+
* Here every knob is a named field, and defaults are APPLIED before hashing so
|
|
794
|
+
* an omitted field and its explicit default land on the same key.
|
|
795
|
+
*/
|
|
796
|
+
/**
|
|
797
|
+
* The transport a CAP request describes. `fragments` is deliberately
|
|
798
|
+
* unreachable from here — the request schema has no way to ask for it, so the
|
|
799
|
+
* cap path can never be handed a fragment child by accident.
|
|
800
|
+
*/
|
|
801
|
+
function egressTransportFromRequest(request) {
|
|
802
|
+
return { transport: request.publishLocally === true ? "push" : "dial" };
|
|
803
|
+
}
|
|
804
|
+
/** Absent optional ⇒ this sentinel, so `undefined` and "not set" agree. */
|
|
805
|
+
var UNSET = "\0unset";
|
|
806
|
+
function canonicalVideo(video) {
|
|
807
|
+
return {
|
|
808
|
+
codec: video.codec,
|
|
809
|
+
profile: video.profile ?? UNSET,
|
|
810
|
+
level: video.level ?? UNSET,
|
|
811
|
+
width: video.width ?? -1,
|
|
812
|
+
height: video.height ?? -1,
|
|
813
|
+
fps: video.fps ?? -1,
|
|
814
|
+
bitrateKbps: video.bitrateKbps ?? -1,
|
|
815
|
+
gopFrames: video.gopFrames ?? -1,
|
|
816
|
+
bf: video.bf ?? -1,
|
|
817
|
+
preset: video.preset ?? UNSET,
|
|
818
|
+
tune: video.tune ?? UNSET
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function canonicalAudio(audio) {
|
|
822
|
+
if (audio === "passthrough") return { codec: "passthrough" };
|
|
823
|
+
return {
|
|
824
|
+
codec: audio.codec,
|
|
825
|
+
bitrateKbps: audio.bitrateKbps ?? -1,
|
|
826
|
+
sampleRateHz: audio.sampleRateHz ?? -1,
|
|
827
|
+
channels: audio.channels ?? -1
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* The normalised plan a key is computed from. Exported so a test — and a
|
|
832
|
+
* future operator-facing "why are these two not sharing?" surface — can diff
|
|
833
|
+
* two requests without reversing a hash.
|
|
834
|
+
*/
|
|
835
|
+
function canonicalEgressPlan(request, delivery = egressTransportFromRequest(request)) {
|
|
836
|
+
return {
|
|
837
|
+
deviceId: request.deviceId,
|
|
838
|
+
source: request.source.kind === "profile" ? `profile:${request.source.profile}` : `cam-stream:${request.source.camStreamId}`,
|
|
839
|
+
transport: delivery.transport,
|
|
840
|
+
fragmentMs: delivery.fragmentMs ?? -1,
|
|
841
|
+
video: canonicalVideo(request.encode.video),
|
|
842
|
+
audio: canonicalAudio(request.encode.audio),
|
|
843
|
+
rateControl: request.rateControl ?? "relaxed",
|
|
844
|
+
bitstreamFilter: request.bitstreamFilter ?? UNSET,
|
|
845
|
+
pixelFormat: request.pixelFormat ?? UNSET,
|
|
846
|
+
decodeHwAccel: request.decodeHwAccel ?? UNSET
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
|
|
851
|
+
* so a request built with a different field order produces the same digest.
|
|
852
|
+
*
|
|
853
|
+
* The handle this keys is IMMUTABLE: there is no `reconfigure`. A consumer
|
|
854
|
+
* whose requirements change RELEASES and re-acquires; the refcount does the
|
|
855
|
+
* rest. That is what stops co-tenants disturbing each other — the co-tenant
|
|
856
|
+
* hazard that made Alexa's shared `derived:alexa-<id>` stream a hazard was
|
|
857
|
+
* exactly a mutable shared object, where one consumer's downgrade dragged
|
|
858
|
+
* every other consumer to 360p.
|
|
859
|
+
*/
|
|
860
|
+
function egressTranscodeSharingKey(request, delivery = egressTransportFromRequest(request)) {
|
|
861
|
+
return `egress:${require_canonical_hash.canonicalHash(canonicalEgressPlan(request, delivery))}`;
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
225
864
|
//#region src/health/wiring-health.ts
|
|
226
865
|
/**
|
|
227
866
|
* Deep wiring healthcheck — snapshot of active reachability probes across
|
|
@@ -331,6 +970,395 @@ var DEFAULT_RETENTION = {
|
|
|
331
970
|
snapshotsDays: 14
|
|
332
971
|
};
|
|
333
972
|
//#endregion
|
|
973
|
+
//#region src/interfaces/camera-switches.ts
|
|
974
|
+
/**
|
|
975
|
+
* Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
|
|
976
|
+
* pipeline functions an operator thinks in terms of.
|
|
977
|
+
*
|
|
978
|
+
* ## This file adds no state
|
|
979
|
+
*
|
|
980
|
+
* Every switch here is a VIEW onto an authority that already existed
|
|
981
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
|
|
982
|
+
* group is that there is exactly one place each function is turned off, and
|
|
983
|
+
* the group routes to it:
|
|
984
|
+
*
|
|
985
|
+
* | Switch | Authority | Proven "off stops the work" gate |
|
|
986
|
+
* | --- | --- | --- |
|
|
987
|
+
* | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
|
|
988
|
+
* | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
|
|
989
|
+
* | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
|
|
990
|
+
* | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
|
|
991
|
+
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
992
|
+
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
993
|
+
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
994
|
+
*
|
|
995
|
+
* ## The two switches whose authority is not on this server
|
|
996
|
+
*
|
|
997
|
+
* `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
|
|
998
|
+
* in "the group stores nothing" — it is the purest form of it: the camera
|
|
999
|
+
* holds the fact, every read is a read-through, and there is no server-side
|
|
1000
|
+
* copy that could drift. Their availability therefore cannot come from
|
|
1001
|
+
* `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
|
|
1002
|
+
* is filtered out there); it comes from the cap's own camera-probed
|
|
1003
|
+
* `privacyMask.getOptions()`, which is strictly more honest — it answers for
|
|
1004
|
+
* THIS camera rather than for the device type
|
|
1005
|
+
* ([D74](../../../../docs/decisions/adr-0074.md)).
|
|
1006
|
+
*
|
|
1007
|
+
* ## `privacy-mask` is the one row whose ON is not "the function is working"
|
|
1008
|
+
*
|
|
1009
|
+
* Every other switch means *this camera's function is doing its job*, so
|
|
1010
|
+
* `enabled: false` is a thing an operator took away. `privacy-mask` means **the
|
|
1011
|
+
* MASK is active** — `enabled: true` is video deliberately obscured. The
|
|
1012
|
+
* polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
|
|
1013
|
+
* (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
|
|
1014
|
+
* a HomeKit toggle that disagreed with the app's toggle for the same camera is
|
|
1015
|
+
* worse than either surface not having one.
|
|
1016
|
+
*
|
|
1017
|
+
* Two consequences follow and both are load-bearing:
|
|
1018
|
+
*
|
|
1019
|
+
* - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
|
|
1020
|
+
* exactly this row. With the polarity above, every camera that has NOT drawn
|
|
1021
|
+
* a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
|
|
1022
|
+
* normal, healthy state of most cameras rendered as an operator disablement.
|
|
1023
|
+
* - **Its cost line names BOTH directions.** `costWhenOff` is rendered
|
|
1024
|
+
* unconditionally by both clients, so for this row it has to read correctly
|
|
1025
|
+
* whichever way the switch is sitting.
|
|
1026
|
+
*
|
|
1027
|
+
* The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
|
|
1028
|
+
* migrated the legacy `audioEnabled` / `pipelineEnabled` /
|
|
1029
|
+
* `motionDetectionEnabled` booleans ONTO `setWrapperActive`. The group is the
|
|
1030
|
+
* surface that decision never got.
|
|
1031
|
+
*
|
|
1032
|
+
* ## Two rules that are load-bearing
|
|
1033
|
+
*
|
|
1034
|
+
* - **Recording's switch is `enabled`, never the bands.** `bands` is the only
|
|
1035
|
+
* authored intent and `mode` is derived from it (`deriveRecordingMode`).
|
|
1036
|
+
* Expressing "off" by clearing bands destroys the operator's schedule and
|
|
1037
|
+
* turning the camera back on would then silently record nothing.
|
|
1038
|
+
* - **A switch that is off must be reported as off**, not merely produce
|
|
1039
|
+
* nothing. {@link CameraSwitch.enabled} is what a status surface renders as
|
|
1040
|
+
* "disabled by an operator" instead of "broken" — see
|
|
1041
|
+
* `CameraStatus.switchedOff`.
|
|
1042
|
+
*/
|
|
1043
|
+
/**
|
|
1044
|
+
* The functions the operator named — five on 2026-08-05, plus the camera's own
|
|
1045
|
+
* microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
|
|
1046
|
+
* recognition and plate/LPR are per-step toggles on
|
|
1047
|
+
* `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
|
|
1048
|
+
* editor, not in a safety group.
|
|
1049
|
+
*/
|
|
1050
|
+
var CameraSwitchIdSchema = zod.z.enum([
|
|
1051
|
+
"stream-broker",
|
|
1052
|
+
"object-detection",
|
|
1053
|
+
"privacy-mask",
|
|
1054
|
+
"device-audio",
|
|
1055
|
+
"audio-analysis",
|
|
1056
|
+
"recording",
|
|
1057
|
+
"notifications"
|
|
1058
|
+
]);
|
|
1059
|
+
/**
|
|
1060
|
+
* Stable render order — broadest blast radius first, and a source before the
|
|
1061
|
+
* thing that consumes it. `device-audio` sits ABOVE `audio-analysis` because
|
|
1062
|
+
* turning the microphone off leaves the analyzer with nothing to analyse; the
|
|
1063
|
+
* reverse is not true.
|
|
1064
|
+
*
|
|
1065
|
+
* `privacy-mask` sits directly ABOVE `device-audio` because they are literal
|
|
1066
|
+
* siblings — one cap, one device plane, video then audio — and NOT above
|
|
1067
|
+
* `object-detection` despite feeding it: a mask blanks REGIONS, so its blast
|
|
1068
|
+
* radius is partial, and the "broadest first" rule does not rank a partial
|
|
1069
|
+
* control above a whole-function one.
|
|
1070
|
+
*/
|
|
1071
|
+
var CAMERA_SWITCH_ORDER = [
|
|
1072
|
+
"stream-broker",
|
|
1073
|
+
"object-detection",
|
|
1074
|
+
"privacy-mask",
|
|
1075
|
+
"device-audio",
|
|
1076
|
+
"audio-analysis",
|
|
1077
|
+
"recording",
|
|
1078
|
+
"notifications"
|
|
1079
|
+
];
|
|
1080
|
+
/**
|
|
1081
|
+
* WHERE the switch's state actually lives. A discriminated union rather than a
|
|
1082
|
+
* string so both the writer (the orchestrator's `setCameraSwitch`) and any
|
|
1083
|
+
* reader can exhaustively narrow — and so "the group added a parallel map" is
|
|
1084
|
+
* a compile error rather than a review comment.
|
|
1085
|
+
*/
|
|
1086
|
+
var CameraSwitchAuthoritySchema = zod.z.discriminatedUnion("kind", [
|
|
1087
|
+
zod.z.object({ kind: zod.z.literal("device-disabled") }),
|
|
1088
|
+
zod.z.object({
|
|
1089
|
+
kind: zod.z.literal("wrapper-binding"),
|
|
1090
|
+
capName: zod.z.string()
|
|
1091
|
+
}),
|
|
1092
|
+
zod.z.object({ kind: zod.z.literal("recording-config") }),
|
|
1093
|
+
zod.z.object({ kind: zod.z.literal("notification-mute") }),
|
|
1094
|
+
zod.z.object({
|
|
1095
|
+
kind: zod.z.literal("camera-audio"),
|
|
1096
|
+
capName: zod.z.string()
|
|
1097
|
+
}),
|
|
1098
|
+
zod.z.object({
|
|
1099
|
+
kind: zod.z.literal("camera-mask"),
|
|
1100
|
+
capName: zod.z.string()
|
|
1101
|
+
})
|
|
1102
|
+
]);
|
|
1103
|
+
/**
|
|
1104
|
+
* Why a switch is not offered for this camera. Rendered instead of the
|
|
1105
|
+
* control, never as a dead control — an absent function and a broken one must
|
|
1106
|
+
* not look the same.
|
|
1107
|
+
*/
|
|
1108
|
+
var CameraSwitchUnavailableReasonSchema = zod.z.enum([
|
|
1109
|
+
"no-provider",
|
|
1110
|
+
"source-unreachable",
|
|
1111
|
+
"not-configured"
|
|
1112
|
+
]);
|
|
1113
|
+
/**
|
|
1114
|
+
* One switch, resolved for one camera.
|
|
1115
|
+
*
|
|
1116
|
+
* `label` and `costWhenOff` travel ON THE WIRE rather than being looked up
|
|
1117
|
+
* client-side: the viewer is a separate repository that does not import
|
|
1118
|
+
* `@camstack/types`, and a cost line duplicated in two clients is a cost line
|
|
1119
|
+
* that will disagree with itself. Five rows per camera is nothing.
|
|
1120
|
+
*/
|
|
1121
|
+
var CameraSwitchSchema = zod.z.object({
|
|
1122
|
+
id: CameraSwitchIdSchema,
|
|
1123
|
+
label: zod.z.string(),
|
|
1124
|
+
/**
|
|
1125
|
+
* What the operator LOSES while this is off, in one sentence. Required, not
|
|
1126
|
+
* optional: a switch that cannot say what it costs should not ship.
|
|
1127
|
+
*/
|
|
1128
|
+
costWhenOff: zod.z.string(),
|
|
1129
|
+
/** False = do not render a control. `unavailableReason` says why. */
|
|
1130
|
+
available: zod.z.boolean(),
|
|
1131
|
+
unavailableReason: CameraSwitchUnavailableReasonSchema.optional(),
|
|
1132
|
+
/** Current state. Meaningless when `available` is false — read it as `true`. */
|
|
1133
|
+
enabled: zod.z.boolean(),
|
|
1134
|
+
authority: CameraSwitchAuthoritySchema
|
|
1135
|
+
});
|
|
1136
|
+
/** The whole group for one camera. */
|
|
1137
|
+
var CameraSwitchGroupSchema = zod.z.object({
|
|
1138
|
+
deviceId: zod.z.number().int(),
|
|
1139
|
+
switches: zod.z.array(CameraSwitchSchema).readonly(),
|
|
1140
|
+
/** Unix ms when the group was composed server-side. */
|
|
1141
|
+
fetchedAt: zod.z.number()
|
|
1142
|
+
});
|
|
1143
|
+
/**
|
|
1144
|
+
* The wrapper capability each wrapper-backed switch controls. Named constants
|
|
1145
|
+
* because the same strings appear in `legacy-migrations.ts`, in
|
|
1146
|
+
* `isCapActiveForDevice` call sites and in the fake harness — a typo in any of
|
|
1147
|
+
* them is a switch that silently writes a binding nobody reads.
|
|
1148
|
+
*/
|
|
1149
|
+
var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
|
|
1150
|
+
var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
|
|
1151
|
+
/**
|
|
1152
|
+
* The device-NATIVE cap that owns what the camera does not capture — masked
|
|
1153
|
+
* video regions and, since 2026-08-07, the microphone. Named here because the
|
|
1154
|
+
* `camera-audio` authority, the orchestrator's gather and the fake harness all
|
|
1155
|
+
* have to agree on it.
|
|
1156
|
+
*/
|
|
1157
|
+
var PRIVACY_MASK_CAP_NAME = "privacy-mask";
|
|
1158
|
+
/**
|
|
1159
|
+
* THE catalog. One entry per switch; the cost lines are the operator-facing
|
|
1160
|
+
* contract and are written to be true rather than reassuring.
|
|
1161
|
+
*/
|
|
1162
|
+
var CAMERA_SWITCH_CATALOG = {
|
|
1163
|
+
"stream-broker": {
|
|
1164
|
+
id: "stream-broker",
|
|
1165
|
+
label: "Camera",
|
|
1166
|
+
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.",
|
|
1167
|
+
authority: { kind: "device-disabled" },
|
|
1168
|
+
countsAsSwitchedOff: true
|
|
1169
|
+
},
|
|
1170
|
+
"object-detection": {
|
|
1171
|
+
id: "object-detection",
|
|
1172
|
+
label: "Object detection & tracking",
|
|
1173
|
+
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.",
|
|
1174
|
+
authority: {
|
|
1175
|
+
kind: "wrapper-binding",
|
|
1176
|
+
capName: DETECTION_PIPELINE_CAP_NAME
|
|
1177
|
+
},
|
|
1178
|
+
countsAsSwitchedOff: true
|
|
1179
|
+
},
|
|
1180
|
+
"privacy-mask": {
|
|
1181
|
+
id: "privacy-mask",
|
|
1182
|
+
label: "Privacy mask",
|
|
1183
|
+
costWhenOff: "On: the zones you drew on this camera are blacked out by the camera itself — live view, playback, exports and detection all see the black boxes, and nothing behind them was ever recorded. Off: the camera captures the whole frame, and your zones are kept for when you turn it back on.",
|
|
1184
|
+
authority: {
|
|
1185
|
+
kind: "camera-mask",
|
|
1186
|
+
capName: PRIVACY_MASK_CAP_NAME
|
|
1187
|
+
},
|
|
1188
|
+
countsAsSwitchedOff: false
|
|
1189
|
+
},
|
|
1190
|
+
"device-audio": {
|
|
1191
|
+
id: "device-audio",
|
|
1192
|
+
label: "Camera microphone",
|
|
1193
|
+
costWhenOff: "Off: the camera captures no sound at all. Live view and recordings become silent video, and audio detection and classification have nothing left to analyse — turning them back on will not recover it. The picture, motion and object detection are unaffected, and two-way talk still works. Applies at the camera, so every consumer sees the same silence.",
|
|
1194
|
+
authority: {
|
|
1195
|
+
kind: "camera-audio",
|
|
1196
|
+
capName: PRIVACY_MASK_CAP_NAME
|
|
1197
|
+
},
|
|
1198
|
+
countsAsSwitchedOff: true
|
|
1199
|
+
},
|
|
1200
|
+
"audio-analysis": {
|
|
1201
|
+
id: "audio-analysis",
|
|
1202
|
+
label: "Audio detection & classification",
|
|
1203
|
+
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.",
|
|
1204
|
+
authority: {
|
|
1205
|
+
kind: "wrapper-binding",
|
|
1206
|
+
capName: AUDIO_ANALYSIS_CAP_NAME
|
|
1207
|
+
},
|
|
1208
|
+
countsAsSwitchedOff: true
|
|
1209
|
+
},
|
|
1210
|
+
recording: {
|
|
1211
|
+
id: "recording",
|
|
1212
|
+
label: "Recording",
|
|
1213
|
+
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.",
|
|
1214
|
+
authority: { kind: "recording-config" },
|
|
1215
|
+
countsAsSwitchedOff: true
|
|
1216
|
+
},
|
|
1217
|
+
notifications: {
|
|
1218
|
+
id: "notifications",
|
|
1219
|
+
label: "Notifications",
|
|
1220
|
+
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.",
|
|
1221
|
+
authority: { kind: "notification-mute" },
|
|
1222
|
+
countsAsSwitchedOff: true
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
/** Resolve one switch's `{ available, enabled }` pair. */
|
|
1226
|
+
function resolveState(descriptor, input) {
|
|
1227
|
+
switch (descriptor.authority.kind) {
|
|
1228
|
+
case "device-disabled": return {
|
|
1229
|
+
available: true,
|
|
1230
|
+
enabled: !input.deviceDisabled
|
|
1231
|
+
};
|
|
1232
|
+
case "wrapper-binding": {
|
|
1233
|
+
const capName = descriptor.authority.capName;
|
|
1234
|
+
if (input.bindableCapNames === null || input.activeWrapperCapNames === null) return {
|
|
1235
|
+
available: false,
|
|
1236
|
+
enabled: true,
|
|
1237
|
+
unavailableReason: "source-unreachable"
|
|
1238
|
+
};
|
|
1239
|
+
if (!input.bindableCapNames.includes(capName)) return {
|
|
1240
|
+
available: false,
|
|
1241
|
+
enabled: true,
|
|
1242
|
+
unavailableReason: "no-provider"
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
available: true,
|
|
1246
|
+
enabled: input.activeWrapperCapNames.includes(capName)
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
case "recording-config":
|
|
1250
|
+
if (input.recordingEnabled === null) return {
|
|
1251
|
+
available: false,
|
|
1252
|
+
enabled: true,
|
|
1253
|
+
unavailableReason: "source-unreachable"
|
|
1254
|
+
};
|
|
1255
|
+
return {
|
|
1256
|
+
available: true,
|
|
1257
|
+
enabled: input.recordingEnabled
|
|
1258
|
+
};
|
|
1259
|
+
case "notification-mute":
|
|
1260
|
+
if (input.notificationsMuted === null) return {
|
|
1261
|
+
available: false,
|
|
1262
|
+
enabled: true,
|
|
1263
|
+
unavailableReason: "source-unreachable"
|
|
1264
|
+
};
|
|
1265
|
+
return {
|
|
1266
|
+
available: true,
|
|
1267
|
+
enabled: !input.notificationsMuted
|
|
1268
|
+
};
|
|
1269
|
+
case "camera-audio": {
|
|
1270
|
+
const audio = input.deviceAudio;
|
|
1271
|
+
if (audio === null) return {
|
|
1272
|
+
available: false,
|
|
1273
|
+
enabled: true,
|
|
1274
|
+
unavailableReason: "source-unreachable"
|
|
1275
|
+
};
|
|
1276
|
+
if (!audio.supported) return {
|
|
1277
|
+
available: false,
|
|
1278
|
+
enabled: true,
|
|
1279
|
+
unavailableReason: "no-provider"
|
|
1280
|
+
};
|
|
1281
|
+
if (audio.enabled === null) return {
|
|
1282
|
+
available: false,
|
|
1283
|
+
enabled: true,
|
|
1284
|
+
unavailableReason: "source-unreachable"
|
|
1285
|
+
};
|
|
1286
|
+
return {
|
|
1287
|
+
available: true,
|
|
1288
|
+
enabled: audio.enabled
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
case "camera-mask": {
|
|
1292
|
+
const mask = input.privacyMask;
|
|
1293
|
+
if (mask === null) return {
|
|
1294
|
+
available: false,
|
|
1295
|
+
enabled: false,
|
|
1296
|
+
unavailableReason: "source-unreachable"
|
|
1297
|
+
};
|
|
1298
|
+
if (!mask.supported) return {
|
|
1299
|
+
available: false,
|
|
1300
|
+
enabled: false,
|
|
1301
|
+
unavailableReason: "no-provider"
|
|
1302
|
+
};
|
|
1303
|
+
if (mask.configuredRegions === null || mask.enabled === null) return {
|
|
1304
|
+
available: false,
|
|
1305
|
+
enabled: false,
|
|
1306
|
+
unavailableReason: "source-unreachable"
|
|
1307
|
+
};
|
|
1308
|
+
if (mask.configuredRegions === 0) return {
|
|
1309
|
+
available: false,
|
|
1310
|
+
enabled: false,
|
|
1311
|
+
unavailableReason: "not-configured"
|
|
1312
|
+
};
|
|
1313
|
+
return {
|
|
1314
|
+
available: true,
|
|
1315
|
+
enabled: mask.enabled
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Pure derivation of the whole group. No I/O — the orchestrator gathers, this
|
|
1322
|
+
* decides, so the decision is testable without a hub.
|
|
1323
|
+
*
|
|
1324
|
+
* Order is {@link CAMERA_SWITCH_ORDER}; unavailable switches are RETURNED
|
|
1325
|
+
* rather than filtered out, so a client can explain the gap instead of
|
|
1326
|
+
* silently rendering four buttons where another camera shows five.
|
|
1327
|
+
*/
|
|
1328
|
+
function deriveCameraSwitches(input) {
|
|
1329
|
+
return CAMERA_SWITCH_ORDER.map((id) => {
|
|
1330
|
+
const descriptor = CAMERA_SWITCH_CATALOG[id];
|
|
1331
|
+
const state = resolveState(descriptor, input);
|
|
1332
|
+
return {
|
|
1333
|
+
id: descriptor.id,
|
|
1334
|
+
label: descriptor.label,
|
|
1335
|
+
costWhenOff: descriptor.costWhenOff,
|
|
1336
|
+
available: state.available,
|
|
1337
|
+
...state.unavailableReason !== void 0 ? { unavailableReason: state.unavailableReason } : {},
|
|
1338
|
+
enabled: state.enabled,
|
|
1339
|
+
authority: descriptor.authority
|
|
1340
|
+
};
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* The ids an operator has switched OFF, for a status surface.
|
|
1345
|
+
*
|
|
1346
|
+
* This is the answer to "a disabled function must be visible as DISABLED, not
|
|
1347
|
+
* merely quiet": a camera reporting zero detections with
|
|
1348
|
+
* `switchedOff: ['object-detection']` was turned off; the same camera with an
|
|
1349
|
+
* empty list is broken. Unavailable switches never appear — a function nobody
|
|
1350
|
+
* provides was not switched off by anyone.
|
|
1351
|
+
*
|
|
1352
|
+
* `privacy-mask` never appears either, whichever way it is sitting, because its
|
|
1353
|
+
* ON means "the mask is active" rather than "the function works"
|
|
1354
|
+
* ({@link CameraSwitchDescriptor.countsAsSwitchedOff}). Without that filter the
|
|
1355
|
+
* ordinary state of every camera nobody has masked would carry a
|
|
1356
|
+
* "switched off" badge, and the badge that matters would be lost in it.
|
|
1357
|
+
*/
|
|
1358
|
+
function switchedOffIds(switches) {
|
|
1359
|
+
return switches.filter((s) => CAMERA_SWITCH_CATALOG[s.id].countsAsSwitchedOff && s.available && !s.enabled).map((s) => s.id);
|
|
1360
|
+
}
|
|
1361
|
+
//#endregion
|
|
334
1362
|
//#region src/interfaces/device-capabilities/camera.ts
|
|
335
1363
|
/** Friendly display labels for stream quality IDs. */
|
|
336
1364
|
var STREAM_QUALITY_LABELS = {
|
|
@@ -526,14 +1554,16 @@ var OpsLogOpSchema = zod.z.enum([
|
|
|
526
1554
|
"manual-delete",
|
|
527
1555
|
"rescan",
|
|
528
1556
|
"retention-run",
|
|
529
|
-
"relocate"
|
|
1557
|
+
"relocate",
|
|
1558
|
+
"orphan-audit"
|
|
530
1559
|
]);
|
|
531
1560
|
/** Why the operation ran. */
|
|
532
1561
|
var OpsLogReasonSchema = zod.z.enum([
|
|
533
1562
|
"retention",
|
|
534
1563
|
"quota",
|
|
535
1564
|
"manual",
|
|
536
|
-
"operator"
|
|
1565
|
+
"operator",
|
|
1566
|
+
"maintenance"
|
|
537
1567
|
]);
|
|
538
1568
|
/** One audit row, shared verbatim by both domains. */
|
|
539
1569
|
var OpsLogEntrySchema = zod.z.object({
|
|
@@ -3314,6 +4344,126 @@ var RtpSourceSchema = zod.z.object({
|
|
|
3314
4344
|
encoder: zod.z.string(),
|
|
3315
4345
|
pipelineKey: zod.z.string()
|
|
3316
4346
|
});
|
|
4347
|
+
/**
|
|
4348
|
+
* The encode request — **structured and serialisable, with NO raw-flag escape
|
|
4349
|
+
* hatch.** This is deliberate and it is the one lesson taken from
|
|
4350
|
+
* `getStreamWithCodec`: that method's `outputArgs: string[]` is simultaneously
|
|
4351
|
+
* its extensibility mechanism AND part of `pipelineKeyFor`'s sharing key, so
|
|
4352
|
+
* adding a flag silently forks the shared child, and two consumers that mean
|
|
4353
|
+
* the same thing but spell it differently never share. Here every knob is a
|
|
4354
|
+
* NAMED field: a new requirement becomes a schema field (and a codegen run),
|
|
4355
|
+
* never an opaque array.
|
|
4356
|
+
*
|
|
4357
|
+
* `inputArgs` / `outputArgs` are omitted from the profile for the same reason.
|
|
4358
|
+
* The operator-facing derived-stream transform editor still has them — that is
|
|
4359
|
+
* a different surface (`publishCameraStream({ kind: 'derived' })`) with a
|
|
4360
|
+
* different purpose (reshaping a badly-behaved SOURCE), and it is unchanged.
|
|
4361
|
+
*/
|
|
4362
|
+
var EgressEncodeSchema = EncodeProfileSchema.omit({
|
|
4363
|
+
inputArgs: true,
|
|
4364
|
+
outputArgs: true
|
|
4365
|
+
});
|
|
4366
|
+
/**
|
|
4367
|
+
* How the encoder is bounded. `'tight'` is a one-second VBV window for a
|
|
4368
|
+
* consumer whose budget is enforced per second (HomeKit); `'relaxed'` is two
|
|
4369
|
+
* seconds, letting a keyframe spike borrow from the next second (a browser,
|
|
4370
|
+
* an Echo). Named rather than numeric so the INTENT survives.
|
|
4371
|
+
*/
|
|
4372
|
+
var EgressRateControlSchema = zod.z.enum(["tight", "relaxed"]);
|
|
4373
|
+
var EgressTranscodeRequestSchema = zod.z.object({
|
|
4374
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
4375
|
+
/** Which published stream to read. */
|
|
4376
|
+
source: zod.z.discriminatedUnion("kind", [zod.z.object({
|
|
4377
|
+
kind: zod.z.literal("profile"),
|
|
4378
|
+
profile: require_sleep.CamProfileSchema
|
|
4379
|
+
}), zod.z.object({
|
|
4380
|
+
kind: zod.z.literal("cam-stream"),
|
|
4381
|
+
camStreamId: zod.z.string().min(1)
|
|
4382
|
+
})]),
|
|
4383
|
+
encode: EgressEncodeSchema,
|
|
4384
|
+
rateControl: EgressRateControlSchema.optional(),
|
|
4385
|
+
/**
|
|
4386
|
+
* `-bsf:v`. A consumer that negotiates its OWN SDP (HomeKit) cannot carry
|
|
4387
|
+
* out-of-band extradata and needs `dump_extra` on both the copy and encode
|
|
4388
|
+
* branches. Enumerated, not free text.
|
|
4389
|
+
*/
|
|
4390
|
+
bitstreamFilter: zod.z.enum([
|
|
4391
|
+
"dump_extra",
|
|
4392
|
+
"h264_mp4toannexb",
|
|
4393
|
+
"hevc_mp4toannexb"
|
|
4394
|
+
]).optional(),
|
|
4395
|
+
/**
|
|
4396
|
+
* Publish the transcode as a LOCAL push cam stream, instead of leaving the
|
|
4397
|
+
* consumer to dial the returned url. The broker picks the id and returns it
|
|
4398
|
+
* as `camStreamId` — a caller-supplied one would be circular, since the
|
|
4399
|
+
* sharing key is computed FROM this request.
|
|
4400
|
+
*
|
|
4401
|
+
* The url is still returned and still the contract for a transcode pinned to
|
|
4402
|
+
* another node. But dialling it locally costs an RTSP round trip that changes
|
|
4403
|
+
* the transport underneath the consumer: a dialled stream is an RTP source,
|
|
4404
|
+
* so `isRtpSource()` is true and the session takes the RTP-passthrough +
|
|
4405
|
+
* repacketizer branch. The push branch — the one the derived mechanism has
|
|
4406
|
+
* live hours on — is never reached. Measured on Alexa: broker registered, RTP
|
|
4407
|
+
* arriving, key frame arriving, black screen, on a chain healthy at every
|
|
4408
|
+
* other point.
|
|
4409
|
+
*
|
|
4410
|
+
* Same idea the transport already applies to CALLS, where `classifyCapRoute`
|
|
4411
|
+
* gives priority to `hub-in-process` so a local call never leaves the node.
|
|
4412
|
+
* This is that rule for media.
|
|
4413
|
+
*/
|
|
4414
|
+
publishLocally: zod.z.boolean().optional(),
|
|
4415
|
+
pixelFormat: zod.z.enum(["yuv420p", "nv12"]).optional(),
|
|
4416
|
+
/**
|
|
4417
|
+
* Operator/consumer override for decode hardware. ABSENT is the normal case
|
|
4418
|
+
* and the one that matters: the broker then resolves the backend from the
|
|
4419
|
+
* DECODER ADDON's per-node `probedBestHwaccel` (see
|
|
4420
|
+
* `@camstack/types` `ffmpeg/hwaccel.ts`), which is the ranking known to work
|
|
4421
|
+
* on this hardware — never the raw kernel resolver's qsv-first order.
|
|
4422
|
+
*/
|
|
4423
|
+
decodeHwAccel: zod.z.enum([
|
|
4424
|
+
"auto",
|
|
4425
|
+
"none",
|
|
4426
|
+
"videotoolbox",
|
|
4427
|
+
"vaapi",
|
|
4428
|
+
"qsv",
|
|
4429
|
+
"cuda"
|
|
4430
|
+
]).optional(),
|
|
4431
|
+
/**
|
|
4432
|
+
* Host to embed in the returned restream `url`. The broker mints hub-local
|
|
4433
|
+
* `127.0.0.1` URLs; a consumer on another node passes a cluster-resolvable
|
|
4434
|
+
* host (`NodeTopologyService.reachableHostByNode`) so the returned URL is
|
|
4435
|
+
* dialable from there. Same contract as `getStreamWithCodec.hostname` —
|
|
4436
|
+
* `substituteRtspHost` rewrites only the dial address, never the restreamer.
|
|
4437
|
+
*/
|
|
4438
|
+
hostname: zod.z.string().optional(),
|
|
4439
|
+
/** Attribution for the broker panel. Never part of the sharing key. */
|
|
4440
|
+
tag: zod.z.string().optional()
|
|
4441
|
+
});
|
|
4442
|
+
var EgressTranscodeSchema = zod.z.object({
|
|
4443
|
+
/** Dial-able RTSP url (host-substituted when `hostname` was supplied). */
|
|
4444
|
+
url: zod.z.string(),
|
|
4445
|
+
/** Release handle. Refcounted — the child dies when the last holder releases. */
|
|
4446
|
+
pipelineKey: zod.z.string(),
|
|
4447
|
+
videoCodec: zod.z.enum(["H264", "H265"]),
|
|
4448
|
+
resolution: zod.z.object({
|
|
4449
|
+
width: zod.z.number().int().positive(),
|
|
4450
|
+
height: zod.z.number().int().positive()
|
|
4451
|
+
}),
|
|
4452
|
+
transcoded: zod.z.boolean(),
|
|
4453
|
+
encoder: zod.z.string(),
|
|
4454
|
+
/**
|
|
4455
|
+
* The decode backend the child ACTUALLY ran with — `null` for software.
|
|
4456
|
+
* Returned rather than assumed: a consumer that asked for hardware and got
|
|
4457
|
+
* software needs to be able to see that without reading the broker's logs.
|
|
4458
|
+
*/
|
|
4459
|
+
decodeHwAccel: zod.z.string().nullable(),
|
|
4460
|
+
/**
|
|
4461
|
+
* Set when `publishLocally` was honoured: attach to THIS instead of dialling
|
|
4462
|
+
* `url`, and the session takes the push/deframe transport rather than the
|
|
4463
|
+
* RTP-passthrough one. `null` means the consumer must dial.
|
|
4464
|
+
*/
|
|
4465
|
+
camStreamId: zod.z.string().nullable()
|
|
4466
|
+
});
|
|
3317
4467
|
var streamBrokerCapability = {
|
|
3318
4468
|
name: "stream-broker",
|
|
3319
4469
|
scope: "system",
|
|
@@ -3485,6 +4635,40 @@ var streamBrokerCapability = {
|
|
|
3485
4635
|
auth: "admin"
|
|
3486
4636
|
}),
|
|
3487
4637
|
/**
|
|
4638
|
+
* THE ffmpeg primitive. Acquire an encoded stream matching a structured
|
|
4639
|
+
* encode plan; the broker builds the argv through the ONE builder
|
|
4640
|
+
* (`@camstack/types` `ffmpeg/invocation.ts`), resolves decode hardware from
|
|
4641
|
+
* the DECODER ADDON's per-node ranking, and returns a dialable RTSP url.
|
|
4642
|
+
*
|
|
4643
|
+
* **Refcounted and deduplicated on EXACT match.** Two requesters whose
|
|
4644
|
+
* requests produce the same `egressTranscodeSharingKey` receive the SAME
|
|
4645
|
+
* `pipelineKey` and the same child process. Nearly-identical requests are
|
|
4646
|
+
* NOT merged.
|
|
4647
|
+
*
|
|
4648
|
+
* **The handle is immutable.** There is deliberately no `reconfigure`
|
|
4649
|
+
* method and this one never accepts a `pipelineKey` alongside encode
|
|
4650
|
+
* parameters: a consumer whose requirements change releases and
|
|
4651
|
+
* re-acquires. A mutable shared handle is exactly what made Alexa's old
|
|
4652
|
+
* `derived:alexa-<id>` stream a co-tenant hazard — one consumer's
|
|
4653
|
+
* downgrade dragged every other consumer down with it.
|
|
4654
|
+
*
|
|
4655
|
+
* Placement: unpinned, `classifyCapRoute` serves this hub-in-process
|
|
4656
|
+
* (Priority 1). A caller that wants the transcode elsewhere passes
|
|
4657
|
+
* `nodePin(nodeId)` and a `hostname` it can dial.
|
|
4658
|
+
*/
|
|
4659
|
+
acquireEgressTranscode: require_sleep.method(EgressTranscodeRequestSchema, EgressTranscodeSchema, {
|
|
4660
|
+
kind: "mutation",
|
|
4661
|
+
auth: "admin"
|
|
4662
|
+
}),
|
|
4663
|
+
/** Drop one reference. The child dies when the last holder releases. */
|
|
4664
|
+
releaseEgressTranscode: require_sleep.method(zod.z.object({ pipelineKey: zod.z.string() }), zod.z.object({
|
|
4665
|
+
released: zod.z.boolean(),
|
|
4666
|
+
refcount: zod.z.number().int().nonnegative()
|
|
4667
|
+
}), {
|
|
4668
|
+
kind: "mutation",
|
|
4669
|
+
auth: "admin"
|
|
4670
|
+
}),
|
|
4671
|
+
/**
|
|
3488
4672
|
* ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
|
|
3489
4673
|
*
|
|
3490
4674
|
* The serialisable replacement for the live-object `IStreamBroker.
|
|
@@ -10029,12 +11213,13 @@ var NcConditionsSchema = zod.z.object({
|
|
|
10029
11213
|
* source; otherwise the subject's source must equal it. Legacy records
|
|
10030
11214
|
* with no stamped source are treated as `pipeline`. The union spans both
|
|
10031
11215
|
* record kinds — object events carry `pipeline` | `onboard`, synthetic
|
|
10032
|
-
* tracks carry `sensor
|
|
11216
|
+
* tracks carry `sensor` (a linked device) or `audio` (a D62 audio marker).
|
|
10033
11217
|
*/
|
|
10034
11218
|
source: zod.z.enum([
|
|
10035
11219
|
"pipeline",
|
|
10036
11220
|
"onboard",
|
|
10037
11221
|
"sensor",
|
|
11222
|
+
"audio",
|
|
10038
11223
|
"any"
|
|
10039
11224
|
]).optional(),
|
|
10040
11225
|
/**
|
|
@@ -10588,6 +11773,10 @@ var NC_CONDITION_CATALOG = [
|
|
|
10588
11773
|
{
|
|
10589
11774
|
value: "sensor",
|
|
10590
11775
|
label: "Sensor"
|
|
11776
|
+
},
|
|
11777
|
+
{
|
|
11778
|
+
value: "audio",
|
|
11779
|
+
label: "Audio marker"
|
|
10591
11780
|
}
|
|
10592
11781
|
],
|
|
10593
11782
|
operator: "in",
|
|
@@ -10598,7 +11787,7 @@ var NC_CONDITION_CATALOG = [
|
|
|
10598
11787
|
"package-event"
|
|
10599
11788
|
],
|
|
10600
11789
|
phase: "P1",
|
|
10601
|
-
description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
|
|
11790
|
+
description: "pipeline / onboard / sensor / audio; a record with no stamped source counts as pipeline."
|
|
10602
11791
|
},
|
|
10603
11792
|
{
|
|
10604
11793
|
id: "deviceState",
|
|
@@ -10950,6 +12139,35 @@ var notificationRulesCapability = {
|
|
|
10950
12139
|
auth: "admin"
|
|
10951
12140
|
}),
|
|
10952
12141
|
/**
|
|
12142
|
+
* PERMANENT per-camera mute — the notifications half of the per-camera
|
|
12143
|
+
* function switch group ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
12144
|
+
*
|
|
12145
|
+
* Deliberately NOT a snooze. A snooze is bounded at
|
|
12146
|
+
* {@link NC_SNOOZE_MAX_MINUTES} on purpose — "a snooze that could not
|
|
12147
|
+
* expire would be an outage the operator asked for once and forgot" — and
|
|
12148
|
+
* widening it to express "this camera never notifies" would destroy that
|
|
12149
|
+
* property for every snooze. A mute is the other thing: an explicit,
|
|
12150
|
+
* indefinite, admin-only decision, visible in the switch group next to the
|
|
12151
|
+
* other four, and reported on `CameraStatus.switchedOff` so a silent
|
|
12152
|
+
* camera never reads as a working one.
|
|
12153
|
+
*
|
|
12154
|
+
* Returned as ONE list rather than a per-camera query: the group's reader
|
|
12155
|
+
* needs every camera's state, and a per-camera fan-out over the viewer's
|
|
12156
|
+
* single WebSocket is N frames serialised on one socket.
|
|
12157
|
+
*/
|
|
12158
|
+
listDeviceMutes: require_sleep.method(zod.z.object({}), zod.z.object({ mutedDeviceIds: zod.z.array(zod.z.number().int()).readonly() }), { auth: "admin" }),
|
|
12159
|
+
/**
|
|
12160
|
+
* Mute or unmute one camera. Idempotent; an unmute of a camera that was
|
|
12161
|
+
* never muted succeeds.
|
|
12162
|
+
*/
|
|
12163
|
+
setDeviceMuted: require_sleep.method(zod.z.object({
|
|
12164
|
+
deviceId: zod.z.number().int(),
|
|
12165
|
+
muted: zod.z.boolean()
|
|
12166
|
+
}), zod.z.object({ success: zod.z.literal(true) }), {
|
|
12167
|
+
kind: "mutation",
|
|
12168
|
+
auth: "admin"
|
|
12169
|
+
}),
|
|
12170
|
+
/**
|
|
10953
12171
|
* Dry-run a rule against recently persisted records (object events for
|
|
10954
12172
|
* `immediate`, closed tracks for `track-end`). Mutation kind only to
|
|
10955
12173
|
* carry the full rule object safely; no side effects.
|
|
@@ -11367,12 +12585,60 @@ var TrackAudioLabelSchema = zod.z.object({
|
|
|
11367
12585
|
});
|
|
11368
12586
|
/**
|
|
11369
12587
|
* How a track was produced. `pipeline` (default / absent) = the spatial
|
|
11370
|
-
* detection+tracking pipeline.
|
|
11371
|
-
*
|
|
11372
|
-
*
|
|
11373
|
-
*
|
|
12588
|
+
* detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
|
|
12589
|
+
* no positions, a single snapshot, and no bbox trajectory at all:
|
|
12590
|
+
*
|
|
12591
|
+
* - `sensor` — a linked sensor/control device state change.
|
|
12592
|
+
* - `audio` — an audio event on the camera itself that was anomalous for
|
|
12593
|
+
* THAT camera, loud, and heard while nothing visual was happening (D62).
|
|
12594
|
+
*
|
|
12595
|
+
* The spatial subsystems (tracker association, occupancy count, re-id /
|
|
12596
|
+
* embedding, resurrection) MUST skip every synthetic source. Test for that
|
|
12597
|
+
* with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
|
|
12598
|
+
* check silently readmits every source added after it was written.
|
|
11374
12599
|
*/
|
|
11375
|
-
var TrackSourceSchema = zod.z.enum([
|
|
12600
|
+
var TrackSourceSchema = zod.z.enum([
|
|
12601
|
+
"pipeline",
|
|
12602
|
+
"sensor",
|
|
12603
|
+
"audio"
|
|
12604
|
+
]);
|
|
12605
|
+
/**
|
|
12606
|
+
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
12607
|
+
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
12608
|
+
* so the two surfaces cannot drift.
|
|
12609
|
+
*
|
|
12610
|
+
* **Absent ≠ false.** A track that has never been touched omits the field; an
|
|
12611
|
+
* explicitly un-flagged track carries `false`. Legacy rows written before the
|
|
12612
|
+
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
12613
|
+
* `flag === true`, not `flag !== false`.
|
|
12614
|
+
*
|
|
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.
|
|
12619
|
+
*/
|
|
12620
|
+
var TrackFlagFields = {
|
|
12621
|
+
/** Operator marked this track as training material. */
|
|
12622
|
+
markForTrain: zod.z.boolean().optional(),
|
|
12623
|
+
/** Operator marked this track for diagnostic attention. */
|
|
12624
|
+
debug: zod.z.boolean().optional()
|
|
12625
|
+
};
|
|
12626
|
+
/**
|
|
12627
|
+
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
12628
|
+
* one flag can never clear the other — the toggles are independent and are
|
|
12629
|
+
* driven from three surfaces that do not know about each other.
|
|
12630
|
+
*/
|
|
12631
|
+
var TrackFlagsPatchSchema = zod.z.object(TrackFlagFields);
|
|
12632
|
+
/**
|
|
12633
|
+
* The resolved flag state after a write. Both fields are REQUIRED here (absent
|
|
12634
|
+
* collapses to `false`) so a caller can drive a toggle's checked state off the
|
|
12635
|
+
* mutation result without a re-fetch.
|
|
12636
|
+
*/
|
|
12637
|
+
var TrackFlagsSchema = zod.z.object({
|
|
12638
|
+
trackId: zod.z.string(),
|
|
12639
|
+
markForTrain: zod.z.boolean(),
|
|
12640
|
+
debug: zod.z.boolean()
|
|
12641
|
+
});
|
|
11376
12642
|
var TrackSchema = zod.z.object({
|
|
11377
12643
|
trackId: zod.z.string(),
|
|
11378
12644
|
deviceId: zod.z.number(),
|
|
@@ -11415,7 +12681,8 @@ var TrackSchema = zod.z.object({
|
|
|
11415
12681
|
/** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
|
|
11416
12682
|
* Populated from the persisted envelope columns on historical reads;
|
|
11417
12683
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
11418
|
-
envelope: TrackEnvelopeSchema.optional()
|
|
12684
|
+
envelope: TrackEnvelopeSchema.optional(),
|
|
12685
|
+
...TrackFlagFields
|
|
11419
12686
|
});
|
|
11420
12687
|
var BaseEventFields = {
|
|
11421
12688
|
id: zod.z.string(),
|
|
@@ -11636,7 +12903,8 @@ var KeyEventSchema = zod.z.object({
|
|
|
11636
12903
|
/** Highest-confidence ObjectEvent id for the track (empty when none). */
|
|
11637
12904
|
bestEventId: zod.z.string(),
|
|
11638
12905
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
11639
|
-
windowMs: zod.z.number().optional()
|
|
12906
|
+
windowMs: zod.z.number().optional(),
|
|
12907
|
+
...TrackFlagFields
|
|
11640
12908
|
});
|
|
11641
12909
|
var TrackedDetectionSchema = zod.z.object({
|
|
11642
12910
|
trackId: zod.z.string(),
|
|
@@ -11722,7 +12990,31 @@ var RebuildObjectEmbeddingsInput = zod.z.object({
|
|
|
11722
12990
|
since: zod.z.number().optional(),
|
|
11723
12991
|
until: zod.z.number().optional(),
|
|
11724
12992
|
/** Stop after this many tracks; the result reports whether more remain. */
|
|
11725
|
-
maxTracks: zod.z.number().int().positive().optional()
|
|
12993
|
+
maxTracks: zod.z.number().int().positive().optional(),
|
|
12994
|
+
/**
|
|
12995
|
+
* Run every embedding on THIS node instead of round-robining the fleet.
|
|
12996
|
+
*
|
|
12997
|
+
* Named `executeOnNodeId` and not `nodeId` on purpose: an inline `nodeId`
|
|
12998
|
+
* field in cap args is read by `parent-unowned-call.ts` as a ROUTING PIN, so
|
|
12999
|
+
* calling it that would pin the rebuild REQUEST itself to that node — the
|
|
13000
|
+
* rebuild orchestration lives on the hub, and only the per-track step runs
|
|
13001
|
+
* remotely. This field is data; the per-track pin is applied inside.
|
|
13002
|
+
*
|
|
13003
|
+
* Absent ⇒ round-robin over every online node whose runner can serve the
|
|
13004
|
+
* pinned model.
|
|
13005
|
+
*/
|
|
13006
|
+
executeOnNodeId: zod.z.string().optional(),
|
|
13007
|
+
/**
|
|
13008
|
+
* Milliseconds to wait between tracks; omit for the built-in default, `0` to
|
|
13009
|
+
* run flat out.
|
|
13010
|
+
*
|
|
13011
|
+
* A rebuild is bulk maintenance on hub-main's single thread. Measured
|
|
13012
|
+
* 2026-08-06, an unpaced pass held that thread busy 82.2 s out of 120 and
|
|
13013
|
+
* pushed `nodes.topology` from 0.25 s to 26 s for 43 minutes. The value in
|
|
13014
|
+
* force is logged at start and finish so a deliberately slow pass reads
|
|
13015
|
+
* differently from a stalled one.
|
|
13016
|
+
*/
|
|
13017
|
+
pacingMs: zod.z.number().int().nonnegative().optional()
|
|
11726
13018
|
});
|
|
11727
13019
|
/**
|
|
11728
13020
|
* Result of emptying the CLIP index.
|
|
@@ -11756,13 +13048,23 @@ var RebuildStatusSchema = zod.z.object({
|
|
|
11756
13048
|
/** Tracks with no usable detection box. */
|
|
11757
13049
|
missingBbox: zod.z.number(),
|
|
11758
13050
|
/**
|
|
11759
|
-
* Tracks
|
|
11760
|
-
*
|
|
11761
|
-
*
|
|
11762
|
-
*
|
|
11763
|
-
* otherwise read as a total engine outage.
|
|
13051
|
+
* Tracks an executing node REFUSED rather than broke on — an unreadable key
|
|
13052
|
+
* frame, a step that threw. Separate from `failed` because the remedy is
|
|
13053
|
+
* different, and because a whole camera silently contributing zero vectors
|
|
13054
|
+
* is the shape of failure a rebuild must never hide.
|
|
11764
13055
|
*/
|
|
11765
13056
|
notRunnable: zod.z.number(),
|
|
13057
|
+
/**
|
|
13058
|
+
* The pass stopped because NO node could serve the pinned model.
|
|
13059
|
+
*
|
|
13060
|
+
* Distinct from `notRunnable` on purpose: that one says "this track was
|
|
13061
|
+
* refused", this one says "the cluster cannot do this work at all" — every
|
|
13062
|
+
* candidate node either lacks the `clip-embedding` step, lacks a build of the
|
|
13063
|
+
* pinned model for its engine format, or dropped out. The remedy is a model /
|
|
13064
|
+
* engine change, not a per-camera one. Non-zero here always comes with
|
|
13065
|
+
* `complete: false`.
|
|
13066
|
+
*/
|
|
13067
|
+
noCapableNode: zod.z.number(),
|
|
11766
13068
|
failed: zod.z.number(),
|
|
11767
13069
|
/** Set once a pass ends: true only when EVERYTHING was covered. */
|
|
11768
13070
|
complete: zod.z.boolean().nullable(),
|
|
@@ -11952,6 +13254,31 @@ var pipelineAnalyticsCapability = {
|
|
|
11952
13254
|
auth: "admin"
|
|
11953
13255
|
}),
|
|
11954
13256
|
/**
|
|
13257
|
+
* Set the per-track operator flags (`markForTrain`, `debug`) on ONE track.
|
|
13258
|
+
* The patch is PARTIAL — an omitted key is left untouched — because the
|
|
13259
|
+
* three surfaces that write it (admin Events grid, viewer track detail,
|
|
13260
|
+
* viewer cluster detail) each own one toggle and must not clobber the other.
|
|
13261
|
+
*
|
|
13262
|
+
* Writes the track ROW: `markForTrain`/`debug` are per-TRACK state, so they
|
|
13263
|
+
* live where `label` and `importance` live, not in any per-device settings
|
|
13264
|
+
* store. Updates the in-RAM active track too, so a flag set on a live track
|
|
13265
|
+
* survives its expiry-time persist.
|
|
13266
|
+
*
|
|
13267
|
+
* `auth: 'protected'` (the default), NOT `admin`: the viewer is an
|
|
13268
|
+
* 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.
|
|
13271
|
+
*
|
|
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.
|
|
13274
|
+
*/
|
|
13275
|
+
setTrackFlags: require_sleep.method(zod.z.object({
|
|
13276
|
+
/** Log/audit scope only — the trackId is globally unique on its own. */
|
|
13277
|
+
deviceId: zod.z.number(),
|
|
13278
|
+
trackId: zod.z.string(),
|
|
13279
|
+
flags: TrackFlagsPatchSchema
|
|
13280
|
+
}), TrackFlagsSchema, { kind: "mutation" }),
|
|
13281
|
+
/**
|
|
11955
13282
|
* Durable event-store footprint for the management UI: event rows
|
|
11956
13283
|
* (motion + object + audio) counted per camera + total, plus the
|
|
11957
13284
|
* event-owned media bytes on disk per camera + total. Stat/count-based,
|
|
@@ -12840,6 +14167,53 @@ var DetailResultSchema = zod.z.object({
|
|
|
12840
14167
|
nativeFaceShortSidePx: zod.z.number().optional()
|
|
12841
14168
|
});
|
|
12842
14169
|
/**
|
|
14170
|
+
* Why an executing node REFUSED a stateless step run (`runStatelessStep`).
|
|
14171
|
+
*
|
|
14172
|
+
* A refusal is a first-class answer, not an error, because the caller's next
|
|
14173
|
+
* move depends on WHICH one it is — and because "the pass produced nothing"
|
|
14174
|
+
* must never be reachable without a named, counted cause. The two tiers:
|
|
14175
|
+
*
|
|
14176
|
+
* - **node-level** (`unknown-step`, `model-not-servable`) — this node can
|
|
14177
|
+
* never serve this (step, model) pair. The caller drops it from its rotation
|
|
14178
|
+
* and retries the same work elsewhere; nothing about the work changes.
|
|
14179
|
+
* - **work-level** (`unreadable-frame`, `execution-failed`) — this node is
|
|
14180
|
+
* fine, this one request is not. Retrying it on another node would only
|
|
14181
|
+
* spread the same failure.
|
|
14182
|
+
*/
|
|
14183
|
+
var StatelessStepRefusalSchema = zod.z.enum([
|
|
14184
|
+
"unknown-step",
|
|
14185
|
+
"model-not-servable",
|
|
14186
|
+
"unreadable-frame",
|
|
14187
|
+
"execution-failed"
|
|
14188
|
+
]);
|
|
14189
|
+
/**
|
|
14190
|
+
* Answer to `runStatelessStep` — a discriminated union rather than a nullable
|
|
14191
|
+
* result, because `null` is exactly what made the camera-bound detail path
|
|
14192
|
+
* unable to tell "refused" from "never asked".
|
|
14193
|
+
*/
|
|
14194
|
+
var RunStatelessStepResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
|
|
14195
|
+
kind: zod.z.literal("ran"),
|
|
14196
|
+
/** The node that actually executed it — the pin, echoed back for the log. */
|
|
14197
|
+
nodeId: zod.z.string(),
|
|
14198
|
+
/**
|
|
14199
|
+
* The model the step ran with.
|
|
14200
|
+
*
|
|
14201
|
+
* The node verified this exact id has a build for the format it dispatched
|
|
14202
|
+
* on BEFORE running, so the executor's format resolution returns it
|
|
14203
|
+
* unchanged. A caller that pinned a model must compare this field and
|
|
14204
|
+
* treat a mismatch as a refusal — the whole point of the pin is that a
|
|
14205
|
+
* pass writes one feature space.
|
|
14206
|
+
*/
|
|
14207
|
+
modelId: zod.z.string(),
|
|
14208
|
+
details: zod.z.array(DetailResultSchema)
|
|
14209
|
+
}), zod.z.object({
|
|
14210
|
+
kind: zod.z.literal("refused"),
|
|
14211
|
+
nodeId: zod.z.string(),
|
|
14212
|
+
reason: StatelessStepRefusalSchema,
|
|
14213
|
+
/** Human-readable specifics — the format tried, the formats shipped, etc. */
|
|
14214
|
+
detail: zod.z.string()
|
|
14215
|
+
})]);
|
|
14216
|
+
/**
|
|
12843
14217
|
* Per-camera tunable ranges + defaults. Single source of truth used
|
|
12844
14218
|
* by both the Zod data schema (validation + default fallback) and
|
|
12845
14219
|
* the device settings UI (slider min/max/step). Touch one place and
|
|
@@ -13423,7 +14797,77 @@ var pipelineRunnerCapability = {
|
|
|
13423
14797
|
cropJpeg: zod.z.string().optional(),
|
|
13424
14798
|
parent: DetailParentSchema,
|
|
13425
14799
|
steps: zod.z.array(zod.z.string()).optional()
|
|
13426
|
-
}), zod.z.object({ details: zod.z.array(DetailResultSchema) }).nullable(), { kind: "mutation" })
|
|
14800
|
+
}), zod.z.object({ details: zod.z.array(DetailResultSchema) }).nullable(), { kind: "mutation" }),
|
|
14801
|
+
/**
|
|
14802
|
+
* Run ONE enrichment step against caller-supplied pixels, with NO camera
|
|
14803
|
+
* session — no attach, no frame handle, no device affinity.
|
|
14804
|
+
*
|
|
14805
|
+
* ## Why this exists next to `runDetailSubtree` and not inside it
|
|
14806
|
+
*
|
|
14807
|
+
* `runDetailSubtree` resolves its step tree from the LIVE ATTACH STATE. That
|
|
14808
|
+
* is correct for it: its reason to exist is a frame HANDLE, and a handle is
|
|
14809
|
+
* only resolvable while a decode session is open. But it makes the method
|
|
14810
|
+
* useless to anything walking history — cameras run `detectionMode:
|
|
14811
|
+
* 'on-motion'` and detach ~30 s after motion stops, so a fleet-wide
|
|
14812
|
+
* embedding rebuild found most cameras detached and rebuilt only whatever
|
|
14813
|
+
* happened to be attached at that second (measured 2026-08-06: 1125 tracks
|
|
14814
|
+
* scanned, 121 rebuilt, 601 refused as "camera is not attached").
|
|
14815
|
+
*
|
|
14816
|
+
* A caller that ships its own pixels needs none of that machinery. So this
|
|
14817
|
+
* method takes the three things a step actually needs — an image, a box, a
|
|
14818
|
+
* step id — resolves the step from the CATALOG rather than from an
|
|
14819
|
+
* attachment, and runs it on whichever node the caller pinned.
|
|
14820
|
+
*
|
|
14821
|
+
* ## What it deliberately keeps
|
|
14822
|
+
*
|
|
14823
|
+
* The crop rectangle is derived by the SAME `deriveDetailCropRect` call the
|
|
14824
|
+
* live detail plane uses, from the same cluster-wide convention
|
|
14825
|
+
* ([D52](../../../docs/decisions/adr-0052.md)). `frameJpeg` is a FULL frame,
|
|
14826
|
+
* never a pre-cut tile, for exactly the reason `runDetailSubtree` documents:
|
|
14827
|
+
* a caller that cuts is a second feature space.
|
|
14828
|
+
*
|
|
14829
|
+
* ## What it deliberately drops
|
|
14830
|
+
*
|
|
14831
|
+
* No device jump and no `deviceKey`: the jump roster comes from the attach
|
|
14832
|
+
* config, which does not exist here. The step runs on the node's default
|
|
14833
|
+
* engine. This is a maintenance path, not a latency-sensitive one.
|
|
14834
|
+
*
|
|
14835
|
+
* ## Routing
|
|
14836
|
+
*
|
|
14837
|
+
* NOT device-bound. `sourceDeviceId` is DIAGNOSTIC — the camera whose track
|
|
14838
|
+
* these pixels came from, so every log line on the executing node can carry
|
|
14839
|
+
* `tags: { deviceId }`. It is deliberately not named `deviceId`: that field
|
|
14840
|
+
* is a routing hint that would send the call to the camera's owning node,
|
|
14841
|
+
* which is the placement constraint this method exists to remove. Callers
|
|
14842
|
+
* choose the node with `nodePin(nodeId)`; unpinned, an unowned call is
|
|
14843
|
+
* served hub-in-process like any other singleton.
|
|
14844
|
+
*/
|
|
14845
|
+
runStatelessStep: require_sleep.method(zod.z.object({
|
|
14846
|
+
/** Catalog step id, e.g. `clip-embedding`. */
|
|
14847
|
+
stepId: zod.z.string(),
|
|
14848
|
+
/**
|
|
14849
|
+
* REQUIRED model pin. The node runs this exact model or refuses with
|
|
14850
|
+
* `model-not-servable` — it never substitutes a format default, because
|
|
14851
|
+
* a fleet pass that round-robins across nodes would then fill one index
|
|
14852
|
+
* from several encoders.
|
|
14853
|
+
*/
|
|
14854
|
+
modelId: zod.z.string(),
|
|
14855
|
+
/** FULL FRAME, base64 JPEG. The runner cuts — do NOT pre-crop. */
|
|
14856
|
+
frameJpeg: zod.z.string(),
|
|
14857
|
+
/**
|
|
14858
|
+
* The subject box, NORMALISED [0,1] against `frameJpeg`. Normalised on
|
|
14859
|
+
* purpose: the caller stores boxes against a downscaled analysis frame
|
|
14860
|
+
* while the stored key frame is native-resolution, and the only side
|
|
14861
|
+
* that reliably knows the image's pixel dimensions is the side that
|
|
14862
|
+
* decodes it. Denormalising here removes a second reader of the
|
|
14863
|
+
* dimensions and the class of mismatch that comes with it.
|
|
14864
|
+
*/
|
|
14865
|
+
bbox: NativeCropBboxSchema,
|
|
14866
|
+
/** Parent class of the subject (`person`, `vehicle`, …) — carried into the result. */
|
|
14867
|
+
className: zod.z.string(),
|
|
14868
|
+
/** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
|
|
14869
|
+
sourceDeviceId: zod.z.number()
|
|
14870
|
+
}), RunStatelessStepResultSchema, { kind: "mutation" })
|
|
13427
14871
|
}
|
|
13428
14872
|
};
|
|
13429
14873
|
//#endregion
|
|
@@ -13733,6 +15177,20 @@ var CameraStatusSchema = zod.z.object({
|
|
|
13733
15177
|
detection: CameraDetectionStatusSchema.nullable(),
|
|
13734
15178
|
audio: CameraAudioStatusSchema.nullable(),
|
|
13735
15179
|
recording: CameraRecordingStatusSchema.nullable(),
|
|
15180
|
+
/**
|
|
15181
|
+
* Per-camera function switches an OPERATOR has turned off
|
|
15182
|
+
* ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
15183
|
+
*
|
|
15184
|
+
* This is the difference between DISABLED and BROKEN. A camera whose
|
|
15185
|
+
* `detection` block reports zero fps and whose `switchedOff` contains
|
|
15186
|
+
* `'object-detection'` was switched off by a person; the same camera with an
|
|
15187
|
+
* empty list is failing. Every status surface must render the two
|
|
15188
|
+
* differently — a quiet camera that looks identical to a dead one is the
|
|
15189
|
+
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
15190
|
+
*
|
|
15191
|
+
* Empty when nothing is off. Never contains a switch no provider offers.
|
|
15192
|
+
*/
|
|
15193
|
+
switchedOff: zod.z.array(CameraSwitchIdSchema).readonly(),
|
|
13736
15194
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
13737
15195
|
fetchedAt: zod.z.number()
|
|
13738
15196
|
});
|
|
@@ -14132,6 +15590,43 @@ var pipelineOrchestratorCapability = {
|
|
|
14132
15590
|
agentNodeId: zod.z.string().optional()
|
|
14133
15591
|
}), CameraPipelineConfigSchema),
|
|
14134
15592
|
/**
|
|
15593
|
+
* The whole per-camera function switch group, DERIVED — never a stored
|
|
15594
|
+
* list ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
15595
|
+
*
|
|
15596
|
+
* The group adds no state. Each switch is a view onto the authority that
|
|
15597
|
+
* already owned it (`deviceManager.setDisabled`,
|
|
15598
|
+
* `deviceManager.setWrapperActive`, `RecordingConfig.enabled`,
|
|
15599
|
+
* `notificationRules.setDeviceMuted`), and `switch.authority` says which.
|
|
15600
|
+
* Availability comes from `deviceManager.listBindableCapsForDeviceType`,
|
|
15601
|
+
* so a deployment with no audio analyzer renders no audio switch.
|
|
15602
|
+
*
|
|
15603
|
+
* `auth: 'view'` deliberately — a NON-admin must be able to see that a
|
|
15604
|
+
* camera is quiet because somebody switched it off. Only the mutation is
|
|
15605
|
+
* admin-gated.
|
|
15606
|
+
*/
|
|
15607
|
+
getCameraSwitches: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), CameraSwitchGroupSchema),
|
|
15608
|
+
/**
|
|
15609
|
+
* Flip ONE switch, routed to its existing authority.
|
|
15610
|
+
*
|
|
15611
|
+
* Never writes a parallel map: `recording` patches `RecordingConfig.enabled`
|
|
15612
|
+
* and leaves `bands` byte-identical (clearing bands to express "off"
|
|
15613
|
+
* destroys the operator's authored schedule and turning the camera back on
|
|
15614
|
+
* would silently record nothing), and the two pipeline switches write the
|
|
15615
|
+
* SAME wrapper binding the legacy `pipelineEnabled` / `audioEnabled`
|
|
15616
|
+
* booleans were migrated onto.
|
|
15617
|
+
*
|
|
15618
|
+
* Rejects a switch this camera does not offer rather than persisting a
|
|
15619
|
+
* write nothing reads.
|
|
15620
|
+
*/
|
|
15621
|
+
setCameraSwitch: require_sleep.method(zod.z.object({
|
|
15622
|
+
deviceId: zod.z.number(),
|
|
15623
|
+
switchId: CameraSwitchIdSchema,
|
|
15624
|
+
enabled: zod.z.boolean()
|
|
15625
|
+
}), CameraSwitchGroupSchema, {
|
|
15626
|
+
kind: "mutation",
|
|
15627
|
+
auth: "admin"
|
|
15628
|
+
}),
|
|
15629
|
+
/**
|
|
14135
15630
|
* Server-composed aggregated status for a single camera.
|
|
14136
15631
|
*
|
|
14137
15632
|
* Fans out in parallel (bounded, per-stage graceful degradation) to
|
|
@@ -14619,9 +16114,15 @@ var snapshotCapability = {
|
|
|
14619
16114
|
* Bypass the cache freshness check and fetch directly from the
|
|
14620
16115
|
* native (or stream-broker fallback). Triggered by the UI's
|
|
14621
16116
|
* "refresh" button so an operator can force a fresh frame
|
|
14622
|
-
* even when the cache is well within
|
|
14623
|
-
*
|
|
14624
|
-
*
|
|
16117
|
+
* even when the cache is well within the device's
|
|
16118
|
+
* `snapshotMaxAgeS` window.
|
|
16119
|
+
*
|
|
16120
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
16121
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
16122
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
16123
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
16124
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
16125
|
+
* correct answer: stale but honest beats woken.
|
|
14625
16126
|
*/
|
|
14626
16127
|
force: zod.z.boolean().optional()
|
|
14627
16128
|
}), SnapshotImageSchema.nullable()),
|
|
@@ -21244,12 +22745,30 @@ var pressureSensorCapability = {
|
|
|
21244
22745
|
//#endregion
|
|
21245
22746
|
//#region src/capabilities/privacy-mask.cap.ts
|
|
21246
22747
|
/**
|
|
21247
|
-
*
|
|
21248
|
-
*
|
|
21249
|
-
*
|
|
21250
|
-
*
|
|
21251
|
-
*
|
|
21252
|
-
*
|
|
22748
|
+
* PRIVACY — what the camera deliberately does not capture. Two planes:
|
|
22749
|
+
*
|
|
22750
|
+
* - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
|
|
22751
|
+
* grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
|
|
22752
|
+
* `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
|
|
22753
|
+
* 4 vertices, not necessarily axis-aligned). The cap composes the shared
|
|
22754
|
+
* rect|polygon subset of the MaskShape vocabulary. All coords are
|
|
22755
|
+
* normalized 0..1 (top-left origin).
|
|
22756
|
+
* - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
|
|
22757
|
+
* camera encoding an audio track at all, so EVERY consumer — live view,
|
|
22758
|
+
* recording, the audio analyzer, an export — sees silent video. There is
|
|
22759
|
+
* no server-side copy of this fact; the camera is the store and every read
|
|
22760
|
+
* is a read-through, which is why a switch over it cannot drift
|
|
22761
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)).
|
|
22762
|
+
*
|
|
22763
|
+
* Both belong here for one reason: they are the two things an operator turns
|
|
22764
|
+
* off when the answer to "what is this camera allowed to record" changes, and
|
|
22765
|
+
* both are applied ON the device, before anything leaves it.
|
|
22766
|
+
*
|
|
22767
|
+
* **The audio flag has exactly one writer.** `stream-params` used to carry a
|
|
22768
|
+
* per-profile `audio` in its patch schema — reachable from no UI and honoured
|
|
22769
|
+
* by one provider — and it was removed when this landed. A second writer onto
|
|
22770
|
+
* one device register is the shape of every knob this repo has shipped that
|
|
22771
|
+
* disagreed with the one the reader read.
|
|
21253
22772
|
*/
|
|
21254
22773
|
/** A privacy-mask region's geometry — rectangle or free polygon. */
|
|
21255
22774
|
var PrivacyMaskShapeSchema = zod.z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
|
|
@@ -21261,21 +22780,45 @@ var PrivacyMaskRegionSchema = zod.z.object({
|
|
|
21261
22780
|
enabled: zod.z.boolean(),
|
|
21262
22781
|
shape: PrivacyMaskShapeSchema
|
|
21263
22782
|
});
|
|
21264
|
-
/** Current on-camera privacy
|
|
22783
|
+
/** Current on-camera privacy state — mask master enable + zones + microphone. */
|
|
21265
22784
|
var PrivacyMaskStatusSchema = zod.z.object({
|
|
21266
22785
|
enabled: zod.z.boolean(),
|
|
21267
22786
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
21268
22787
|
regions: zod.z.array(PrivacyMaskRegionSchema),
|
|
22788
|
+
/**
|
|
22789
|
+
* Is the camera capturing sound right now? Read from the camera, never from
|
|
22790
|
+
* a server-side mirror.
|
|
22791
|
+
*
|
|
22792
|
+
* `null` means "no answer" — either this camera exposes no controllable
|
|
22793
|
+
* microphone (`getOptions().supportsAudioMute === false`) or the read
|
|
22794
|
+
* failed. A consumer must render `null` as UNKNOWN and never as `false`:
|
|
22795
|
+
* "the microphone is off" and "we could not ask" look identical to an
|
|
22796
|
+
* operator only until one of them is wrong.
|
|
22797
|
+
*
|
|
22798
|
+
* On a camera whose profiles carry the flag independently (Reolink writes
|
|
22799
|
+
* it per stream), `true` means AT LEAST ONE profile still carries audio —
|
|
22800
|
+
* privacy is only satisfied when every one of them is silent.
|
|
22801
|
+
*/
|
|
22802
|
+
audioEnabled: zod.z.boolean().nullable(),
|
|
21269
22803
|
lastFetchedAt: zod.z.number()
|
|
21270
22804
|
});
|
|
21271
|
-
/** Per-camera availability. */
|
|
22805
|
+
/** Per-camera availability. Probed, never assumed from the model name. */
|
|
21272
22806
|
var PrivacyMaskOptionsSchema = zod.z.object({
|
|
21273
22807
|
/** Maximum number of supported zones. */
|
|
21274
22808
|
maxRegions: zod.z.number(),
|
|
21275
22809
|
/** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
|
|
21276
22810
|
supportedShapes: zod.z.array(MaskShapeKindSchema),
|
|
21277
22811
|
/** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
|
|
21278
|
-
polygonVertices: MaskPolygonVerticesSchema.optional()
|
|
22812
|
+
polygonVertices: MaskPolygonVerticesSchema.optional(),
|
|
22813
|
+
/**
|
|
22814
|
+
* Does this camera expose a microphone switch we can actually write?
|
|
22815
|
+
*
|
|
22816
|
+
* Camera-probed: `true` only when the firmware answered with an audio flag
|
|
22817
|
+
* we know how to patch. A camera that never answered is `false` — a control
|
|
22818
|
+
* the operator can press that changes nothing is worse than no control, and
|
|
22819
|
+
* the switch group renders "not available" instead.
|
|
22820
|
+
*/
|
|
22821
|
+
supportsAudioMute: zod.z.boolean()
|
|
21279
22822
|
});
|
|
21280
22823
|
/** Partial change — every field optional. */
|
|
21281
22824
|
var PrivacyMaskPatchSchema = zod.z.object({
|
|
@@ -21302,6 +22845,27 @@ var privacyMaskCapability = {
|
|
|
21302
22845
|
}), zod.z.void(), {
|
|
21303
22846
|
kind: "mutation",
|
|
21304
22847
|
auth: "admin"
|
|
22848
|
+
}),
|
|
22849
|
+
/**
|
|
22850
|
+
* Turn the camera's microphone on or off, at the camera.
|
|
22851
|
+
*
|
|
22852
|
+
* Deliberately its OWN mutation rather than a field on
|
|
22853
|
+
* {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
|
|
22854
|
+
* mask master switch", and overloading it would make one boolean mean two
|
|
22855
|
+
* unrelated things on the same call. It is also the only method here whose
|
|
22856
|
+
* write leaves the device in a state a later `getStatus` reads back
|
|
22857
|
+
* verbatim, which is what makes it safe as a switch authority.
|
|
22858
|
+
*
|
|
22859
|
+
* A camera whose `getOptions().supportsAudioMute` is false must REJECT
|
|
22860
|
+
* this rather than silently accept it — a write nothing applies is exactly
|
|
22861
|
+
* what the switch group exists to remove.
|
|
22862
|
+
*/
|
|
22863
|
+
setAudioEnabled: require_sleep.method(zod.z.object({
|
|
22864
|
+
deviceId: zod.z.number(),
|
|
22865
|
+
enabled: zod.z.boolean()
|
|
22866
|
+
}), zod.z.void(), {
|
|
22867
|
+
kind: "mutation",
|
|
22868
|
+
auth: "admin"
|
|
21305
22869
|
})
|
|
21306
22870
|
},
|
|
21307
22871
|
status: {
|
|
@@ -21310,6 +22874,26 @@ var privacyMaskCapability = {
|
|
|
21310
22874
|
},
|
|
21311
22875
|
runtimeState: PrivacyMaskStatusSchema
|
|
21312
22876
|
};
|
|
22877
|
+
/**
|
|
22878
|
+
* Collapse a camera's PER-PROFILE audio flags into the one answer
|
|
22879
|
+
* {@link PrivacyMaskStatusSchema.shape.audioEnabled} promises.
|
|
22880
|
+
*
|
|
22881
|
+
* Both firmwares this cap talks to store the flag per stream profile, and
|
|
22882
|
+
* both let those profiles disagree. The rule is `some`, not `every`: privacy
|
|
22883
|
+
* is only satisfied when NOTHING is carrying sound, so a camera whose sub
|
|
22884
|
+
* stream is still audible must read as `true` and be switchable off — not as
|
|
22885
|
+
* `false` because the main stream happens to be muted already.
|
|
22886
|
+
*
|
|
22887
|
+
* An empty list is `null` ("this camera reported no audio flag at all"),
|
|
22888
|
+
* never `false`.
|
|
22889
|
+
*
|
|
22890
|
+
* Lives here rather than in each provider so the rule the schema documents
|
|
22891
|
+
* and the rule the providers apply cannot drift apart.
|
|
22892
|
+
*/
|
|
22893
|
+
function summarisePrivacyAudio(profiles) {
|
|
22894
|
+
if (profiles.length === 0) return null;
|
|
22895
|
+
return profiles.some((p) => p.audioEnabled);
|
|
22896
|
+
}
|
|
21313
22897
|
//#endregion
|
|
21314
22898
|
//#region src/capabilities/ptz.cap.ts
|
|
21315
22899
|
var PtzPresetSchema = zod.z.object({
|
|
@@ -21694,6 +23278,21 @@ var LocateSegmentResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
|
|
|
21694
23278
|
})]);
|
|
21695
23279
|
/** Raw bytes of one finalized footage segment (read off disk on the recording node). */
|
|
21696
23280
|
var ReadSegmentBytesResultSchema = zod.z.object({ data: zod.z.instanceof(Uint8Array) });
|
|
23281
|
+
/**
|
|
23282
|
+
* One GOP of a finalized segment, cut by byte range through the segment's own
|
|
23283
|
+
* `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
|
|
23284
|
+
* the single `moof`+`mdat` covering the requested instant — standalone-
|
|
23285
|
+
* demuxable, never the whole file. When the segment's index cannot be parsed
|
|
23286
|
+
* the provider degrades INSIDE the mechanism to the whole segment (still one
|
|
23287
|
+
* `data`, `gopStartMs` = the segment start) — a worse read, not another path.
|
|
23288
|
+
*/
|
|
23289
|
+
var ReadGopBytesResultSchema = zod.z.object({
|
|
23290
|
+
data: zod.z.instanceof(Uint8Array),
|
|
23291
|
+
/** Absolute epoch ms of the returned fragment's first sample. */
|
|
23292
|
+
gopStartMs: zod.z.number(),
|
|
23293
|
+
/** Media ms the returned fragment covers. */
|
|
23294
|
+
gopDurMs: zod.z.number()
|
|
23295
|
+
});
|
|
21697
23296
|
var recordingCapability = {
|
|
21698
23297
|
name: "recording",
|
|
21699
23298
|
scope: "system",
|
|
@@ -21761,6 +23360,18 @@ var recordingCapability = {
|
|
|
21761
23360
|
kind: "query",
|
|
21762
23361
|
auth: "admin"
|
|
21763
23362
|
}),
|
|
23363
|
+
/** Read the single GOP of segment `startMs` covering `epochMs`, by mfra
|
|
23364
|
+
* byte range — the scrub-granular read (D31 letter on the feeder path).
|
|
23365
|
+
* See {@link ReadGopBytesResultSchema} for the degradation contract. */
|
|
23366
|
+
readGopBytes: require_sleep.method(zod.z.object({
|
|
23367
|
+
deviceId: zod.z.number(),
|
|
23368
|
+
profile: zod.z.string(),
|
|
23369
|
+
startMs: zod.z.number(),
|
|
23370
|
+
epochMs: zod.z.number()
|
|
23371
|
+
}), ReadGopBytesResultSchema, {
|
|
23372
|
+
kind: "query",
|
|
23373
|
+
auth: "admin"
|
|
23374
|
+
}),
|
|
21764
23375
|
setDeviceConfig: require_sleep.method(zod.z.object({
|
|
21765
23376
|
deviceId: zod.z.number(),
|
|
21766
23377
|
config: RecordingConfigSchema
|
|
@@ -22422,6 +24033,16 @@ var StreamProfileConfigSchema = zod.z.object({
|
|
|
22422
24033
|
"baseline"
|
|
22423
24034
|
]).optional(),
|
|
22424
24035
|
gop: zod.z.number().optional(),
|
|
24036
|
+
/**
|
|
24037
|
+
* Whether THIS profile currently carries an audio track. READ-ONLY here.
|
|
24038
|
+
*
|
|
24039
|
+
* There is no matching field on {@link StreamProfilePatchSchema}: the
|
|
24040
|
+
* camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
|
|
24041
|
+
* writes every profile at once so "audio off" means silent everywhere. A
|
|
24042
|
+
* per-profile writer beside it would let a camera be half-muted and would be
|
|
24043
|
+
* a second knob onto one device register — the failure D62 exists to
|
|
24044
|
+
* prevent. Absent when the firmware does not report the flag.
|
|
24045
|
+
*/
|
|
22425
24046
|
audio: zod.z.boolean().optional()
|
|
22426
24047
|
});
|
|
22427
24048
|
var StreamParamsStatusSchema = zod.z.object({
|
|
@@ -22462,7 +24083,13 @@ var StreamParamsOptionsSchema = zod.z.object({
|
|
|
22462
24083
|
ext: StreamProfileOptionsSchema.optional()
|
|
22463
24084
|
});
|
|
22464
24085
|
/** A partial change to one profile — every field optional; a provider
|
|
22465
|
-
* ignores fields it doesn't support.
|
|
24086
|
+
* ignores fields it doesn't support.
|
|
24087
|
+
*
|
|
24088
|
+
* There is deliberately NO `audio` here. It existed until 2026-08-07,
|
|
24089
|
+
* reachable from no form and honoured by exactly one provider, while the
|
|
24090
|
+
* camera's microphone is a whole-device fact. It now has one writer,
|
|
24091
|
+
* `privacyMask.setAudioEnabled`, which writes every profile — see
|
|
24092
|
+
* `privacy-mask.cap.ts`. */
|
|
22466
24093
|
var StreamProfilePatchSchema = zod.z.object({
|
|
22467
24094
|
width: zod.z.number().optional(),
|
|
22468
24095
|
height: zod.z.number().optional(),
|
|
@@ -22475,8 +24102,7 @@ var StreamProfilePatchSchema = zod.z.object({
|
|
|
22475
24102
|
"main",
|
|
22476
24103
|
"baseline"
|
|
22477
24104
|
]).optional(),
|
|
22478
|
-
gop: zod.z.number().optional()
|
|
22479
|
-
audio: zod.z.boolean().optional()
|
|
24105
|
+
gop: zod.z.number().optional()
|
|
22480
24106
|
});
|
|
22481
24107
|
var streamParamsCapability = {
|
|
22482
24108
|
name: "stream-params",
|
|
@@ -25395,10 +27021,7 @@ var BATTERY_DEVICE_PROFILE = {
|
|
|
25395
27021
|
audioMode: "disabled",
|
|
25396
27022
|
detectionMode: "on-motion"
|
|
25397
27023
|
},
|
|
25398
|
-
settings: {
|
|
25399
|
-
"snapshot.minRefreshIntervalSec": 3600,
|
|
25400
|
-
"streamBroker.preBufferEnabled": false
|
|
25401
|
-
}
|
|
27024
|
+
settings: {}
|
|
25402
27025
|
};
|
|
25403
27026
|
/**
|
|
25404
27027
|
* Profile registry — order matters when multiple profiles match the
|
|
@@ -26250,7 +27873,7 @@ var SystemMirror = class {
|
|
|
26250
27873
|
}
|
|
26251
27874
|
async refreshDeviceMetadata(deviceId, kind) {
|
|
26252
27875
|
try {
|
|
26253
|
-
const info =
|
|
27876
|
+
const info = await this.api.deviceManager.getDevice.query({ deviceId });
|
|
26254
27877
|
if (!info) return;
|
|
26255
27878
|
const wasNew = !this.devices.has(deviceId);
|
|
26256
27879
|
this.devices.set(deviceId, info);
|
|
@@ -30152,6 +31775,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30152
31775
|
addonId: null,
|
|
30153
31776
|
access: "view"
|
|
30154
31777
|
},
|
|
31778
|
+
"notificationRules.listDeviceMutes": {
|
|
31779
|
+
capName: "notification-rules",
|
|
31780
|
+
capScope: "system",
|
|
31781
|
+
addonId: null,
|
|
31782
|
+
access: "view"
|
|
31783
|
+
},
|
|
30155
31784
|
"notificationRules.listRules": {
|
|
30156
31785
|
capName: "notification-rules",
|
|
30157
31786
|
capScope: "system",
|
|
@@ -30170,6 +31799,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30170
31799
|
addonId: null,
|
|
30171
31800
|
access: "create"
|
|
30172
31801
|
},
|
|
31802
|
+
"notificationRules.setDeviceMuted": {
|
|
31803
|
+
capName: "notification-rules",
|
|
31804
|
+
capScope: "system",
|
|
31805
|
+
addonId: null,
|
|
31806
|
+
access: "create"
|
|
31807
|
+
},
|
|
30173
31808
|
"notificationRules.setRuleEnabled": {
|
|
30174
31809
|
capName: "notification-rules",
|
|
30175
31810
|
capScope: "system",
|
|
@@ -30446,6 +32081,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30446
32081
|
addonId: null,
|
|
30447
32082
|
access: "view"
|
|
30448
32083
|
},
|
|
32084
|
+
"pipelineAnalytics.setTrackFlags": {
|
|
32085
|
+
capName: "pipeline-analytics",
|
|
32086
|
+
capScope: "device",
|
|
32087
|
+
addonId: null,
|
|
32088
|
+
access: "create"
|
|
32089
|
+
},
|
|
30449
32090
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
30450
32091
|
capName: "pipeline-analytics",
|
|
30451
32092
|
capScope: "device",
|
|
@@ -30752,6 +32393,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30752
32393
|
addonId: null,
|
|
30753
32394
|
access: "view"
|
|
30754
32395
|
},
|
|
32396
|
+
"pipelineOrchestrator.getCameraSwitches": {
|
|
32397
|
+
capName: "pipeline-orchestrator",
|
|
32398
|
+
capScope: "system",
|
|
32399
|
+
addonId: null,
|
|
32400
|
+
access: "view"
|
|
32401
|
+
},
|
|
30755
32402
|
"pipelineOrchestrator.getCapabilityBindings": {
|
|
30756
32403
|
capName: "pipeline-orchestrator",
|
|
30757
32404
|
capScope: "system",
|
|
@@ -30884,6 +32531,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30884
32531
|
addonId: null,
|
|
30885
32532
|
access: "create"
|
|
30886
32533
|
},
|
|
32534
|
+
"pipelineOrchestrator.setCameraSwitch": {
|
|
32535
|
+
capName: "pipeline-orchestrator",
|
|
32536
|
+
capScope: "system",
|
|
32537
|
+
addonId: null,
|
|
32538
|
+
access: "create"
|
|
32539
|
+
},
|
|
30887
32540
|
"pipelineOrchestrator.setCapabilityBinding": {
|
|
30888
32541
|
capName: "pipeline-orchestrator",
|
|
30889
32542
|
capScope: "system",
|
|
@@ -30974,6 +32627,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30974
32627
|
addonId: null,
|
|
30975
32628
|
access: "create"
|
|
30976
32629
|
},
|
|
32630
|
+
"pipelineRunner.runStatelessStep": {
|
|
32631
|
+
capName: "pipeline-runner",
|
|
32632
|
+
capScope: "system",
|
|
32633
|
+
addonId: null,
|
|
32634
|
+
access: "create"
|
|
32635
|
+
},
|
|
30977
32636
|
"plateGallery.assignPlate": {
|
|
30978
32637
|
capName: "plate-gallery",
|
|
30979
32638
|
capScope: "system",
|
|
@@ -31106,6 +32765,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31106
32765
|
addonId: null,
|
|
31107
32766
|
access: "view"
|
|
31108
32767
|
},
|
|
32768
|
+
"privacyMask.setAudioEnabled": {
|
|
32769
|
+
capName: "privacy-mask",
|
|
32770
|
+
capScope: "device",
|
|
32771
|
+
addonId: null,
|
|
32772
|
+
access: "create"
|
|
32773
|
+
},
|
|
31109
32774
|
"privacyMask.setMask": {
|
|
31110
32775
|
capName: "privacy-mask",
|
|
31111
32776
|
capScope: "device",
|
|
@@ -31274,6 +32939,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31274
32939
|
addonId: null,
|
|
31275
32940
|
access: "create"
|
|
31276
32941
|
},
|
|
32942
|
+
"recording.readGopBytes": {
|
|
32943
|
+
capName: "recording",
|
|
32944
|
+
capScope: "system",
|
|
32945
|
+
addonId: null,
|
|
32946
|
+
access: "view"
|
|
32947
|
+
},
|
|
31277
32948
|
"recording.readSegmentBytes": {
|
|
31278
32949
|
capName: "recording",
|
|
31279
32950
|
capScope: "system",
|
|
@@ -31790,6 +33461,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31790
33461
|
addonId: null,
|
|
31791
33462
|
access: "create"
|
|
31792
33463
|
},
|
|
33464
|
+
"streamBroker.acquireEgressTranscode": {
|
|
33465
|
+
capName: "stream-broker",
|
|
33466
|
+
capScope: "system",
|
|
33467
|
+
addonId: null,
|
|
33468
|
+
access: "create"
|
|
33469
|
+
},
|
|
31793
33470
|
"streamBroker.assignProfile": {
|
|
31794
33471
|
capName: "stream-broker",
|
|
31795
33472
|
capScope: "system",
|
|
@@ -31898,6 +33575,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31898
33575
|
addonId: null,
|
|
31899
33576
|
access: "create"
|
|
31900
33577
|
},
|
|
33578
|
+
"streamBroker.releaseEgressTranscode": {
|
|
33579
|
+
capName: "stream-broker",
|
|
33580
|
+
capScope: "system",
|
|
33581
|
+
addonId: null,
|
|
33582
|
+
access: "create"
|
|
33583
|
+
},
|
|
31901
33584
|
"streamBroker.releaseStreamWithCodec": {
|
|
31902
33585
|
capName: "stream-broker",
|
|
31903
33586
|
capScope: "system",
|
|
@@ -33310,7 +34993,8 @@ function createSystemProxy(api) {
|
|
|
33310
34993
|
getLocalMetrics: (input) => dispatch("pipelineRunner", "getLocalMetrics", "query", input),
|
|
33311
34994
|
getAllCameraMetrics: (input) => dispatch("pipelineRunner", "getAllCameraMetrics", "query", input),
|
|
33312
34995
|
getLocalCameras: (input) => dispatch("pipelineRunner", "getLocalCameras", "query", input),
|
|
33313
|
-
getNativeCrop: (input) => dispatch("pipelineRunner", "getNativeCrop", "query", input)
|
|
34996
|
+
getNativeCrop: (input) => dispatch("pipelineRunner", "getNativeCrop", "query", input),
|
|
34997
|
+
runStatelessStep: (input) => dispatch("pipelineRunner", "runStatelessStep", "mutation", input)
|
|
33314
34998
|
},
|
|
33315
34999
|
plateGallery: {
|
|
33316
35000
|
getPlateMedia: (input) => dispatch("plateGallery", "getPlateMedia", "query", input),
|
|
@@ -33397,6 +35081,8 @@ function createSystemProxy(api) {
|
|
|
33397
35081
|
getStreamUrl: (input) => dispatch("streamBroker", "getStreamUrl", "query", input),
|
|
33398
35082
|
getStreamWithCodec: (input) => dispatch("streamBroker", "getStreamWithCodec", "mutation", input),
|
|
33399
35083
|
releaseStreamWithCodec: (input) => dispatch("streamBroker", "releaseStreamWithCodec", "mutation", input),
|
|
35084
|
+
acquireEgressTranscode: (input) => dispatch("streamBroker", "acquireEgressTranscode", "mutation", input),
|
|
35085
|
+
releaseEgressTranscode: (input) => dispatch("streamBroker", "releaseEgressTranscode", "mutation", input),
|
|
33400
35086
|
subscribeAudioChunks: (input) => dispatch("streamBroker", "subscribeAudioChunks", "mutation", input),
|
|
33401
35087
|
pullAudioChunks: (input) => dispatch("streamBroker", "pullAudioChunks", "query", input),
|
|
33402
35088
|
unsubscribeAudioChunks: (input) => dispatch("streamBroker", "unsubscribeAudioChunks", "mutation", input),
|
|
@@ -34277,6 +35963,44 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
|
|
|
34277
35963
|
return best;
|
|
34278
35964
|
}
|
|
34279
35965
|
//#endregion
|
|
35966
|
+
//#region src/utils/addon-id.ts
|
|
35967
|
+
/**
|
|
35968
|
+
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
35969
|
+
*
|
|
35970
|
+
* `AddonContext.id` does not agree with itself across the two context
|
|
35971
|
+
* factories:
|
|
35972
|
+
*
|
|
35973
|
+
* - a FORKED addon gets the bare manifest id
|
|
35974
|
+
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
35975
|
+
* - an addon co-located in hub-main gets it PREFIXED
|
|
35976
|
+
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
35977
|
+
* ``id: `addon:${addonId}` ``).
|
|
35978
|
+
*
|
|
35979
|
+
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
35980
|
+
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
35981
|
+
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
35982
|
+
*
|
|
35983
|
+
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
35984
|
+
* for a builtin — and only for a builtin, which is why it survives a green
|
|
35985
|
+
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
35986
|
+
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
35987
|
+
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
35988
|
+
*
|
|
35989
|
+
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
35990
|
+
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
35991
|
+
* enforces it in the processes where the prefix exists.
|
|
35992
|
+
*/
|
|
35993
|
+
/** The prefix the hub-main context factory prepends to the manifest id. */
|
|
35994
|
+
var ADDON_ID_PREFIX = "addon:";
|
|
35995
|
+
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
35996
|
+
function bareAddonId(id) {
|
|
35997
|
+
return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
|
|
35998
|
+
}
|
|
35999
|
+
/** True when both ids name the same addon, prefixed or not. */
|
|
36000
|
+
function isSameAddonId(a, b) {
|
|
36001
|
+
return bareAddonId(a) === bareAddonId(b);
|
|
36002
|
+
}
|
|
36003
|
+
//#endregion
|
|
34280
36004
|
//#region src/utils/cosine-similarity.ts
|
|
34281
36005
|
/** Cosine similarity between two embedding vectors */
|
|
34282
36006
|
function cosineSimilarity(a, b) {
|
|
@@ -35003,7 +36727,7 @@ function readDetailCropConvention(config) {
|
|
|
35003
36727
|
square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
|
|
35004
36728
|
};
|
|
35005
36729
|
}
|
|
35006
|
-
function isHydratedField(entry) {
|
|
36730
|
+
function isHydratedField$1(entry) {
|
|
35007
36731
|
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
35008
36732
|
}
|
|
35009
36733
|
/**
|
|
@@ -35018,7 +36742,7 @@ function pickDetailCropConvention(view) {
|
|
|
35018
36742
|
if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
|
|
35019
36743
|
const flat = {};
|
|
35020
36744
|
for (const section of view.sections) for (const entry of section.fields) {
|
|
35021
|
-
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
36745
|
+
if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
|
|
35022
36746
|
if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
|
|
35023
36747
|
}
|
|
35024
36748
|
return readDetailCropConvention(flat);
|
|
@@ -35105,6 +36829,245 @@ function slideInsideFrame(rect, frameWidth, frameHeight) {
|
|
|
35105
36829
|
};
|
|
35106
36830
|
}
|
|
35107
36831
|
//#endregion
|
|
36832
|
+
//#region src/pipeline/native-lease.ts
|
|
36833
|
+
/**
|
|
36834
|
+
* THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
|
|
36835
|
+
* decode worker's native-resolution frame retention.
|
|
36836
|
+
*
|
|
36837
|
+
* ## Why they live here and not in the addon that reads them
|
|
36838
|
+
*
|
|
36839
|
+
* The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
|
|
36840
|
+
* the READER is a private child process of `addon-pipeline`'s pipeline-runner.
|
|
36841
|
+
* Addons never import each other, so a key owned by either side would have to
|
|
36842
|
+
* be hand-copied by the other — and a hand-copied key is how a setting silently
|
|
36843
|
+
* stops arriving while both sides still look correct. Same reasoning, same
|
|
36844
|
+
* placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
|
|
36845
|
+
*
|
|
36846
|
+
* ## Why cluster-wide and not per-node
|
|
36847
|
+
*
|
|
36848
|
+
* The lease is a per-decode-worker RAM window. Its purpose — the late
|
|
36849
|
+
* cross-process native crop landing on a full-resolution frame rather than the
|
|
36850
|
+
* ≤640 detection fallback — is a property of the PIPELINE, not of a node's
|
|
36851
|
+
* hardware: a per-node TTL would mean the same camera produces different crop
|
|
36852
|
+
* quality depending on which node the balancer placed it on, and nobody could
|
|
36853
|
+
* tell that from the stored media. Node-level RAM pressure is already handled
|
|
36854
|
+
* by the per-session budget ceiling, which is itself one of these knobs.
|
|
36855
|
+
*
|
|
36856
|
+
* ## What each knob costs
|
|
36857
|
+
*
|
|
36858
|
+
* A retained frame is a full NATIVE-resolution copy in system RAM. With the
|
|
36859
|
+
* default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
|
|
36860
|
+
* 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
|
|
36861
|
+
* for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
|
|
36862
|
+
* resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
|
|
36863
|
+
* clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
|
|
36864
|
+
* admission" for what actually gets admitted.
|
|
36865
|
+
*/
|
|
36866
|
+
/**
|
|
36867
|
+
* Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
|
|
36868
|
+
* (cluster-wide) settings. Keys are unique across that addon's whole schema, so
|
|
36869
|
+
* the reader can walk every section instead of trusting the section id.
|
|
36870
|
+
*/
|
|
36871
|
+
var NATIVE_LEASE_SECTION_ID = "native-lease";
|
|
36872
|
+
var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
|
|
36873
|
+
var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
|
|
36874
|
+
var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
|
|
36875
|
+
var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
|
|
36876
|
+
/**
|
|
36877
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
36878
|
+
*
|
|
36879
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
36880
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
36881
|
+
* frame the runner never sent to inference.
|
|
36882
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
36883
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
36884
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
36885
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
36886
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
36887
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
36888
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
36889
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
36890
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
36891
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
36892
|
+
* consumes it.
|
|
36893
|
+
*/
|
|
36894
|
+
var NativeLeaseAdmissionSchema = zod.z.enum(["all", "inferred"]);
|
|
36895
|
+
/**
|
|
36896
|
+
* Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
|
|
36897
|
+
* the slider) because the value also travels to a forked child process, where a
|
|
36898
|
+
* junk number would silently become a 0-length or unbounded retention window.
|
|
36899
|
+
*/
|
|
36900
|
+
var NativeLeaseSettingsSchema = zod.z.object({
|
|
36901
|
+
/**
|
|
36902
|
+
* How long a retained native frame is served before it counts as a miss.
|
|
36903
|
+
*
|
|
36904
|
+
* Must cover the FULL late-crop horizon: detection inference + the
|
|
36905
|
+
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
36906
|
+
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
36907
|
+
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
36908
|
+
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
36909
|
+
*/
|
|
36910
|
+
ttlMs: zod.z.number().int().min(250).max(1e4),
|
|
36911
|
+
/**
|
|
36912
|
+
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
36913
|
+
*
|
|
36914
|
+
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
36915
|
+
* which one is actually binding before reasoning from that. At the shipped
|
|
36916
|
+
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
36917
|
+
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
36918
|
+
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
36919
|
+
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
36920
|
+
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
36921
|
+
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
36922
|
+
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
36923
|
+
* to replace).
|
|
36924
|
+
*/
|
|
36925
|
+
budgetMb: zod.z.number().int().min(0).max(4096),
|
|
36926
|
+
/**
|
|
36927
|
+
* Demand window: eager per-frame native retention runs only within this many
|
|
36928
|
+
* ms of the last native-crop request (or of the dial starting).
|
|
36929
|
+
*
|
|
36930
|
+
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
36931
|
+
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
36932
|
+
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
36933
|
+
* you are reproducing that.
|
|
36934
|
+
*/
|
|
36935
|
+
activityMs: zod.z.number().int().min(0).max(12e4),
|
|
36936
|
+
/**
|
|
36937
|
+
* Which delivered frames are retained at all — see
|
|
36938
|
+
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
36939
|
+
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
36940
|
+
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
36941
|
+
* crop request naming a frame it did NOT see marked
|
|
36942
|
+
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
36943
|
+
* there is the signal that some caller names frames outside the inference set
|
|
36944
|
+
* and that this must go back to `all`.
|
|
36945
|
+
*/
|
|
36946
|
+
admission: NativeLeaseAdmissionSchema
|
|
36947
|
+
});
|
|
36948
|
+
/**
|
|
36949
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
36950
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
36951
|
+
* settings changed no behaviour on the day it landed.
|
|
36952
|
+
*/
|
|
36953
|
+
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
36954
|
+
ttlMs: 1200,
|
|
36955
|
+
budgetMb: 1024,
|
|
36956
|
+
activityMs: 15e3,
|
|
36957
|
+
admission: "inferred"
|
|
36958
|
+
};
|
|
36959
|
+
/** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
|
|
36960
|
+
var NATIVE_LEASE_TTL_FIELD = {
|
|
36961
|
+
min: 250,
|
|
36962
|
+
max: 1e4,
|
|
36963
|
+
step: 50,
|
|
36964
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
|
|
36965
|
+
};
|
|
36966
|
+
var NATIVE_LEASE_BUDGET_FIELD = {
|
|
36967
|
+
min: 0,
|
|
36968
|
+
max: 4096,
|
|
36969
|
+
step: 64,
|
|
36970
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
|
|
36971
|
+
};
|
|
36972
|
+
var NATIVE_LEASE_ACTIVITY_FIELD = {
|
|
36973
|
+
min: 0,
|
|
36974
|
+
max: 12e4,
|
|
36975
|
+
step: 1e3,
|
|
36976
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
|
|
36977
|
+
};
|
|
36978
|
+
/** Select options for the admission knob (orchestrator settings UI). */
|
|
36979
|
+
var NATIVE_LEASE_ADMISSION_FIELD = {
|
|
36980
|
+
options: [{
|
|
36981
|
+
value: "all",
|
|
36982
|
+
label: "Every delivered frame"
|
|
36983
|
+
}, {
|
|
36984
|
+
value: "inferred",
|
|
36985
|
+
label: "Only frames sent to inference"
|
|
36986
|
+
}],
|
|
36987
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
|
|
36988
|
+
};
|
|
36989
|
+
/**
|
|
36990
|
+
* Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
|
|
36991
|
+
* value equal to the shipped default.
|
|
36992
|
+
*
|
|
36993
|
+
* That last rule is not tidiness, it is the difference between the documented
|
|
36994
|
+
* precedence being true and being a lie. `addon-settings.getGlobalSettings`
|
|
36995
|
+
* returns a HYDRATED payload, and `hydrateField` fills an unstored field with
|
|
36996
|
+
* the schema's own `default` (verified live on the hub: a cluster that has never
|
|
36997
|
+
* opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
|
|
36998
|
+
* that at face value would report all three knobs as "set" on every cluster on
|
|
36999
|
+
* the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
|
|
37000
|
+
* emergency override that the precedence promises. There is no raw-store read on
|
|
37001
|
+
* this cap to distinguish the two, so the default value itself is treated as
|
|
37002
|
+
* "the operator has expressed no preference" — which is also what leaving a
|
|
37003
|
+
* slider untouched means.
|
|
37004
|
+
*
|
|
37005
|
+
* The cost is one honest edge: an operator who deliberately selects the default
|
|
37006
|
+
* value in order to overrule an env var does not get it. Clear the env var
|
|
37007
|
+
* instead; the worker's spawn line names the source, so this is visible rather
|
|
37008
|
+
* than mysterious.
|
|
37009
|
+
*/
|
|
37010
|
+
function readKnob(knob, raw) {
|
|
37011
|
+
const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
|
|
37012
|
+
if (!parsed.success) return null;
|
|
37013
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
|
|
37014
|
+
}
|
|
37015
|
+
/** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
|
|
37016
|
+
function readAdmissionKnob(raw) {
|
|
37017
|
+
const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
|
|
37018
|
+
if (!parsed.success) return null;
|
|
37019
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
|
|
37020
|
+
}
|
|
37021
|
+
/**
|
|
37022
|
+
* Narrow a FLAT settings record to the knobs the operator set.
|
|
37023
|
+
*
|
|
37024
|
+
* Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
|
|
37025
|
+
* budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
|
|
37026
|
+
* clamped, not defaulted) so the caller can still fall through to the env
|
|
37027
|
+
* override — clamping here would turn a typo into a value nobody chose. See
|
|
37028
|
+
* {@link readKnob} for why the default counts as unset.
|
|
37029
|
+
*/
|
|
37030
|
+
function readNativeLeaseOverride(config) {
|
|
37031
|
+
const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
|
|
37032
|
+
const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
|
|
37033
|
+
const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
|
|
37034
|
+
const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
|
|
37035
|
+
return {
|
|
37036
|
+
...ttlMs === null ? {} : { ttlMs },
|
|
37037
|
+
...budgetMb === null ? {} : { budgetMb },
|
|
37038
|
+
...activityMs === null ? {} : { activityMs },
|
|
37039
|
+
...admission === null ? {} : { admission }
|
|
37040
|
+
};
|
|
37041
|
+
}
|
|
37042
|
+
function isHydratedField(entry) {
|
|
37043
|
+
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
37044
|
+
}
|
|
37045
|
+
var LEASE_KEYS = [
|
|
37046
|
+
NATIVE_LEASE_TTL_KEY,
|
|
37047
|
+
NATIVE_LEASE_BUDGET_KEY,
|
|
37048
|
+
NATIVE_LEASE_ACTIVITY_KEY,
|
|
37049
|
+
NATIVE_LEASE_ADMISSION_KEY
|
|
37050
|
+
];
|
|
37051
|
+
/**
|
|
37052
|
+
* Extract the operator's lease overrides from an
|
|
37053
|
+
* `addon-settings.getGlobalSettings` payload.
|
|
37054
|
+
*
|
|
37055
|
+
* Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
|
|
37056
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
37057
|
+
* must not silently revert the whole cluster to the defaults. A `null` payload
|
|
37058
|
+
* (addon mid-boot) means "operator set nothing" — the env/default fallback then
|
|
37059
|
+
* applies, which is the correct read of "I could not ask".
|
|
37060
|
+
*/
|
|
37061
|
+
function pickNativeLeaseOverride(view) {
|
|
37062
|
+
if (view === null) return {};
|
|
37063
|
+
const flat = {};
|
|
37064
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
37065
|
+
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
37066
|
+
if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
|
|
37067
|
+
}
|
|
37068
|
+
return readNativeLeaseOverride(flat);
|
|
37069
|
+
}
|
|
37070
|
+
//#endregion
|
|
35108
37071
|
//#region src/helpers/bind-addon-actions.ts
|
|
35109
37072
|
/**
|
|
35110
37073
|
* Bind an addon's custom-action catalog to its tRPC surface, returning a
|
|
@@ -35357,10 +37320,13 @@ function enumerateInferenceDevices(hw) {
|
|
|
35357
37320
|
}
|
|
35358
37321
|
//#endregion
|
|
35359
37322
|
exports.ACCESSORY_LABEL = ACCESSORY_LABEL;
|
|
37323
|
+
exports.ALEXA_EGRESS_PROFILE = ALEXA_EGRESS_PROFILE;
|
|
35360
37324
|
exports.ALL_CAPABILITY_DEFINITIONS = ALL_CAPABILITY_DEFINITIONS;
|
|
35361
37325
|
exports.APPLE_SA_TO_MACRO = APPLE_SA_TO_MACRO;
|
|
37326
|
+
exports.AUDIO_ANALYSIS_CAP_NAME = AUDIO_ANALYSIS_CAP_NAME;
|
|
35362
37327
|
exports.AUDIO_BACKEND_CHOICES = AUDIO_BACKEND_CHOICES;
|
|
35363
37328
|
exports.AUDIO_MACRO_LABELS = AUDIO_MACRO_LABELS;
|
|
37329
|
+
exports.AUDIO_PRESETS = AUDIO_PRESETS;
|
|
35364
37330
|
exports.AccessoriesStatusSchema = AccessoriesStatusSchema;
|
|
35365
37331
|
exports.AccessoryKind = AccessoryKind;
|
|
35366
37332
|
exports.AddBrokerInputSchema = AddBrokerInputSchema;
|
|
@@ -35414,6 +37380,7 @@ exports.AutoUpdateSettingsSchema = AutoUpdateSettingsSchema;
|
|
|
35414
37380
|
exports.AutomationControlStatusSchema = AutomationControlStatusSchema;
|
|
35415
37381
|
exports.AvailableIntegrationTypeSchema = AvailableIntegrationTypeSchema;
|
|
35416
37382
|
exports.BACKEND_TO_FORMAT = BACKEND_TO_FORMAT;
|
|
37383
|
+
exports.BASE_LIVE_EGRESS_PROFILE = BASE_LIVE_EGRESS_PROFILE;
|
|
35417
37384
|
exports.BATTERY_DEVICE_PROFILE = BATTERY_DEVICE_PROFILE;
|
|
35418
37385
|
exports.BacklightModeSchema = BacklightModeSchema;
|
|
35419
37386
|
exports.BackupDestinationInfoSchema = BackupDestinationInfoSchema;
|
|
@@ -35446,6 +37413,8 @@ exports.BrokerSubscribeInputSchema = SubscribeInputSchema;
|
|
|
35446
37413
|
exports.BrokerSubscribeResultSchema = SubscribeResultSchema;
|
|
35447
37414
|
exports.BrokerTestConnectionResultSchema = TestConnectionResultSchema;
|
|
35448
37415
|
exports.BrokerUnsubscribeInputSchema = UnsubscribeInputSchema;
|
|
37416
|
+
exports.CAMERA_SWITCH_CATALOG = CAMERA_SWITCH_CATALOG;
|
|
37417
|
+
exports.CAMERA_SWITCH_ORDER = CAMERA_SWITCH_ORDER;
|
|
35449
37418
|
exports.CAM_PROFILE_ORDER = require_sleep.CAM_PROFILE_ORDER;
|
|
35450
37419
|
exports.CAPABILITY_NAMES = CAPABILITY_NAMES;
|
|
35451
37420
|
exports.CAPABILITY_ROUTER_KEYS = CAPABILITY_ROUTER_KEYS;
|
|
@@ -35479,6 +37448,11 @@ exports.CameraSourceStatusSchema = CameraSourceStatusSchema;
|
|
|
35479
37448
|
exports.CameraSourceStreamSchema = CameraSourceStreamSchema;
|
|
35480
37449
|
exports.CameraStatusSchema = CameraStatusSchema;
|
|
35481
37450
|
exports.CameraStreamSchema = require_sleep.CameraStreamSchema;
|
|
37451
|
+
exports.CameraSwitchAuthoritySchema = CameraSwitchAuthoritySchema;
|
|
37452
|
+
exports.CameraSwitchGroupSchema = CameraSwitchGroupSchema;
|
|
37453
|
+
exports.CameraSwitchIdSchema = CameraSwitchIdSchema;
|
|
37454
|
+
exports.CameraSwitchSchema = CameraSwitchSchema;
|
|
37455
|
+
exports.CameraSwitchUnavailableReasonSchema = CameraSwitchUnavailableReasonSchema;
|
|
35482
37456
|
exports.CandidateQueryFilterSchema = CandidateQueryFilterSchema;
|
|
35483
37457
|
exports.CapScopeSchema = CapScopeSchema;
|
|
35484
37458
|
exports.CapabilityBindingsSchema = CapabilityBindingsSchema;
|
|
@@ -35527,12 +37501,14 @@ exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
|
|
|
35527
37501
|
exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
|
|
35528
37502
|
exports.DEFAULT_EVENT_COLOR = DEFAULT_EVENT_COLOR;
|
|
35529
37503
|
exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
37504
|
+
exports.DEFAULT_NATIVE_LEASE_SETTINGS = DEFAULT_NATIVE_LEASE_SETTINGS;
|
|
35530
37505
|
exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
|
|
35531
37506
|
exports.DEFAULT_SCRUB_THUMBNAIL_PRESET = DEFAULT_SCRUB_THUMBNAIL_PRESET;
|
|
35532
37507
|
exports.DETAIL_CROP_PADDING_FIELD = DETAIL_CROP_PADDING_FIELD;
|
|
35533
37508
|
exports.DETAIL_CROP_PADDING_KEY = DETAIL_CROP_PADDING_KEY;
|
|
35534
37509
|
exports.DETAIL_CROP_SECTION_ID = DETAIL_CROP_SECTION_ID;
|
|
35535
37510
|
exports.DETAIL_CROP_SQUARE_KEY = DETAIL_CROP_SQUARE_KEY;
|
|
37511
|
+
exports.DETECTION_PIPELINE_CAP_NAME = DETECTION_PIPELINE_CAP_NAME;
|
|
35536
37512
|
exports.DEVICE_BACKEND_TO_FORMAT = DEVICE_BACKEND_TO_FORMAT;
|
|
35537
37513
|
exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
|
|
35538
37514
|
exports.DEVICE_PROFILES = DEVICE_PROFILES;
|
|
@@ -35584,6 +37560,10 @@ exports.EXPRESSION_BUILTIN_NAMES = EXPRESSION_BUILTIN_NAMES;
|
|
|
35584
37560
|
exports.EXPRESSION_COMPILE_CACHE_CAPACITY = EXPRESSION_COMPILE_CACHE_CAPACITY;
|
|
35585
37561
|
exports.EXPRESSION_IDENTIFIER_RE = EXPRESSION_IDENTIFIER_RE;
|
|
35586
37562
|
exports.EXPRESSION_INJECTED_NOW = EXPRESSION_INJECTED_NOW;
|
|
37563
|
+
exports.EgressEncodeSchema = EgressEncodeSchema;
|
|
37564
|
+
exports.EgressRateControlSchema = EgressRateControlSchema;
|
|
37565
|
+
exports.EgressTranscodeRequestSchema = EgressTranscodeRequestSchema;
|
|
37566
|
+
exports.EgressTranscodeSchema = EgressTranscodeSchema;
|
|
35587
37567
|
exports.ElementConfigStore = ElementConfigStore;
|
|
35588
37568
|
exports.EmbeddingInfoSchema = EmbeddingInfoSchema;
|
|
35589
37569
|
exports.EmbeddingResultSchema = EmbeddingResultSchema;
|
|
@@ -35625,6 +37605,10 @@ exports.FrameInputSchema = FrameInputSchema;
|
|
|
35625
37605
|
exports.GasStatusSchema = GasStatusSchema;
|
|
35626
37606
|
exports.GetStreamWithCodecInputSchema = GetStreamWithCodecInputSchema;
|
|
35627
37607
|
exports.GlobalMetricsSchema = GlobalMetricsSchema;
|
|
37608
|
+
exports.HAP_AUDIO_BASE = HAP_AUDIO_BASE;
|
|
37609
|
+
exports.HAP_AUDIO_BITRATE_KBPS = HAP_AUDIO_BITRATE_KBPS;
|
|
37610
|
+
exports.HAP_AUDIO_VBV_KBITS = HAP_AUDIO_VBV_KBITS;
|
|
37611
|
+
exports.HAP_KEYFRAME_INTERVAL_SEC = HAP_KEYFRAME_INTERVAL_SEC;
|
|
35628
37612
|
exports.HF_BASE_URL = HF_BASE_URL;
|
|
35629
37613
|
exports.HF_REPO = HF_REPO;
|
|
35630
37614
|
exports.HWACCEL_OPTIONS = HWACCEL_OPTIONS;
|
|
@@ -35735,6 +37719,15 @@ exports.MotionZoneRegionSchema = MotionZoneRegionSchema;
|
|
|
35735
37719
|
exports.MotionZoneStatusSchema = MotionZoneStatusSchema;
|
|
35736
37720
|
exports.MqttBrokerStatusSchema = StatusSchema;
|
|
35737
37721
|
exports.MutationFilterSchema = MutationFilterSchema;
|
|
37722
|
+
exports.NATIVE_LEASE_ACTIVITY_FIELD = NATIVE_LEASE_ACTIVITY_FIELD;
|
|
37723
|
+
exports.NATIVE_LEASE_ACTIVITY_KEY = NATIVE_LEASE_ACTIVITY_KEY;
|
|
37724
|
+
exports.NATIVE_LEASE_ADMISSION_FIELD = NATIVE_LEASE_ADMISSION_FIELD;
|
|
37725
|
+
exports.NATIVE_LEASE_ADMISSION_KEY = NATIVE_LEASE_ADMISSION_KEY;
|
|
37726
|
+
exports.NATIVE_LEASE_BUDGET_FIELD = NATIVE_LEASE_BUDGET_FIELD;
|
|
37727
|
+
exports.NATIVE_LEASE_BUDGET_KEY = NATIVE_LEASE_BUDGET_KEY;
|
|
37728
|
+
exports.NATIVE_LEASE_SECTION_ID = NATIVE_LEASE_SECTION_ID;
|
|
37729
|
+
exports.NATIVE_LEASE_TTL_FIELD = NATIVE_LEASE_TTL_FIELD;
|
|
37730
|
+
exports.NATIVE_LEASE_TTL_KEY = NATIVE_LEASE_TTL_KEY;
|
|
35738
37731
|
exports.NC_BASE_CONDITION_KEYS = NC_BASE_CONDITION_KEYS;
|
|
35739
37732
|
exports.NC_CONDITION_CATALOG = NC_CONDITION_CATALOG;
|
|
35740
37733
|
exports.NC_HISTORY_LIMIT_DEFAULT = NC_HISTORY_LIMIT_DEFAULT;
|
|
@@ -35746,6 +37739,8 @@ exports.NativeCropBboxSchema = NativeCropBboxSchema;
|
|
|
35746
37739
|
exports.NativeCropRefSchema = NativeCropRefSchema;
|
|
35747
37740
|
exports.NativeCropResultSchema = NativeCropResultSchema;
|
|
35748
37741
|
exports.NativeDetectionSchema = NativeDetectionSchema;
|
|
37742
|
+
exports.NativeLeaseAdmissionSchema = NativeLeaseAdmissionSchema;
|
|
37743
|
+
exports.NativeLeaseSettingsSchema = NativeLeaseSettingsSchema;
|
|
35749
37744
|
exports.NativeObjectClassEnum = NativeObjectClassEnum;
|
|
35750
37745
|
exports.NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionRuntimeStateSchema;
|
|
35751
37746
|
exports.NativeObjectDetectionStatusSchema = NativeObjectDetectionStatusSchema;
|
|
@@ -35815,6 +37810,7 @@ exports.PET_FEEDER_MANUAL_FEED_MAX = PET_FEEDER_MANUAL_FEED_MAX;
|
|
|
35815
37810
|
exports.PET_FEEDER_MANUAL_FEED_MIN = PET_FEEDER_MANUAL_FEED_MIN;
|
|
35816
37811
|
exports.PIPELINE_FLOW_CAPABILITY_NAMES = PIPELINE_FLOW_CAPABILITY_NAMES;
|
|
35817
37812
|
exports.PIPELINE_OWNER_CAPABILITY_NAMES = PIPELINE_OWNER_CAPABILITY_NAMES;
|
|
37813
|
+
exports.PRIVACY_MASK_CAP_NAME = PRIVACY_MASK_CAP_NAME;
|
|
35818
37814
|
exports.PROVIDER_KIND_CAP_NAMES = PROVIDER_KIND_CAP_NAMES;
|
|
35819
37815
|
exports.PYTHON_SCRIPT = PYTHON_SCRIPT;
|
|
35820
37816
|
exports.PackageUpdateSchema = PackageUpdateSchema;
|
|
@@ -35858,6 +37854,8 @@ exports.PtzPositionSchema = PtzPositionSchema;
|
|
|
35858
37854
|
exports.PtzPresetSchema = PtzPresetSchema;
|
|
35859
37855
|
exports.PtzStatusSchema = PtzStatusSchema;
|
|
35860
37856
|
exports.QueryFilterSchema = QueryFilterSchema;
|
|
37857
|
+
exports.RATE_CONTROL_RELAXED = RATE_CONTROL_RELAXED;
|
|
37858
|
+
exports.RATE_CONTROL_TIGHT = RATE_CONTROL_TIGHT;
|
|
35861
37859
|
exports.REACHABILITY_FAILURES_TO_OFFLINE = REACHABILITY_FAILURES_TO_OFFLINE;
|
|
35862
37860
|
exports.REACHABILITY_POLL_INTERVAL_MS = REACHABILITY_POLL_INTERVAL_MS;
|
|
35863
37861
|
exports.REACHABILITY_PROBE_TIMEOUT_MS = REACHABILITY_PROBE_TIMEOUT_MS;
|
|
@@ -35866,6 +37864,7 @@ exports.RESERVED_BINDING_NAMES = RESERVED_BINDING_NAMES;
|
|
|
35866
37864
|
exports.RUNTIME_DEFAULTS = RUNTIME_DEFAULTS;
|
|
35867
37865
|
exports.RUNTIME_TO_FORMAT = RUNTIME_TO_FORMAT;
|
|
35868
37866
|
exports.RawStateResultSchema = require_sleep.RawStateResultSchema;
|
|
37867
|
+
exports.ReadGopBytesResultSchema = ReadGopBytesResultSchema;
|
|
35869
37868
|
exports.ReadSegmentBytesResultSchema = ReadSegmentBytesResultSchema;
|
|
35870
37869
|
exports.ReadinessRegistry = require_sleep.ReadinessRegistry;
|
|
35871
37870
|
exports.ReadinessTimeoutError = require_sleep.ReadinessTimeoutError;
|
|
@@ -35984,6 +37983,8 @@ exports.SystemMetricsSchema = SystemMetricsSchema;
|
|
|
35984
37983
|
exports.SystemMirror = SystemMirror;
|
|
35985
37984
|
exports.TAXONOMY_COLORS = TAXONOMY_COLORS;
|
|
35986
37985
|
exports.TIMEZONES = TIMEZONES;
|
|
37986
|
+
exports.TRANSCODE_DOWN_MAX_BITRATE_KBPS = TRANSCODE_DOWN_MAX_BITRATE_KBPS;
|
|
37987
|
+
exports.TRANSCODE_DOWN_MAX_HEIGHT = TRANSCODE_DOWN_MAX_HEIGHT;
|
|
35987
37988
|
exports.TamperStatusSchema = TamperStatusSchema;
|
|
35988
37989
|
exports.TankStatusSchema = TankStatusSchema;
|
|
35989
37990
|
exports.TargetKindCapsSchema = TargetKindCapsSchema;
|
|
@@ -36006,8 +38007,11 @@ exports.TopologyProcessSchema = TopologyProcessSchema;
|
|
|
36006
38007
|
exports.TopologyServiceSchema = TopologyServiceSchema;
|
|
36007
38008
|
exports.TrackCascadeCountsSchema = TrackCascadeCountsSchema;
|
|
36008
38009
|
exports.TrackEnvelopeSchema = TrackEnvelopeSchema;
|
|
38010
|
+
exports.TrackFlagsPatchSchema = TrackFlagsPatchSchema;
|
|
38011
|
+
exports.TrackFlagsSchema = TrackFlagsSchema;
|
|
36009
38012
|
exports.TrackProjectionSchema = TrackProjectionSchema;
|
|
36010
38013
|
exports.TrackSchema = TrackSchema;
|
|
38014
|
+
exports.TrackSourceSchema = TrackSourceSchema;
|
|
36011
38015
|
exports.TrackStateSchema = TrackStateSchema;
|
|
36012
38016
|
exports.TrackZoneFilterSchema = TrackZoneFilterSchema;
|
|
36013
38017
|
exports.TrackedDetectionSchema = TrackedDetectionSchema;
|
|
@@ -36043,6 +38047,7 @@ exports.VectorUpsertInputSchema = VectorUpsertInputSchema;
|
|
|
36043
38047
|
exports.VectorUpsertResultSchema = VectorUpsertResultSchema;
|
|
36044
38048
|
exports.VibrationStatusSchema = VibrationStatusSchema;
|
|
36045
38049
|
exports.VideoEncodeSchema = VideoEncodeSchema;
|
|
38050
|
+
exports.WEBRTC_EGRESS_PROFILE = WEBRTC_EGRESS_PROFILE;
|
|
36046
38051
|
exports.WELL_KNOWN_TABS = require_sleep.WELL_KNOWN_TABS;
|
|
36047
38052
|
exports.WELL_KNOWN_TAB_MAP = require_sleep.WELL_KNOWN_TAB_MAP;
|
|
36048
38053
|
exports.WaterHeaterStatusSchema = WaterHeaterStatusSchema;
|
|
@@ -36089,10 +38094,12 @@ exports.audioAnalysisCapability = audioAnalysisCapability;
|
|
|
36089
38094
|
exports.audioAnalyzerCapability = audioAnalyzerCapability;
|
|
36090
38095
|
exports.audioCodecCapability = audioCodecCapability;
|
|
36091
38096
|
exports.audioMetricsCapability = audioMetricsCapability;
|
|
38097
|
+
exports.audioPlanFromEncodeProfile = audioPlanFromEncodeProfile;
|
|
36092
38098
|
exports.authProviderCapability = authProviderCapability;
|
|
36093
38099
|
exports.autoAssignProfiles = autoAssignProfiles;
|
|
36094
38100
|
exports.automationControlCapability = automationControlCapability;
|
|
36095
38101
|
exports.backupCapability = backupCapability;
|
|
38102
|
+
exports.bareAddonId = bareAddonId;
|
|
36096
38103
|
exports.batteryCapability = batteryCapability;
|
|
36097
38104
|
exports.bestLocationMatch = bestLocationMatch;
|
|
36098
38105
|
exports.binaryCapability = binaryCapability;
|
|
@@ -36100,15 +38107,20 @@ exports.bindAddonActions = bindAddonActions;
|
|
|
36100
38107
|
exports.brightnessCapability = brightnessCapability;
|
|
36101
38108
|
exports.brokerCapability = brokerCapability;
|
|
36102
38109
|
exports.buildAddonRouteProvider = buildAddonRouteProvider;
|
|
38110
|
+
exports.buildAudioArgs = buildAudioArgs;
|
|
36103
38111
|
exports.buildEventKindDescriptor = buildEventKindDescriptor;
|
|
38112
|
+
exports.buildFfmpegArgs = buildFfmpegArgs;
|
|
38113
|
+
exports.buildInputArgs = buildInputArgs;
|
|
36104
38114
|
exports.buildModelVariantGroups = buildModelVariantGroups;
|
|
36105
38115
|
exports.buildNcTaxonomy = buildNcTaxonomy;
|
|
36106
38116
|
exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
|
|
38117
|
+
exports.buildVideoArgs = buildVideoArgs;
|
|
36107
38118
|
exports.buttonCapability = buttonCapability;
|
|
36108
38119
|
exports.cameraCredentialsCapability = cameraCredentialsCapability;
|
|
36109
38120
|
exports.cameraPipelineConfigCapability = cameraPipelineConfigCapability;
|
|
36110
38121
|
exports.cameraStreamsCapability = cameraStreamsCapability;
|
|
36111
38122
|
exports.canConvertUnit = canConvertUnit;
|
|
38123
|
+
exports.canonicalEgressPlan = canonicalEgressPlan;
|
|
36112
38124
|
exports.carbonMonoxideCapability = carbonMonoxideCapability;
|
|
36113
38125
|
exports.cellsToRects = cellsToRects;
|
|
36114
38126
|
exports.classifyStream = classifyStream;
|
|
@@ -36132,6 +38144,7 @@ exports.createDeviceProxy = require_sleep.createDeviceProxy;
|
|
|
36132
38144
|
exports.createDurableState = require_sleep.createDurableState;
|
|
36133
38145
|
exports.createEvent = require_sleep.createEvent;
|
|
36134
38146
|
exports.createExpressionScope = createExpressionScope;
|
|
38147
|
+
exports.createHwAccelCache = createHwAccelCache;
|
|
36135
38148
|
exports.createLazyTrpcSource = require_sleep.createLazyTrpcSource;
|
|
36136
38149
|
exports.createMirrorSource = require_sleep.createMirrorSource;
|
|
36137
38150
|
exports.createRuntimeStateBridge = createRuntimeStateBridge;
|
|
@@ -36145,6 +38158,7 @@ exports.decodeVectorBase64 = decodeVectorBase64;
|
|
|
36145
38158
|
exports.decoderCapability = decoderCapability;
|
|
36146
38159
|
exports.defaultDeviceFor = defaultDeviceFor;
|
|
36147
38160
|
exports.defineCustomActions = defineCustomActions;
|
|
38161
|
+
exports.deriveCameraSwitches = deriveCameraSwitches;
|
|
36148
38162
|
exports.deriveDetailCropRect = deriveDetailCropRect;
|
|
36149
38163
|
exports.deriveRecordingMode = deriveRecordingMode;
|
|
36150
38164
|
exports.describeModelVariant = describeModelVariant;
|
|
@@ -36161,6 +38175,8 @@ exports.deviceProviderCapability = deviceProviderCapability;
|
|
|
36161
38175
|
exports.deviceStateCapability = deviceStateCapability;
|
|
36162
38176
|
exports.deviceStatusCapability = deviceStatusCapability;
|
|
36163
38177
|
exports.doorbellCapability = doorbellCapability;
|
|
38178
|
+
exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
|
|
38179
|
+
exports.egressTransportFromRequest = egressTransportFromRequest;
|
|
36164
38180
|
exports.embeddingEncoderCapability = embeddingEncoderCapability;
|
|
36165
38181
|
exports.emitDownForOwnedCaps = require_sleep.emitDownForOwnedCaps;
|
|
36166
38182
|
exports.emitReadiness = require_sleep.emitReadiness;
|
|
@@ -36203,6 +38219,7 @@ exports.imageCapability = imageCapability;
|
|
|
36203
38219
|
exports.imageSettingsCapability = imageSettingsCapability;
|
|
36204
38220
|
exports.integrationsCapability = integrationsCapability;
|
|
36205
38221
|
exports.intercomCapability = intercomCapability;
|
|
38222
|
+
exports.invocationFromEncodeProfile = invocationFromEncodeProfile;
|
|
36206
38223
|
exports.isAgentOnlyPlacement = isAgentOnlyPlacement;
|
|
36207
38224
|
exports.isArrayOutputSchema = isArrayOutputSchema;
|
|
36208
38225
|
exports.isBaseConditionKey = isBaseConditionKey;
|
|
@@ -36213,6 +38230,8 @@ exports.isDeviceScopedCap = require_sleep.isDeviceScopedCap;
|
|
|
36213
38230
|
exports.isEvent = require_sleep.isEvent;
|
|
36214
38231
|
exports.isNode = isNode;
|
|
36215
38232
|
exports.isObjectInput = isObjectInput;
|
|
38233
|
+
exports.isSameAddonId = isSameAddonId;
|
|
38234
|
+
exports.isSoftwareDecode = isSoftwareDecode;
|
|
36216
38235
|
exports.isVoidInput = isVoidInput;
|
|
36217
38236
|
exports.jobKindSchema = jobKindSchema;
|
|
36218
38237
|
exports.kebabToCamel = kebabToCamel;
|
|
@@ -36227,6 +38246,7 @@ exports.llmRuntimeCapability = llmRuntimeCapability;
|
|
|
36227
38246
|
exports.localNetworkCapability = localNetworkCapability;
|
|
36228
38247
|
exports.locationSimilarity = locationSimilarity;
|
|
36229
38248
|
exports.lockControlCapability = lockControlCapability;
|
|
38249
|
+
exports.logBannerArgs = logBannerArgs;
|
|
36230
38250
|
exports.logDestinationCapability = logDestinationCapability;
|
|
36231
38251
|
exports.logLevelAtMost = logLevelAtMost;
|
|
36232
38252
|
exports.loginMethodCapability = loginMethodCapability;
|
|
@@ -36274,7 +38294,9 @@ exports.parseStreamParamsFormPatch = parseStreamParamsFormPatch;
|
|
|
36274
38294
|
exports.petFeederCapability = petFeederCapability;
|
|
36275
38295
|
exports.pickAccessoryControl = pickAccessoryControl;
|
|
36276
38296
|
exports.pickDetailCropConvention = pickDetailCropConvention;
|
|
38297
|
+
exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
|
|
36277
38298
|
exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
|
|
38299
|
+
exports.pickVideoEncoder = pickVideoEncoder;
|
|
36278
38300
|
exports.pickerForCondition = pickerForCondition;
|
|
36279
38301
|
exports.pipelineAnalyticsCapability = pipelineAnalyticsCapability;
|
|
36280
38302
|
exports.pipelineExecutorCapability = pipelineExecutorCapability;
|
|
@@ -36293,6 +38315,7 @@ exports.ptzCapability = ptzCapability;
|
|
|
36293
38315
|
exports.pythonScriptForBackend = pythonScriptForBackend;
|
|
36294
38316
|
exports.readDetailCropConvention = readDetailCropConvention;
|
|
36295
38317
|
exports.readDeviceStateFrom = readDeviceStateFrom;
|
|
38318
|
+
exports.readNativeLeaseOverride = readNativeLeaseOverride;
|
|
36296
38319
|
exports.readNodePin = require_sleep.readNodePin;
|
|
36297
38320
|
exports.readinessKey = require_sleep.readinessKey;
|
|
36298
38321
|
exports.rebootCapability = rebootCapability;
|
|
@@ -36308,6 +38331,7 @@ exports.resolveCapMount = require_sleep.resolveCapMount;
|
|
|
36308
38331
|
exports.resolveDetectionRuntime = resolveDetectionRuntime;
|
|
36309
38332
|
exports.resolveDeviceControlKind = resolveDeviceControlKind;
|
|
36310
38333
|
exports.resolveDeviceProfile = resolveDeviceProfile;
|
|
38334
|
+
exports.resolveEgressDecodeHwAccel = resolveEgressDecodeHwAccel;
|
|
36311
38335
|
exports.resolveFormat = resolveFormat;
|
|
36312
38336
|
exports.resolveHydratedFieldValue = require_sleep.resolveHydratedFieldValue;
|
|
36313
38337
|
exports.resolveModelFormat = resolveModelFormat;
|
|
@@ -36342,8 +38366,10 @@ exports.streamParamsCapability = streamParamsCapability;
|
|
|
36342
38366
|
exports.streamPixels = streamPixels;
|
|
36343
38367
|
exports.streamQualityLabel = streamQualityLabel;
|
|
36344
38368
|
exports.subKindsOf = subKindsOf;
|
|
38369
|
+
exports.summarisePrivacyAudio = summarisePrivacyAudio;
|
|
36345
38370
|
exports.supportedRuntimes = supportedRuntimes;
|
|
36346
38371
|
exports.switchCapability = switchCapability;
|
|
38372
|
+
exports.switchedOffIds = switchedOffIds;
|
|
36347
38373
|
exports.synthesizeSourceInfo = synthesizeSourceInfo;
|
|
36348
38374
|
exports.systemCapability = systemCapability;
|
|
36349
38375
|
exports.tamperCapability = tamperCapability;
|
|
@@ -36355,6 +38381,7 @@ exports.terminalSessionCapability = terminalSessionCapability;
|
|
|
36355
38381
|
exports.textToHtml = textToHtml;
|
|
36356
38382
|
exports.toDeviceSummary = toDeviceSummary;
|
|
36357
38383
|
exports.toExpressionValue = toExpressionValue;
|
|
38384
|
+
exports.toNodeId = require_sleep.toNodeId;
|
|
36358
38385
|
exports.toStreamSourceEntry = toStreamSourceEntry;
|
|
36359
38386
|
exports.toastCapability = toastCapability;
|
|
36360
38387
|
exports.tokenize = tokenize;
|