@camstack/addon-pipeline 1.2.43 → 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 +158 -81
- package/dist/detection-pipeline/index.mjs +158 -81
- package/dist/{dist-B-VVBzrL.mjs → dist-BDkEhx4Z.mjs} +1315 -127
- package/dist/{dist-UHbKZiSa.js → dist-QyZ7QSRP.js} +1386 -126
- package/dist/{event-loop-stall-monitor-C7_L6vHO.js → event-loop-stall-monitor-COEKVBQK.js} +53 -8
- package/dist/{event-loop-stall-monitor-DfKdC7G4.mjs → event-loop-stall-monitor-IAuNEdmG.mjs} +53 -8
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +342 -69
- package/dist/pipeline-runner/index.mjs +342 -69
- package/dist/recorder/index.js +189 -6
- package/dist/recorder/index.mjs +189 -6
- package/dist/session-decode/decode-worker-child.js +197 -24
- package/dist/session-decode/decode-worker-child.mjs +197 -24
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D7iQbuKY.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-54IklEep.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-CKYS9yZO.mjs → hostInit-BMCrULpc.mjs} +2 -2
- package/dist/stream-broker/index.js +4602 -1442
- package/dist/stream-broker/index.mjs +4601 -1442
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-DFH_z2-q.mjs +244 -0
- package/dist/worker-protocol-le27Mw_8.js +279 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DxZ75vB8.js → MaskShapeCanvas-DI4BY7W2-C7bsz4cP.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BBY2Ztz5.js → MotionZonesSettings-NcxxQN8r-BmhLinUd.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BC-3QmHa.js → PrivacyMaskSettings-APgPLF7p-BJjyGWFy.js} +1 -1
- package/embed-dist/assets/index-DC63og2c.css +2 -0
- package/embed-dist/assets/{index-on1wyOnd.js → index-a4CMt-Uc.js} +12 -12
- package/embed-dist/index.html +2 -2
- 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-CCC9fqHH.mjs +0 -26
- package/dist/worker-protocol-D7RzZIla.mjs +0 -77
- package/dist/worker-protocol-DextwlTX.js +0 -94
- package/embed-dist/assets/index-CGdwTcwE.css +0 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
let node_crypto = require("node:crypto");
|
|
1
2
|
//#region ../types/dist/event-category-41fKf-q9.mjs
|
|
2
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
3
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -6540,7 +6541,20 @@ var BrokerStatsSchema = object({
|
|
|
6540
6541
|
sampleRate: number(),
|
|
6541
6542
|
channels: number(),
|
|
6542
6543
|
supported: boolean()
|
|
6543
|
-
}).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()
|
|
6544
6558
|
});
|
|
6545
6559
|
/**
|
|
6546
6560
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -6589,9 +6603,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
|
|
|
6589
6603
|
/**
|
|
6590
6604
|
* Build the tRPC request options that pin a single capability call to `nodeId`.
|
|
6591
6605
|
* Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
|
|
6606
|
+
*
|
|
6607
|
+
* ## The id is normalised here, and it has to be
|
|
6608
|
+
*
|
|
6609
|
+
* A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
|
|
6610
|
+
* worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
|
|
6611
|
+
* compares a pin against real node ids, so such a pin matches nothing and the
|
|
6612
|
+
* call fails with `no provider registered for cap "…"`. The local-first
|
|
6613
|
+
* resolver already guarded against this (`localNodeId.split('/')[0]`), which
|
|
6614
|
+
* made the hazard invisible: unpinned calls worked, and only an explicit pin —
|
|
6615
|
+
* the thing you reach for when you specifically need THIS node — silently
|
|
6616
|
+
* addressed a node that does not exist.
|
|
6617
|
+
*
|
|
6618
|
+
* Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
|
|
6619
|
+
* own node to read the host's hardware-decode backend. It never once answered,
|
|
6620
|
+
* so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
|
|
6621
|
+
* while D67's whole premise was that the decoder addon is the authority on
|
|
6622
|
+
* hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
|
|
6623
|
+
* hardware", which was false.
|
|
6624
|
+
*
|
|
6625
|
+
* Normalising in the ONE constructor fixes every caller at once, which is why
|
|
6626
|
+
* it is here and not at the call sites.
|
|
6592
6627
|
*/
|
|
6593
6628
|
function nodePin(nodeId) {
|
|
6594
|
-
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
|
|
6629
|
+
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
|
|
6630
|
+
}
|
|
6631
|
+
/**
|
|
6632
|
+
* A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
|
|
6633
|
+
* is idempotent, so passing an already-clean id costs nothing.
|
|
6634
|
+
*/
|
|
6635
|
+
function toNodeId(idOrRunnerId) {
|
|
6636
|
+
const head = idOrRunnerId.split("/")[0];
|
|
6637
|
+
return head === void 0 || head.length === 0 ? idOrRunnerId : head;
|
|
6595
6638
|
}
|
|
6596
6639
|
/**
|
|
6597
6640
|
* Output schema shared by the contribution + live methods.
|
|
@@ -7090,83 +7133,7 @@ function sleep(ms) {
|
|
|
7090
7133
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7091
7134
|
}
|
|
7092
7135
|
//#endregion
|
|
7093
|
-
//#region ../types/dist/
|
|
7094
|
-
/**
|
|
7095
|
-
import { errMsg } from '@camstack/types'
|
|
7096
|
-
* Extract a human-readable message from an unknown error value.
|
|
7097
|
-
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
7098
|
-
*/
|
|
7099
|
-
function errMsg(err) {
|
|
7100
|
-
if (err instanceof Error) return err.message;
|
|
7101
|
-
if (typeof err === "string") return err;
|
|
7102
|
-
return String(err);
|
|
7103
|
-
}
|
|
7104
|
-
var EncodeProfileSchema = object({
|
|
7105
|
-
video: object({
|
|
7106
|
-
codec: _enum([
|
|
7107
|
-
"h264",
|
|
7108
|
-
"h265",
|
|
7109
|
-
"copy"
|
|
7110
|
-
]),
|
|
7111
|
-
profile: _enum([
|
|
7112
|
-
"baseline",
|
|
7113
|
-
"main",
|
|
7114
|
-
"high"
|
|
7115
|
-
]).optional(),
|
|
7116
|
-
/**
|
|
7117
|
-
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
7118
|
-
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
7119
|
-
* it, or it ships a stream that does not match its own advertisement — the
|
|
7120
|
-
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
7121
|
-
* silently. Optional because a browser negotiates the level itself.
|
|
7122
|
-
*/
|
|
7123
|
-
level: string().optional(),
|
|
7124
|
-
width: number().int().positive().optional(),
|
|
7125
|
-
height: number().int().positive().optional(),
|
|
7126
|
-
fps: number().positive().optional(),
|
|
7127
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7128
|
-
gopFrames: number().int().positive().optional(),
|
|
7129
|
-
bf: number().int().min(0).optional(),
|
|
7130
|
-
preset: _enum([
|
|
7131
|
-
"ultrafast",
|
|
7132
|
-
"superfast",
|
|
7133
|
-
"veryfast",
|
|
7134
|
-
"faster",
|
|
7135
|
-
"fast",
|
|
7136
|
-
"medium"
|
|
7137
|
-
]).optional(),
|
|
7138
|
-
tune: _enum([
|
|
7139
|
-
"zerolatency",
|
|
7140
|
-
"film",
|
|
7141
|
-
"animation"
|
|
7142
|
-
]).optional()
|
|
7143
|
-
}),
|
|
7144
|
-
audio: union([literal("passthrough"), object({
|
|
7145
|
-
codec: _enum([
|
|
7146
|
-
"opus",
|
|
7147
|
-
"aac",
|
|
7148
|
-
"pcmu",
|
|
7149
|
-
"pcma",
|
|
7150
|
-
"copy"
|
|
7151
|
-
]),
|
|
7152
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7153
|
-
sampleRateHz: number().int().positive().optional(),
|
|
7154
|
-
channels: union([literal(1), literal(2)]).optional()
|
|
7155
|
-
})]),
|
|
7156
|
-
/**
|
|
7157
|
-
* ffmpeg input-side args, inserted between the fixed global flags
|
|
7158
|
-
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
7159
|
-
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
7160
|
-
* used demuxer/format options.
|
|
7161
|
-
*/
|
|
7162
|
-
inputArgs: array(string()).optional(),
|
|
7163
|
-
/**
|
|
7164
|
-
* ffmpeg output-side args, inserted between the encode block and
|
|
7165
|
-
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
7166
|
-
* filters, codec-specific overrides. Free-text array.
|
|
7167
|
-
*/
|
|
7168
|
-
outputArgs: array(string()).optional()
|
|
7169
|
-
});
|
|
7136
|
+
//#region ../types/dist/fmp4-box-splitter-B53u9-Nu.mjs
|
|
7170
7137
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7171
7138
|
opus: "libopus",
|
|
7172
7139
|
aac: "aac",
|
|
@@ -7224,6 +7191,8 @@ function buildInputArgs(input, decodeHwAccel) {
|
|
|
7224
7191
|
const args = [];
|
|
7225
7192
|
if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
|
|
7226
7193
|
if (input.extraArgs?.length) args.push(...input.extraArgs);
|
|
7194
|
+
if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
|
|
7195
|
+
if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
|
|
7227
7196
|
if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
|
|
7228
7197
|
if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
|
|
7229
7198
|
args.push("-i", input.url);
|
|
@@ -7272,6 +7241,7 @@ function buildVideoArgs(video, outputArgs) {
|
|
|
7272
7241
|
if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
|
|
7273
7242
|
if (video.fps !== void 0) args.push("-r", String(video.fps));
|
|
7274
7243
|
if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
|
|
7244
|
+
if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
|
|
7275
7245
|
if (video.bf !== void 0) args.push("-bf", String(video.bf));
|
|
7276
7246
|
args.push(...buildRateControlArgs(video));
|
|
7277
7247
|
if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
|
|
@@ -7307,6 +7277,85 @@ function isElementaryVideoSink(sink) {
|
|
|
7307
7277
|
return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
|
|
7308
7278
|
}
|
|
7309
7279
|
/**
|
|
7280
|
+
* The fragmented-MP4 muxer flags, in the order the recorder has proven them
|
|
7281
|
+
* (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
|
|
7282
|
+
* `-segment_format_options`, across every vendor in the fleet):
|
|
7283
|
+
*
|
|
7284
|
+
* - `frag_keyframe` — cut a fragment at each key frame, so every fragment
|
|
7285
|
+
* opens on a sync sample. HKSV's whole requirement.
|
|
7286
|
+
* - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
|
|
7287
|
+
* is what makes the head a standalone INITIALISATION segment.
|
|
7288
|
+
* - `default_base_moof` — fragment offsets are self-relative, so a fragment is
|
|
7289
|
+
* demuxable without the bytes that preceded it. D31's byte-range read path
|
|
7290
|
+
* depends on exactly this property of the recorder's segments.
|
|
7291
|
+
*/
|
|
7292
|
+
var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
|
|
7293
|
+
/**
|
|
7294
|
+
* The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
|
|
7295
|
+
* union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
|
|
7296
|
+
* which is what a plain `container` read would have done for `mp4` — a valid
|
|
7297
|
+
* argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
|
|
7298
|
+
* unusable byte stream.
|
|
7299
|
+
*/
|
|
7300
|
+
function buildStdoutOrRtspSinkArgs(sink) {
|
|
7301
|
+
if (sink.kind === "rtsp-listen") return [
|
|
7302
|
+
"-f",
|
|
7303
|
+
"rtsp",
|
|
7304
|
+
"-rtsp_transport",
|
|
7305
|
+
"tcp",
|
|
7306
|
+
"-rtsp_flags",
|
|
7307
|
+
"listen",
|
|
7308
|
+
sink.url
|
|
7309
|
+
];
|
|
7310
|
+
if (sink.kind === "rtp-outputs") return [];
|
|
7311
|
+
return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
|
|
7312
|
+
"-f",
|
|
7313
|
+
sink.container,
|
|
7314
|
+
"pipe:1"
|
|
7315
|
+
];
|
|
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;
|
|
7346
|
+
/** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
|
|
7347
|
+
function buildFmp4SinkArgs(sink) {
|
|
7348
|
+
return [
|
|
7349
|
+
"-movflags",
|
|
7350
|
+
FMP4_MOVFLAGS,
|
|
7351
|
+
"-min_frag_duration",
|
|
7352
|
+
String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
|
|
7353
|
+
"-f",
|
|
7354
|
+
"mp4",
|
|
7355
|
+
"pipe:1"
|
|
7356
|
+
];
|
|
7357
|
+
}
|
|
7358
|
+
/**
|
|
7310
7359
|
* A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
|
|
7311
7360
|
* audio optional so a source with no audio skips it instead of failing the
|
|
7312
7361
|
* whole invocation.
|
|
@@ -7367,19 +7416,7 @@ function buildFfmpegArgs(inv) {
|
|
|
7367
7416
|
];
|
|
7368
7417
|
}
|
|
7369
7418
|
const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
|
|
7370
|
-
const sinkArgs = inv.sink
|
|
7371
|
-
"-f",
|
|
7372
|
-
inv.sink.container,
|
|
7373
|
-
"pipe:1"
|
|
7374
|
-
] : [
|
|
7375
|
-
"-f",
|
|
7376
|
-
"rtsp",
|
|
7377
|
-
"-rtsp_transport",
|
|
7378
|
-
"tcp",
|
|
7379
|
-
"-rtsp_flags",
|
|
7380
|
-
"listen",
|
|
7381
|
-
inv.sink.url
|
|
7382
|
-
];
|
|
7419
|
+
const sinkArgs = buildStdoutOrRtspSinkArgs(inv.sink);
|
|
7383
7420
|
return [
|
|
7384
7421
|
...head,
|
|
7385
7422
|
...buildVideoArgs(inv.video, inv.outputArgs),
|
|
@@ -7464,7 +7501,10 @@ function invocationFromEncodeProfile(input) {
|
|
|
7464
7501
|
height: v.height
|
|
7465
7502
|
} : null;
|
|
7466
7503
|
const target = v.codec === "h265" ? "h265" : "h264";
|
|
7467
|
-
const video = shouldCopy ? {
|
|
7504
|
+
const video = shouldCopy ? {
|
|
7505
|
+
kind: "copy",
|
|
7506
|
+
...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
|
|
7507
|
+
} : {
|
|
7468
7508
|
kind: "encode",
|
|
7469
7509
|
encoder: pickVideoEncoder(target, input.decodeHwAccel, input.hardwareEncoders === true),
|
|
7470
7510
|
scale,
|
|
@@ -7472,10 +7512,14 @@ function invocationFromEncodeProfile(input) {
|
|
|
7472
7512
|
...v.tune !== void 0 ? { tune: v.tune } : {},
|
|
7473
7513
|
...v.profile !== void 0 ? { profile: v.profile } : {},
|
|
7474
7514
|
...v.level !== void 0 ? { level: v.level } : {},
|
|
7515
|
+
...input.pixelFormat !== void 0 ? { pixelFormat: input.pixelFormat } : {},
|
|
7475
7516
|
...v.fps !== void 0 ? { fps: v.fps } : {},
|
|
7476
7517
|
...v.gopFrames !== void 0 ? { gopFrames: v.gopFrames } : {},
|
|
7518
|
+
...input.forceKeyFramesSeconds !== void 0 ? { forceKeyFramesSeconds: input.forceKeyFramesSeconds } : {},
|
|
7477
7519
|
...v.bf !== void 0 ? { bf: v.bf } : {},
|
|
7478
|
-
...v.bitrateKbps !== void 0 ? { bitrateKbps: v.bitrateKbps } : {}
|
|
7520
|
+
...v.bitrateKbps !== void 0 ? { bitrateKbps: v.bitrateKbps } : {},
|
|
7521
|
+
...input.rateControl !== void 0 ? { rateControl: input.rateControl } : {},
|
|
7522
|
+
...input.bitstreamFilter !== void 0 ? { bitstreamFilter: input.bitstreamFilter } : {}
|
|
7479
7523
|
};
|
|
7480
7524
|
return {
|
|
7481
7525
|
logLevel: input.logLevel ?? "error",
|
|
@@ -7495,6 +7539,287 @@ function invocationFromEncodeProfile(input) {
|
|
|
7495
7539
|
};
|
|
7496
7540
|
}
|
|
7497
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 (0, node_crypto.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
|
+
/**
|
|
7498
7823
|
* The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
|
|
7499
7824
|
* Baseline because it is the one profile every consumer in this repo decodes
|
|
7500
7825
|
* (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
|
|
@@ -7517,6 +7842,159 @@ var BASE_LIVE_EGRESS_PROFILE = {
|
|
|
7517
7842
|
};
|
|
7518
7843
|
({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
|
|
7519
7844
|
({ ...BASE_LIVE_EGRESS_PROFILE });
|
|
7845
|
+
/** VBV window for a consumer whose budget is enforced per second (HomeKit). */
|
|
7846
|
+
var RATE_CONTROL_TIGHT = {
|
|
7847
|
+
kind: "cbr",
|
|
7848
|
+
vbvSeconds: 1
|
|
7849
|
+
};
|
|
7850
|
+
/** VBV window for a consumer that tolerates a keyframe spike (browser, Echo). */
|
|
7851
|
+
var RATE_CONTROL_RELAXED = {
|
|
7852
|
+
kind: "cap",
|
|
7853
|
+
vbvSeconds: 2
|
|
7854
|
+
};
|
|
7855
|
+
function createHwAccelCache(options) {
|
|
7856
|
+
const now = options.now ?? (() => Date.now());
|
|
7857
|
+
let value = null;
|
|
7858
|
+
let writtenAt = Number.NEGATIVE_INFINITY;
|
|
7859
|
+
return {
|
|
7860
|
+
read() {
|
|
7861
|
+
return now() - writtenAt < options.ttlMs ? value : void 0;
|
|
7862
|
+
},
|
|
7863
|
+
write(next) {
|
|
7864
|
+
value = next;
|
|
7865
|
+
writtenAt = now();
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7868
|
+
}
|
|
7869
|
+
/** `true` when a value means "decode in software". */
|
|
7870
|
+
function meansSoftware(value) {
|
|
7871
|
+
return !value || value === "none" || value === "copy";
|
|
7872
|
+
}
|
|
7873
|
+
/**
|
|
7874
|
+
* Resolve the `-hwaccel` value for an egress transcode. Hardware is the
|
|
7875
|
+
* DEFAULT — an egress that decodes in software on a hub with working vaapi is
|
|
7876
|
+
* paying for nothing — and every software outcome is announced.
|
|
7877
|
+
*
|
|
7878
|
+
* Returns a concrete backend name, the literal `'auto'`, or `null` for
|
|
7879
|
+
* software decode (⇒ {@link buildFfmpegArgs} emits no `-hwaccel` at all).
|
|
7880
|
+
*/
|
|
7881
|
+
async function resolveEgressDecodeHwAccel(deps) {
|
|
7882
|
+
const override = deps.override;
|
|
7883
|
+
if (override !== void 0 && override !== null && override !== "") return meansSoftware(override) ? null : override;
|
|
7884
|
+
const cached = deps.cache?.read();
|
|
7885
|
+
if (cached !== void 0) return cached;
|
|
7886
|
+
let backend;
|
|
7887
|
+
try {
|
|
7888
|
+
backend = await deps.readDecoderBackend();
|
|
7889
|
+
} catch (err) {
|
|
7890
|
+
const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
|
|
7891
|
+
deps.onFallback({
|
|
7892
|
+
reason: "decoder-unreadable",
|
|
7893
|
+
kernelPreferred,
|
|
7894
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7895
|
+
});
|
|
7896
|
+
deps.cache?.write(null);
|
|
7897
|
+
return null;
|
|
7898
|
+
}
|
|
7899
|
+
if (backend === "") {
|
|
7900
|
+
const kernelPreferred = await deps.readKernelPreferred().catch(() => []);
|
|
7901
|
+
deps.onFallback({
|
|
7902
|
+
reason: "decoder-unprobed",
|
|
7903
|
+
kernelPreferred
|
|
7904
|
+
});
|
|
7905
|
+
deps.cache?.write(null);
|
|
7906
|
+
return null;
|
|
7907
|
+
}
|
|
7908
|
+
const resolved = meansSoftware(backend) ? null : backend;
|
|
7909
|
+
deps.cache?.write(resolved);
|
|
7910
|
+
return resolved;
|
|
7911
|
+
}
|
|
7912
|
+
/**
|
|
7913
|
+
* The sharing key for `streamBroker.acquireEgressTranscode`.
|
|
7914
|
+
*
|
|
7915
|
+
* **Two requesters asking for exactly the same argument set reach the same
|
|
7916
|
+
* process.** Exact match, never fuzzy: a quantised ladder that merged
|
|
7917
|
+
* NEARLY-identical requests would make the stream a consumer receives depend
|
|
7918
|
+
* on who else is watching and in what order they arrived — unpredictable in
|
|
7919
|
+
* precisely the way a debugging session cannot tolerate. Same arguments ⇒ same
|
|
7920
|
+
* process. Different arguments ⇒ different process, and that is fine; the
|
|
7921
|
+
* operator accepted the cost ("it's fine to have several processes").
|
|
7922
|
+
*
|
|
7923
|
+
* Derived from the STRUCTURED plan, not from a flag array. `pipelineKeyFor`
|
|
7924
|
+
* (`transcode-pipeline.ts`) folds `getStreamWithCodec`'s raw `outputArgs` into
|
|
7925
|
+
* its key, so that method's extensibility hatch and its sharing key are the
|
|
7926
|
+
* same field — a consumer needing one extra flag silently forks the child, and
|
|
7927
|
+
* two consumers that want the same thing but spell it differently never share.
|
|
7928
|
+
* Here every knob is a named field, and defaults are APPLIED before hashing so
|
|
7929
|
+
* an omitted field and its explicit default land on the same key.
|
|
7930
|
+
*/
|
|
7931
|
+
/**
|
|
7932
|
+
* The transport a CAP request describes. `fragments` is deliberately
|
|
7933
|
+
* unreachable from here — the request schema has no way to ask for it, so the
|
|
7934
|
+
* cap path can never be handed a fragment child by accident.
|
|
7935
|
+
*/
|
|
7936
|
+
function egressTransportFromRequest(request) {
|
|
7937
|
+
return { transport: request.publishLocally === true ? "push" : "dial" };
|
|
7938
|
+
}
|
|
7939
|
+
/** Absent optional ⇒ this sentinel, so `undefined` and "not set" agree. */
|
|
7940
|
+
var UNSET = "\0unset";
|
|
7941
|
+
function canonicalVideo(video) {
|
|
7942
|
+
return {
|
|
7943
|
+
codec: video.codec,
|
|
7944
|
+
profile: video.profile ?? UNSET,
|
|
7945
|
+
level: video.level ?? UNSET,
|
|
7946
|
+
width: video.width ?? -1,
|
|
7947
|
+
height: video.height ?? -1,
|
|
7948
|
+
fps: video.fps ?? -1,
|
|
7949
|
+
bitrateKbps: video.bitrateKbps ?? -1,
|
|
7950
|
+
gopFrames: video.gopFrames ?? -1,
|
|
7951
|
+
bf: video.bf ?? -1,
|
|
7952
|
+
preset: video.preset ?? UNSET,
|
|
7953
|
+
tune: video.tune ?? UNSET
|
|
7954
|
+
};
|
|
7955
|
+
}
|
|
7956
|
+
function canonicalAudio(audio) {
|
|
7957
|
+
if (audio === "passthrough") return { codec: "passthrough" };
|
|
7958
|
+
return {
|
|
7959
|
+
codec: audio.codec,
|
|
7960
|
+
bitrateKbps: audio.bitrateKbps ?? -1,
|
|
7961
|
+
sampleRateHz: audio.sampleRateHz ?? -1,
|
|
7962
|
+
channels: audio.channels ?? -1
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* The normalised plan a key is computed from. Exported so a test — and a
|
|
7967
|
+
* future operator-facing "why are these two not sharing?" surface — can diff
|
|
7968
|
+
* two requests without reversing a hash.
|
|
7969
|
+
*/
|
|
7970
|
+
function canonicalEgressPlan(request, delivery = egressTransportFromRequest(request)) {
|
|
7971
|
+
return {
|
|
7972
|
+
deviceId: request.deviceId,
|
|
7973
|
+
source: request.source.kind === "profile" ? `profile:${request.source.profile}` : `cam-stream:${request.source.camStreamId}`,
|
|
7974
|
+
transport: delivery.transport,
|
|
7975
|
+
fragmentMs: delivery.fragmentMs ?? -1,
|
|
7976
|
+
video: canonicalVideo(request.encode.video),
|
|
7977
|
+
audio: canonicalAudio(request.encode.audio),
|
|
7978
|
+
rateControl: request.rateControl ?? "relaxed",
|
|
7979
|
+
bitstreamFilter: request.bitstreamFilter ?? UNSET,
|
|
7980
|
+
pixelFormat: request.pixelFormat ?? UNSET,
|
|
7981
|
+
decodeHwAccel: request.decodeHwAccel ?? UNSET
|
|
7982
|
+
};
|
|
7983
|
+
}
|
|
7984
|
+
/**
|
|
7985
|
+
* The refcount / dedup key. `canonicalHash` sorts object keys at every depth,
|
|
7986
|
+
* so a request built with a different field order produces the same digest.
|
|
7987
|
+
*
|
|
7988
|
+
* The handle this keys is IMMUTABLE: there is no `reconfigure`. A consumer
|
|
7989
|
+
* whose requirements change RELEASES and re-acquires; the refcount does the
|
|
7990
|
+
* rest. That is what stops co-tenants disturbing each other — the co-tenant
|
|
7991
|
+
* hazard that made Alexa's shared `derived:alexa-<id>` stream a hazard was
|
|
7992
|
+
* exactly a mutable shared object, where one consumer's downgrade dragged
|
|
7993
|
+
* every other consumer to 360p.
|
|
7994
|
+
*/
|
|
7995
|
+
function egressTranscodeSharingKey(request, delivery = egressTransportFromRequest(request)) {
|
|
7996
|
+
return `egress:${canonicalHash(canonicalEgressPlan(request, delivery))}`;
|
|
7997
|
+
}
|
|
7520
7998
|
/**
|
|
7521
7999
|
* Deep wiring healthcheck — snapshot of active reachability probes across
|
|
7522
8000
|
* every declared capability + widget of every installed plugin, on every
|
|
@@ -7573,7 +8051,7 @@ object({
|
|
|
7573
8051
|
* ## This file adds no state
|
|
7574
8052
|
*
|
|
7575
8053
|
* Every switch here is a VIEW onto an authority that already existed
|
|
7576
|
-
* ([
|
|
8054
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
|
|
7577
8055
|
* group is that there is exactly one place each function is turned off, and
|
|
7578
8056
|
* the group routes to it:
|
|
7579
8057
|
*
|
|
@@ -7584,6 +8062,53 @@ object({
|
|
|
7584
8062
|
* | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
|
|
7585
8063
|
* | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
|
|
7586
8064
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
8065
|
+
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
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.
|
|
8080
|
+
*
|
|
8081
|
+
* ## The two switches whose authority is not on this server
|
|
8082
|
+
*
|
|
8083
|
+
* `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
|
|
8084
|
+
* in "the group stores nothing" — it is the purest form of it: the camera
|
|
8085
|
+
* holds the fact, every read is a read-through, and there is no server-side
|
|
8086
|
+
* copy that could drift. Their availability therefore cannot come from
|
|
8087
|
+
* `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
|
|
8088
|
+
* is filtered out there); it comes from the cap's own camera-probed
|
|
8089
|
+
* `privacyMask.getOptions()`, which is strictly more honest — it answers for
|
|
8090
|
+
* THIS camera rather than for the device type
|
|
8091
|
+
* ([D74](../../../../docs/decisions/adr-0074.md)).
|
|
8092
|
+
*
|
|
8093
|
+
* ## `privacy-mask` is the one row whose ON is not "the function is working"
|
|
8094
|
+
*
|
|
8095
|
+
* Every other switch means *this camera's function is doing its job*, so
|
|
8096
|
+
* `enabled: false` is a thing an operator took away. `privacy-mask` means **the
|
|
8097
|
+
* MASK is active** — `enabled: true` is video deliberately obscured. The
|
|
8098
|
+
* polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
|
|
8099
|
+
* (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
|
|
8100
|
+
* a HomeKit toggle that disagreed with the app's toggle for the same camera is
|
|
8101
|
+
* worse than either surface not having one.
|
|
8102
|
+
*
|
|
8103
|
+
* Two consequences follow and both are load-bearing:
|
|
8104
|
+
*
|
|
8105
|
+
* - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
|
|
8106
|
+
* exactly this row. With the polarity above, every camera that has NOT drawn
|
|
8107
|
+
* a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
|
|
8108
|
+
* normal, healthy state of most cameras rendered as an operator disablement.
|
|
8109
|
+
* - **Its cost line names BOTH directions.** `costWhenOff` is rendered
|
|
8110
|
+
* unconditionally by both clients, so for this row it has to read correctly
|
|
8111
|
+
* whichever way the switch is sitting.
|
|
7587
8112
|
*
|
|
7588
8113
|
* The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
|
|
7589
8114
|
* migrated the legacy `audioEnabled` / `pipelineEnabled` /
|
|
@@ -7602,14 +8127,18 @@ object({
|
|
|
7602
8127
|
* `CameraStatus.switchedOff`.
|
|
7603
8128
|
*/
|
|
7604
8129
|
/**
|
|
7605
|
-
* The
|
|
7606
|
-
*
|
|
8130
|
+
* The functions the operator named — five on 2026-08-05, plus the camera's own
|
|
8131
|
+
* microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
|
|
8132
|
+
* recognition and plate/LPR are per-step toggles on
|
|
7607
8133
|
* `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
|
|
7608
|
-
* editor, not in a
|
|
8134
|
+
* editor, not in a safety group.
|
|
7609
8135
|
*/
|
|
7610
8136
|
var CameraSwitchIdSchema = _enum([
|
|
7611
8137
|
"stream-broker",
|
|
7612
8138
|
"object-detection",
|
|
8139
|
+
"privacy-mask",
|
|
8140
|
+
"device-audio",
|
|
8141
|
+
"broker-audio",
|
|
7613
8142
|
"audio-analysis",
|
|
7614
8143
|
"recording",
|
|
7615
8144
|
"notifications"
|
|
@@ -7627,14 +8156,27 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7627
8156
|
capName: string()
|
|
7628
8157
|
}),
|
|
7629
8158
|
object({ kind: literal("recording-config") }),
|
|
7630
|
-
object({ kind: literal("notification-mute") })
|
|
8159
|
+
object({ kind: literal("notification-mute") }),
|
|
8160
|
+
object({
|
|
8161
|
+
kind: literal("camera-audio"),
|
|
8162
|
+
capName: string()
|
|
8163
|
+
}),
|
|
8164
|
+
object({
|
|
8165
|
+
kind: literal("camera-mask"),
|
|
8166
|
+
capName: string()
|
|
8167
|
+
}),
|
|
8168
|
+
object({ kind: literal("broker-audio-mute") })
|
|
7631
8169
|
]);
|
|
7632
8170
|
/**
|
|
7633
8171
|
* Why a switch is not offered for this camera. Rendered instead of the
|
|
7634
8172
|
* control, never as a dead control — an absent function and a broken one must
|
|
7635
8173
|
* not look the same.
|
|
7636
8174
|
*/
|
|
7637
|
-
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
8175
|
+
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
8176
|
+
"no-provider",
|
|
8177
|
+
"source-unreachable",
|
|
8178
|
+
"not-configured"
|
|
8179
|
+
]);
|
|
7638
8180
|
/**
|
|
7639
8181
|
* One switch, resolved for one camera.
|
|
7640
8182
|
*
|
|
@@ -9728,6 +10270,26 @@ var EgressTranscodeRequestSchema = object({
|
|
|
9728
10270
|
"h264_mp4toannexb",
|
|
9729
10271
|
"hevc_mp4toannexb"
|
|
9730
10272
|
]).optional(),
|
|
10273
|
+
/**
|
|
10274
|
+
* Publish the transcode as a LOCAL push cam stream, instead of leaving the
|
|
10275
|
+
* consumer to dial the returned url. The broker picks the id and returns it
|
|
10276
|
+
* as `camStreamId` — a caller-supplied one would be circular, since the
|
|
10277
|
+
* sharing key is computed FROM this request.
|
|
10278
|
+
*
|
|
10279
|
+
* The url is still returned and still the contract for a transcode pinned to
|
|
10280
|
+
* another node. But dialling it locally costs an RTSP round trip that changes
|
|
10281
|
+
* the transport underneath the consumer: a dialled stream is an RTP source,
|
|
10282
|
+
* so `isRtpSource()` is true and the session takes the RTP-passthrough +
|
|
10283
|
+
* repacketizer branch. The push branch — the one the derived mechanism has
|
|
10284
|
+
* live hours on — is never reached. Measured on Alexa: broker registered, RTP
|
|
10285
|
+
* arriving, key frame arriving, black screen, on a chain healthy at every
|
|
10286
|
+
* other point.
|
|
10287
|
+
*
|
|
10288
|
+
* Same idea the transport already applies to CALLS, where `classifyCapRoute`
|
|
10289
|
+
* gives priority to `hub-in-process` so a local call never leaves the node.
|
|
10290
|
+
* This is that rule for media.
|
|
10291
|
+
*/
|
|
10292
|
+
publishLocally: boolean().optional(),
|
|
9731
10293
|
pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
|
|
9732
10294
|
/**
|
|
9733
10295
|
* Operator/consumer override for decode hardware. ABSENT is the normal case
|
|
@@ -9772,7 +10334,13 @@ var EgressTranscodeSchema = object({
|
|
|
9772
10334
|
* Returned rather than assumed: a consumer that asked for hardware and got
|
|
9773
10335
|
* software needs to be able to see that without reading the broker's logs.
|
|
9774
10336
|
*/
|
|
9775
|
-
decodeHwAccel: string().nullable()
|
|
10337
|
+
decodeHwAccel: string().nullable(),
|
|
10338
|
+
/**
|
|
10339
|
+
* Set when `publishLocally` was honoured: attach to THIS instead of dialling
|
|
10340
|
+
* `url`, and the session takes the push/deframe transport rather than the
|
|
10341
|
+
* RTP-passthrough one. `null` means the consumer must dial.
|
|
10342
|
+
*/
|
|
10343
|
+
camStreamId: string().nullable()
|
|
9776
10344
|
});
|
|
9777
10345
|
var streamBrokerCapability = {
|
|
9778
10346
|
name: "stream-broker",
|
|
@@ -10066,7 +10634,44 @@ var streamBrokerCapability = {
|
|
|
10066
10634
|
kind: "mutation",
|
|
10067
10635
|
auth: "admin"
|
|
10068
10636
|
}),
|
|
10069
|
-
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
|
+
})
|
|
10070
10675
|
},
|
|
10071
10676
|
events: {
|
|
10072
10677
|
onCamStreamDemand: event(object({
|
|
@@ -15802,6 +16407,30 @@ var TrackSourceSchema = _enum([
|
|
|
15802
16407
|
"audio"
|
|
15803
16408
|
]);
|
|
15804
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
|
+
/**
|
|
15805
16434
|
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
15806
16435
|
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
15807
16436
|
* so the two surfaces cannot drift.
|
|
@@ -15811,18 +16440,31 @@ var TrackSourceSchema = _enum([
|
|
|
15811
16440
|
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
15812
16441
|
* `flag === true`, not `flag !== false`.
|
|
15813
16442
|
*
|
|
15814
|
-
*
|
|
15815
|
-
*
|
|
15816
|
-
*
|
|
15817
|
-
* `
|
|
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.
|
|
15818
16451
|
*/
|
|
15819
16452
|
var TrackFlagFields = {
|
|
15820
|
-
/** Operator marked this track as training material.
|
|
16453
|
+
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16454
|
+
* `'staging'`. */
|
|
15821
16455
|
markForTrain: boolean().optional(),
|
|
15822
16456
|
/** Operator marked this track for diagnostic attention. */
|
|
15823
16457
|
debug: boolean().optional()
|
|
15824
16458
|
};
|
|
15825
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
|
+
/**
|
|
15826
16468
|
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
15827
16469
|
* one flag can never clear the other — the toggles are independent and are
|
|
15828
16470
|
* driven from three surfaces that do not know about each other.
|
|
@@ -15836,7 +16478,32 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
|
15836
16478
|
var TrackFlagsSchema = object({
|
|
15837
16479
|
trackId: string(),
|
|
15838
16480
|
markForTrain: boolean(),
|
|
15839
|
-
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()
|
|
15840
16507
|
});
|
|
15841
16508
|
var TrackSchema = object({
|
|
15842
16509
|
trackId: string(),
|
|
@@ -15881,7 +16548,8 @@ var TrackSchema = object({
|
|
|
15881
16548
|
* Populated from the persisted envelope columns on historical reads;
|
|
15882
16549
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
15883
16550
|
envelope: TrackEnvelopeSchema.optional(),
|
|
15884
|
-
...TrackFlagFields
|
|
16551
|
+
...TrackFlagFields,
|
|
16552
|
+
...TrackRetrainFields
|
|
15885
16553
|
});
|
|
15886
16554
|
var BaseEventFields = {
|
|
15887
16555
|
id: string(),
|
|
@@ -16095,7 +16763,8 @@ var KeyEventSchema = object({
|
|
|
16095
16763
|
bestEventId: string(),
|
|
16096
16764
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
16097
16765
|
windowMs: number().optional(),
|
|
16098
|
-
...TrackFlagFields
|
|
16766
|
+
...TrackFlagFields,
|
|
16767
|
+
...TrackRetrainFields
|
|
16099
16768
|
});
|
|
16100
16769
|
object({
|
|
16101
16770
|
trackId: string(),
|
|
@@ -16356,6 +17025,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16356
17025
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
16357
17026
|
kind: "query",
|
|
16358
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"
|
|
16359
17034
|
}), method(object({
|
|
16360
17035
|
eventId: string(),
|
|
16361
17036
|
kind: MediaFileKindEnum.optional()
|
|
@@ -18467,9 +19142,15 @@ DeviceType.Camera, method(object({
|
|
|
18467
19142
|
* Bypass the cache freshness check and fetch directly from the
|
|
18468
19143
|
* native (or stream-broker fallback). Triggered by the UI's
|
|
18469
19144
|
* "refresh" button so an operator can force a fresh frame
|
|
18470
|
-
* even when the cache is well within
|
|
18471
|
-
*
|
|
18472
|
-
*
|
|
19145
|
+
* even when the cache is well within the device's
|
|
19146
|
+
* `snapshotMaxAgeS` window.
|
|
19147
|
+
*
|
|
19148
|
+
* **`force` is an OPERATOR signal, not a freshness preference.** On a
|
|
19149
|
+
* battery camera it is the one thing that walks past the wrapper's
|
|
19150
|
+
* sleep gate and wakes the camera, so a background caller — a poller,
|
|
19151
|
+
* an event handler, a thumbnail — must NEVER set it. Every such caller
|
|
19152
|
+
* gets the cached frame, which on a sleeping battery camera is the
|
|
19153
|
+
* correct answer: stale but honest beats woken.
|
|
18473
19154
|
*/
|
|
18474
19155
|
force: boolean().optional()
|
|
18475
19156
|
}), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
@@ -22399,6 +23080,173 @@ DeviceType.Camera, method(object({
|
|
|
22399
23080
|
status: OsdStatusSchema
|
|
22400
23081
|
});
|
|
22401
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
|
+
/**
|
|
22402
23250
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
22403
23251
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
22404
23252
|
* `on_batteries` (running on battery backup). `null` until first reported.
|
|
@@ -22846,12 +23694,30 @@ object({
|
|
|
22846
23694
|
});
|
|
22847
23695
|
DeviceType.Sensor;
|
|
22848
23696
|
/**
|
|
22849
|
-
*
|
|
22850
|
-
*
|
|
22851
|
-
*
|
|
22852
|
-
*
|
|
22853
|
-
*
|
|
22854
|
-
*
|
|
23697
|
+
* PRIVACY — what the camera deliberately does not capture. Two planes:
|
|
23698
|
+
*
|
|
23699
|
+
* - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
|
|
23700
|
+
* grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
|
|
23701
|
+
* `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
|
|
23702
|
+
* 4 vertices, not necessarily axis-aligned). The cap composes the shared
|
|
23703
|
+
* rect|polygon subset of the MaskShape vocabulary. All coords are
|
|
23704
|
+
* normalized 0..1 (top-left origin).
|
|
23705
|
+
* - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
|
|
23706
|
+
* camera encoding an audio track at all, so EVERY consumer — live view,
|
|
23707
|
+
* recording, the audio analyzer, an export — sees silent video. There is
|
|
23708
|
+
* no server-side copy of this fact; the camera is the store and every read
|
|
23709
|
+
* is a read-through, which is why a switch over it cannot drift
|
|
23710
|
+
* ([D62](../../../../docs/decisions/adr-0062.md)).
|
|
23711
|
+
*
|
|
23712
|
+
* Both belong here for one reason: they are the two things an operator turns
|
|
23713
|
+
* off when the answer to "what is this camera allowed to record" changes, and
|
|
23714
|
+
* both are applied ON the device, before anything leaves it.
|
|
23715
|
+
*
|
|
23716
|
+
* **The audio flag has exactly one writer.** `stream-params` used to carry a
|
|
23717
|
+
* per-profile `audio` in its patch schema — reachable from no UI and honoured
|
|
23718
|
+
* by one provider — and it was removed when this landed. A second writer onto
|
|
23719
|
+
* one device register is the shape of every knob this repo has shipped that
|
|
23720
|
+
* disagreed with the one the reader read.
|
|
22855
23721
|
*/
|
|
22856
23722
|
/** A privacy-mask region's geometry — rectangle or free polygon. */
|
|
22857
23723
|
var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
|
|
@@ -22867,16 +23733,40 @@ object({
|
|
|
22867
23733
|
enabled: boolean(),
|
|
22868
23734
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
22869
23735
|
regions: array(PrivacyMaskRegionSchema),
|
|
23736
|
+
/**
|
|
23737
|
+
* Is the camera capturing sound right now? Read from the camera, never from
|
|
23738
|
+
* a server-side mirror.
|
|
23739
|
+
*
|
|
23740
|
+
* `null` means "no answer" — either this camera exposes no controllable
|
|
23741
|
+
* microphone (`getOptions().supportsAudioMute === false`) or the read
|
|
23742
|
+
* failed. A consumer must render `null` as UNKNOWN and never as `false`:
|
|
23743
|
+
* "the microphone is off" and "we could not ask" look identical to an
|
|
23744
|
+
* operator only until one of them is wrong.
|
|
23745
|
+
*
|
|
23746
|
+
* On a camera whose profiles carry the flag independently (Reolink writes
|
|
23747
|
+
* it per stream), `true` means AT LEAST ONE profile still carries audio —
|
|
23748
|
+
* privacy is only satisfied when every one of them is silent.
|
|
23749
|
+
*/
|
|
23750
|
+
audioEnabled: boolean().nullable(),
|
|
22870
23751
|
lastFetchedAt: number()
|
|
22871
23752
|
});
|
|
22872
|
-
/** Per-camera availability. */
|
|
23753
|
+
/** Per-camera availability. Probed, never assumed from the model name. */
|
|
22873
23754
|
var PrivacyMaskOptionsSchema = object({
|
|
22874
23755
|
/** Maximum number of supported zones. */
|
|
22875
23756
|
maxRegions: number(),
|
|
22876
23757
|
/** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
|
|
22877
23758
|
supportedShapes: array(MaskShapeKindSchema),
|
|
22878
23759
|
/** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
|
|
22879
|
-
polygonVertices: MaskPolygonVerticesSchema.optional()
|
|
23760
|
+
polygonVertices: MaskPolygonVerticesSchema.optional(),
|
|
23761
|
+
/**
|
|
23762
|
+
* Does this camera expose a microphone switch we can actually write?
|
|
23763
|
+
*
|
|
23764
|
+
* Camera-probed: `true` only when the firmware answered with an audio flag
|
|
23765
|
+
* we know how to patch. A camera that never answered is `false` — a control
|
|
23766
|
+
* the operator can press that changes nothing is worse than no control, and
|
|
23767
|
+
* the switch group renders "not available" instead.
|
|
23768
|
+
*/
|
|
23769
|
+
supportsAudioMute: boolean()
|
|
22880
23770
|
});
|
|
22881
23771
|
/** Partial change — every field optional. */
|
|
22882
23772
|
var PrivacyMaskPatchSchema = object({
|
|
@@ -22889,6 +23779,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), PrivacyMaskOptionsSche
|
|
|
22889
23779
|
}), _void(), {
|
|
22890
23780
|
kind: "mutation",
|
|
22891
23781
|
auth: "admin"
|
|
23782
|
+
}), method(object({
|
|
23783
|
+
deviceId: number(),
|
|
23784
|
+
enabled: boolean()
|
|
23785
|
+
}), _void(), {
|
|
23786
|
+
kind: "mutation",
|
|
23787
|
+
auth: "admin"
|
|
22892
23788
|
});
|
|
22893
23789
|
var PtzPresetSchema = object({
|
|
22894
23790
|
id: string(),
|
|
@@ -23098,6 +23994,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
|
|
|
23098
23994
|
})]);
|
|
23099
23995
|
/** Raw bytes of one finalized footage segment (read off disk on the recording node). */
|
|
23100
23996
|
var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
|
|
23997
|
+
/**
|
|
23998
|
+
* One GOP of a finalized segment, cut by byte range through the segment's own
|
|
23999
|
+
* `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
|
|
24000
|
+
* the single `moof`+`mdat` covering the requested instant — standalone-
|
|
24001
|
+
* demuxable, never the whole file. When the segment's index cannot be parsed
|
|
24002
|
+
* the provider degrades INSIDE the mechanism to the whole segment (still one
|
|
24003
|
+
* `data`, `gopStartMs` = the segment start) — a worse read, not another path.
|
|
24004
|
+
*/
|
|
24005
|
+
var ReadGopBytesResultSchema = object({
|
|
24006
|
+
data: _instanceof(Uint8Array),
|
|
24007
|
+
/** Absolute epoch ms of the returned fragment's first sample. */
|
|
24008
|
+
gopStartMs: number(),
|
|
24009
|
+
/** Media ms the returned fragment covers. */
|
|
24010
|
+
gopDurMs: number()
|
|
24011
|
+
});
|
|
23101
24012
|
var recordingCapability = {
|
|
23102
24013
|
name: "recording",
|
|
23103
24014
|
scope: "system",
|
|
@@ -23165,6 +24076,18 @@ var recordingCapability = {
|
|
|
23165
24076
|
kind: "query",
|
|
23166
24077
|
auth: "admin"
|
|
23167
24078
|
}),
|
|
24079
|
+
/** Read the single GOP of segment `startMs` covering `epochMs`, by mfra
|
|
24080
|
+
* byte range — the scrub-granular read (D31 letter on the feeder path).
|
|
24081
|
+
* See {@link ReadGopBytesResultSchema} for the degradation contract. */
|
|
24082
|
+
readGopBytes: method(object({
|
|
24083
|
+
deviceId: number(),
|
|
24084
|
+
profile: string(),
|
|
24085
|
+
startMs: number(),
|
|
24086
|
+
epochMs: number()
|
|
24087
|
+
}), ReadGopBytesResultSchema, {
|
|
24088
|
+
kind: "query",
|
|
24089
|
+
auth: "admin"
|
|
24090
|
+
}),
|
|
23168
24091
|
setDeviceConfig: method(object({
|
|
23169
24092
|
deviceId: number(),
|
|
23170
24093
|
config: RecordingConfigSchema
|
|
@@ -23716,6 +24639,16 @@ var StreamProfileConfigSchema = object({
|
|
|
23716
24639
|
"baseline"
|
|
23717
24640
|
]).optional(),
|
|
23718
24641
|
gop: number().optional(),
|
|
24642
|
+
/**
|
|
24643
|
+
* Whether THIS profile currently carries an audio track. READ-ONLY here.
|
|
24644
|
+
*
|
|
24645
|
+
* There is no matching field on {@link StreamProfilePatchSchema}: the
|
|
24646
|
+
* camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
|
|
24647
|
+
* writes every profile at once so "audio off" means silent everywhere. A
|
|
24648
|
+
* per-profile writer beside it would let a camera be half-muted and would be
|
|
24649
|
+
* a second knob onto one device register — the failure D62 exists to
|
|
24650
|
+
* prevent. Absent when the firmware does not report the flag.
|
|
24651
|
+
*/
|
|
23719
24652
|
audio: boolean().optional()
|
|
23720
24653
|
});
|
|
23721
24654
|
object({
|
|
@@ -23756,7 +24689,13 @@ var StreamParamsOptionsSchema = object({
|
|
|
23756
24689
|
ext: StreamProfileOptionsSchema.optional()
|
|
23757
24690
|
});
|
|
23758
24691
|
/** A partial change to one profile — every field optional; a provider
|
|
23759
|
-
* ignores fields it doesn't support.
|
|
24692
|
+
* ignores fields it doesn't support.
|
|
24693
|
+
*
|
|
24694
|
+
* There is deliberately NO `audio` here. It existed until 2026-08-07,
|
|
24695
|
+
* reachable from no form and honoured by exactly one provider, while the
|
|
24696
|
+
* camera's microphone is a whole-device fact. It now has one writer,
|
|
24697
|
+
* `privacyMask.setAudioEnabled`, which writes every profile — see
|
|
24698
|
+
* `privacy-mask.cap.ts`. */
|
|
23760
24699
|
var StreamProfilePatchSchema = object({
|
|
23761
24700
|
width: number().optional(),
|
|
23762
24701
|
height: number().optional(),
|
|
@@ -23769,8 +24708,7 @@ var StreamProfilePatchSchema = object({
|
|
|
23769
24708
|
"main",
|
|
23770
24709
|
"baseline"
|
|
23771
24710
|
]).optional(),
|
|
23772
|
-
gop: number().optional()
|
|
23773
|
-
audio: boolean().optional()
|
|
24711
|
+
gop: number().optional()
|
|
23774
24712
|
});
|
|
23775
24713
|
DeviceType.Camera, method(object({ deviceId: number() }), StreamParamsOptionsSchema), method(object({
|
|
23776
24714
|
deviceId: number(),
|
|
@@ -27365,6 +28303,48 @@ Object.freeze({
|
|
|
27365
28303
|
addonId: null,
|
|
27366
28304
|
access: "create"
|
|
27367
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
|
+
},
|
|
27368
28348
|
"petFeeder.callPet": {
|
|
27369
28349
|
capName: "pet-feeder",
|
|
27370
28350
|
capScope: "device",
|
|
@@ -27527,6 +28507,18 @@ Object.freeze({
|
|
|
27527
28507
|
addonId: null,
|
|
27528
28508
|
access: "view"
|
|
27529
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
|
+
},
|
|
27530
28522
|
"pipelineAnalytics.listEventKinds": {
|
|
27531
28523
|
capName: "pipeline-analytics",
|
|
27532
28524
|
capScope: "device",
|
|
@@ -28283,6 +29275,12 @@ Object.freeze({
|
|
|
28283
29275
|
addonId: null,
|
|
28284
29276
|
access: "view"
|
|
28285
29277
|
},
|
|
29278
|
+
"privacyMask.setAudioEnabled": {
|
|
29279
|
+
capName: "privacy-mask",
|
|
29280
|
+
capScope: "device",
|
|
29281
|
+
addonId: null,
|
|
29282
|
+
access: "create"
|
|
29283
|
+
},
|
|
28286
29284
|
"privacyMask.setMask": {
|
|
28287
29285
|
capName: "privacy-mask",
|
|
28288
29286
|
capScope: "device",
|
|
@@ -28451,6 +29449,12 @@ Object.freeze({
|
|
|
28451
29449
|
addonId: null,
|
|
28452
29450
|
access: "create"
|
|
28453
29451
|
},
|
|
29452
|
+
"recording.readGopBytes": {
|
|
29453
|
+
capName: "recording",
|
|
29454
|
+
capScope: "system",
|
|
29455
|
+
addonId: null,
|
|
29456
|
+
access: "view"
|
|
29457
|
+
},
|
|
28454
29458
|
"recording.readSegmentBytes": {
|
|
28455
29459
|
capName: "recording",
|
|
28456
29460
|
capScope: "system",
|
|
@@ -28991,6 +29995,12 @@ Object.freeze({
|
|
|
28991
29995
|
addonId: null,
|
|
28992
29996
|
access: "view"
|
|
28993
29997
|
},
|
|
29998
|
+
"streamBroker.getDeviceAudioMute": {
|
|
29999
|
+
capName: "stream-broker",
|
|
30000
|
+
capScope: "system",
|
|
30001
|
+
addonId: null,
|
|
30002
|
+
access: "view"
|
|
30003
|
+
},
|
|
28994
30004
|
"streamBroker.getPreBufferInfo": {
|
|
28995
30005
|
capName: "stream-broker",
|
|
28996
30006
|
capScope: "system",
|
|
@@ -29111,6 +30121,12 @@ Object.freeze({
|
|
|
29111
30121
|
addonId: null,
|
|
29112
30122
|
access: "create"
|
|
29113
30123
|
},
|
|
30124
|
+
"streamBroker.setDeviceAudioMute": {
|
|
30125
|
+
capName: "stream-broker",
|
|
30126
|
+
capScope: "system",
|
|
30127
|
+
addonId: null,
|
|
30128
|
+
access: "create"
|
|
30129
|
+
},
|
|
29114
30130
|
"streamBroker.setPreBufferDuration": {
|
|
29115
30131
|
capName: "stream-broker",
|
|
29116
30132
|
capScope: "system",
|
|
@@ -30085,7 +31101,7 @@ function readDetailCropConvention(config) {
|
|
|
30085
31101
|
square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
|
|
30086
31102
|
};
|
|
30087
31103
|
}
|
|
30088
|
-
function isHydratedField(entry) {
|
|
31104
|
+
function isHydratedField$1(entry) {
|
|
30089
31105
|
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
30090
31106
|
}
|
|
30091
31107
|
/**
|
|
@@ -30100,7 +31116,7 @@ function pickDetailCropConvention(view) {
|
|
|
30100
31116
|
if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
|
|
30101
31117
|
const flat = {};
|
|
30102
31118
|
for (const section of view.sections) for (const entry of section.fields) {
|
|
30103
|
-
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
31119
|
+
if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
|
|
30104
31120
|
if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
|
|
30105
31121
|
}
|
|
30106
31122
|
return readDetailCropConvention(flat);
|
|
@@ -30180,6 +31196,178 @@ function slideInsideFrame(rect, frameWidth, frameHeight) {
|
|
|
30180
31196
|
h
|
|
30181
31197
|
};
|
|
30182
31198
|
}
|
|
31199
|
+
var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
|
|
31200
|
+
var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
|
|
31201
|
+
var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
|
|
31202
|
+
var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
|
|
31203
|
+
/**
|
|
31204
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
31205
|
+
*
|
|
31206
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
31207
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
31208
|
+
* frame the runner never sent to inference.
|
|
31209
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
31210
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
31211
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
31212
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
31213
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
31214
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
31215
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
31216
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
31217
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
31218
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
31219
|
+
* consumes it.
|
|
31220
|
+
*/
|
|
31221
|
+
var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
|
|
31222
|
+
/**
|
|
31223
|
+
* Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
|
|
31224
|
+
* the slider) because the value also travels to a forked child process, where a
|
|
31225
|
+
* junk number would silently become a 0-length or unbounded retention window.
|
|
31226
|
+
*/
|
|
31227
|
+
var NativeLeaseSettingsSchema = object({
|
|
31228
|
+
/**
|
|
31229
|
+
* How long a retained native frame is served before it counts as a miss.
|
|
31230
|
+
*
|
|
31231
|
+
* Must cover the FULL late-crop horizon: detection inference + the
|
|
31232
|
+
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
31233
|
+
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
31234
|
+
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
31235
|
+
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
31236
|
+
*/
|
|
31237
|
+
ttlMs: number().int().min(250).max(1e4),
|
|
31238
|
+
/**
|
|
31239
|
+
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
31240
|
+
*
|
|
31241
|
+
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
31242
|
+
* which one is actually binding before reasoning from that. At the shipped
|
|
31243
|
+
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
31244
|
+
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
31245
|
+
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
31246
|
+
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
31247
|
+
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
31248
|
+
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
31249
|
+
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
31250
|
+
* to replace).
|
|
31251
|
+
*/
|
|
31252
|
+
budgetMb: number().int().min(0).max(4096),
|
|
31253
|
+
/**
|
|
31254
|
+
* Demand window: eager per-frame native retention runs only within this many
|
|
31255
|
+
* ms of the last native-crop request (or of the dial starting).
|
|
31256
|
+
*
|
|
31257
|
+
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
31258
|
+
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
31259
|
+
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
31260
|
+
* you are reproducing that.
|
|
31261
|
+
*/
|
|
31262
|
+
activityMs: number().int().min(0).max(12e4),
|
|
31263
|
+
/**
|
|
31264
|
+
* Which delivered frames are retained at all — see
|
|
31265
|
+
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
31266
|
+
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
31267
|
+
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
31268
|
+
* crop request naming a frame it did NOT see marked
|
|
31269
|
+
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
31270
|
+
* there is the signal that some caller names frames outside the inference set
|
|
31271
|
+
* and that this must go back to `all`.
|
|
31272
|
+
*/
|
|
31273
|
+
admission: NativeLeaseAdmissionSchema
|
|
31274
|
+
});
|
|
31275
|
+
/**
|
|
31276
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
31277
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
31278
|
+
* settings changed no behaviour on the day it landed.
|
|
31279
|
+
*/
|
|
31280
|
+
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
31281
|
+
ttlMs: 1200,
|
|
31282
|
+
budgetMb: 1024,
|
|
31283
|
+
activityMs: 15e3,
|
|
31284
|
+
admission: "inferred"
|
|
31285
|
+
};
|
|
31286
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
|
|
31287
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
31288
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
31289
|
+
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
31290
|
+
/**
|
|
31291
|
+
* Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
|
|
31292
|
+
* value equal to the shipped default.
|
|
31293
|
+
*
|
|
31294
|
+
* That last rule is not tidiness, it is the difference between the documented
|
|
31295
|
+
* precedence being true and being a lie. `addon-settings.getGlobalSettings`
|
|
31296
|
+
* returns a HYDRATED payload, and `hydrateField` fills an unstored field with
|
|
31297
|
+
* the schema's own `default` (verified live on the hub: a cluster that has never
|
|
31298
|
+
* opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
|
|
31299
|
+
* that at face value would report all three knobs as "set" on every cluster on
|
|
31300
|
+
* the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
|
|
31301
|
+
* emergency override that the precedence promises. There is no raw-store read on
|
|
31302
|
+
* this cap to distinguish the two, so the default value itself is treated as
|
|
31303
|
+
* "the operator has expressed no preference" — which is also what leaving a
|
|
31304
|
+
* slider untouched means.
|
|
31305
|
+
*
|
|
31306
|
+
* The cost is one honest edge: an operator who deliberately selects the default
|
|
31307
|
+
* value in order to overrule an env var does not get it. Clear the env var
|
|
31308
|
+
* instead; the worker's spawn line names the source, so this is visible rather
|
|
31309
|
+
* than mysterious.
|
|
31310
|
+
*/
|
|
31311
|
+
function readKnob(knob, raw) {
|
|
31312
|
+
const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
|
|
31313
|
+
if (!parsed.success) return null;
|
|
31314
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
|
|
31315
|
+
}
|
|
31316
|
+
/** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
|
|
31317
|
+
function readAdmissionKnob(raw) {
|
|
31318
|
+
const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
|
|
31319
|
+
if (!parsed.success) return null;
|
|
31320
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
|
|
31321
|
+
}
|
|
31322
|
+
/**
|
|
31323
|
+
* Narrow a FLAT settings record to the knobs the operator set.
|
|
31324
|
+
*
|
|
31325
|
+
* Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
|
|
31326
|
+
* budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
|
|
31327
|
+
* clamped, not defaulted) so the caller can still fall through to the env
|
|
31328
|
+
* override — clamping here would turn a typo into a value nobody chose. See
|
|
31329
|
+
* {@link readKnob} for why the default counts as unset.
|
|
31330
|
+
*/
|
|
31331
|
+
function readNativeLeaseOverride(config) {
|
|
31332
|
+
const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
|
|
31333
|
+
const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
|
|
31334
|
+
const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
|
|
31335
|
+
const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
|
|
31336
|
+
return {
|
|
31337
|
+
...ttlMs === null ? {} : { ttlMs },
|
|
31338
|
+
...budgetMb === null ? {} : { budgetMb },
|
|
31339
|
+
...activityMs === null ? {} : { activityMs },
|
|
31340
|
+
...admission === null ? {} : { admission }
|
|
31341
|
+
};
|
|
31342
|
+
}
|
|
31343
|
+
function isHydratedField(entry) {
|
|
31344
|
+
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
31345
|
+
}
|
|
31346
|
+
var LEASE_KEYS = [
|
|
31347
|
+
NATIVE_LEASE_TTL_KEY,
|
|
31348
|
+
NATIVE_LEASE_BUDGET_KEY,
|
|
31349
|
+
NATIVE_LEASE_ACTIVITY_KEY,
|
|
31350
|
+
NATIVE_LEASE_ADMISSION_KEY
|
|
31351
|
+
];
|
|
31352
|
+
/**
|
|
31353
|
+
* Extract the operator's lease overrides from an
|
|
31354
|
+
* `addon-settings.getGlobalSettings` payload.
|
|
31355
|
+
*
|
|
31356
|
+
* Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
|
|
31357
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
31358
|
+
* must not silently revert the whole cluster to the defaults. A `null` payload
|
|
31359
|
+
* (addon mid-boot) means "operator set nothing" — the env/default fallback then
|
|
31360
|
+
* applies, which is the correct read of "I could not ask".
|
|
31361
|
+
*/
|
|
31362
|
+
function pickNativeLeaseOverride(view) {
|
|
31363
|
+
if (view === null) return {};
|
|
31364
|
+
const flat = {};
|
|
31365
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
31366
|
+
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
31367
|
+
if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
|
|
31368
|
+
}
|
|
31369
|
+
return readNativeLeaseOverride(flat);
|
|
31370
|
+
}
|
|
30183
31371
|
var AUTO = {
|
|
30184
31372
|
value: "auto",
|
|
30185
31373
|
label: "Auto"
|
|
@@ -30457,6 +31645,12 @@ Object.defineProperty(exports, "DEFAULT_DETAIL_CROP_CONVENTION", {
|
|
|
30457
31645
|
return DEFAULT_DETAIL_CROP_CONVENTION;
|
|
30458
31646
|
}
|
|
30459
31647
|
});
|
|
31648
|
+
Object.defineProperty(exports, "DEFAULT_NATIVE_LEASE_SETTINGS", {
|
|
31649
|
+
enumerable: true,
|
|
31650
|
+
get: function() {
|
|
31651
|
+
return DEFAULT_NATIVE_LEASE_SETTINGS;
|
|
31652
|
+
}
|
|
31653
|
+
});
|
|
30460
31654
|
Object.defineProperty(exports, "DEVICE_BACKEND_TO_FORMAT", {
|
|
30461
31655
|
enumerable: true,
|
|
30462
31656
|
get: function() {
|
|
@@ -30499,18 +31693,48 @@ Object.defineProperty(exports, "ExportRecordSchema", {
|
|
|
30499
31693
|
return ExportRecordSchema;
|
|
30500
31694
|
}
|
|
30501
31695
|
});
|
|
31696
|
+
Object.defineProperty(exports, "Fmp4BoxSplitter", {
|
|
31697
|
+
enumerable: true,
|
|
31698
|
+
get: function() {
|
|
31699
|
+
return Fmp4BoxSplitter;
|
|
31700
|
+
}
|
|
31701
|
+
});
|
|
30502
31702
|
Object.defineProperty(exports, "HF_BASE_URL", {
|
|
30503
31703
|
enumerable: true,
|
|
30504
31704
|
get: function() {
|
|
30505
31705
|
return HF_BASE_URL;
|
|
30506
31706
|
}
|
|
30507
31707
|
});
|
|
31708
|
+
Object.defineProperty(exports, "NativeLeaseAdmissionSchema", {
|
|
31709
|
+
enumerable: true,
|
|
31710
|
+
get: function() {
|
|
31711
|
+
return NativeLeaseAdmissionSchema;
|
|
31712
|
+
}
|
|
31713
|
+
});
|
|
31714
|
+
Object.defineProperty(exports, "NativeLeaseSettingsSchema", {
|
|
31715
|
+
enumerable: true,
|
|
31716
|
+
get: function() {
|
|
31717
|
+
return NativeLeaseSettingsSchema;
|
|
31718
|
+
}
|
|
31719
|
+
});
|
|
30508
31720
|
Object.defineProperty(exports, "OpsLogEntrySchema", {
|
|
30509
31721
|
enumerable: true,
|
|
30510
31722
|
get: function() {
|
|
30511
31723
|
return OpsLogEntrySchema;
|
|
30512
31724
|
}
|
|
30513
31725
|
});
|
|
31726
|
+
Object.defineProperty(exports, "RATE_CONTROL_RELAXED", {
|
|
31727
|
+
enumerable: true,
|
|
31728
|
+
get: function() {
|
|
31729
|
+
return RATE_CONTROL_RELAXED;
|
|
31730
|
+
}
|
|
31731
|
+
});
|
|
31732
|
+
Object.defineProperty(exports, "RATE_CONTROL_TIGHT", {
|
|
31733
|
+
enumerable: true,
|
|
31734
|
+
get: function() {
|
|
31735
|
+
return RATE_CONTROL_TIGHT;
|
|
31736
|
+
}
|
|
31737
|
+
});
|
|
30514
31738
|
Object.defineProperty(exports, "RecordingConfigSchema", {
|
|
30515
31739
|
enumerable: true,
|
|
30516
31740
|
get: function() {
|
|
@@ -30583,6 +31807,12 @@ Object.defineProperty(exports, "createEvent", {
|
|
|
30583
31807
|
return createEvent;
|
|
30584
31808
|
}
|
|
30585
31809
|
});
|
|
31810
|
+
Object.defineProperty(exports, "createHwAccelCache", {
|
|
31811
|
+
enumerable: true,
|
|
31812
|
+
get: function() {
|
|
31813
|
+
return createHwAccelCache;
|
|
31814
|
+
}
|
|
31815
|
+
});
|
|
30586
31816
|
Object.defineProperty(exports, "customAction", {
|
|
30587
31817
|
enumerable: true,
|
|
30588
31818
|
get: function() {
|
|
@@ -30625,6 +31855,18 @@ Object.defineProperty(exports, "discriminatedUnion", {
|
|
|
30625
31855
|
return discriminatedUnion;
|
|
30626
31856
|
}
|
|
30627
31857
|
});
|
|
31858
|
+
Object.defineProperty(exports, "egressTranscodeSharingKey", {
|
|
31859
|
+
enumerable: true,
|
|
31860
|
+
get: function() {
|
|
31861
|
+
return egressTranscodeSharingKey;
|
|
31862
|
+
}
|
|
31863
|
+
});
|
|
31864
|
+
Object.defineProperty(exports, "egressTransportFromRequest", {
|
|
31865
|
+
enumerable: true,
|
|
31866
|
+
get: function() {
|
|
31867
|
+
return egressTransportFromRequest;
|
|
31868
|
+
}
|
|
31869
|
+
});
|
|
30628
31870
|
Object.defineProperty(exports, "enumerateInferenceDevices", {
|
|
30629
31871
|
enumerable: true,
|
|
30630
31872
|
get: function() {
|
|
@@ -30667,6 +31909,12 @@ Object.defineProperty(exports, "isEvent", {
|
|
|
30667
31909
|
return isEvent;
|
|
30668
31910
|
}
|
|
30669
31911
|
});
|
|
31912
|
+
Object.defineProperty(exports, "isSoftwareDecode", {
|
|
31913
|
+
enumerable: true,
|
|
31914
|
+
get: function() {
|
|
31915
|
+
return isSoftwareDecode;
|
|
31916
|
+
}
|
|
31917
|
+
});
|
|
30670
31918
|
Object.defineProperty(exports, "lazy", {
|
|
30671
31919
|
enumerable: true,
|
|
30672
31920
|
get: function() {
|
|
@@ -30745,6 +31993,12 @@ Object.defineProperty(exports, "pickDetailCropConvention", {
|
|
|
30745
31993
|
return pickDetailCropConvention;
|
|
30746
31994
|
}
|
|
30747
31995
|
});
|
|
31996
|
+
Object.defineProperty(exports, "pickNativeLeaseOverride", {
|
|
31997
|
+
enumerable: true,
|
|
31998
|
+
get: function() {
|
|
31999
|
+
return pickNativeLeaseOverride;
|
|
32000
|
+
}
|
|
32001
|
+
});
|
|
30748
32002
|
Object.defineProperty(exports, "pipelineExecutorCapability", {
|
|
30749
32003
|
enumerable: true,
|
|
30750
32004
|
get: function() {
|
|
@@ -30775,6 +32029,12 @@ Object.defineProperty(exports, "recordingExportCapability", {
|
|
|
30775
32029
|
return recordingExportCapability;
|
|
30776
32030
|
}
|
|
30777
32031
|
});
|
|
32032
|
+
Object.defineProperty(exports, "resolveEgressDecodeHwAccel", {
|
|
32033
|
+
enumerable: true,
|
|
32034
|
+
get: function() {
|
|
32035
|
+
return resolveEgressDecodeHwAccel;
|
|
32036
|
+
}
|
|
32037
|
+
});
|
|
30778
32038
|
Object.defineProperty(exports, "runtimeDevices", {
|
|
30779
32039
|
enumerable: true,
|
|
30780
32040
|
get: function() {
|