@camstack/addon-pipeline 1.2.44 → 1.2.45
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +3 -2
- package/dist/detection-pipeline/index.mjs +3 -2
- package/dist/{dist-DOu93i_g.mjs → dist-BDkEhx4Z.mjs} +692 -123
- package/dist/{dist-BuFE5rOK.js → dist-QyZ7QSRP.js} +697 -122
- package/dist/{event-loop-stall-monitor-L08yG6bB.js → event-loop-stall-monitor-COEKVBQK.js} +14 -7
- package/dist/{event-loop-stall-monitor-CShgQE6l.mjs → event-loop-stall-monitor-IAuNEdmG.mjs} +14 -7
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DiXoZnou.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C68YfSUH.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CT2bGWqM.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_hakWAb.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B3GxBnXM.mjs} +1 -1
- package/dist/stream-broker/{hostInit-khRXMnjl.mjs → hostInit-BMCrULpc.mjs} +2 -2
- package/dist/stream-broker/index.js +735 -424
- package/dist/stream-broker/index.mjs +731 -421
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BOXlUhWO.mjs → worker-protocol-DFH_z2-q.mjs} +1 -1
- package/dist/{worker-protocol-VURr0nUh.js → worker-protocol-le27Mw_8.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DHZuYYjH.js → MaskShapeCanvas-DI4BY7W2-C7bsz4cP.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-jFDOzPCD.js → MotionZonesSettings-NcxxQN8r-BmhLinUd.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BPBXgIX8.js → PrivacyMaskSettings-APgPLF7p-BJjyGWFy.js} +1 -1
- package/embed-dist/assets/{index-4CtQAybX.js → index-a4CMt-Uc.js} +12 -12
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/postprocessors/__init__.py +2 -0
- package/python/postprocessors/rfdetr.py +161 -0
- package/python/postprocessors/test_rfdetr.py +193 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BRc-m6W-.mjs +0 -26
|
@@ -6541,7 +6541,20 @@ var BrokerStatsSchema = object({
|
|
|
6541
6541
|
sampleRate: number(),
|
|
6542
6542
|
channels: number(),
|
|
6543
6543
|
supported: boolean()
|
|
6544
|
-
}).nullable().optional()
|
|
6544
|
+
}).nullable().optional(),
|
|
6545
|
+
/**
|
|
6546
|
+
* BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
|
|
6547
|
+
* distributing none of the device's audio, on live or recording.
|
|
6548
|
+
*
|
|
6549
|
+
* Present so a silent camera can be told apart from a broken one on the
|
|
6550
|
+
* stream panel itself, without cross-referencing the switch group: a
|
|
6551
|
+
* broker holding an `audio` track descriptor while `audioMuted` is true is
|
|
6552
|
+
* working exactly as asked. `audioMutedDropped` counts the audio units
|
|
6553
|
+
* thrown away since the current dial — it is how you confirm from stats
|
|
6554
|
+
* alone that the mute is on the packet path and not merely persisted.
|
|
6555
|
+
*/
|
|
6556
|
+
audioMuted: boolean().optional(),
|
|
6557
|
+
audioMutedDropped: number().optional()
|
|
6545
6558
|
});
|
|
6546
6559
|
/**
|
|
6547
6560
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -7120,116 +7133,7 @@ function sleep(ms) {
|
|
|
7120
7133
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7121
7134
|
}
|
|
7122
7135
|
//#endregion
|
|
7123
|
-
//#region ../types/dist/
|
|
7124
|
-
/**
|
|
7125
|
-
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7126
|
-
* canonical form sorts object keys alphabetically at every depth so two
|
|
7127
|
-
* structurally-equal inputs with different key insertion orders produce
|
|
7128
|
-
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7129
|
-
*
|
|
7130
|
-
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7131
|
-
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7132
|
-
* the last applied state — preventing user-visible "re-discovery"
|
|
7133
|
-
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7134
|
-
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7135
|
-
* this guard would propagate redundant pushes.
|
|
7136
|
-
*
|
|
7137
|
-
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7138
|
-
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7139
|
-
* barrier so exports react only when the full cap set has landed —
|
|
7140
|
-
* tracked separately for post-HA-integration work.
|
|
7141
|
-
*/
|
|
7142
|
-
function canonicalHash(value) {
|
|
7143
|
-
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7144
|
-
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7145
|
-
}
|
|
7146
|
-
function replaceWithSortedKeys(_key, value) {
|
|
7147
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7148
|
-
const obj = value;
|
|
7149
|
-
const out = {};
|
|
7150
|
-
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7151
|
-
return out;
|
|
7152
|
-
}
|
|
7153
|
-
return value;
|
|
7154
|
-
}
|
|
7155
|
-
//#endregion
|
|
7156
|
-
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7157
|
-
/**
|
|
7158
|
-
import { errMsg } from '@camstack/types'
|
|
7159
|
-
* Extract a human-readable message from an unknown error value.
|
|
7160
|
-
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
7161
|
-
*/
|
|
7162
|
-
function errMsg(err) {
|
|
7163
|
-
if (err instanceof Error) return err.message;
|
|
7164
|
-
if (typeof err === "string") return err;
|
|
7165
|
-
return String(err);
|
|
7166
|
-
}
|
|
7167
|
-
var EncodeProfileSchema = object({
|
|
7168
|
-
video: object({
|
|
7169
|
-
codec: _enum([
|
|
7170
|
-
"h264",
|
|
7171
|
-
"h265",
|
|
7172
|
-
"copy"
|
|
7173
|
-
]),
|
|
7174
|
-
profile: _enum([
|
|
7175
|
-
"baseline",
|
|
7176
|
-
"main",
|
|
7177
|
-
"high"
|
|
7178
|
-
]).optional(),
|
|
7179
|
-
/**
|
|
7180
|
-
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
7181
|
-
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
7182
|
-
* it, or it ships a stream that does not match its own advertisement — the
|
|
7183
|
-
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
7184
|
-
* silently. Optional because a browser negotiates the level itself.
|
|
7185
|
-
*/
|
|
7186
|
-
level: string().optional(),
|
|
7187
|
-
width: number().int().positive().optional(),
|
|
7188
|
-
height: number().int().positive().optional(),
|
|
7189
|
-
fps: number().positive().optional(),
|
|
7190
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7191
|
-
gopFrames: number().int().positive().optional(),
|
|
7192
|
-
bf: number().int().min(0).optional(),
|
|
7193
|
-
preset: _enum([
|
|
7194
|
-
"ultrafast",
|
|
7195
|
-
"superfast",
|
|
7196
|
-
"veryfast",
|
|
7197
|
-
"faster",
|
|
7198
|
-
"fast",
|
|
7199
|
-
"medium"
|
|
7200
|
-
]).optional(),
|
|
7201
|
-
tune: _enum([
|
|
7202
|
-
"zerolatency",
|
|
7203
|
-
"film",
|
|
7204
|
-
"animation"
|
|
7205
|
-
]).optional()
|
|
7206
|
-
}),
|
|
7207
|
-
audio: union([literal("passthrough"), object({
|
|
7208
|
-
codec: _enum([
|
|
7209
|
-
"opus",
|
|
7210
|
-
"aac",
|
|
7211
|
-
"pcmu",
|
|
7212
|
-
"pcma",
|
|
7213
|
-
"copy"
|
|
7214
|
-
]),
|
|
7215
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7216
|
-
sampleRateHz: number().int().positive().optional(),
|
|
7217
|
-
channels: union([literal(1), literal(2)]).optional()
|
|
7218
|
-
})]),
|
|
7219
|
-
/**
|
|
7220
|
-
* ffmpeg input-side args, inserted between the fixed global flags
|
|
7221
|
-
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
7222
|
-
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
7223
|
-
* used demuxer/format options.
|
|
7224
|
-
*/
|
|
7225
|
-
inputArgs: array(string()).optional(),
|
|
7226
|
-
/**
|
|
7227
|
-
* ffmpeg output-side args, inserted between the encode block and
|
|
7228
|
-
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
7229
|
-
* filters, codec-specific overrides. Free-text array.
|
|
7230
|
-
*/
|
|
7231
|
-
outputArgs: array(string()).optional()
|
|
7232
|
-
});
|
|
7136
|
+
//#region ../types/dist/fmp4-box-splitter-B53u9-Nu.mjs
|
|
7233
7137
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7234
7138
|
opus: "libopus",
|
|
7235
7139
|
aac: "aac",
|
|
@@ -7410,13 +7314,42 @@ function buildStdoutOrRtspSinkArgs(sink) {
|
|
|
7410
7314
|
"pipe:1"
|
|
7411
7315
|
];
|
|
7412
7316
|
}
|
|
7317
|
+
/**
|
|
7318
|
+
* How far BELOW the negotiated fragment length `-min_frag_duration` is set.
|
|
7319
|
+
*
|
|
7320
|
+
* `-min_frag_duration` refuses to cut before that much media has accumulated,
|
|
7321
|
+
* and then waits for the next key frame. Set to exactly `fragmentMs`, the
|
|
7322
|
+
* commonest camera configuration in existence — a key-frame grid EQUAL to the
|
|
7323
|
+
* requested fragment length — lands the deadline on the same instant as the key
|
|
7324
|
+
* frame, loses the race, and skips to the following one: **every fragment comes
|
|
7325
|
+
* out at twice the requested length.**
|
|
7326
|
+
*
|
|
7327
|
+
* Measured on the live fleet 2026-08-07, camera 615, `-c:v copy` (D84):
|
|
7328
|
+
*
|
|
7329
|
+
* | slot | GOP | `-min_frag_duration` | median gap |
|
|
7330
|
+
* | --- | --- | --- | --- |
|
|
7331
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 4000 ms | **7944 ms** |
|
|
7332
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 3600 ms | 3973 ms |
|
|
7333
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 4000 ms | 8042 ms |
|
|
7334
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 3600 ms | 3998 ms |
|
|
7335
|
+
*
|
|
7336
|
+
* A doubled fragment is not a cosmetic overshoot: HKSV requires every fragment
|
|
7337
|
+
* to be no longer than the length the controller SELECTED, so the shipped-but-
|
|
7338
|
+
* inert phase-1 sink would have violated the contract on its first real clip.
|
|
7339
|
+
*
|
|
7340
|
+
* 10 % is chosen against the two failures either side of it. Too small and
|
|
7341
|
+
* ordinary jitter (measured spread 3953-4096 ms) re-loses the race; too large
|
|
7342
|
+
* and a source with a key frame slightly EARLY than the grid gets cut there,
|
|
7343
|
+
* yielding a short fragment for no reason.
|
|
7344
|
+
*/
|
|
7345
|
+
var FMP4_MIN_FRAG_MARGIN = .9;
|
|
7413
7346
|
/** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
|
|
7414
7347
|
function buildFmp4SinkArgs(sink) {
|
|
7415
7348
|
return [
|
|
7416
7349
|
"-movflags",
|
|
7417
7350
|
FMP4_MOVFLAGS,
|
|
7418
7351
|
"-min_frag_duration",
|
|
7419
|
-
String(Math.max(0, Math.round(sink.fragmentMs * 1e3))),
|
|
7352
|
+
String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
|
|
7420
7353
|
"-f",
|
|
7421
7354
|
"mp4",
|
|
7422
7355
|
"pipe:1"
|
|
@@ -7606,6 +7539,287 @@ function invocationFromEncodeProfile(input) {
|
|
|
7606
7539
|
};
|
|
7607
7540
|
}
|
|
7608
7541
|
/**
|
|
7542
|
+
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7543
|
+
* canonical form sorts object keys alphabetically at every depth so two
|
|
7544
|
+
* structurally-equal inputs with different key insertion orders produce
|
|
7545
|
+
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7546
|
+
*
|
|
7547
|
+
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7548
|
+
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7549
|
+
* the last applied state — preventing user-visible "re-discovery"
|
|
7550
|
+
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7551
|
+
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7552
|
+
* this guard would propagate redundant pushes.
|
|
7553
|
+
*
|
|
7554
|
+
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7555
|
+
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7556
|
+
* barrier so exports react only when the full cap set has landed —
|
|
7557
|
+
* tracked separately for post-HA-integration work.
|
|
7558
|
+
*/
|
|
7559
|
+
function canonicalHash(value) {
|
|
7560
|
+
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7561
|
+
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7562
|
+
}
|
|
7563
|
+
function replaceWithSortedKeys(_key, value) {
|
|
7564
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7565
|
+
const obj = value;
|
|
7566
|
+
const out = {};
|
|
7567
|
+
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7568
|
+
return out;
|
|
7569
|
+
}
|
|
7570
|
+
return value;
|
|
7571
|
+
}
|
|
7572
|
+
var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
|
|
7573
|
+
/** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
|
|
7574
|
+
var BOX_HEADER_BYTES = 8;
|
|
7575
|
+
var LARGE_BOX_HEADER_BYTES = 16;
|
|
7576
|
+
var Fmp4BoxSplitter = class {
|
|
7577
|
+
maxUnitBytes;
|
|
7578
|
+
/** Bytes of the CURRENT unit plus any partial box after it. */
|
|
7579
|
+
buffer = new Uint8Array(0);
|
|
7580
|
+
/** Where the current unit starts inside {@link buffer}. */
|
|
7581
|
+
unitStart = 0;
|
|
7582
|
+
/** Where the box scanner has reached inside {@link buffer}. */
|
|
7583
|
+
cursor = 0;
|
|
7584
|
+
state = "init";
|
|
7585
|
+
nextSequence = 0;
|
|
7586
|
+
faultReason = null;
|
|
7587
|
+
interstitial = /* @__PURE__ */ new Set();
|
|
7588
|
+
constructor(options = {}) {
|
|
7589
|
+
this.maxUnitBytes = options.maxUnitBytes ?? DEFAULT_MAX_UNIT_BYTES;
|
|
7590
|
+
}
|
|
7591
|
+
/**
|
|
7592
|
+
* Non-null once the stream cannot be split. The splitter emits nothing
|
|
7593
|
+
* further, so a caller polls this to kill the child rather than watching a
|
|
7594
|
+
* silent stall — a fragmenter that quietly stops producing looks exactly like
|
|
7595
|
+
* a camera with no motion.
|
|
7596
|
+
*/
|
|
7597
|
+
get fault() {
|
|
7598
|
+
return this.faultReason;
|
|
7599
|
+
}
|
|
7600
|
+
/** Bytes currently held. The memory bound, observable rather than asserted. */
|
|
7601
|
+
get pendingBytes() {
|
|
7602
|
+
return this.buffer.length - this.unitStart;
|
|
7603
|
+
}
|
|
7604
|
+
/**
|
|
7605
|
+
* Top-level box types seen BETWEEN fragments and discarded — `mfra`, `free`,
|
|
7606
|
+
* a stray `sidx`. Reported rather than dropped in silence: they are legal and
|
|
7607
|
+
* useless to a fragment consumer, but a type nobody expected showing up here
|
|
7608
|
+
* is the first symptom of a muxer that is not writing what we think it is.
|
|
7609
|
+
*/
|
|
7610
|
+
get discardedInterstitialTypes() {
|
|
7611
|
+
return [...this.interstitial];
|
|
7612
|
+
}
|
|
7613
|
+
/**
|
|
7614
|
+
* Feed bytes; get back whatever units completed. Returns `[]` once faulted.
|
|
7615
|
+
*/
|
|
7616
|
+
push(chunk) {
|
|
7617
|
+
if (this.faultReason !== null || chunk.length === 0) return [];
|
|
7618
|
+
this.append(chunk);
|
|
7619
|
+
if (this.pendingBytes > this.maxUnitBytes) return this.fail(`a single fMP4 unit exceeded ${this.maxUnitBytes} bytes — this stream is not fragmented`);
|
|
7620
|
+
return this.drainBoxes();
|
|
7621
|
+
}
|
|
7622
|
+
append(chunk) {
|
|
7623
|
+
if (this.buffer.length === 0) {
|
|
7624
|
+
this.buffer = chunk.slice();
|
|
7625
|
+
return;
|
|
7626
|
+
}
|
|
7627
|
+
const next = new Uint8Array(this.buffer.length + chunk.length);
|
|
7628
|
+
next.set(this.buffer, 0);
|
|
7629
|
+
next.set(chunk, this.buffer.length);
|
|
7630
|
+
this.buffer = next;
|
|
7631
|
+
}
|
|
7632
|
+
/** Consume every COMPLETE top-level box now in the buffer. */
|
|
7633
|
+
drainBoxes() {
|
|
7634
|
+
const units = [];
|
|
7635
|
+
for (;;) {
|
|
7636
|
+
const header = this.readHeader();
|
|
7637
|
+
if (this.faultReason !== null) return units;
|
|
7638
|
+
if (header === null) break;
|
|
7639
|
+
if (this.cursor + header.totalBytes > this.buffer.length) break;
|
|
7640
|
+
const boxStart = this.cursor;
|
|
7641
|
+
const boxEnd = boxStart + header.totalBytes;
|
|
7642
|
+
this.cursor = boxEnd;
|
|
7643
|
+
const unit = this.consumeBox(header.type, boxStart, boxEnd);
|
|
7644
|
+
if (this.faultReason !== null) return units;
|
|
7645
|
+
if (unit !== null) units.push(unit);
|
|
7646
|
+
}
|
|
7647
|
+
this.compact();
|
|
7648
|
+
return units;
|
|
7649
|
+
}
|
|
7650
|
+
/**
|
|
7651
|
+
* Apply one box to the state machine. Returns a unit when this box CLOSED
|
|
7652
|
+
* one, `null` otherwise.
|
|
7653
|
+
*/
|
|
7654
|
+
consumeBox(type, boxStart, boxEnd) {
|
|
7655
|
+
if (this.state === "init") {
|
|
7656
|
+
if (type !== "moof") return null;
|
|
7657
|
+
if (boxStart === this.unitStart) {
|
|
7658
|
+
this.fail("a moof arrived before any initialisation box — there is no ftyp/moov to send");
|
|
7659
|
+
return null;
|
|
7660
|
+
}
|
|
7661
|
+
const init = this.emit("init", this.unitStart, boxStart);
|
|
7662
|
+
this.unitStart = boxStart;
|
|
7663
|
+
this.state = "fragment";
|
|
7664
|
+
return init;
|
|
7665
|
+
}
|
|
7666
|
+
if (this.state === "idle") {
|
|
7667
|
+
if (type !== "moof") {
|
|
7668
|
+
this.interstitial.add(type);
|
|
7669
|
+
this.unitStart = boxEnd;
|
|
7670
|
+
return null;
|
|
7671
|
+
}
|
|
7672
|
+
this.unitStart = boxStart;
|
|
7673
|
+
this.state = "fragment";
|
|
7674
|
+
return null;
|
|
7675
|
+
}
|
|
7676
|
+
if (type !== "mdat") return null;
|
|
7677
|
+
const fragment = this.emit("fragment", this.unitStart, boxEnd);
|
|
7678
|
+
this.unitStart = boxEnd;
|
|
7679
|
+
this.state = "idle";
|
|
7680
|
+
return fragment;
|
|
7681
|
+
}
|
|
7682
|
+
/**
|
|
7683
|
+
* Parse the header at {@link cursor}, or `null` when too few bytes have
|
|
7684
|
+
* arrived to know. Faults on a size the splitter cannot honour.
|
|
7685
|
+
*/
|
|
7686
|
+
readHeader() {
|
|
7687
|
+
const available = this.buffer.length - this.cursor;
|
|
7688
|
+
if (available < BOX_HEADER_BYTES) return null;
|
|
7689
|
+
const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
|
|
7690
|
+
const size = view.getUint32(this.cursor);
|
|
7691
|
+
const type = String.fromCharCode(this.buffer[this.cursor + 4] ?? 0, this.buffer[this.cursor + 5] ?? 0, this.buffer[this.cursor + 6] ?? 0, this.buffer[this.cursor + 7] ?? 0);
|
|
7692
|
+
if (size === 0) {
|
|
7693
|
+
this.fail(`box "${type}" declares size 0 (to EOF) — an unbounded box cannot be fragmented`);
|
|
7694
|
+
return null;
|
|
7695
|
+
}
|
|
7696
|
+
if (size === 1) {
|
|
7697
|
+
if (available < LARGE_BOX_HEADER_BYTES) return null;
|
|
7698
|
+
const large = view.getBigUint64(this.cursor + BOX_HEADER_BYTES);
|
|
7699
|
+
if (large > BigInt(this.maxUnitBytes)) {
|
|
7700
|
+
this.fail(`box "${type}" declares ${large} bytes, over the ${this.maxUnitBytes} byte bound`);
|
|
7701
|
+
return null;
|
|
7702
|
+
}
|
|
7703
|
+
return {
|
|
7704
|
+
type,
|
|
7705
|
+
totalBytes: Number(large)
|
|
7706
|
+
};
|
|
7707
|
+
}
|
|
7708
|
+
if (size < BOX_HEADER_BYTES) {
|
|
7709
|
+
this.fail(`box "${type}" declares an impossible size of ${size} bytes`);
|
|
7710
|
+
return null;
|
|
7711
|
+
}
|
|
7712
|
+
return {
|
|
7713
|
+
type,
|
|
7714
|
+
totalBytes: size
|
|
7715
|
+
};
|
|
7716
|
+
}
|
|
7717
|
+
emit(kind, start, end) {
|
|
7718
|
+
const sequence = this.nextSequence;
|
|
7719
|
+
this.nextSequence += 1;
|
|
7720
|
+
return {
|
|
7721
|
+
kind,
|
|
7722
|
+
data: this.buffer.slice(start, end),
|
|
7723
|
+
sequence
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
/**
|
|
7727
|
+
* Drop everything already emitted or discarded. Without this the buffer is
|
|
7728
|
+
* the whole stream and the process dies in hours, not minutes.
|
|
7729
|
+
*/
|
|
7730
|
+
compact() {
|
|
7731
|
+
if (this.unitStart === 0) return;
|
|
7732
|
+
this.buffer = this.buffer.slice(this.unitStart);
|
|
7733
|
+
this.cursor -= this.unitStart;
|
|
7734
|
+
this.unitStart = 0;
|
|
7735
|
+
}
|
|
7736
|
+
fail(reason) {
|
|
7737
|
+
this.faultReason = reason;
|
|
7738
|
+
this.buffer = new Uint8Array(0);
|
|
7739
|
+
this.unitStart = 0;
|
|
7740
|
+
this.cursor = 0;
|
|
7741
|
+
return [];
|
|
7742
|
+
}
|
|
7743
|
+
};
|
|
7744
|
+
//#endregion
|
|
7745
|
+
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7746
|
+
/**
|
|
7747
|
+
import { errMsg } from '@camstack/types'
|
|
7748
|
+
* Extract a human-readable message from an unknown error value.
|
|
7749
|
+
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
7750
|
+
*/
|
|
7751
|
+
function errMsg(err) {
|
|
7752
|
+
if (err instanceof Error) return err.message;
|
|
7753
|
+
if (typeof err === "string") return err;
|
|
7754
|
+
return String(err);
|
|
7755
|
+
}
|
|
7756
|
+
var EncodeProfileSchema = object({
|
|
7757
|
+
video: object({
|
|
7758
|
+
codec: _enum([
|
|
7759
|
+
"h264",
|
|
7760
|
+
"h265",
|
|
7761
|
+
"copy"
|
|
7762
|
+
]),
|
|
7763
|
+
profile: _enum([
|
|
7764
|
+
"baseline",
|
|
7765
|
+
"main",
|
|
7766
|
+
"high"
|
|
7767
|
+
]).optional(),
|
|
7768
|
+
/**
|
|
7769
|
+
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
7770
|
+
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
7771
|
+
* it, or it ships a stream that does not match its own advertisement — the
|
|
7772
|
+
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
7773
|
+
* silently. Optional because a browser negotiates the level itself.
|
|
7774
|
+
*/
|
|
7775
|
+
level: string().optional(),
|
|
7776
|
+
width: number().int().positive().optional(),
|
|
7777
|
+
height: number().int().positive().optional(),
|
|
7778
|
+
fps: number().positive().optional(),
|
|
7779
|
+
bitrateKbps: number().int().positive().optional(),
|
|
7780
|
+
gopFrames: number().int().positive().optional(),
|
|
7781
|
+
bf: number().int().min(0).optional(),
|
|
7782
|
+
preset: _enum([
|
|
7783
|
+
"ultrafast",
|
|
7784
|
+
"superfast",
|
|
7785
|
+
"veryfast",
|
|
7786
|
+
"faster",
|
|
7787
|
+
"fast",
|
|
7788
|
+
"medium"
|
|
7789
|
+
]).optional(),
|
|
7790
|
+
tune: _enum([
|
|
7791
|
+
"zerolatency",
|
|
7792
|
+
"film",
|
|
7793
|
+
"animation"
|
|
7794
|
+
]).optional()
|
|
7795
|
+
}),
|
|
7796
|
+
audio: union([literal("passthrough"), object({
|
|
7797
|
+
codec: _enum([
|
|
7798
|
+
"opus",
|
|
7799
|
+
"aac",
|
|
7800
|
+
"pcmu",
|
|
7801
|
+
"pcma",
|
|
7802
|
+
"copy"
|
|
7803
|
+
]),
|
|
7804
|
+
bitrateKbps: number().int().positive().optional(),
|
|
7805
|
+
sampleRateHz: number().int().positive().optional(),
|
|
7806
|
+
channels: union([literal(1), literal(2)]).optional()
|
|
7807
|
+
})]),
|
|
7808
|
+
/**
|
|
7809
|
+
* ffmpeg input-side args, inserted between the fixed global flags
|
|
7810
|
+
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
7811
|
+
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
7812
|
+
* used demuxer/format options.
|
|
7813
|
+
*/
|
|
7814
|
+
inputArgs: array(string()).optional(),
|
|
7815
|
+
/**
|
|
7816
|
+
* ffmpeg output-side args, inserted between the encode block and
|
|
7817
|
+
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
7818
|
+
* filters, codec-specific overrides. Free-text array.
|
|
7819
|
+
*/
|
|
7820
|
+
outputArgs: array(string()).optional()
|
|
7821
|
+
});
|
|
7822
|
+
/**
|
|
7609
7823
|
* The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
|
|
7610
7824
|
* Baseline because it is the one profile every consumer in this repo decodes
|
|
7611
7825
|
* (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
|
|
@@ -7850,6 +8064,19 @@ object({
|
|
|
7850
8064
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
7851
8065
|
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
7852
8066
|
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
8067
|
+
* | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
|
|
8068
|
+
*
|
|
8069
|
+
* ## `device-audio` and `broker-audio` are two functions, not two knobs
|
|
8070
|
+
*
|
|
8071
|
+
* They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
|
|
8072
|
+
* `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
|
|
8073
|
+
* genuinely stops, it survives CamStack entirely, and it costs a multi-second
|
|
8074
|
+
* encoder restart on every flip. `broker-audio` writes THIS server, so it is
|
|
8075
|
+
* instant, vendor-independent and reversible without touching the camera, and
|
|
8076
|
+
* a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
|
|
8077
|
+
* D62 forbids a second switch that *disagrees* with the first; these two
|
|
8078
|
+
* cannot disagree, because neither reads the other's store — the camera holds
|
|
8079
|
+
* one, the broker holds the other, and each reports its own fact.
|
|
7853
8080
|
*
|
|
7854
8081
|
* ## The two switches whose authority is not on this server
|
|
7855
8082
|
*
|
|
@@ -7911,6 +8138,7 @@ var CameraSwitchIdSchema = _enum([
|
|
|
7911
8138
|
"object-detection",
|
|
7912
8139
|
"privacy-mask",
|
|
7913
8140
|
"device-audio",
|
|
8141
|
+
"broker-audio",
|
|
7914
8142
|
"audio-analysis",
|
|
7915
8143
|
"recording",
|
|
7916
8144
|
"notifications"
|
|
@@ -7936,7 +8164,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7936
8164
|
object({
|
|
7937
8165
|
kind: literal("camera-mask"),
|
|
7938
8166
|
capName: string()
|
|
7939
|
-
})
|
|
8167
|
+
}),
|
|
8168
|
+
object({ kind: literal("broker-audio-mute") })
|
|
7940
8169
|
]);
|
|
7941
8170
|
/**
|
|
7942
8171
|
* Why a switch is not offered for this camera. Rendered instead of the
|
|
@@ -10405,7 +10634,44 @@ var streamBrokerCapability = {
|
|
|
10405
10634
|
kind: "mutation",
|
|
10406
10635
|
auth: "admin"
|
|
10407
10636
|
}),
|
|
10408
|
-
isRtspEnabled: method(object({ brokerId: string() }), boolean())
|
|
10637
|
+
isRtspEnabled: method(object({ brokerId: string() }), boolean()),
|
|
10638
|
+
/**
|
|
10639
|
+
* ── Per-device audio-plane policy (D83) ───────────────────────────
|
|
10640
|
+
*
|
|
10641
|
+
* The BROKER-side mute: while `muted`, this node distributes none of
|
|
10642
|
+
* the device's audio, on any plane it serves — live (WebRTC / encoded
|
|
10643
|
+
* subscribers) AND recording (the RTSP restreamer the recorder pulls,
|
|
10644
|
+
* which additionally serves the video-only SDP so the recorder's ffmpeg
|
|
10645
|
+
* never declares an audio stream it will not receive).
|
|
10646
|
+
*
|
|
10647
|
+
* Keyed by `deviceId`, not `brokerId`: every one of a camera's streams
|
|
10648
|
+
* carries the same microphone, and a per-stream answer would let main
|
|
10649
|
+
* and sub disagree about whether the camera is silent.
|
|
10650
|
+
*
|
|
10651
|
+
* The state lives in the broker's existing `DeviceOverride` blob — no
|
|
10652
|
+
* new store — and the mute is applied to a fresh broker at creation, so
|
|
10653
|
+
* a restart, a re-dial or a catalog republish never un-mutes a camera.
|
|
10654
|
+
*/
|
|
10655
|
+
getDeviceAudioMute: method(object({ deviceId: number().int() }), object({
|
|
10656
|
+
muted: boolean(),
|
|
10657
|
+
/**
|
|
10658
|
+
* How many live non-derived brokers currently hold the mute. Purely
|
|
10659
|
+
* diagnostic: `muted` is the policy and is authoritative on its own
|
|
10660
|
+
* (it applies to brokers that do not exist yet), while this says
|
|
10661
|
+
* whether anything is presently being silenced.
|
|
10662
|
+
*/
|
|
10663
|
+
appliedBrokers: number().int().nonnegative()
|
|
10664
|
+
})),
|
|
10665
|
+
setDeviceAudioMute: method(object({
|
|
10666
|
+
deviceId: number().int(),
|
|
10667
|
+
muted: boolean()
|
|
10668
|
+
}), object({
|
|
10669
|
+
muted: boolean(),
|
|
10670
|
+
appliedBrokers: number().int().nonnegative()
|
|
10671
|
+
}), {
|
|
10672
|
+
kind: "mutation",
|
|
10673
|
+
auth: "admin"
|
|
10674
|
+
})
|
|
10409
10675
|
},
|
|
10410
10676
|
events: {
|
|
10411
10677
|
onCamStreamDemand: event(object({
|
|
@@ -16141,6 +16407,30 @@ var TrackSourceSchema = _enum([
|
|
|
16141
16407
|
"audio"
|
|
16142
16408
|
]);
|
|
16143
16409
|
/**
|
|
16410
|
+
* Where a track sits in the RETRAIN lifecycle (D81).
|
|
16411
|
+
*
|
|
16412
|
+
* - `none` — never marked, or un-marked. Evictable.
|
|
16413
|
+
* - `staging` — the operator wants this track as training material and has not
|
|
16414
|
+
* finished with it. **This is the only state retention holds**: the track and
|
|
16415
|
+
* everything it owns (object events, crops, keyframes, CLIP vector) survive
|
|
16416
|
+
* the device's age window.
|
|
16417
|
+
* - `trained` — the retrain page has taken what it needed. The frames it chose
|
|
16418
|
+
* were COPIED into the retrain dataset at selection time, so the dataset no
|
|
16419
|
+
* longer depends on the track's media and the track becomes EVICTABLE again.
|
|
16420
|
+
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
16421
|
+
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
16422
|
+
*
|
|
16423
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
|
|
16424
|
+
* the store's filter language has only positive equality and `whereIn` — no
|
|
16425
|
+
* negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
|
|
16426
|
+
* would make the entire pre-column history immortal in one deploy.
|
|
16427
|
+
*/
|
|
16428
|
+
var RetrainStatusSchema = _enum([
|
|
16429
|
+
"none",
|
|
16430
|
+
"staging",
|
|
16431
|
+
"trained"
|
|
16432
|
+
]);
|
|
16433
|
+
/**
|
|
16144
16434
|
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
16145
16435
|
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
16146
16436
|
* so the two surfaces cannot drift.
|
|
@@ -16150,18 +16440,31 @@ var TrackSourceSchema = _enum([
|
|
|
16150
16440
|
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
16151
16441
|
* `flag === true`, not `flag !== false`.
|
|
16152
16442
|
*
|
|
16153
|
-
*
|
|
16154
|
-
*
|
|
16155
|
-
*
|
|
16156
|
-
* `
|
|
16443
|
+
* `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
|
|
16444
|
+
* it is exactly `retrainStatus === 'staging'`, in both directions. Writing
|
|
16445
|
+
* `true` moves `none → staging`, writing `false` moves `staging → none`, and a
|
|
16446
|
+
* `trained` track reports `false` while refusing both writes. The boolean is
|
|
16447
|
+
* kept because three surfaces drive a toggle off it; anything that needs to tell
|
|
16448
|
+
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16449
|
+
*
|
|
16450
|
+
* `debug` does NOT pin; it is attention, not durability.
|
|
16157
16451
|
*/
|
|
16158
16452
|
var TrackFlagFields = {
|
|
16159
|
-
/** Operator marked this track as training material.
|
|
16453
|
+
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16454
|
+
* `'staging'`. */
|
|
16160
16455
|
markForTrain: boolean().optional(),
|
|
16161
16456
|
/** Operator marked this track for diagnostic attention. */
|
|
16162
16457
|
debug: boolean().optional()
|
|
16163
16458
|
};
|
|
16164
16459
|
/**
|
|
16460
|
+
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
16461
|
+
* Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
|
|
16462
|
+
* write patch, and the status is not something the toggle sets — it is what the
|
|
16463
|
+
* toggle's boolean is derived from. Absent on an in-RAM track never touched;
|
|
16464
|
+
* always present on a persisted row (the column default materialises `'none'`).
|
|
16465
|
+
*/
|
|
16466
|
+
var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
|
|
16467
|
+
/**
|
|
16165
16468
|
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
16166
16469
|
* one flag can never clear the other — the toggles are independent and are
|
|
16167
16470
|
* driven from three surfaces that do not know about each other.
|
|
@@ -16175,7 +16478,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
|
16175
16478
|
var TrackFlagsSchema = object({
|
|
16176
16479
|
trackId: string(),
|
|
16177
16480
|
markForTrain: boolean(),
|
|
16178
|
-
debug: boolean()
|
|
16481
|
+
debug: boolean(),
|
|
16482
|
+
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16483
|
+
* a track row) because this shape is only ever produced by the write body,
|
|
16484
|
+
* which always knows it — and a surface that has just written needs to render
|
|
16485
|
+
* `trained` without a re-fetch. */
|
|
16486
|
+
retrainStatus: RetrainStatusSchema
|
|
16487
|
+
});
|
|
16488
|
+
/** Per-camera slice of a training-export estimate. */
|
|
16489
|
+
var TrainingExportDeviceTotalsSchema = object({
|
|
16490
|
+
deviceId: number(),
|
|
16491
|
+
tracks: number().int(),
|
|
16492
|
+
files: number().int(),
|
|
16493
|
+
bytes: number().int()
|
|
16494
|
+
});
|
|
16495
|
+
/**
|
|
16496
|
+
* What a training export WOULD contain. Computed from media index rows only —
|
|
16497
|
+
* no blob is read to produce this.
|
|
16498
|
+
*/
|
|
16499
|
+
var TrainingExportSummarySchema = object({
|
|
16500
|
+
generatedAt: number(),
|
|
16501
|
+
trackCount: number().int(),
|
|
16502
|
+
fileCount: number().int(),
|
|
16503
|
+
byteCount: number().int(),
|
|
16504
|
+
/** More marked tracks exist than a single pass carries. */
|
|
16505
|
+
truncated: boolean(),
|
|
16506
|
+
devices: array(TrainingExportDeviceTotalsSchema).readonly()
|
|
16179
16507
|
});
|
|
16180
16508
|
var TrackSchema = object({
|
|
16181
16509
|
trackId: string(),
|
|
@@ -16220,7 +16548,8 @@ var TrackSchema = object({
|
|
|
16220
16548
|
* Populated from the persisted envelope columns on historical reads;
|
|
16221
16549
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
16222
16550
|
envelope: TrackEnvelopeSchema.optional(),
|
|
16223
|
-
...TrackFlagFields
|
|
16551
|
+
...TrackFlagFields,
|
|
16552
|
+
...TrackRetrainFields
|
|
16224
16553
|
});
|
|
16225
16554
|
var BaseEventFields = {
|
|
16226
16555
|
id: string(),
|
|
@@ -16434,7 +16763,8 @@ var KeyEventSchema = object({
|
|
|
16434
16763
|
bestEventId: string(),
|
|
16435
16764
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
16436
16765
|
windowMs: number().optional(),
|
|
16437
|
-
...TrackFlagFields
|
|
16766
|
+
...TrackFlagFields,
|
|
16767
|
+
...TrackRetrainFields
|
|
16438
16768
|
});
|
|
16439
16769
|
object({
|
|
16440
16770
|
trackId: string(),
|
|
@@ -16695,6 +17025,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16695
17025
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
16696
17026
|
kind: "query",
|
|
16697
17027
|
auth: "admin"
|
|
17028
|
+
}), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
|
|
17029
|
+
kind: "query",
|
|
17030
|
+
auth: "admin"
|
|
17031
|
+
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
17032
|
+
kind: "query",
|
|
17033
|
+
auth: "admin"
|
|
16698
17034
|
}), method(object({
|
|
16699
17035
|
eventId: string(),
|
|
16700
17036
|
kind: MediaFileKindEnum.optional()
|
|
@@ -22744,6 +23080,173 @@ DeviceType.Camera, method(object({
|
|
|
22744
23080
|
status: OsdStatusSchema
|
|
22745
23081
|
});
|
|
22746
23082
|
/**
|
|
23083
|
+
* `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
|
|
23084
|
+
*
|
|
23085
|
+
* The `osd` cap is the firmware contract: it probes a camera's overlay
|
|
23086
|
+
* SLOTS and writes literal text into one. It has no idea WHERE that text
|
|
23087
|
+
* comes from, and it must not — a driver that grew a "show the temperature
|
|
23088
|
+
* here" feature would grow it once per vendor.
|
|
23089
|
+
*
|
|
23090
|
+
* This cap owns the other half: a per-(camera, slot) BINDING that says
|
|
23091
|
+
* which value feeds the slot, how it is formatted, and under which
|
|
23092
|
+
* conditions it is shown at all. One addon renders every binding on every
|
|
23093
|
+
* camera, so a new source costs zero driver code.
|
|
23094
|
+
*
|
|
23095
|
+
* Three deliberate choices, each with a rejected alternative:
|
|
23096
|
+
*
|
|
23097
|
+
* 1. A source is `(capName, valuePath)` over the kernel's device
|
|
23098
|
+
* runtime-state mirror — NOT a closed enum of source kinds. Every
|
|
23099
|
+
* cap-keyed slice a device publishes is bindable the day the cap
|
|
23100
|
+
* ships. The rejected alternative (one enum member per source, with
|
|
23101
|
+
* a resolver branch each) is what makes "add the humidity too" a
|
|
23102
|
+
* code change.
|
|
23103
|
+
* 2. The display gate reuses `NcConditionsSchema` verbatim — the
|
|
23104
|
+
* notification centre's condition vocabulary — rather than a parallel
|
|
23105
|
+
* model. An operator who has learned one condition editor has learned
|
|
23106
|
+
* both.
|
|
23107
|
+
* 3. Because the renderer's facts are device STATE and not a detection
|
|
23108
|
+
* record, only a SUBSET of that vocabulary can be answered here.
|
|
23109
|
+
* `setSlotBinding` REJECTS the rest at write time (see
|
|
23110
|
+
* `getConditionSupport`). It does not accept-then-fail-closed: a
|
|
23111
|
+
* condition that can never be true renders a permanently blank
|
|
23112
|
+
* overlay, and a blank overlay looks exactly like a broken camera.
|
|
23113
|
+
*/
|
|
23114
|
+
/** Where a slot's value comes from. */
|
|
23115
|
+
var OsdSourceSchema = discriminatedUnion("kind", [
|
|
23116
|
+
object({
|
|
23117
|
+
kind: literal("static"),
|
|
23118
|
+
text: string().max(64)
|
|
23119
|
+
}),
|
|
23120
|
+
object({
|
|
23121
|
+
kind: literal("clock"),
|
|
23122
|
+
/** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
|
|
23123
|
+
pattern: string().min(1).max(32).default("HH:mm"),
|
|
23124
|
+
/** IANA zone. Omitted = the server's zone. */
|
|
23125
|
+
timezone: string().min(1).max(64).optional()
|
|
23126
|
+
}),
|
|
23127
|
+
object({
|
|
23128
|
+
kind: literal("device-state"),
|
|
23129
|
+
deviceId: number().int().optional(),
|
|
23130
|
+
capName: string().min(1).max(64),
|
|
23131
|
+
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23132
|
+
valuePath: string().min(1).max(64)
|
|
23133
|
+
})
|
|
23134
|
+
]);
|
|
23135
|
+
var OsdSlotBindingSchema = object({
|
|
23136
|
+
/** Off = the manager stops driving this slot. It does NOT clear it. */
|
|
23137
|
+
enabled: boolean().default(true),
|
|
23138
|
+
source: OsdSourceSchema,
|
|
23139
|
+
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
23140
|
+
template: string().max(96).default("${value}"),
|
|
23141
|
+
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
23142
|
+
maxCharacters: number().int().min(4).max(64).optional(),
|
|
23143
|
+
/**
|
|
23144
|
+
* Decimal places for a numeric value. `0` yields an integer — the
|
|
23145
|
+
* documented workaround for firmwares that reject `.` in overlay text.
|
|
23146
|
+
*/
|
|
23147
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
23148
|
+
/** Appended via `${unit}`. The state mirror does not carry units. */
|
|
23149
|
+
unitLabel: string().max(8).optional(),
|
|
23150
|
+
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
23151
|
+
valueMap: record(string(), string()).optional(),
|
|
23152
|
+
/** Time windows in which the slot is shown. Absent = always. */
|
|
23153
|
+
schedule: NcScheduleSchema.optional(),
|
|
23154
|
+
/**
|
|
23155
|
+
* Display gate, in the notification centre's condition vocabulary.
|
|
23156
|
+
* Only the keys reported by `getConditionSupport` are accepted.
|
|
23157
|
+
*/
|
|
23158
|
+
conditions: NcConditionsSchema.optional(),
|
|
23159
|
+
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
23160
|
+
fallbackText: string().max(64).default("")
|
|
23161
|
+
});
|
|
23162
|
+
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
23163
|
+
var OsdSlotViewSchema = object({
|
|
23164
|
+
slotId: string(),
|
|
23165
|
+
kind: OsdOverlayKindEnum,
|
|
23166
|
+
/** Firmware refuses text edits (a timestamp, the channel name). */
|
|
23167
|
+
readOnly: boolean(),
|
|
23168
|
+
cameraEnabled: boolean(),
|
|
23169
|
+
cameraText: string().optional(),
|
|
23170
|
+
binding: OsdSlotBindingSchema.nullable()
|
|
23171
|
+
});
|
|
23172
|
+
/**
|
|
23173
|
+
* What happened to one slot on one render pass. `unchanged` exists so the
|
|
23174
|
+
* operator can tell "we are driving this and the value is steady" from
|
|
23175
|
+
* "we never got there" — and so the loop can prove it is not rewriting
|
|
23176
|
+
* identical text to the camera every tick.
|
|
23177
|
+
*/
|
|
23178
|
+
var OsdRenderOutcomeEnum = _enum([
|
|
23179
|
+
"written",
|
|
23180
|
+
"unchanged",
|
|
23181
|
+
"gated",
|
|
23182
|
+
"unreadable",
|
|
23183
|
+
"disabled",
|
|
23184
|
+
"unbound",
|
|
23185
|
+
"failed"
|
|
23186
|
+
]);
|
|
23187
|
+
var OsdRenderResultSchema = object({
|
|
23188
|
+
slotId: string(),
|
|
23189
|
+
outcome: OsdRenderOutcomeEnum,
|
|
23190
|
+
/** The text the slot should carry. Empty = the slot is switched off. */
|
|
23191
|
+
text: string(),
|
|
23192
|
+
/** Why, whenever the outcome is not a plain write. Never silent. */
|
|
23193
|
+
reason: string().optional()
|
|
23194
|
+
});
|
|
23195
|
+
var OsdSourceValueTypeEnum = _enum([
|
|
23196
|
+
"number",
|
|
23197
|
+
"boolean",
|
|
23198
|
+
"string",
|
|
23199
|
+
"enum"
|
|
23200
|
+
]);
|
|
23201
|
+
/**
|
|
23202
|
+
* One bindable value, derived from a cap's `runtimeState` schema — never
|
|
23203
|
+
* hand-listed. The editor renders from this, so a cap that ships a new
|
|
23204
|
+
* state field becomes bindable with no UI change.
|
|
23205
|
+
*/
|
|
23206
|
+
var OsdSourceOptionSchema = object({
|
|
23207
|
+
deviceId: number().int(),
|
|
23208
|
+
deviceName: string(),
|
|
23209
|
+
capName: string(),
|
|
23210
|
+
valuePath: string(),
|
|
23211
|
+
label: string(),
|
|
23212
|
+
valueType: OsdSourceValueTypeEnum,
|
|
23213
|
+
/** Present for `enum`; the editor offers these as `valueMap` keys. */
|
|
23214
|
+
enumValues: array(string()).readonly().optional()
|
|
23215
|
+
});
|
|
23216
|
+
method(object({ deviceId: number().int() }), object({
|
|
23217
|
+
supported: boolean(),
|
|
23218
|
+
slots: array(OsdSlotViewSchema)
|
|
23219
|
+
}), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
|
|
23220
|
+
supported: array(string()),
|
|
23221
|
+
catalog: array(NcConditionDescriptorSchema)
|
|
23222
|
+
}), { auth: "admin" }), method(object({
|
|
23223
|
+
deviceId: number().int(),
|
|
23224
|
+
slotId: string().min(1),
|
|
23225
|
+
binding: OsdSlotBindingSchema
|
|
23226
|
+
}), object({
|
|
23227
|
+
slot: OsdSlotViewSchema,
|
|
23228
|
+
render: OsdRenderResultSchema
|
|
23229
|
+
}), {
|
|
23230
|
+
kind: "mutation",
|
|
23231
|
+
auth: "admin"
|
|
23232
|
+
}), method(object({
|
|
23233
|
+
deviceId: number().int(),
|
|
23234
|
+
slotId: string().min(1)
|
|
23235
|
+
}), object({ success: literal(true) }), {
|
|
23236
|
+
kind: "mutation",
|
|
23237
|
+
auth: "admin"
|
|
23238
|
+
}), method(object({
|
|
23239
|
+
deviceId: number().int(),
|
|
23240
|
+
slotId: string().min(1),
|
|
23241
|
+
binding: OsdSlotBindingSchema.optional()
|
|
23242
|
+
}), OsdRenderResultSchema, {
|
|
23243
|
+
kind: "mutation",
|
|
23244
|
+
auth: "admin"
|
|
23245
|
+
}), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
|
|
23246
|
+
kind: "mutation",
|
|
23247
|
+
auth: "admin"
|
|
23248
|
+
});
|
|
23249
|
+
/**
|
|
22747
23250
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
22748
23251
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
22749
23252
|
* `on_batteries` (running on battery backup). `null` until first reported.
|
|
@@ -27800,6 +28303,48 @@ Object.freeze({
|
|
|
27800
28303
|
addonId: null,
|
|
27801
28304
|
access: "create"
|
|
27802
28305
|
},
|
|
28306
|
+
"osdManager.clearSlotBinding": {
|
|
28307
|
+
capName: "osd-manager",
|
|
28308
|
+
capScope: "system",
|
|
28309
|
+
addonId: null,
|
|
28310
|
+
access: "delete"
|
|
28311
|
+
},
|
|
28312
|
+
"osdManager.getConditionSupport": {
|
|
28313
|
+
capName: "osd-manager",
|
|
28314
|
+
capScope: "system",
|
|
28315
|
+
addonId: null,
|
|
28316
|
+
access: "view"
|
|
28317
|
+
},
|
|
28318
|
+
"osdManager.getDeviceOsd": {
|
|
28319
|
+
capName: "osd-manager",
|
|
28320
|
+
capScope: "system",
|
|
28321
|
+
addonId: null,
|
|
28322
|
+
access: "view"
|
|
28323
|
+
},
|
|
28324
|
+
"osdManager.getSourceCatalog": {
|
|
28325
|
+
capName: "osd-manager",
|
|
28326
|
+
capScope: "system",
|
|
28327
|
+
addonId: null,
|
|
28328
|
+
access: "view"
|
|
28329
|
+
},
|
|
28330
|
+
"osdManager.previewSlot": {
|
|
28331
|
+
capName: "osd-manager",
|
|
28332
|
+
capScope: "system",
|
|
28333
|
+
addonId: null,
|
|
28334
|
+
access: "create"
|
|
28335
|
+
},
|
|
28336
|
+
"osdManager.renderDevice": {
|
|
28337
|
+
capName: "osd-manager",
|
|
28338
|
+
capScope: "system",
|
|
28339
|
+
addonId: null,
|
|
28340
|
+
access: "create"
|
|
28341
|
+
},
|
|
28342
|
+
"osdManager.setSlotBinding": {
|
|
28343
|
+
capName: "osd-manager",
|
|
28344
|
+
capScope: "system",
|
|
28345
|
+
addonId: null,
|
|
28346
|
+
access: "create"
|
|
28347
|
+
},
|
|
27803
28348
|
"petFeeder.callPet": {
|
|
27804
28349
|
capName: "pet-feeder",
|
|
27805
28350
|
capScope: "device",
|
|
@@ -27962,6 +28507,18 @@ Object.freeze({
|
|
|
27962
28507
|
addonId: null,
|
|
27963
28508
|
access: "view"
|
|
27964
28509
|
},
|
|
28510
|
+
"pipelineAnalytics.getTrainingExportSummary": {
|
|
28511
|
+
capName: "pipeline-analytics",
|
|
28512
|
+
capScope: "device",
|
|
28513
|
+
addonId: null,
|
|
28514
|
+
access: "view"
|
|
28515
|
+
},
|
|
28516
|
+
"pipelineAnalytics.getTrainingExportUrl": {
|
|
28517
|
+
capName: "pipeline-analytics",
|
|
28518
|
+
capScope: "device",
|
|
28519
|
+
addonId: null,
|
|
28520
|
+
access: "view"
|
|
28521
|
+
},
|
|
27965
28522
|
"pipelineAnalytics.listEventKinds": {
|
|
27966
28523
|
capName: "pipeline-analytics",
|
|
27967
28524
|
capScope: "device",
|
|
@@ -29438,6 +29995,12 @@ Object.freeze({
|
|
|
29438
29995
|
addonId: null,
|
|
29439
29996
|
access: "view"
|
|
29440
29997
|
},
|
|
29998
|
+
"streamBroker.getDeviceAudioMute": {
|
|
29999
|
+
capName: "stream-broker",
|
|
30000
|
+
capScope: "system",
|
|
30001
|
+
addonId: null,
|
|
30002
|
+
access: "view"
|
|
30003
|
+
},
|
|
29441
30004
|
"streamBroker.getPreBufferInfo": {
|
|
29442
30005
|
capName: "stream-broker",
|
|
29443
30006
|
capScope: "system",
|
|
@@ -29558,6 +30121,12 @@ Object.freeze({
|
|
|
29558
30121
|
addonId: null,
|
|
29559
30122
|
access: "create"
|
|
29560
30123
|
},
|
|
30124
|
+
"streamBroker.setDeviceAudioMute": {
|
|
30125
|
+
capName: "stream-broker",
|
|
30126
|
+
capScope: "system",
|
|
30127
|
+
addonId: null,
|
|
30128
|
+
access: "create"
|
|
30129
|
+
},
|
|
29561
30130
|
"streamBroker.setPreBufferDuration": {
|
|
29562
30131
|
capName: "stream-broker",
|
|
29563
30132
|
capScope: "system",
|
|
@@ -31016,4 +31585,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
31016
31585
|
return out;
|
|
31017
31586
|
}
|
|
31018
31587
|
//#endregion
|
|
31019
|
-
export {
|
|
31588
|
+
export { errMsg as $, deriveDetailCropRect as A, EventCategory as At, motionDetectionCapability as B, audioAnalysisCapability as C, lazy as Ct, customAction as D, record as Dt, createHwAccelCache as E, object as Et, enumerateInferenceDevices as F, recordingCapability as G, pickNativeLeaseOverride as H, evaluateZoneRules as I, runtimeDevices as J, recordingExportCapability as K, hfModelUrl as L, detectionPipelineCapability as M, egressTranscodeSharingKey as N, defaultDeviceFor as O, string as Ot, egressTransportFromRequest as P, webrtcSessionCapability as Q, mapAudioLabelToMacro as R, addonWidgetsSourceCapability as S, discriminatedUnion as St, cameraStreamsCapability as T, number as Tt, pipelineExecutorCapability as U, pickDetailCropConvention as V, pipelineRunnerCapability as W, streamBrokerCapability as X, storageEvictableCapability as Y, supportedRuntimes as Z, RATE_CONTROL_RELAXED as _, selectAssignedProfileSlots as _t, COCO_TO_MACRO as a, BaseAddon as at, RingBuffer as b, array as bt, DEFAULT_NATIVE_LEASE_SETTINGS as c, DeviceType as ct, EncodeProfileSchema as d, isEvent as dt, AUDIO_PRESETS as et, ExportRecordSchema as f, makeProfileBrokerId as ft, OpsLogEntrySchema as g, parseProfileBrokerId as gt, NativeLeaseSettingsSchema as h, parseJsonUnknown as ht, COCO_80_LABELS as i, isSoftwareDecode as it, deriveRecordingMode as j, defineCustomActions as k, union as kt, DEVICE_BACKEND_TO_FORMAT as l, createEvent as lt, NativeLeaseAdmissionSchema as m, nodePin as mt, AUDIO_BACKEND_CHOICES as n, buildFfmpegArgs as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, CAM_PROFILE_ORDER as ot, HF_BASE_URL as p, makeSourceBrokerId as pt, resolveEgressDecodeHwAccel as q, AUDIO_MACRO_LABELS as r, invocationFromEncodeProfile as rt, DEFAULT_DETAIL_CROP_CONVENTION as s, DeviceFeature as st, APPLE_SA_TO_MACRO as t, Fmp4BoxSplitter as tt, EVENT_PAD_MS as u, hydrateSchema as ut, RATE_CONTROL_TIGHT as v, sleep as vt, audioAnalyzerCapability as w, literal as wt, YAMNET_TO_MACRO as x, boolean as xt, RecordingConfigSchema as y, _enum as yt, maskUrlCredentials as z };
|