@camstack/addon-decoder-nodeav 1.2.119 → 1.2.120
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/index.js +1052 -2029
- package/dist/index.mjs +1053 -2029
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
|
-
//#region ../types/dist/event-category-
|
|
6
|
+
//#region ../types/dist/event-category-B_0XEcLF.mjs
|
|
7
7
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
8
8
|
EventCategory["SystemBoot"] = "system.boot";
|
|
9
9
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -504,15 +504,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
504
504
|
EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
|
|
505
505
|
EventCategory["AgentBackupActivated"] = "agent.backup.activated";
|
|
506
506
|
EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
|
|
507
|
-
/**
|
|
508
|
-
* Per-agent hwaccel preference changed (user override set, cleared, or
|
|
509
|
-
* re-probed). Observability event — decoders pull the current pref at
|
|
510
|
-
* `createSession`, so running sessions keep their current backend until
|
|
511
|
-
* they rotate naturally (camera add/remove, stream restart). Future
|
|
512
|
-
* work can wire a listener in stream-broker that force-rotates live
|
|
513
|
-
* sessions; for now this event powers logs + admin-UI toast feedback.
|
|
514
|
-
*/
|
|
515
|
-
EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
|
|
516
507
|
EventCategory["MotionAnalysis"] = "detection.motion-analysis";
|
|
517
508
|
/** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
|
|
518
509
|
EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
|
|
@@ -3775,7 +3766,7 @@ function initializeContext(params) {
|
|
|
3775
3766
|
external: params?.external ?? void 0
|
|
3776
3767
|
};
|
|
3777
3768
|
}
|
|
3778
|
-
function process
|
|
3769
|
+
function process(schema, ctx, _params = {
|
|
3779
3770
|
path: [],
|
|
3780
3771
|
schemaPath: []
|
|
3781
3772
|
}) {
|
|
@@ -3812,7 +3803,7 @@ function process$1(schema, ctx, _params = {
|
|
|
3812
3803
|
const parent = schema._zod.parent;
|
|
3813
3804
|
if (parent) {
|
|
3814
3805
|
if (!result.ref) result.ref = parent;
|
|
3815
|
-
process
|
|
3806
|
+
process(parent, ctx, params);
|
|
3816
3807
|
ctx.seen.get(parent).isParent = true;
|
|
3817
3808
|
}
|
|
3818
3809
|
}
|
|
@@ -4032,7 +4023,7 @@ var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
|
|
|
4032
4023
|
...params,
|
|
4033
4024
|
processors
|
|
4034
4025
|
});
|
|
4035
|
-
process
|
|
4026
|
+
process(schema, ctx);
|
|
4036
4027
|
extractDefs(ctx, schema);
|
|
4037
4028
|
return finalize(ctx, schema);
|
|
4038
4029
|
};
|
|
@@ -4044,7 +4035,7 @@ var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) =
|
|
|
4044
4035
|
io,
|
|
4045
4036
|
processors
|
|
4046
4037
|
});
|
|
4047
|
-
process
|
|
4038
|
+
process(schema, ctx);
|
|
4048
4039
|
extractDefs(ctx, schema);
|
|
4049
4040
|
return finalize(ctx, schema);
|
|
4050
4041
|
};
|
|
@@ -4158,7 +4149,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
|
|
|
4158
4149
|
if (typeof minimum === "number") json.minItems = minimum;
|
|
4159
4150
|
if (typeof maximum === "number") json.maxItems = maximum;
|
|
4160
4151
|
json.type = "array";
|
|
4161
|
-
json.items = process
|
|
4152
|
+
json.items = process(def.element, ctx, {
|
|
4162
4153
|
...params,
|
|
4163
4154
|
path: [...params.path, "items"]
|
|
4164
4155
|
});
|
|
@@ -4169,7 +4160,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4169
4160
|
json.type = "object";
|
|
4170
4161
|
json.properties = {};
|
|
4171
4162
|
const shape = def.shape;
|
|
4172
|
-
for (const key in shape) json.properties[key] = process
|
|
4163
|
+
for (const key in shape) json.properties[key] = process(shape[key], ctx, {
|
|
4173
4164
|
...params,
|
|
4174
4165
|
path: [
|
|
4175
4166
|
...params.path,
|
|
@@ -4187,7 +4178,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4187
4178
|
if (def.catchall?._zod.def.type === "never") json.additionalProperties = false;
|
|
4188
4179
|
else if (!def.catchall) {
|
|
4189
4180
|
if (ctx.io === "output") json.additionalProperties = false;
|
|
4190
|
-
} else if (def.catchall) json.additionalProperties = process
|
|
4181
|
+
} else if (def.catchall) json.additionalProperties = process(def.catchall, ctx, {
|
|
4191
4182
|
...params,
|
|
4192
4183
|
path: [...params.path, "additionalProperties"]
|
|
4193
4184
|
});
|
|
@@ -4195,7 +4186,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4195
4186
|
var unionProcessor = (schema, ctx, json, params) => {
|
|
4196
4187
|
const def = schema._zod.def;
|
|
4197
4188
|
const isExclusive = def.inclusive === false;
|
|
4198
|
-
const options = def.options.map((x, i) => process
|
|
4189
|
+
const options = def.options.map((x, i) => process(x, ctx, {
|
|
4199
4190
|
...params,
|
|
4200
4191
|
path: [
|
|
4201
4192
|
...params.path,
|
|
@@ -4208,7 +4199,7 @@ var unionProcessor = (schema, ctx, json, params) => {
|
|
|
4208
4199
|
};
|
|
4209
4200
|
var intersectionProcessor = (schema, ctx, json, params) => {
|
|
4210
4201
|
const def = schema._zod.def;
|
|
4211
|
-
const a = process
|
|
4202
|
+
const a = process(def.left, ctx, {
|
|
4212
4203
|
...params,
|
|
4213
4204
|
path: [
|
|
4214
4205
|
...params.path,
|
|
@@ -4216,7 +4207,7 @@ var intersectionProcessor = (schema, ctx, json, params) => {
|
|
|
4216
4207
|
0
|
|
4217
4208
|
]
|
|
4218
4209
|
});
|
|
4219
|
-
const b = process
|
|
4210
|
+
const b = process(def.right, ctx, {
|
|
4220
4211
|
...params,
|
|
4221
4212
|
path: [
|
|
4222
4213
|
...params.path,
|
|
@@ -4233,7 +4224,7 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
4233
4224
|
json.type = "array";
|
|
4234
4225
|
const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
4235
4226
|
const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
4236
|
-
const prefixItems = def.items.map((x, i) => process
|
|
4227
|
+
const prefixItems = def.items.map((x, i) => process(x, ctx, {
|
|
4237
4228
|
...params,
|
|
4238
4229
|
path: [
|
|
4239
4230
|
...params.path,
|
|
@@ -4241,7 +4232,7 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
4241
4232
|
i
|
|
4242
4233
|
]
|
|
4243
4234
|
}));
|
|
4244
|
-
const rest = def.rest ? process
|
|
4235
|
+
const rest = def.rest ? process(def.rest, ctx, {
|
|
4245
4236
|
...params,
|
|
4246
4237
|
path: [
|
|
4247
4238
|
...params.path,
|
|
@@ -4272,7 +4263,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4272
4263
|
const keyType = def.keyType;
|
|
4273
4264
|
const patterns = keyType._zod.bag?.patterns;
|
|
4274
4265
|
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
4275
|
-
const valueSchema = process
|
|
4266
|
+
const valueSchema = process(def.valueType, ctx, {
|
|
4276
4267
|
...params,
|
|
4277
4268
|
path: [
|
|
4278
4269
|
...params.path,
|
|
@@ -4283,11 +4274,11 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4283
4274
|
json.patternProperties = {};
|
|
4284
4275
|
for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
|
|
4285
4276
|
} else {
|
|
4286
|
-
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process
|
|
4277
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process(def.keyType, ctx, {
|
|
4287
4278
|
...params,
|
|
4288
4279
|
path: [...params.path, "propertyNames"]
|
|
4289
4280
|
});
|
|
4290
|
-
json.additionalProperties = process
|
|
4281
|
+
json.additionalProperties = process(def.valueType, ctx, {
|
|
4291
4282
|
...params,
|
|
4292
4283
|
path: [...params.path, "additionalProperties"]
|
|
4293
4284
|
});
|
|
@@ -4300,7 +4291,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4300
4291
|
};
|
|
4301
4292
|
var nullableProcessor = (schema, ctx, json, params) => {
|
|
4302
4293
|
const def = schema._zod.def;
|
|
4303
|
-
const inner = process
|
|
4294
|
+
const inner = process(def.innerType, ctx, params);
|
|
4304
4295
|
const seen = ctx.seen.get(schema);
|
|
4305
4296
|
if (ctx.target === "openapi-3.0") {
|
|
4306
4297
|
seen.ref = def.innerType;
|
|
@@ -4309,27 +4300,27 @@ var nullableProcessor = (schema, ctx, json, params) => {
|
|
|
4309
4300
|
};
|
|
4310
4301
|
var nonoptionalProcessor = (schema, ctx, _json, params) => {
|
|
4311
4302
|
const def = schema._zod.def;
|
|
4312
|
-
process
|
|
4303
|
+
process(def.innerType, ctx, params);
|
|
4313
4304
|
const seen = ctx.seen.get(schema);
|
|
4314
4305
|
seen.ref = def.innerType;
|
|
4315
4306
|
};
|
|
4316
4307
|
var defaultProcessor = (schema, ctx, json, params) => {
|
|
4317
4308
|
const def = schema._zod.def;
|
|
4318
|
-
process
|
|
4309
|
+
process(def.innerType, ctx, params);
|
|
4319
4310
|
const seen = ctx.seen.get(schema);
|
|
4320
4311
|
seen.ref = def.innerType;
|
|
4321
4312
|
json.default = JSON.parse(JSON.stringify(def.defaultValue));
|
|
4322
4313
|
};
|
|
4323
4314
|
var prefaultProcessor = (schema, ctx, json, params) => {
|
|
4324
4315
|
const def = schema._zod.def;
|
|
4325
|
-
process
|
|
4316
|
+
process(def.innerType, ctx, params);
|
|
4326
4317
|
const seen = ctx.seen.get(schema);
|
|
4327
4318
|
seen.ref = def.innerType;
|
|
4328
4319
|
if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
|
|
4329
4320
|
};
|
|
4330
4321
|
var catchProcessor = (schema, ctx, json, params) => {
|
|
4331
4322
|
const def = schema._zod.def;
|
|
4332
|
-
process
|
|
4323
|
+
process(def.innerType, ctx, params);
|
|
4333
4324
|
const seen = ctx.seen.get(schema);
|
|
4334
4325
|
seen.ref = def.innerType;
|
|
4335
4326
|
let catchValue;
|
|
@@ -4344,26 +4335,26 @@ var pipeProcessor = (schema, ctx, _json, params) => {
|
|
|
4344
4335
|
const def = schema._zod.def;
|
|
4345
4336
|
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
|
|
4346
4337
|
const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
|
|
4347
|
-
process
|
|
4338
|
+
process(innerType, ctx, params);
|
|
4348
4339
|
const seen = ctx.seen.get(schema);
|
|
4349
4340
|
seen.ref = innerType;
|
|
4350
4341
|
};
|
|
4351
4342
|
var readonlyProcessor = (schema, ctx, json, params) => {
|
|
4352
4343
|
const def = schema._zod.def;
|
|
4353
|
-
process
|
|
4344
|
+
process(def.innerType, ctx, params);
|
|
4354
4345
|
const seen = ctx.seen.get(schema);
|
|
4355
4346
|
seen.ref = def.innerType;
|
|
4356
4347
|
json.readOnly = true;
|
|
4357
4348
|
};
|
|
4358
4349
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4359
4350
|
const def = schema._zod.def;
|
|
4360
|
-
process
|
|
4351
|
+
process(def.innerType, ctx, params);
|
|
4361
4352
|
const seen = ctx.seen.get(schema);
|
|
4362
4353
|
seen.ref = def.innerType;
|
|
4363
4354
|
};
|
|
4364
4355
|
var lazyProcessor = (schema, ctx, _json, params) => {
|
|
4365
4356
|
const innerType = schema._zod.innerType;
|
|
4366
|
-
process
|
|
4357
|
+
process(innerType, ctx, params);
|
|
4367
4358
|
const seen = ctx.seen.get(schema);
|
|
4368
4359
|
seen.ref = innerType;
|
|
4369
4360
|
};
|
|
@@ -5383,7 +5374,7 @@ var ZodIssueCode = {
|
|
|
5383
5374
|
var ZodFirstPartyTypeKind;
|
|
5384
5375
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5385
5376
|
//#endregion
|
|
5386
|
-
//#region ../types/dist/sleep-
|
|
5377
|
+
//#region ../types/dist/sleep-BNeXenvr.mjs
|
|
5387
5378
|
/**
|
|
5388
5379
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5389
5380
|
* window to float samples (D455).
|
|
@@ -6749,32 +6740,12 @@ object({
|
|
|
6749
6740
|
audioCodec: string().optional(),
|
|
6750
6741
|
metadata: record(string(), unknown()).readonly().optional()
|
|
6751
6742
|
});
|
|
6752
|
-
var EncodedPacketSchema = object({
|
|
6753
|
-
type: _enum(["video", "audio"]),
|
|
6754
|
-
data: _instanceof(Uint8Array),
|
|
6755
|
-
pts: number(),
|
|
6756
|
-
dts: number(),
|
|
6757
|
-
keyframe: boolean(),
|
|
6758
|
-
codec: string()
|
|
6759
|
-
});
|
|
6760
|
-
var DecodedFrameSchema = object({
|
|
6761
|
-
data: _instanceof(Uint8Array),
|
|
6762
|
-
width: number(),
|
|
6763
|
-
height: number(),
|
|
6764
|
-
format: _enum([
|
|
6765
|
-
"jpeg",
|
|
6766
|
-
"rgb",
|
|
6767
|
-
"bgr",
|
|
6768
|
-
"yuv420",
|
|
6769
|
-
"gray"
|
|
6770
|
-
]),
|
|
6771
|
-
timestamp: number()
|
|
6772
|
-
});
|
|
6773
6743
|
/**
|
|
6774
6744
|
* Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
|
|
6775
6745
|
* frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
|
|
6776
|
-
* interface in `interfaces/frame-handle.ts` field-for-field so a
|
|
6777
|
-
*
|
|
6746
|
+
* interface in `interfaces/frame-handle.ts` field-for-field so a cap method
|
|
6747
|
+
* (the runner's native-crop and pipeline-executor inputs) can carry handles
|
|
6748
|
+
* over tRPC / Moleculer.
|
|
6778
6749
|
*
|
|
6779
6750
|
* The `satisfies` assertion below pins this schema to the interface: if a
|
|
6780
6751
|
* field is added to / removed from `FrameHandle` without a matching schema
|
|
@@ -6800,50 +6771,6 @@ var FrameHandleSchema = object({
|
|
|
6800
6771
|
slotCount: number().int().positive()
|
|
6801
6772
|
});
|
|
6802
6773
|
/**
|
|
6803
|
-
* Pixel format a frame-handle subscriber can request from the broker
|
|
6804
|
-
* (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
|
|
6805
|
-
* `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
|
|
6806
|
-
* `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
|
|
6807
|
-
* payload, and no decoded-frame consumer requests it over the shm plane.
|
|
6808
|
-
*/
|
|
6809
|
-
var FrameHandleFormatSchema = _enum([
|
|
6810
|
-
"rgb",
|
|
6811
|
-
"bgr",
|
|
6812
|
-
"yuv420",
|
|
6813
|
-
"gray"
|
|
6814
|
-
]);
|
|
6815
|
-
/**
|
|
6816
|
-
* Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
|
|
6817
|
-
* broker for a `FrameHandle` stream of one `format`; the broker maintains one
|
|
6818
|
-
* shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
|
|
6819
|
-
* subscriber (motion) and an `rgb` subscriber (detection) each read the format
|
|
6820
|
-
* they asked for with no broker-side conversion.
|
|
6821
|
-
*/
|
|
6822
|
-
var SubscribeFramesInputSchema = object({
|
|
6823
|
-
brokerId: string(),
|
|
6824
|
-
format: FrameHandleFormatSchema,
|
|
6825
|
-
/**
|
|
6826
|
-
* Optional reader-side cadence hint in frames per second. The broker does
|
|
6827
|
-
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
6828
|
-
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
6829
|
-
* the consumer can pace its own `pullFrameHandles` polling.
|
|
6830
|
-
*/
|
|
6831
|
-
maxFps: number().positive().optional(),
|
|
6832
|
-
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
6833
|
-
tag: string().optional()
|
|
6834
|
-
});
|
|
6835
|
-
/**
|
|
6836
|
-
* Result of `stream-broker.subscribeFrames`. The consumer then polls
|
|
6837
|
-
* `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
|
|
6838
|
-
* `FrameHandle` to a `FrameRingReader`.
|
|
6839
|
-
*/
|
|
6840
|
-
var SubscribeFramesResultSchema = object({
|
|
6841
|
-
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
6842
|
-
subscriptionId: string(),
|
|
6843
|
-
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
6844
|
-
maxFps: number().nonnegative()
|
|
6845
|
-
});
|
|
6846
|
-
/**
|
|
6847
6774
|
* Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
|
|
6848
6775
|
* `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
|
|
6849
6776
|
* sample bytes plus the track parameters a consumer needs to interpret them.
|
|
@@ -6851,8 +6778,8 @@ var SubscribeFramesResultSchema = object({
|
|
|
6851
6778
|
* Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
|
|
6852
6779
|
* Float32 PCM), so unlike video frames they ship their bytes INLINE over
|
|
6853
6780
|
* tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
|
|
6854
|
-
* (the wire-serialisable supertype of `Buffer`)
|
|
6855
|
-
*
|
|
6781
|
+
* (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
|
|
6782
|
+
* it.
|
|
6856
6783
|
*/
|
|
6857
6784
|
var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
|
|
6858
6785
|
var DecodedAudioChunkSchema = object({
|
|
@@ -6870,11 +6797,10 @@ var DecodedAudioChunkSchema = object({
|
|
|
6870
6797
|
format: AudioChunkFormatSchema.optional()
|
|
6871
6798
|
});
|
|
6872
6799
|
/**
|
|
6873
|
-
* Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9).
|
|
6874
|
-
*
|
|
6875
|
-
*
|
|
6876
|
-
*
|
|
6877
|
-
* `pullAudioChunks`.
|
|
6800
|
+
* Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
|
|
6801
|
+
* its bytes inline: a consumer asks the broker for the decoded audio-chunk
|
|
6802
|
+
* stream of one broker; the broker keeps a per-subscription bounded FIFO queue
|
|
6803
|
+
* the consumer drains via `pullAudioChunks`.
|
|
6878
6804
|
*/
|
|
6879
6805
|
var SubscribeAudioChunksInputSchema = object({
|
|
6880
6806
|
brokerId: string(),
|
|
@@ -6905,9 +6831,7 @@ var BrokerStatsSchema = object({
|
|
|
6905
6831
|
"stopped"
|
|
6906
6832
|
]),
|
|
6907
6833
|
inputFps: number(),
|
|
6908
|
-
decodeFps: number(),
|
|
6909
6834
|
encodedSubscribers: number(),
|
|
6910
|
-
decodedSubscribers: number(),
|
|
6911
6835
|
uptimeMs: number(),
|
|
6912
6836
|
bitrateKbps: number(),
|
|
6913
6837
|
idrIntervalMs: number(),
|
|
@@ -6920,14 +6844,6 @@ var BrokerStatsSchema = object({
|
|
|
6920
6844
|
preBufferMs: number(),
|
|
6921
6845
|
preBufferPackets: number(),
|
|
6922
6846
|
/**
|
|
6923
|
-
* Moleculer node id of the decoder provider currently servicing this
|
|
6924
|
-
* stream's decoded subscribers. `null` until the deferred decoder is
|
|
6925
|
-
* created (no decoded clients yet, or codec not detected). Surfaces the
|
|
6926
|
-
* runtime decoder placement so the UI can show "Decoder: <agent>" without
|
|
6927
|
-
* a separate cap call per broker.
|
|
6928
|
-
*/
|
|
6929
|
-
decoderNodeId: string().nullable(),
|
|
6930
|
-
/**
|
|
6931
6847
|
* Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
|
|
6932
6848
|
* when the stream has no audio track or the broker is in cold start.
|
|
6933
6849
|
* `supported = false` means the codec was detected but the local
|
|
@@ -10259,70 +10175,6 @@ var StorageLocationDeclarationSchema = object({
|
|
|
10259
10175
|
"backup"
|
|
10260
10176
|
]).optional()
|
|
10261
10177
|
});
|
|
10262
|
-
var DecoderStatsSchema = object({
|
|
10263
|
-
inputFps: number(),
|
|
10264
|
-
outputFps: number(),
|
|
10265
|
-
avgDecodeTimeMs: number(),
|
|
10266
|
-
droppedFrames: number(),
|
|
10267
|
-
/**
|
|
10268
|
-
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
10269
|
-
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
10270
|
-
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
10271
|
-
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
10272
|
-
*/
|
|
10273
|
-
lagMs: number().optional(),
|
|
10274
|
-
effectiveFps: number().optional(),
|
|
10275
|
-
adaptiveFps: number().optional()
|
|
10276
|
-
});
|
|
10277
|
-
var DecoderSessionConfigSchema = object({
|
|
10278
|
-
codec: string(),
|
|
10279
|
-
maxFps: number().default(0),
|
|
10280
|
-
outputFormat: _enum([
|
|
10281
|
-
"jpeg",
|
|
10282
|
-
"rgb",
|
|
10283
|
-
"bgr",
|
|
10284
|
-
"yuv420",
|
|
10285
|
-
"gray"
|
|
10286
|
-
]).default("jpeg"),
|
|
10287
|
-
scale: number().default(1),
|
|
10288
|
-
width: number().optional(),
|
|
10289
|
-
height: number().optional(),
|
|
10290
|
-
/**
|
|
10291
|
-
* Identifier of the camera this decoder session serves. Optional
|
|
10292
|
-
* because the cap is generic (any caller could request decode), but
|
|
10293
|
-
* stream-broker passes it so decoder logs include `deviceId` for
|
|
10294
|
-
* per-camera filtering when diagnosing failures (e.g. node-av
|
|
10295
|
-
* sendPacket errors on a single hung camera).
|
|
10296
|
-
*/
|
|
10297
|
-
deviceId: number().int().nonnegative().optional(),
|
|
10298
|
-
/**
|
|
10299
|
-
* Free-form tag for log scoping. Stream-broker uses
|
|
10300
|
-
* `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
|
|
10301
|
-
* on every line so `grep tag=broker:5/high` filters one camera
|
|
10302
|
-
* profile cleanly.
|
|
10303
|
-
*/
|
|
10304
|
-
tag: string().optional(),
|
|
10305
|
-
/**
|
|
10306
|
-
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
10307
|
-
*
|
|
10308
|
-
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
10309
|
-
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
10310
|
-
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
10311
|
-
* into an OS shared-memory ring and drained as zero-pixel
|
|
10312
|
-
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
10313
|
-
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
10314
|
-
* `pullHandles` returns nothing for a `'callback'` session.
|
|
10315
|
-
*/
|
|
10316
|
-
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
10317
|
-
/**
|
|
10318
|
-
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
10319
|
-
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
10320
|
-
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
10321
|
-
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
10322
|
-
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
10323
|
-
*/
|
|
10324
|
-
debug: boolean().optional()
|
|
10325
|
-
});
|
|
10326
10178
|
/**
|
|
10327
10179
|
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
10328
10180
|
* an addon declares its channels in.
|
|
@@ -10465,6 +10317,26 @@ var ClipSearchStrictnessSchema = _enum([
|
|
|
10465
10317
|
"balanced",
|
|
10466
10318
|
"strict"
|
|
10467
10319
|
]);
|
|
10320
|
+
/** Steps whose effect a root label can BE. */
|
|
10321
|
+
var LABEL_SUPPLY_STEP_IDS = [
|
|
10322
|
+
"animal-classifier",
|
|
10323
|
+
"vehicle-classifier",
|
|
10324
|
+
"package-detection"
|
|
10325
|
+
];
|
|
10326
|
+
/** Steps whose box a root detection can BE. */
|
|
10327
|
+
var CROP_SUPPLY_STEP_IDS = ["plate-detection", "face-detection"];
|
|
10328
|
+
/**
|
|
10329
|
+
* Steps whose per-detection MASK a root detection can carry (D737). The live
|
|
10330
|
+
* mask step is `segmentation` — a frame-plane refiner that runs its own model
|
|
10331
|
+
* once per detection crop. (`instance-segmentation` is a dormant step id that
|
|
10332
|
+
* no camera tree carries.)
|
|
10333
|
+
*/
|
|
10334
|
+
var MASK_SUPPLY_STEP_IDS = ["segmentation"];
|
|
10335
|
+
[
|
|
10336
|
+
...LABEL_SUPPLY_STEP_IDS,
|
|
10337
|
+
...CROP_SUPPLY_STEP_IDS,
|
|
10338
|
+
...MASK_SUPPLY_STEP_IDS
|
|
10339
|
+
];
|
|
10468
10340
|
var MODEL_FORMATS = [
|
|
10469
10341
|
"onnx",
|
|
10470
10342
|
"coreml",
|
|
@@ -10493,7 +10365,53 @@ var ModelFormatEntrySchema = object({
|
|
|
10493
10365
|
/** Multi-file payload (directory members or sibling files). */
|
|
10494
10366
|
files: array(string()).readonly().optional(),
|
|
10495
10367
|
/** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
|
|
10496
|
-
runtimes: array(_enum(["python"])).readonly().optional()
|
|
10368
|
+
runtimes: array(_enum(["python"])).readonly().optional(),
|
|
10369
|
+
/**
|
|
10370
|
+
* Content revision of the artefact, for one hosted OFF the CamStack mirror.
|
|
10371
|
+
* A format on the mirror leaves it unset: its revision (the sha256 of its
|
|
10372
|
+
* weight file) comes from the downloader's generated table, kept in step
|
|
10373
|
+
* with the mirror by `scripts/check-hosted-model-revisions.ts` (D744). A
|
|
10374
|
+
* node records the revision it downloaded next to the file
|
|
10375
|
+
* (`<file>.revision`); a copy with no record or a different one is stale
|
|
10376
|
+
* unless its weight file hashes to the revision, and a stale copy is
|
|
10377
|
+
* downloaded again, the old one serving until the new one is complete. No
|
|
10378
|
+
* revision at all ⇒ a file on disk is current, whatever it holds (D743).
|
|
10379
|
+
*/
|
|
10380
|
+
revision: string().min(1).optional()
|
|
10381
|
+
});
|
|
10382
|
+
/**
|
|
10383
|
+
* What a node holds for one (model, format) (D746). `current` is a complete
|
|
10384
|
+
* copy at its catalog revision — exactly what `downloaded: true` means.
|
|
10385
|
+
* `stale` is bytes on disk that are not (yet) proven current under D743/D744:
|
|
10386
|
+
* no or a different revision record, or a missing sibling. `missing` is no
|
|
10387
|
+
* copy. `downloaded` alone folds `stale` into "not downloaded", which hid
|
|
10388
|
+
* reclaimable bytes and a copy about to be replaced.
|
|
10389
|
+
*/
|
|
10390
|
+
var ModelOnDiskStateSchema = _enum([
|
|
10391
|
+
"current",
|
|
10392
|
+
"stale",
|
|
10393
|
+
"missing"
|
|
10394
|
+
]);
|
|
10395
|
+
/**
|
|
10396
|
+
* The numeric precision of one format's BUILD (D746) — reported only when the
|
|
10397
|
+
* build states it (file-name token, an INT8 variant, an EdgeTPU compile, a
|
|
10398
|
+
* CamStack Core ML export). Absent means "not stated", never "fp32".
|
|
10399
|
+
*/
|
|
10400
|
+
var ModelFormatPrecisionSchema = _enum([
|
|
10401
|
+
"fp32",
|
|
10402
|
+
"fp16",
|
|
10403
|
+
"int8"
|
|
10404
|
+
]);
|
|
10405
|
+
/**
|
|
10406
|
+
* One format of one model as a node's `pipelineExecutor.getSchema` reports it.
|
|
10407
|
+
* `onDisk` / `precision` are optional for version skew: a runner that predates
|
|
10408
|
+
* them omits them, and a reader falls back to `downloaded`.
|
|
10409
|
+
*/
|
|
10410
|
+
var PipelineModelFormatStatusSchema = object({
|
|
10411
|
+
downloaded: boolean(),
|
|
10412
|
+
sizeMB: number(),
|
|
10413
|
+
onDisk: ModelOnDiskStateSchema.optional(),
|
|
10414
|
+
precision: ModelFormatPrecisionSchema.optional()
|
|
10497
10415
|
});
|
|
10498
10416
|
/**
|
|
10499
10417
|
* Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
|
|
@@ -10534,11 +10452,11 @@ var ModelVariantGroupSchema = object({
|
|
|
10534
10452
|
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
10535
10453
|
precision: _enum(["fp32", "int8"]).optional(),
|
|
10536
10454
|
/**
|
|
10537
|
-
*
|
|
10538
|
-
*
|
|
10539
|
-
*
|
|
10455
|
+
* Training / latency-variant axis. Omit ⇒ the standard build. `test` marks
|
|
10456
|
+
* an alternate training of the same family (Scrypted `relu_test`); chip
|
|
10457
|
+
* label is "Test".
|
|
10540
10458
|
*/
|
|
10541
|
-
optimization: _enum(["standard", "
|
|
10459
|
+
optimization: _enum(["standard", "test"]).optional(),
|
|
10542
10460
|
/**
|
|
10543
10461
|
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
10544
10462
|
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
@@ -10561,9 +10479,10 @@ var ModelProviderIdSchema = _enum([
|
|
|
10561
10479
|
* are per upstream. A persisted custom-model row written when this field was a
|
|
10562
10480
|
* string still parses — the string is lifted into `{ weights }`.
|
|
10563
10481
|
*
|
|
10564
|
-
* Every licence surface (the
|
|
10565
|
-
*
|
|
10566
|
-
*
|
|
10482
|
+
* Every machine licence surface (`THIRD_PARTY_MODELS.md`, the Hugging Face
|
|
10483
|
+
* model table) is GENERATED from these records by
|
|
10484
|
+
* `scripts/gen-model-licenses.ts` (D661). Operator-facing docs live in
|
|
10485
|
+
* `camstack/docs` → `/docs/detection/models`.
|
|
10567
10486
|
*/
|
|
10568
10487
|
var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
|
|
10569
10488
|
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
@@ -10596,6 +10515,46 @@ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { wei
|
|
|
10596
10515
|
/** True when the weights or their data forbid commercial use. */
|
|
10597
10516
|
nonCommercial: boolean().optional()
|
|
10598
10517
|
}));
|
|
10518
|
+
/**
|
|
10519
|
+
* One step a ROOT detector can supply from its own output (D730) — see
|
|
10520
|
+
* `ModelCatalogEntry.suppliesSteps`.
|
|
10521
|
+
*
|
|
10522
|
+
* - `label`: the root's raw class IS the step's answer. `labelToEffect` maps
|
|
10523
|
+
* the root's label to the step's vocabulary; `uncovered` is the vocabulary
|
|
10524
|
+
* the root cannot produce (shown as unavailable, never a blocker).
|
|
10525
|
+
* - `crop`: the root draws the box the step would have found (`license_plate`
|
|
10526
|
+
* for plate-detection, `face` for face-detection). The box becomes a detail
|
|
10527
|
+
* child of the `parentMacro` detection that contains it — never a track.
|
|
10528
|
+
* - `mask`: the root's decode emits an instance mask with every detection (a
|
|
10529
|
+
* YOLO26-seg root, D737) — the `segmentation` step's result, carried on the
|
|
10530
|
+
* detection in the shape that step produces. Derived from the model's
|
|
10531
|
+
* postprocessor on read, never stored by an import.
|
|
10532
|
+
*/
|
|
10533
|
+
var RootStepSupplySchema = discriminatedUnion("kind", [
|
|
10534
|
+
object({
|
|
10535
|
+
kind: literal("label"),
|
|
10536
|
+
stepId: _enum(LABEL_SUPPLY_STEP_IDS),
|
|
10537
|
+
labelToEffect: record(string(), string()),
|
|
10538
|
+
uncovered: array(string()).readonly()
|
|
10539
|
+
}),
|
|
10540
|
+
object({
|
|
10541
|
+
kind: literal("crop"),
|
|
10542
|
+
stepId: _enum(CROP_SUPPLY_STEP_IDS),
|
|
10543
|
+
rootLabels: array(string()).readonly(),
|
|
10544
|
+
parentMacro: _enum(["vehicle", "person"]),
|
|
10545
|
+
childClass: _enum(["plate", "face"]),
|
|
10546
|
+
/**
|
|
10547
|
+
* The root's smaller input side, stamped on read by `rootSuppliesOf` from
|
|
10548
|
+
* the model entry — what a step's `minRootInputPx` gate judges. Never
|
|
10549
|
+
* stored by an import.
|
|
10550
|
+
*/
|
|
10551
|
+
rootInputPx: number().int().positive().optional()
|
|
10552
|
+
}),
|
|
10553
|
+
object({
|
|
10554
|
+
kind: literal("mask"),
|
|
10555
|
+
stepId: _enum(MASK_SUPPLY_STEP_IDS)
|
|
10556
|
+
})
|
|
10557
|
+
]);
|
|
10599
10558
|
var ModelCatalogEntrySchema = object({
|
|
10600
10559
|
id: string(),
|
|
10601
10560
|
name: string(),
|
|
@@ -10661,6 +10620,35 @@ var ModelCatalogEntrySchema = object({
|
|
|
10661
10620
|
*/
|
|
10662
10621
|
defaultConfidence: number().min(0).max(1).optional(),
|
|
10663
10622
|
/**
|
|
10623
|
+
* Per-MODEL default of the per-class confidence floors (`minConfidence<Macro>`
|
|
10624
|
+
* on a detector step), keyed by macro class: `{ person: 0.45 }`. A class
|
|
10625
|
+
* absent here runs at the step slider's global default. The same row shape
|
|
10626
|
+
* the floor measurement produces (`model-floors.json`: modelId → floors), so
|
|
10627
|
+
* a measured table is copied in as-is. An operator override is bound to the
|
|
10628
|
+
* model it was made for and does not survive a model change (D738). A custom
|
|
10629
|
+
* / imported model may declare it; without it, the global defaults apply.
|
|
10630
|
+
*/
|
|
10631
|
+
defaultClassFloors: record(string(), number().min(0).max(1)).optional(),
|
|
10632
|
+
/**
|
|
10633
|
+
* The inference precision this model's OpenVINO IR needs on an Intel GPU.
|
|
10634
|
+
* Absent ⇒ the device default (f16 on a GPU). Set to `'f32'` for an FP32 IR
|
|
10635
|
+
* that produces invalid output at f16 — measured 2026-10-07 for YOLO26
|
|
10636
|
+
* base IRs (`yolo26s`: 0 true / 146 false positives; `yolo26m*`: NaN
|
|
10637
|
+
* scores) on the Core Ultra iGPU and the N100. Threaded to the pool as
|
|
10638
|
+
* `PoolModelConfig.gpuInferencePrecision`; the pool sets
|
|
10639
|
+
* INFERENCE_PRECISION_HINT on a GPU only. Costs ~60% latency on the GPU.
|
|
10640
|
+
*/
|
|
10641
|
+
gpuInferencePrecision: _enum(["f32", "f16"]).optional(),
|
|
10642
|
+
/**
|
|
10643
|
+
* ROOT detectors only: per supplied step (`animal-classifier`, …), the
|
|
10644
|
+
* minimum DETECTION score from which this root's own fine label (species,
|
|
10645
|
+
* vehicle type — D736 root supply) is taken (D741). Below it the detection
|
|
10646
|
+
* keeps its macro class without the fine label. `1` = never take it from
|
|
10647
|
+
* this root. Absent ⇒ no floor beyond the macro floor. The supplied step's
|
|
10648
|
+
* `minConfidence` slider, bound to the ROOT model, overrides it.
|
|
10649
|
+
*/
|
|
10650
|
+
suppliedLabelFloors: record(string(), number().min(0).max(1)).optional(),
|
|
10651
|
+
/**
|
|
10664
10652
|
* When true, the executor produces a landmark-aligned crop (similarity warp
|
|
10665
10653
|
* onto the canonical template) before this step runs, instead of a plain
|
|
10666
10654
|
* axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
|
|
@@ -10722,6 +10710,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
10722
10710
|
*/
|
|
10723
10711
|
classMap: DetectionCatalogClassMapSchema.optional(),
|
|
10724
10712
|
/**
|
|
10713
|
+
* The pipeline steps this ROOT detector can supply from its own output
|
|
10714
|
+
* (D730): its labels name a step's effect (`deer` for `animal-classifier`)
|
|
10715
|
+
* or draw a step's box (`license_plate` for `plate-detection`). Computed at
|
|
10716
|
+
* import from THIS model's labelmap by `deriveRootSupplies`, never written
|
|
10717
|
+
* by hand and never assumed per family; a row without it is derived on read
|
|
10718
|
+
* (`rootSuppliesOf`). Whether a camera uses it is the step's own
|
|
10719
|
+
* `useRootModel` flag (`resolveRootSupply`).
|
|
10720
|
+
*/
|
|
10721
|
+
suppliesSteps: array(RootStepSupplySchema).readonly().optional(),
|
|
10722
|
+
/**
|
|
10725
10723
|
* The model's CLIP feature-space contract (text encoder, dimension, vector
|
|
10726
10724
|
* index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
|
|
10727
10725
|
* CLIP image encoder; its absence is what "not a CLIP model" means to the
|
|
@@ -12360,13 +12358,6 @@ var BrokerRtspClientSchema = object({
|
|
|
12360
12358
|
lastRtpAt: number(),
|
|
12361
12359
|
bytesSent: number()
|
|
12362
12360
|
});
|
|
12363
|
-
var BrokerDecodedClientSchema = object({
|
|
12364
|
-
tag: string(),
|
|
12365
|
-
subscribedAt: number(),
|
|
12366
|
-
maxFps: number(),
|
|
12367
|
-
framesDelivered: number(),
|
|
12368
|
-
framesDropped: number()
|
|
12369
|
-
});
|
|
12370
12361
|
var BrokerAudioClientSchema = object({
|
|
12371
12362
|
tag: string(),
|
|
12372
12363
|
subscribedAt: number(),
|
|
@@ -12458,7 +12449,6 @@ var BrokerEncodedClientSchema = object({
|
|
|
12458
12449
|
});
|
|
12459
12450
|
var BrokerClientsSchema = object({
|
|
12460
12451
|
rtsp: array(BrokerRtspClientSchema).readonly(),
|
|
12461
|
-
decoded: array(BrokerDecodedClientSchema).readonly(),
|
|
12462
12452
|
audio: array(BrokerAudioClientSchema).readonly(),
|
|
12463
12453
|
encoded: array(BrokerEncodedClientSchema).readonly(),
|
|
12464
12454
|
pipeClients: number(),
|
|
@@ -12827,11 +12817,7 @@ method(object({
|
|
|
12827
12817
|
auth: "admin"
|
|
12828
12818
|
}), method(object({ brokerId: string() }), BrokerClientsSchema), method(object({
|
|
12829
12819
|
brokerId: string(),
|
|
12830
|
-
channel: _enum([
|
|
12831
|
-
"rtsp",
|
|
12832
|
-
"decoded",
|
|
12833
|
-
"audio"
|
|
12834
|
-
]),
|
|
12820
|
+
channel: _enum(["rtsp", "audio"]),
|
|
12835
12821
|
handle: string()
|
|
12836
12822
|
}), object({ killed: boolean() }), {
|
|
12837
12823
|
kind: "mutation",
|
|
@@ -12866,10 +12852,7 @@ method(object({
|
|
|
12866
12852
|
}), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
|
|
12867
12853
|
subscriptionId: string(),
|
|
12868
12854
|
maxCount: number().int().positive().default(8)
|
|
12869
|
-
}), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(
|
|
12870
|
-
subscriptionId: string(),
|
|
12871
|
-
maxCount: number().int().positive().default(4)
|
|
12872
|
-
}), array(FrameHandleSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
|
|
12855
|
+
}), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
|
|
12873
12856
|
brokerId: string(),
|
|
12874
12857
|
seconds: number().min(0).max(30)
|
|
12875
12858
|
}), _void(), {
|
|
@@ -14086,72 +14069,18 @@ var DEFAULT_DECODER_HWACCEL_CONFIG = {
|
|
|
14086
14069
|
hwaccel: "auto",
|
|
14087
14070
|
probedBestHwaccel: ""
|
|
14088
14071
|
};
|
|
14089
|
-
/**
|
|
14090
|
-
* shm ring usage stats for a `frameSink: 'shm'` decoder session —
|
|
14091
|
-
* exposed via `decoder.getShmStats` so downstream consumers can
|
|
14092
|
-
* observe ring pressure (slot count, byte budget, hit/miss ratio).
|
|
14093
|
-
*/
|
|
14094
|
-
var ShmRingStatsSchema = object({
|
|
14095
|
-
sessionId: string(),
|
|
14096
|
-
slotCount: number().int(),
|
|
14097
|
-
slotByteLength: number().int(),
|
|
14098
|
-
segmentBytes: number().int(),
|
|
14099
|
-
budgetMb: number().int(),
|
|
14100
|
-
framesWritten: number().int(),
|
|
14101
|
-
getFrameHits: number().int(),
|
|
14102
|
-
getFrameMisses: number().int()
|
|
14103
|
-
});
|
|
14104
14072
|
var decoderCapability = {
|
|
14105
14073
|
name: "decoder",
|
|
14106
14074
|
scope: "system",
|
|
14107
14075
|
mode: "singleton",
|
|
14108
14076
|
preferredProvider: "decoder-nodeav",
|
|
14109
14077
|
methods: {
|
|
14110
|
-
supportsCodec: method(object({ codec: string() }), boolean()),
|
|
14111
14078
|
getInfo: method(_void(), object({
|
|
14112
14079
|
id: string(),
|
|
14113
14080
|
name: string(),
|
|
14114
|
-
isPullMode: boolean().optional(),
|
|
14115
|
-
priority: number().optional(),
|
|
14116
14081
|
hwaccel: string().optional(),
|
|
14117
14082
|
probedBestHwaccel: string().optional()
|
|
14118
14083
|
})),
|
|
14119
|
-
createSession: method(DecoderSessionConfigSchema, object({
|
|
14120
|
-
sessionId: string(),
|
|
14121
|
-
nodeId: string()
|
|
14122
|
-
})),
|
|
14123
|
-
destroySession: method(object({ sessionId: string() }), _void()),
|
|
14124
|
-
pushPacket: method(object({
|
|
14125
|
-
sessionId: string(),
|
|
14126
|
-
packet: EncodedPacketSchema
|
|
14127
|
-
}), _void()),
|
|
14128
|
-
openStream: method(object({
|
|
14129
|
-
sessionId: string(),
|
|
14130
|
-
url: string()
|
|
14131
|
-
}), _void()),
|
|
14132
|
-
pullFrames: method(object({
|
|
14133
|
-
sessionId: string(),
|
|
14134
|
-
maxCount: number().default(1),
|
|
14135
|
-
waitMs: number().optional()
|
|
14136
|
-
}), array(DecodedFrameSchema)),
|
|
14137
|
-
pullHandles: method(object({
|
|
14138
|
-
sessionId: string(),
|
|
14139
|
-
maxCount: number().default(1),
|
|
14140
|
-
waitMs: number().optional()
|
|
14141
|
-
}), array(FrameHandleSchema)),
|
|
14142
|
-
getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
|
|
14143
|
-
getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
|
|
14144
|
-
updateConfig: method(object({
|
|
14145
|
-
sessionId: string(),
|
|
14146
|
-
config: DecoderSessionConfigSchema.partial()
|
|
14147
|
-
}), _void()),
|
|
14148
|
-
getStats: method(object({ sessionId: string() }), DecoderStatsSchema),
|
|
14149
|
-
listActiveSessions: method(_void(), array(object({
|
|
14150
|
-
sessionId: string(),
|
|
14151
|
-
codec: string(),
|
|
14152
|
-
outputFormat: string(),
|
|
14153
|
-
createdAtMs: number()
|
|
14154
|
-
})).readonly()),
|
|
14155
14084
|
/**
|
|
14156
14085
|
* Re-run the host platform-probe for the decoder's hwaccel backend
|
|
14157
14086
|
* and write the detected value into the addon's own global
|
|
@@ -14222,7 +14151,7 @@ DeviceType.Camera;
|
|
|
14222
14151
|
* This matters here more than anywhere else in the repo, because every provider
|
|
14223
14152
|
* that knows a device's host knows its PASSWORD too: `{ host, port, username,
|
|
14224
14153
|
* password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
|
|
14225
|
-
* and `http://admin:hunter2@192.168.
|
|
14154
|
+
* and `http://admin:hunter2@192.168.60.139/` is a URL a browser accepts. The
|
|
14226
14155
|
* camera's own page will ask for its own login. That is correct, and pre-
|
|
14227
14156
|
* filling it is the operator's business, not ours.
|
|
14228
14157
|
*
|
|
@@ -16010,6 +15939,12 @@ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({
|
|
|
16010
15939
|
kind: "mutation",
|
|
16011
15940
|
auth: "admin"
|
|
16012
15941
|
});
|
|
15942
|
+
var HUB_ADDRESS_ROLES = [
|
|
15943
|
+
"appConnections",
|
|
15944
|
+
"notificationTaps",
|
|
15945
|
+
"internal",
|
|
15946
|
+
"publicEndpoint"
|
|
15947
|
+
];
|
|
16013
15948
|
/**
|
|
16014
15949
|
* Transport bounds for AI capability methods.
|
|
16015
15950
|
*
|
|
@@ -17206,7 +17141,7 @@ method(object({
|
|
|
17206
17141
|
timeoutMs: 6e5
|
|
17207
17142
|
});
|
|
17208
17143
|
method(object({
|
|
17209
|
-
|
|
17144
|
+
targetNodeId: string(),
|
|
17210
17145
|
modelId: string(),
|
|
17211
17146
|
format: _enum(MODEL_FORMATS),
|
|
17212
17147
|
entry: ModelCatalogEntrySchema
|
|
@@ -18574,6 +18509,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
18574
18509
|
"stream-offline",
|
|
18575
18510
|
"node-online",
|
|
18576
18511
|
"node-offline",
|
|
18512
|
+
"node-restarting",
|
|
18513
|
+
"node-updating",
|
|
18577
18514
|
"node-inference-unavailable",
|
|
18578
18515
|
"detection-blind",
|
|
18579
18516
|
"addon-crash-loop",
|
|
@@ -19672,8 +19609,9 @@ var NcRuleSchema = NcRuleInputSchema.extend({
|
|
|
19672
19609
|
disabledTargetIds: array(string()).default([])
|
|
19673
19610
|
});
|
|
19674
19611
|
/**
|
|
19675
|
-
* The state-summary SECTIONS (D647 / D698): the D637 kind groups that
|
|
19676
|
-
* still-true state. `
|
|
19612
|
+
* The state-summary SECTIONS (D647 / D698 / D708): the D637 kind groups that
|
|
19613
|
+
* report a still-true state. `addons` is the crash-looped addons only and
|
|
19614
|
+
* `addon-updates` the addons that are behind (D708 split them). `backups-exports` and `alarm` are not sections — a completed
|
|
19677
19615
|
* job is not an open state, and the alarm stays instant.
|
|
19678
19616
|
*/
|
|
19679
19617
|
var NcStateSummarySectionIdSchema = _enum([
|
|
@@ -19685,6 +19623,7 @@ var NcStateSummarySectionIdSchema = _enum([
|
|
|
19685
19623
|
"detection-blind",
|
|
19686
19624
|
"nodes",
|
|
19687
19625
|
"addons",
|
|
19626
|
+
"addon-updates",
|
|
19688
19627
|
"updates"
|
|
19689
19628
|
]);
|
|
19690
19629
|
/** One section of a previewed state summary. `unavailable` = its state could not be read. */
|
|
@@ -20165,7 +20104,25 @@ var NcAlarmModeDelaysListSchema = array(object({
|
|
|
20165
20104
|
* When this mode may be armed. Absent / null = always allowed (same as a
|
|
20166
20105
|
* rule with no `schedule`). Evaluated with {@link isScheduleActive}.
|
|
20167
20106
|
*/
|
|
20168
|
-
schedule: NcScheduleSchema.optional()
|
|
20107
|
+
schedule: NcScheduleSchema.optional(),
|
|
20108
|
+
/**
|
|
20109
|
+
* D706: while this mode's ENTRY DELAY runs, do the alarm rules' own pushes go out (as an
|
|
20110
|
+
* intrusion naming the locations)? Absent = false — the entry-delay notification is the
|
|
20111
|
+
* only one, and the matches ride the triggered notification's mosaic (D705).
|
|
20112
|
+
*/
|
|
20113
|
+
notifyDuringEntryDelay: boolean().optional(),
|
|
20114
|
+
/**
|
|
20115
|
+
* D706: notify "watched again" when a sensor this arm bypassed closes and opens again.
|
|
20116
|
+
* Absent = false (logged, not sent). Even when true it is not sent when that same sensor
|
|
20117
|
+
* is the one that starts the entry delay — the entry-delay notification already names it.
|
|
20118
|
+
*/
|
|
20119
|
+
notifySensorRewatched: boolean().optional(),
|
|
20120
|
+
/**
|
|
20121
|
+
* D735: notify "ignored" when a sensor this arm bypassed really MOVED while armed (its own
|
|
20122
|
+
* change time is after the arm) and set nothing off. Absent = false (logged, not sent). A
|
|
20123
|
+
* re-sync, a recovery from `unavailable` or a duplicate is never announced, switch or not.
|
|
20124
|
+
*/
|
|
20125
|
+
notifySensorIgnored: boolean().optional()
|
|
20169
20126
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
|
|
20170
20127
|
var NcAlarmModeDelaysEditListSchema = array(object({
|
|
20171
20128
|
mode: AlarmArmModeSchema,
|
|
@@ -20174,7 +20131,13 @@ var NcAlarmModeDelaysEditListSchema = array(object({
|
|
|
20174
20131
|
triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
|
|
20175
20132
|
postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
|
|
20176
20133
|
/** Set a schedule, or `null` to clear (mode always allowed again). */
|
|
20177
|
-
schedule: NcScheduleSchema.nullable().optional()
|
|
20134
|
+
schedule: NcScheduleSchema.nullable().optional(),
|
|
20135
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20136
|
+
notifyDuringEntryDelay: boolean().nullable().optional(),
|
|
20137
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20138
|
+
notifySensorRewatched: boolean().nullable().optional(),
|
|
20139
|
+
/** D735: set the switch, or `null` to clear it (back to the default, false). */
|
|
20140
|
+
notifySensorIgnored: boolean().nullable().optional()
|
|
20178
20141
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
|
|
20179
20142
|
/** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
|
|
20180
20143
|
var NcAlarmNonBlockingEditSchema = object({
|
|
@@ -21135,45 +21098,25 @@ Object.freeze(Object.fromEntries([{
|
|
|
21135
21098
|
label: "Face recognition model",
|
|
21136
21099
|
defaultModelId: "auraface-r100",
|
|
21137
21100
|
indexName: "the enrolled face gallery",
|
|
21138
|
-
options: [
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
},
|
|
21143
|
-
{
|
|
21144
|
-
id: "arcface-r100",
|
|
21145
|
-
label: "ArcFace (ResNet34)"
|
|
21146
|
-
},
|
|
21147
|
-
{
|
|
21148
|
-
id: "inception-resnet-v1",
|
|
21149
|
-
label: "Inception ResNet V1"
|
|
21150
|
-
}
|
|
21151
|
-
]
|
|
21101
|
+
options: [{
|
|
21102
|
+
id: "auraface-r100",
|
|
21103
|
+
label: "AuraFace R100"
|
|
21104
|
+
}]
|
|
21152
21105
|
}, {
|
|
21153
21106
|
stepId: "clip-embedding",
|
|
21154
21107
|
label: "Semantic search model",
|
|
21155
|
-
defaultModelId: "
|
|
21108
|
+
defaultModelId: "siglip2-b16-224",
|
|
21156
21109
|
indexName: "the object semantic-search index",
|
|
21157
|
-
options: [
|
|
21158
|
-
|
|
21159
|
-
|
|
21160
|
-
|
|
21161
|
-
},
|
|
21162
|
-
{
|
|
21163
|
-
id: "mobileclip-s2",
|
|
21164
|
-
label: "MobileCLIP S2"
|
|
21165
|
-
},
|
|
21166
|
-
{
|
|
21167
|
-
id: "siglip2-b16-224",
|
|
21168
|
-
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21169
|
-
}
|
|
21170
|
-
]
|
|
21110
|
+
options: [{
|
|
21111
|
+
id: "siglip2-b16-224",
|
|
21112
|
+
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21113
|
+
}]
|
|
21171
21114
|
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
21172
21115
|
var CLUSTER_STEP_SETTING_FIELDS = [{
|
|
21173
21116
|
stepId: "face-embedding",
|
|
21174
21117
|
key: "minLandmarkFaceSize",
|
|
21175
|
-
label: "Min face size
|
|
21176
|
-
description: "
|
|
21118
|
+
label: "Min face size to embed (detection-frame px)",
|
|
21119
|
+
description: "Faces smaller than this in the small frame the detector sees are not embedded, so they can never be recognised. Below it the face cannot be aligned reliably and a bad embedding matches everyone. Same value on every node. 0 = no limit.",
|
|
21177
21120
|
type: "slider",
|
|
21178
21121
|
min: 0,
|
|
21179
21122
|
max: 64,
|
|
@@ -23093,6 +23036,34 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
23093
23036
|
timestamp: number()
|
|
23094
23037
|
});
|
|
23095
23038
|
/**
|
|
23039
|
+
* A root model's boxes standing in for one crop-kind step's own inference
|
|
23040
|
+
* (D730) — see `runPipeline.suppliedRoot`.
|
|
23041
|
+
*/
|
|
23042
|
+
var SuppliedRootOutputSchema = object({
|
|
23043
|
+
stepId: string(),
|
|
23044
|
+
/** The root model that drew the boxes — stamped on the result. */
|
|
23045
|
+
modelId: string(),
|
|
23046
|
+
detections: array(object({
|
|
23047
|
+
/** `[x1, y1, x2, y2]` in the pixels of the call's image. */
|
|
23048
|
+
bbox: tuple([
|
|
23049
|
+
number(),
|
|
23050
|
+
number(),
|
|
23051
|
+
number(),
|
|
23052
|
+
number()
|
|
23053
|
+
]),
|
|
23054
|
+
score: number(),
|
|
23055
|
+
/**
|
|
23056
|
+
* Five facial landmarks in the same pixels — a face the step's own
|
|
23057
|
+
* detector already re-found inside a root face box (D730), so its
|
|
23058
|
+
* embedder child aligns it exactly as it aligns any detected face.
|
|
23059
|
+
*/
|
|
23060
|
+
landmarks: array(object({
|
|
23061
|
+
x: number(),
|
|
23062
|
+
y: number()
|
|
23063
|
+
})).optional()
|
|
23064
|
+
})).min(1)
|
|
23065
|
+
});
|
|
23066
|
+
/**
|
|
23096
23067
|
* Reference to the frame's retained NATIVE surface + the parent crop's placement
|
|
23097
23068
|
* within the frame, so the executor can re-cut a leaf child ROI at native
|
|
23098
23069
|
* resolution on the detail plane. See the `runPipeline` `nativeCropRef` field.
|
|
@@ -23140,7 +23111,7 @@ var FrameRefSchema = object({
|
|
|
23140
23111
|
timestamp: number(),
|
|
23141
23112
|
capturedAt: number().optional()
|
|
23142
23113
|
});
|
|
23143
|
-
var ModelFormatSchema$
|
|
23114
|
+
var ModelFormatSchema$2 = _enum([
|
|
23144
23115
|
"onnx",
|
|
23145
23116
|
"coreml",
|
|
23146
23117
|
"openvino",
|
|
@@ -23158,7 +23129,7 @@ var PipelineSlotSchema = _enum([
|
|
|
23158
23129
|
var PipelineEngineChoiceSchema = object({
|
|
23159
23130
|
runtime: literal("python"),
|
|
23160
23131
|
backend: string(),
|
|
23161
|
-
format: ModelFormatSchema$
|
|
23132
|
+
format: ModelFormatSchema$2,
|
|
23162
23133
|
device: string().optional()
|
|
23163
23134
|
});
|
|
23164
23135
|
/**
|
|
@@ -23197,32 +23168,15 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
23197
23168
|
group: string().optional(),
|
|
23198
23169
|
settings: record(string(), unknown()).optional()
|
|
23199
23170
|
}));
|
|
23200
|
-
var PipelineTemplateStepSchema = lazy(() => object({
|
|
23201
|
-
addonId: string(),
|
|
23202
|
-
enabled: boolean(),
|
|
23203
|
-
modelId: string(),
|
|
23204
|
-
children: array(PipelineTemplateStepSchema).readonly(),
|
|
23205
|
-
settings: record(string(), unknown()).optional()
|
|
23206
|
-
}));
|
|
23207
|
-
var PipelineTemplateSchema$1 = object({
|
|
23208
|
-
id: string(),
|
|
23209
|
-
name: string(),
|
|
23210
|
-
createdAt: string(),
|
|
23211
|
-
updatedAt: string(),
|
|
23212
|
-
engine: PipelineEngineChoiceSchema,
|
|
23213
|
-
steps: array(PipelineTemplateStepSchema).readonly()
|
|
23214
|
-
});
|
|
23215
23171
|
var PipelineModelOptionSchema = object({
|
|
23216
23172
|
id: string(),
|
|
23217
23173
|
name: string(),
|
|
23218
|
-
formats: record(string(),
|
|
23219
|
-
downloaded: boolean(),
|
|
23220
|
-
sizeMB: number()
|
|
23221
|
-
})),
|
|
23174
|
+
formats: record(string(), PipelineModelFormatStatusSchema),
|
|
23222
23175
|
group: ModelVariantGroupSchema.optional(),
|
|
23223
23176
|
legacy: boolean().optional(),
|
|
23224
23177
|
provider: ModelProviderIdSchema.optional(),
|
|
23225
|
-
defaultConfidence: number().optional()
|
|
23178
|
+
defaultConfidence: number().optional(),
|
|
23179
|
+
suppliesSteps: array(RootStepSupplySchema).readonly().optional()
|
|
23226
23180
|
});
|
|
23227
23181
|
var ConfigFieldBridge = custom();
|
|
23228
23182
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -23384,6 +23338,22 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23384
23338
|
}), {
|
|
23385
23339
|
kind: "mutation",
|
|
23386
23340
|
auth: "admin"
|
|
23341
|
+
}), method(object({
|
|
23342
|
+
nodeId: string(),
|
|
23343
|
+
deviceIds: array(number()).readonly().optional(),
|
|
23344
|
+
dryRun: boolean()
|
|
23345
|
+
}), object({
|
|
23346
|
+
entries: array(object({
|
|
23347
|
+
deviceId: number(),
|
|
23348
|
+
keys: array(string()).readonly()
|
|
23349
|
+
})).readonly(),
|
|
23350
|
+
refused: array(object({
|
|
23351
|
+
deviceId: number(),
|
|
23352
|
+
reason: string()
|
|
23353
|
+
})).readonly()
|
|
23354
|
+
}), {
|
|
23355
|
+
kind: "mutation",
|
|
23356
|
+
auth: "admin"
|
|
23387
23357
|
}), method(object({ nodeId: string() }), object({ unhealthy: array(object({
|
|
23388
23358
|
/** `<backend>:<device>`, e.g. `openvino:gpu`. */
|
|
23389
23359
|
deviceKey: string(),
|
|
@@ -23409,22 +23379,14 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23409
23379
|
}), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({
|
|
23410
23380
|
steps: array(PipelineStepInputSchema),
|
|
23411
23381
|
deviceKey: string().optional()
|
|
23412
|
-
}), PipelineValidationResultSchema), method(_void(),
|
|
23413
|
-
name: string(),
|
|
23414
|
-
steps: array(PipelineTemplateStepSchema).readonly(),
|
|
23415
|
-
engine: PipelineEngineChoiceSchema
|
|
23416
|
-
}), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({
|
|
23417
|
-
id: string(),
|
|
23418
|
-
name: string().optional(),
|
|
23419
|
-
steps: array(PipelineTemplateStepSchema).readonly().optional()
|
|
23420
|
-
}), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({ id: string() }), _void(), { kind: "mutation" }), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
|
|
23382
|
+
}), PipelineValidationResultSchema), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
|
|
23421
23383
|
addonId: string(),
|
|
23422
23384
|
modelId: string(),
|
|
23423
|
-
format: ModelFormatSchema$
|
|
23385
|
+
format: ModelFormatSchema$2
|
|
23424
23386
|
}), DownloadModelResultSchema, { kind: "mutation" }), method(object({
|
|
23425
23387
|
addonId: string(),
|
|
23426
23388
|
modelId: string(),
|
|
23427
|
-
format: ModelFormatSchema$
|
|
23389
|
+
format: ModelFormatSchema$2
|
|
23428
23390
|
}), object({ success: literal(true) }), { kind: "mutation" }), method(object({
|
|
23429
23391
|
steps: array(PipelineStepInputSchema).min(1),
|
|
23430
23392
|
frame: FrameInputSchema.optional(),
|
|
@@ -23490,7 +23452,16 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23490
23452
|
* `device-not-servable`), and nothing reaches the camera's live
|
|
23491
23453
|
* trace stream. Absent ⇒ unchanged behaviour.
|
|
23492
23454
|
*/
|
|
23493
|
-
replay: literal(true).optional()
|
|
23455
|
+
replay: literal(true).optional(),
|
|
23456
|
+
/**
|
|
23457
|
+
* Root-supplied detections for the tree's ROOT step (D730). Set by
|
|
23458
|
+
* `runDetailSubtree` when the camera's root model supplies a crop-kind
|
|
23459
|
+
* step (a Frigate+ `license_plate` for `plate-detection`): the step is
|
|
23460
|
+
* NOT inferred, these boxes are its output, and its children (the plate
|
|
23461
|
+
* reader) run on them through their own crop policy. Boxes are in the
|
|
23462
|
+
* PIXELS of the image this call carries. Absent ⇒ the root step runs.
|
|
23463
|
+
*/
|
|
23464
|
+
suppliedRoot: SuppliedRootOutputSchema.optional()
|
|
23494
23465
|
}), PipelineRunResultBridge, { kind: "mutation" }), method(object({
|
|
23495
23466
|
steps: array(PipelineStepInputSchema).min(1),
|
|
23496
23467
|
frames: array(FrameInputSchema).min(1).max(255),
|
|
@@ -23572,6 +23543,375 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23572
23543
|
filename: string().optional(),
|
|
23573
23544
|
settings: record(string(), unknown()).optional()
|
|
23574
23545
|
}), AudioTestResultSchema, { kind: "mutation" }), method(_void(), ConfigUISchemaNullableBridge);
|
|
23546
|
+
/**
|
|
23547
|
+
* The wire shapes of the Auto tune plan and apply (D745), used by the
|
|
23548
|
+
* `pipeline-orchestrator` cap. The recommender's own types
|
|
23549
|
+
* (`recommend.ts`) are the TS face of the same data; these
|
|
23550
|
+
* schemas are what crosses the cap boundary.
|
|
23551
|
+
*/
|
|
23552
|
+
var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
|
|
23553
|
+
/** The operator's range for it. */
|
|
23554
|
+
var MIN_SESSIONS_PER_DEVICE_RANGE = {
|
|
23555
|
+
min: 1,
|
|
23556
|
+
max: 8
|
|
23557
|
+
};
|
|
23558
|
+
var HardwareClassIdSchema = _enum([
|
|
23559
|
+
"intel-core-ultra",
|
|
23560
|
+
"intel-n-series",
|
|
23561
|
+
"apple-m-base",
|
|
23562
|
+
"apple-m-pro",
|
|
23563
|
+
"unknown"
|
|
23564
|
+
]);
|
|
23565
|
+
var AcceleratorKindSchema = _enum([
|
|
23566
|
+
"intel-npu",
|
|
23567
|
+
"intel-igpu",
|
|
23568
|
+
"x86-cpu",
|
|
23569
|
+
"arm-cpu",
|
|
23570
|
+
"apple-ane",
|
|
23571
|
+
"coral"
|
|
23572
|
+
]);
|
|
23573
|
+
var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
23574
|
+
object({
|
|
23575
|
+
kind: literal("capacity"),
|
|
23576
|
+
sessions: number(),
|
|
23577
|
+
minSessions: number()
|
|
23578
|
+
}),
|
|
23579
|
+
object({
|
|
23580
|
+
kind: literal("tie-faster"),
|
|
23581
|
+
over: string(),
|
|
23582
|
+
recallDelta: number()
|
|
23583
|
+
}),
|
|
23584
|
+
object({
|
|
23585
|
+
kind: literal("stepped-down"),
|
|
23586
|
+
from: string(),
|
|
23587
|
+
to: string(),
|
|
23588
|
+
limitedBy: string()
|
|
23589
|
+
}),
|
|
23590
|
+
object({
|
|
23591
|
+
kind: literal("no-fit-lightest"),
|
|
23592
|
+
sessions: number(),
|
|
23593
|
+
minSessions: number()
|
|
23594
|
+
}),
|
|
23595
|
+
object({
|
|
23596
|
+
kind: literal("derived-latency"),
|
|
23597
|
+
modelId: string()
|
|
23598
|
+
}),
|
|
23599
|
+
object({ kind: literal("cpu-device") }),
|
|
23600
|
+
object({ kind: literal("coral-untouched") }),
|
|
23601
|
+
object({ kind: literal("device-disabled") }),
|
|
23602
|
+
object({ kind: literal("device-unavailable") }),
|
|
23603
|
+
object({ kind: literal("node-offline") }),
|
|
23604
|
+
object({ kind: literal("no-hardware-facts") }),
|
|
23605
|
+
object({
|
|
23606
|
+
kind: literal("no-measured-data"),
|
|
23607
|
+
hardwareClass: HardwareClassIdSchema
|
|
23608
|
+
}),
|
|
23609
|
+
object({
|
|
23610
|
+
kind: literal("no-family-build"),
|
|
23611
|
+
format: ModelFormatSchema$1
|
|
23612
|
+
})
|
|
23613
|
+
]);
|
|
23614
|
+
var DeviceModelChoiceSchema = object({
|
|
23615
|
+
modelId: string(),
|
|
23616
|
+
p50Ms: number(),
|
|
23617
|
+
fps: number(),
|
|
23618
|
+
measured: boolean(),
|
|
23619
|
+
/** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
|
|
23620
|
+
capacitySessions: number(),
|
|
23621
|
+
personRecall: number()
|
|
23622
|
+
});
|
|
23623
|
+
var DeviceRecommendationSchema = object({
|
|
23624
|
+
deviceKey: string(),
|
|
23625
|
+
accelerator: AcceleratorKindSchema.nullable(),
|
|
23626
|
+
enabled: boolean(),
|
|
23627
|
+
available: boolean(),
|
|
23628
|
+
chosen: DeviceModelChoiceSchema.nullable(),
|
|
23629
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
23630
|
+
});
|
|
23631
|
+
var NodeRecommendationSchema = object({
|
|
23632
|
+
nodeId: string(),
|
|
23633
|
+
reachable: boolean(),
|
|
23634
|
+
hardwareClass: HardwareClassIdSchema.nullable(),
|
|
23635
|
+
cpuModel: string().nullable(),
|
|
23636
|
+
skipped: boolean(),
|
|
23637
|
+
acceleratorModelId: string().nullable(),
|
|
23638
|
+
devices: array(DeviceRecommendationSchema).readonly(),
|
|
23639
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
23640
|
+
});
|
|
23641
|
+
var ModelPinChangeSchema = object({
|
|
23642
|
+
stepId: string(),
|
|
23643
|
+
before: string().nullable(),
|
|
23644
|
+
after: string().nullable()
|
|
23645
|
+
});
|
|
23646
|
+
var NodeDeviceChangeSchema = object({
|
|
23647
|
+
nodeId: string(),
|
|
23648
|
+
deviceKey: string(),
|
|
23649
|
+
madeExplicit: boolean(),
|
|
23650
|
+
modelPins: array(ModelPinChangeSchema).readonly(),
|
|
23651
|
+
thresholds: array(object({
|
|
23652
|
+
stepId: string(),
|
|
23653
|
+
keys: array(string()).readonly()
|
|
23654
|
+
})).readonly()
|
|
23655
|
+
});
|
|
23656
|
+
var CameraOverrideClearSchema = object({
|
|
23657
|
+
deviceId: number(),
|
|
23658
|
+
source: _enum(["step-override", "wholesale"]),
|
|
23659
|
+
nodeId: string(),
|
|
23660
|
+
deviceKey: string().nullable(),
|
|
23661
|
+
stepId: string(),
|
|
23662
|
+
modelIdBefore: string().nullable(),
|
|
23663
|
+
keys: array(string()).readonly()
|
|
23664
|
+
});
|
|
23665
|
+
var DetectorStoreClearSchema = object({
|
|
23666
|
+
deviceId: number(),
|
|
23667
|
+
keys: array(string()).readonly()
|
|
23668
|
+
});
|
|
23669
|
+
var AutoTuneWarningSchema = discriminatedUnion("kind", [
|
|
23670
|
+
object({
|
|
23671
|
+
kind: literal("detector-store-cluster-wide"),
|
|
23672
|
+
skippedNodes: array(string()).readonly()
|
|
23673
|
+
}),
|
|
23674
|
+
object({
|
|
23675
|
+
kind: literal("detector-store-unreadable"),
|
|
23676
|
+
error: string()
|
|
23677
|
+
}),
|
|
23678
|
+
object({
|
|
23679
|
+
kind: literal("coral-enabled"),
|
|
23680
|
+
nodeId: string(),
|
|
23681
|
+
deviceKey: string()
|
|
23682
|
+
})
|
|
23683
|
+
]);
|
|
23684
|
+
var AutoTuneOptionsSchema = object({
|
|
23685
|
+
/** Share of each accelerator's throughput kept free (0.3–0.5). */
|
|
23686
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23687
|
+
/**
|
|
23688
|
+
* The least number of parallel sessions every device must sustain at the
|
|
23689
|
+
* camera rate with the headroom kept free (D755). The most accurate model
|
|
23690
|
+
* that reaches it wins.
|
|
23691
|
+
*/
|
|
23692
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
23693
|
+
});
|
|
23694
|
+
var AutoTunePlanSchema = object({
|
|
23695
|
+
/** Hash of everything the apply will write — the apply refuses a different one. */
|
|
23696
|
+
planHash: string(),
|
|
23697
|
+
family: literal("yolov9"),
|
|
23698
|
+
headroom: number(),
|
|
23699
|
+
minSessionsPerDevice: number(),
|
|
23700
|
+
/** The detection rate a session is counted at (the fastest camera running the pipeline root). */
|
|
23701
|
+
cameraFps: number(),
|
|
23702
|
+
generatedAt: number(),
|
|
23703
|
+
nodes: array(NodeRecommendationSchema).readonly(),
|
|
23704
|
+
nodeChanges: array(NodeDeviceChangeSchema).readonly(),
|
|
23705
|
+
cameraClears: array(CameraOverrideClearSchema).readonly(),
|
|
23706
|
+
detectorStoreClears: array(DetectorStoreClearSchema).readonly(),
|
|
23707
|
+
warnings: array(AutoTuneWarningSchema).readonly()
|
|
23708
|
+
});
|
|
23709
|
+
var AutoTuneApplyInputSchema = object({
|
|
23710
|
+
planHash: string(),
|
|
23711
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23712
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
23713
|
+
});
|
|
23714
|
+
var AutoTunePhaseResultSchema = object({
|
|
23715
|
+
phase: _enum([
|
|
23716
|
+
"download",
|
|
23717
|
+
"camera-overrides",
|
|
23718
|
+
"detector-thresholds",
|
|
23719
|
+
"node-models",
|
|
23720
|
+
"redispatch"
|
|
23721
|
+
]),
|
|
23722
|
+
ok: boolean(),
|
|
23723
|
+
detail: string()
|
|
23724
|
+
});
|
|
23725
|
+
var AutoTuneApplyResultSchema = discriminatedUnion("kind", [
|
|
23726
|
+
object({
|
|
23727
|
+
kind: literal("applied"),
|
|
23728
|
+
planHash: string(),
|
|
23729
|
+
phases: array(AutoTunePhaseResultSchema).readonly()
|
|
23730
|
+
}),
|
|
23731
|
+
object({
|
|
23732
|
+
kind: literal("aborted"),
|
|
23733
|
+
phases: array(AutoTunePhaseResultSchema).readonly()
|
|
23734
|
+
}),
|
|
23735
|
+
object({
|
|
23736
|
+
kind: literal("stale"),
|
|
23737
|
+
plan: AutoTunePlanSchema
|
|
23738
|
+
}),
|
|
23739
|
+
object({
|
|
23740
|
+
kind: literal("refused"),
|
|
23741
|
+
reason: _enum(["apply-in-progress"])
|
|
23742
|
+
})
|
|
23743
|
+
]);
|
|
23744
|
+
/**
|
|
23745
|
+
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
23746
|
+
* policy, the atomic share write, the observed placement statistics, the
|
|
23747
|
+
* hardware share suggestion and the stale device pins.
|
|
23748
|
+
*/
|
|
23749
|
+
/**
|
|
23750
|
+
* How a new detection session picks its node and device.
|
|
23751
|
+
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
23752
|
+
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
23753
|
+
* weight only bites once a node already holds a session.
|
|
23754
|
+
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
23755
|
+
* full or drowning node yields to a free one (D748).
|
|
23756
|
+
*/
|
|
23757
|
+
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
23758
|
+
var LoadBalancingSharesInputSchema = object({
|
|
23759
|
+
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
23760
|
+
nodes: record(string(), number().positive()).default({}),
|
|
23761
|
+
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
23762
|
+
devices: record(string(), record(string(), number().positive())).default({})
|
|
23763
|
+
});
|
|
23764
|
+
var PlacementStatsInputSchema = object({
|
|
23765
|
+
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
23766
|
+
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
23767
|
+
var PlacementDeviceStatsSchema = object({
|
|
23768
|
+
deviceKey: string(),
|
|
23769
|
+
/** Sessions running on this device now. */
|
|
23770
|
+
sessionsNow: number(),
|
|
23771
|
+
/** Sessions started on this device inside the window. */
|
|
23772
|
+
sessionsStarted: number(),
|
|
23773
|
+
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
23774
|
+
observedShare: number().nullable()
|
|
23775
|
+
});
|
|
23776
|
+
var PlacementNodeStatsSchema = object({
|
|
23777
|
+
nodeId: string(),
|
|
23778
|
+
sessionsNow: number(),
|
|
23779
|
+
sessionsStarted: number(),
|
|
23780
|
+
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
23781
|
+
observedShare: number().nullable(),
|
|
23782
|
+
/** Most sessions this node ran at once inside the window. */
|
|
23783
|
+
peakSessions: number(),
|
|
23784
|
+
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
23785
|
+
yieldsAway: number(),
|
|
23786
|
+
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
23787
|
+
});
|
|
23788
|
+
/** A camera's device pin that the session path cannot honour. */
|
|
23789
|
+
var StaleDevicePinSchema = object({
|
|
23790
|
+
deviceId: number(),
|
|
23791
|
+
deviceKey: string(),
|
|
23792
|
+
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
23793
|
+
reason: _enum(["disabled", "absent"])
|
|
23794
|
+
});
|
|
23795
|
+
var PlacementStatsSchema = object({
|
|
23796
|
+
policy: SessionPlacementPolicySchema,
|
|
23797
|
+
windowMs: number(),
|
|
23798
|
+
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
23799
|
+
observedForMs: number(),
|
|
23800
|
+
sessionsNow: number(),
|
|
23801
|
+
/** Most parallel sessions across the cluster inside the window. */
|
|
23802
|
+
clusterPeakSessions: number(),
|
|
23803
|
+
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
23804
|
+
stalePins: array(StaleDevicePinSchema).readonly()
|
|
23805
|
+
});
|
|
23806
|
+
/**
|
|
23807
|
+
* Where an automatic share's capacity comes from (D755).
|
|
23808
|
+
* - `hardware` — the static capacity of each device at its Auto tune model:
|
|
23809
|
+
* the measured latency table for the node's hardware class.
|
|
23810
|
+
* - `benchmark` — the node's own measured capacity (node benchmark results).
|
|
23811
|
+
* A node with no results falls back to `hardware`, and says so.
|
|
23812
|
+
*/
|
|
23813
|
+
var CapacitySourceSchema = _enum(["hardware", "benchmark"]);
|
|
23814
|
+
/** Why a node under the `benchmark` source was sized from hardware instead. */
|
|
23815
|
+
var CapacityFallbackSchema = _enum(["no-benchmark-results", "partial-benchmark-results"]);
|
|
23816
|
+
var SuggestSharesInputSchema = object({
|
|
23817
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23818
|
+
/** Default `hardware`. */
|
|
23819
|
+
source: CapacitySourceSchema.default("hardware")
|
|
23820
|
+
});
|
|
23821
|
+
var SuggestedDeviceShareSchema = object({
|
|
23822
|
+
deviceKey: string(),
|
|
23823
|
+
/** Share of the node (0–100, rounded). */
|
|
23824
|
+
share: number(),
|
|
23825
|
+
modelId: string().nullable(),
|
|
23826
|
+
/** Inferences per second at that model, from the measured table; null when sized by a benchmark. */
|
|
23827
|
+
fps: number().nullable(),
|
|
23828
|
+
/** False for a table cell derived from another chip; true for a measured cell or a benchmark. */
|
|
23829
|
+
measured: boolean(),
|
|
23830
|
+
/**
|
|
23831
|
+
* Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋ from the table,
|
|
23832
|
+
* or the benchmark's measured max sessions. Never enforced.
|
|
23833
|
+
*/
|
|
23834
|
+
suggestedMaxSessions: number().nullable()
|
|
23835
|
+
});
|
|
23836
|
+
var SuggestedNodeShareSchema = object({
|
|
23837
|
+
nodeId: string(),
|
|
23838
|
+
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
23839
|
+
share: number().nullable(),
|
|
23840
|
+
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
23841
|
+
capacitySessions: number(),
|
|
23842
|
+
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
23843
|
+
skippedReason: string().nullable(),
|
|
23844
|
+
/** What this node's capacity was read from; null when the node is skipped. */
|
|
23845
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
23846
|
+
/** Set when the `benchmark` source was asked for and this node used `hardware`. */
|
|
23847
|
+
capacityFallback: CapacityFallbackSchema.nullable()
|
|
23848
|
+
});
|
|
23849
|
+
var SuggestSharesResultSchema = object({
|
|
23850
|
+
headroom: number(),
|
|
23851
|
+
/** The source asked for; each node says what it actually used. */
|
|
23852
|
+
source: CapacitySourceSchema,
|
|
23853
|
+
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
23854
|
+
cameraFps: number(),
|
|
23855
|
+
/** The Auto tune capacity floor the models were picked at. */
|
|
23856
|
+
minSessionsPerDevice: number(),
|
|
23857
|
+
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
23858
|
+
});
|
|
23859
|
+
/**
|
|
23860
|
+
* Where the node and device shares come from (D750, D755).
|
|
23861
|
+
* - `hardware` (default) — computed by the system from each node's static
|
|
23862
|
+
* capacity (its accelerators at their Auto tune models, from the measured
|
|
23863
|
+
* latency table), recomputed whenever a node or device joins, leaves, is
|
|
23864
|
+
* switched on or off, or changes model. No load input.
|
|
23865
|
+
* - `benchmark` — the same, from each node's measured capacity (node
|
|
23866
|
+
* benchmark results); a node without results falls back to `hardware`.
|
|
23867
|
+
* - `manual` — the operator's stored sliders.
|
|
23868
|
+
* Either way a node or device that cannot take sessions gives its share to
|
|
23869
|
+
* the others in proportion; the stored values are never rewritten for that.
|
|
23870
|
+
*/
|
|
23871
|
+
var ShareSourceSchema = _enum([
|
|
23872
|
+
"hardware",
|
|
23873
|
+
"benchmark",
|
|
23874
|
+
"manual"
|
|
23875
|
+
]);
|
|
23876
|
+
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
23877
|
+
object({
|
|
23878
|
+
nodes: record(string(), number()),
|
|
23879
|
+
devices: record(string(), record(string(), number()))
|
|
23880
|
+
});
|
|
23881
|
+
var EffectiveDeviceShareSchema = object({
|
|
23882
|
+
deviceKey: string(),
|
|
23883
|
+
/** Can take sessions now (enabled, available, can run the root). */
|
|
23884
|
+
eligible: boolean(),
|
|
23885
|
+
/** The share the source assigns, as a percentage of the node's devices. */
|
|
23886
|
+
configuredPct: number(),
|
|
23887
|
+
/** The share actually used, after an ineligible device's share went to the others. */
|
|
23888
|
+
effectivePct: number()
|
|
23889
|
+
});
|
|
23890
|
+
var EffectiveNodeShareSchema = object({
|
|
23891
|
+
nodeId: string(),
|
|
23892
|
+
eligible: boolean(),
|
|
23893
|
+
configuredPct: number(),
|
|
23894
|
+
effectivePct: number(),
|
|
23895
|
+
/** Automatic sources: what this node's share was computed from; null under `manual` or before the first answer. */
|
|
23896
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
23897
|
+
/** Automatic `benchmark`: why this node used `hardware` instead. */
|
|
23898
|
+
capacityFallback: CapacityFallbackSchema.nullable(),
|
|
23899
|
+
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
23900
|
+
});
|
|
23901
|
+
var EffectiveSharesSchema = object({
|
|
23902
|
+
source: ShareSourceSchema,
|
|
23903
|
+
policy: SessionPlacementPolicySchema,
|
|
23904
|
+
/** An automatic source with no computed answer yet: the stored sliders are used meanwhile. */
|
|
23905
|
+
autoPending: boolean(),
|
|
23906
|
+
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
23907
|
+
computedAt: number().nullable(),
|
|
23908
|
+
/** What made the system recompute them last. */
|
|
23909
|
+
trigger: string().nullable(),
|
|
23910
|
+
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
23911
|
+
/** The relative weights the election reads (before eligibility). */
|
|
23912
|
+
nodeWeights: record(string(), number()),
|
|
23913
|
+
deviceWeights: record(string(), record(string(), number()))
|
|
23914
|
+
});
|
|
23575
23915
|
object({
|
|
23576
23916
|
activeCameras: number(),
|
|
23577
23917
|
throttledCameras: number(),
|
|
@@ -24015,6 +24355,22 @@ var MotionSourceEnum = _enum([
|
|
|
24015
24355
|
"device-activity"
|
|
24016
24356
|
]);
|
|
24017
24357
|
/**
|
|
24358
|
+
* What opened an on-motion detection SESSION: the motion source that passed the
|
|
24359
|
+
* camera's `motionSources` gate, and what raised it (the camera itself, or a
|
|
24360
|
+
* sensor the operator attached — D672). `sensorName` is the attached sensor's
|
|
24361
|
+
* device name as the orchestrator's camera-sensors resolution last read it;
|
|
24362
|
+
* absent when unknown or when the camera itself triggered.
|
|
24363
|
+
*
|
|
24364
|
+
* Carried on the session attach ONLY so the runner's `DetectionPhaseTransition`
|
|
24365
|
+
* can say what started the session (D672 rule 11, amended 2026-10-06). The
|
|
24366
|
+
* runner never decides anything on it.
|
|
24367
|
+
*/
|
|
24368
|
+
var SessionTriggerSchema = object({
|
|
24369
|
+
source: MotionSourceEnum,
|
|
24370
|
+
triggeredBy: MotionTriggeredBySchema,
|
|
24371
|
+
sensorName: string().optional()
|
|
24372
|
+
});
|
|
24373
|
+
/**
|
|
24018
24374
|
* List of motion sources active on a camera. Empty array is valid:
|
|
24019
24375
|
* "no source" — happens for battery cams without firmware motion when
|
|
24020
24376
|
* the operator hasn't opted into the SW analyzer (would hold the
|
|
@@ -24266,6 +24622,14 @@ var RunnerCameraConfigSchema = object({
|
|
|
24266
24622
|
*/
|
|
24267
24623
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
24268
24624
|
/**
|
|
24625
|
+
* What opened this attach, on a SESSION attach only (see
|
|
24626
|
+
* {@link SessionTriggerSchema}). OPTIONAL with no default: absent on a
|
|
24627
|
+
* standing attach, an occupancy burst, a re-dispatch with no motion event
|
|
24628
|
+
* behind it, and every attach from an orchestrator older than the field — the
|
|
24629
|
+
* runner then names no source on the phase event.
|
|
24630
|
+
*/
|
|
24631
|
+
sessionTrigger: SessionTriggerSchema.optional(),
|
|
24632
|
+
/**
|
|
24269
24633
|
* WHICH root detector runs for this camera. The detection counterpart of
|
|
24270
24634
|
* {@link motionSources}, same shape and same discipline: a per-camera list
|
|
24271
24635
|
* of sources the system understands, vendor-agnostic, owned here.
|
|
@@ -24425,14 +24789,15 @@ var RunnerLocalLoadSchema = object({
|
|
|
24425
24789
|
* Per-inference-device live load (multi-device C4). One entry per deviceKey
|
|
24426
24790
|
* this runner currently has attached cameras on, so the orchestrator's second
|
|
24427
24791
|
* `balance()` pass (over a node's devices) weights on real per-pool session
|
|
24428
|
-
* counts. Empty on single-device / pre-multi-device runners.
|
|
24429
|
-
* per
|
|
24792
|
+
* counts. Empty on single-device / pre-multi-device runners. There is no
|
|
24793
|
+
* per-device queue depth: the inference pool's backlog has no accessor, and
|
|
24794
|
+
* a field that always read 0 was removed rather than shown as a measurement
|
|
24795
|
+
* (D762).
|
|
24430
24796
|
*/
|
|
24431
24797
|
devices: array(object({
|
|
24432
24798
|
deviceKey: string(),
|
|
24433
24799
|
backend: string(),
|
|
24434
|
-
attachedCameras: number()
|
|
24435
|
-
queueDepthTotal: number()
|
|
24800
|
+
attachedCameras: number()
|
|
24436
24801
|
})).default([]),
|
|
24437
24802
|
/** Hardware capability flags reported by this node. */
|
|
24438
24803
|
hardware: object({
|
|
@@ -24568,7 +24933,22 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
24568
24933
|
*/
|
|
24569
24934
|
cropJpeg: string().optional(),
|
|
24570
24935
|
parent: DetailParentSchema,
|
|
24571
|
-
steps: array(string()).optional()
|
|
24936
|
+
steps: array(string()).optional(),
|
|
24937
|
+
/**
|
|
24938
|
+
* The camera's root model already drew this step's boxes (D730): a
|
|
24939
|
+
* `license_plate` inside the parent vehicle. The named step is NOT
|
|
24940
|
+
* inferred — these FRAME-space boxes are its output — and its
|
|
24941
|
+
* requested children (the plate reader) run on them. Absent ⇒ every
|
|
24942
|
+
* requested step infers as before.
|
|
24943
|
+
*/
|
|
24944
|
+
supplied: object({
|
|
24945
|
+
stepId: string(),
|
|
24946
|
+
modelId: string(),
|
|
24947
|
+
boxes: array(object({
|
|
24948
|
+
bbox: NativeCropBboxSchema,
|
|
24949
|
+
score: number()
|
|
24950
|
+
})).min(1)
|
|
24951
|
+
}).optional()
|
|
24572
24952
|
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
|
|
24573
24953
|
/** Catalog step id, e.g. `clip-embedding`. */
|
|
24574
24954
|
stepId: string(),
|
|
@@ -24631,14 +25011,6 @@ var CameraPipelineConfigSchema = object({
|
|
|
24631
25011
|
settings: record(string(), unknown()).readonly().optional()
|
|
24632
25012
|
}).nullable().optional()
|
|
24633
25013
|
});
|
|
24634
|
-
var PipelineTemplateSchema = object({
|
|
24635
|
-
id: string(),
|
|
24636
|
-
name: string(),
|
|
24637
|
-
description: string().optional(),
|
|
24638
|
-
config: CameraPipelineConfigSchema,
|
|
24639
|
-
createdAt: string(),
|
|
24640
|
-
updatedAt: string()
|
|
24641
|
-
});
|
|
24642
25014
|
var DeviceStepConfigSchema = object({
|
|
24643
25015
|
modelId: string().optional(),
|
|
24644
25016
|
settings: record(string(), unknown()).optional(),
|
|
@@ -24712,9 +25084,10 @@ var CameraStepOverridePatchSchema = object({
|
|
|
24712
25084
|
modelId: string().optional(),
|
|
24713
25085
|
settings: record(string(), unknown()).readonly().optional()
|
|
24714
25086
|
});
|
|
25087
|
+
var CameraStepSettingsSchema = record(string(), record(string(), unknown()).readonly());
|
|
24715
25088
|
var CameraPipelineSettingsSchema = object({
|
|
24716
|
-
pinnedAgentNodeId: string().optional(),
|
|
24717
25089
|
stepToggles: record(string(), boolean()).optional(),
|
|
25090
|
+
stepSettings: CameraStepSettingsSchema.optional(),
|
|
24718
25091
|
stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
|
|
24719
25092
|
pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
|
|
24720
25093
|
});
|
|
@@ -24747,7 +25120,10 @@ var PipelineAssignmentSchema = object({
|
|
|
24747
25120
|
});
|
|
24748
25121
|
/**
|
|
24749
25122
|
* Per-agent load summary surfaced to the load balancer + dashboards.
|
|
24750
|
-
* Aggregated from each runner's `getLocalLoad` cap call.
|
|
25123
|
+
* Aggregated from each runner's `getLocalLoad` cap call. In the
|
|
25124
|
+
* `getAgentLoad` report, a single-device node's default-pool sessions (no
|
|
25125
|
+
* `deviceKey`) are credited to its one eligible device in `load.devices` —
|
|
25126
|
+
* the default pool IS that device (D745); the balancer's census stays raw.
|
|
24751
25127
|
*/
|
|
24752
25128
|
var AgentLoadSummarySchema = object({
|
|
24753
25129
|
nodeId: string(),
|
|
@@ -24836,26 +25212,27 @@ var CameraBrokerStatusSchema = object({
|
|
|
24836
25212
|
width: number(),
|
|
24837
25213
|
height: number(),
|
|
24838
25214
|
subscribers: number(),
|
|
24839
|
-
|
|
24840
|
-
|
|
25215
|
+
/**
|
|
25216
|
+
* Encoded packets/s the broker receives from the camera. null when the
|
|
25217
|
+
* broker's stats read failed — unknown, never 0 (a 0 here reads as a stall).
|
|
25218
|
+
* There is no output rate: decoding runs in the session decode workers, not
|
|
25219
|
+
* in the broker, so the broker's own decoded-frame rate is 0 on every live
|
|
25220
|
+
* camera and is not served (D758).
|
|
25221
|
+
*/
|
|
25222
|
+
inFps: number().nullable()
|
|
24841
25223
|
})).readonly(),
|
|
24842
25224
|
webrtcSessions: number(),
|
|
24843
25225
|
rtspRestream: boolean()
|
|
24844
25226
|
});
|
|
24845
|
-
/**
|
|
24846
|
-
|
|
24847
|
-
|
|
24848
|
-
|
|
24849
|
-
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
var CameraDecoderStatusSchema = object({
|
|
24854
|
-
nodeId: string(),
|
|
24855
|
-
formats: array(string()).readonly(),
|
|
24856
|
-
sessionCount: number(),
|
|
24857
|
-
shm: CameraDecoderShmSchema
|
|
24858
|
-
});
|
|
25227
|
+
/**
|
|
25228
|
+
* Decoder block — WHERE decode runs (co-located with the detection node by
|
|
25229
|
+
* construction); null when the camera has no detection node. It carries no
|
|
25230
|
+
* frame counter: the shared-memory plane whose counters it used to serve is
|
|
25231
|
+
* gone, and those fields read 0 forever. Whether frames actually flow is read
|
|
25232
|
+
* from `detection.actualFps` (frames delivered to and processed by the
|
|
25233
|
+
* detector), never from a counter nothing feeds.
|
|
25234
|
+
*/
|
|
25235
|
+
var CameraDecoderStatusSchema = object({ nodeId: string() });
|
|
24859
25236
|
/** Motion block — null when motion detection is not active for this device. */
|
|
24860
25237
|
var CameraMotionStatusSchema = object({
|
|
24861
25238
|
enabled: boolean(),
|
|
@@ -25212,12 +25589,6 @@ method(object({
|
|
|
25212
25589
|
}), object({ success: literal(true) }), {
|
|
25213
25590
|
kind: "mutation",
|
|
25214
25591
|
auth: "admin"
|
|
25215
|
-
}), method(object({
|
|
25216
|
-
agentNodeId: string(),
|
|
25217
|
-
detectWeight: number().positive().nullable()
|
|
25218
|
-
}), object({ success: literal(true) }), {
|
|
25219
|
-
kind: "mutation",
|
|
25220
|
-
auth: "admin"
|
|
25221
25592
|
}), method(object({
|
|
25222
25593
|
agentNodeId: string(),
|
|
25223
25594
|
detect: boolean().nullable().optional(),
|
|
@@ -25259,6 +25630,15 @@ method(object({
|
|
|
25259
25630
|
}), {
|
|
25260
25631
|
kind: "mutation",
|
|
25261
25632
|
auth: "admin"
|
|
25633
|
+
}), method(AutoTuneOptionsSchema, AutoTunePlanSchema, { auth: "admin" }), method(AutoTuneApplyInputSchema, AutoTuneApplyResultSchema, {
|
|
25634
|
+
kind: "mutation",
|
|
25635
|
+
auth: "admin"
|
|
25636
|
+
}), method(LoadBalancingSharesInputSchema, object({ success: literal(true) }), {
|
|
25637
|
+
kind: "mutation",
|
|
25638
|
+
auth: "admin"
|
|
25639
|
+
}), method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }), method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }), method(_void(), EffectiveSharesSchema, { auth: "admin" }), method(SetShareSourceInputSchema, object({ success: literal(true) }), {
|
|
25640
|
+
kind: "mutation",
|
|
25641
|
+
auth: "admin"
|
|
25262
25642
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
25263
25643
|
deviceId: number(),
|
|
25264
25644
|
addonId: string(),
|
|
@@ -25275,6 +25655,13 @@ method(object({
|
|
|
25275
25655
|
}), object({ success: literal(true) }), {
|
|
25276
25656
|
kind: "mutation",
|
|
25277
25657
|
auth: "admin"
|
|
25658
|
+
}), method(object({
|
|
25659
|
+
deviceId: number(),
|
|
25660
|
+
addonId: string(),
|
|
25661
|
+
settings: record(string(), unknown()).nullable()
|
|
25662
|
+
}), object({ success: literal(true) }), {
|
|
25663
|
+
kind: "mutation",
|
|
25664
|
+
auth: "admin"
|
|
25278
25665
|
}), method(object({
|
|
25279
25666
|
deviceId: number(),
|
|
25280
25667
|
agentNodeId: string(),
|
|
@@ -25298,25 +25685,7 @@ method(object({
|
|
|
25298
25685
|
}), method(_void(), DiskReconcileJobSchema, {
|
|
25299
25686
|
kind: "mutation",
|
|
25300
25687
|
auth: "admin"
|
|
25301
|
-
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" })
|
|
25302
|
-
name: string(),
|
|
25303
|
-
description: string().optional(),
|
|
25304
|
-
config: CameraPipelineConfigSchema
|
|
25305
|
-
}), PipelineTemplateSchema, {
|
|
25306
|
-
kind: "mutation",
|
|
25307
|
-
auth: "admin"
|
|
25308
|
-
}), method(object({
|
|
25309
|
-
id: string(),
|
|
25310
|
-
name: string().optional(),
|
|
25311
|
-
description: string().optional(),
|
|
25312
|
-
config: CameraPipelineConfigSchema.optional()
|
|
25313
|
-
}), PipelineTemplateSchema, {
|
|
25314
|
-
kind: "mutation",
|
|
25315
|
-
auth: "admin"
|
|
25316
|
-
}), method(object({ id: string() }), object({ success: literal(true) }), {
|
|
25317
|
-
kind: "mutation",
|
|
25318
|
-
auth: "admin"
|
|
25319
|
-
});
|
|
25688
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" });
|
|
25320
25689
|
/**
|
|
25321
25690
|
* Cap → event-kind mapping for the SENSOR / CONTROL cap families — the table
|
|
25322
25691
|
* `pipeline-analytics.listEventKinds` uses to turn a linked device's bound
|
|
@@ -27536,8 +27905,6 @@ var WebrtcStreamChoiceSchema = object({
|
|
|
27536
27905
|
]).nullable(),
|
|
27537
27906
|
/** Input FPS from the camera (null for adaptive). */
|
|
27538
27907
|
inputFps: number().nullable(),
|
|
27539
|
-
/** Decoded FPS (null for adaptive). */
|
|
27540
|
-
decodeFps: number().nullable(),
|
|
27541
27908
|
/** Bitrate in kbps (null for adaptive). */
|
|
27542
27909
|
bitrateKbps: number().nullable()
|
|
27543
27910
|
});
|
|
@@ -27869,6 +28236,14 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
27869
28236
|
*/
|
|
27870
28237
|
alive: boolean()
|
|
27871
28238
|
})), method(object({
|
|
28239
|
+
deviceId: number().int().nonnegative(),
|
|
28240
|
+
sessionId: string(),
|
|
28241
|
+
downlinkMbps: number().nonnegative()
|
|
28242
|
+
}), object({
|
|
28243
|
+
outcome: _enum(["switching", "held"]),
|
|
28244
|
+
tier: CamProfileSchema.nullable(),
|
|
28245
|
+
reason: string()
|
|
28246
|
+
}), { kind: "mutation" }), method(object({
|
|
27872
28247
|
deviceId: number().int().nonnegative(),
|
|
27873
28248
|
sessionId: string()
|
|
27874
28249
|
}), WebrtcSessionDebugSchema, { kind: "query" });
|
|
@@ -30489,7 +30864,7 @@ var LocalInterfaceSchema = object({
|
|
|
30489
30864
|
name: string(),
|
|
30490
30865
|
family: _enum(["IPv4", "IPv6"]),
|
|
30491
30866
|
address: string(),
|
|
30492
|
-
/** CIDR shorthand (e.g. `192.168.
|
|
30867
|
+
/** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
|
|
30493
30868
|
cidr: string(),
|
|
30494
30869
|
/** OS netmask. Stable across calls so the SDK can hash on it. */
|
|
30495
30870
|
netmask: string(),
|
|
@@ -30543,7 +30918,9 @@ var ConnectionEndpointSchema = object({
|
|
|
30543
30918
|
"lan-ipv4",
|
|
30544
30919
|
"lan-ipv6",
|
|
30545
30920
|
"public",
|
|
30546
|
-
"loopback"
|
|
30921
|
+
"loopback",
|
|
30922
|
+
"app-link",
|
|
30923
|
+
"custom"
|
|
30547
30924
|
]),
|
|
30548
30925
|
/**
|
|
30549
30926
|
* Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
|
|
@@ -30612,15 +30989,27 @@ var GetConnectionEndpointsResultSchema = object({
|
|
|
30612
30989
|
localPort: AdvertisedLocalPortSchema
|
|
30613
30990
|
});
|
|
30614
30991
|
/**
|
|
30615
|
-
* The
|
|
30616
|
-
*
|
|
30617
|
-
*
|
|
30992
|
+
* The address flagged for notification taps (`notificationTaps`). `baseUrl: null` = AUTO, which
|
|
30993
|
+
* since D709 is the CamStack app link; `resolved` reports the effective value,
|
|
30994
|
+
* so the UI can show it either way.
|
|
30618
30995
|
*/
|
|
30619
30996
|
var NotificationEndpointSchema = object({
|
|
30620
30997
|
/** The operator's explicit choice, or null for AUTO. */
|
|
30621
30998
|
baseUrl: string().nullable(),
|
|
30622
|
-
/**
|
|
30623
|
-
|
|
30999
|
+
/**
|
|
31000
|
+
* The effective flagged address: the explicit choice, else (AUTO) the
|
|
31001
|
+
* CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
|
|
31002
|
+
* answered the public tunnel / mesh here, or null.
|
|
31003
|
+
*/
|
|
31004
|
+
resolved: string().nullable(),
|
|
31005
|
+
/**
|
|
31006
|
+
* The hub origin an app link carries as `hub=` (D709): the hub's best
|
|
31007
|
+
* public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
|
|
31008
|
+
* picked from this cap's own palette by `pickAppLinkHub`. `null` when the
|
|
31009
|
+
* hub advertises neither; the app link is then minted without `hub=`.
|
|
31010
|
+
* Absent on a pre-D709 hub.
|
|
31011
|
+
*/
|
|
31012
|
+
appLinkHub: string().nullable()
|
|
30624
31013
|
});
|
|
30625
31014
|
/**
|
|
30626
31015
|
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
@@ -30633,6 +31022,72 @@ var ViewerEndpointsSchema = object({
|
|
|
30633
31022
|
/** What the ranking currently resolves to (may be empty if nothing is up). */
|
|
30634
31023
|
resolved: array(string()).readonly()
|
|
30635
31024
|
});
|
|
31025
|
+
/**
|
|
31026
|
+
* DEPRECATED compatibility shape of `getHubAddresses` (D729 amendment, kept
|
|
31027
|
+
* for addons built against it): `cluster` is the `internal` role, `browser`
|
|
31028
|
+
* the `publicEndpoint` role; `override` is the flagged address.
|
|
31029
|
+
*/
|
|
31030
|
+
var HubAddressSnapshotSchema = object({
|
|
31031
|
+
override: string().nullable(),
|
|
31032
|
+
resolved: string().nullable(),
|
|
31033
|
+
source: _enum([
|
|
31034
|
+
"override",
|
|
31035
|
+
"auto",
|
|
31036
|
+
"none"
|
|
31037
|
+
])
|
|
31038
|
+
});
|
|
31039
|
+
var HubAddressesSchema = object({
|
|
31040
|
+
cluster: HubAddressSnapshotSchema,
|
|
31041
|
+
browser: HubAddressSnapshotSchema
|
|
31042
|
+
});
|
|
31043
|
+
/**
|
|
31044
|
+
* What an address is used for (`hub-address.ts`, D734). `appConnections` is
|
|
31045
|
+
* many-valued; `notificationTaps`, `publicEndpoint` and `internal` are carried
|
|
31046
|
+
* by at most one address.
|
|
31047
|
+
*/
|
|
31048
|
+
var HubAddressRoleSchema = _enum(HUB_ADDRESS_ROLES);
|
|
31049
|
+
var RoleSourceSchema = _enum([
|
|
31050
|
+
"flagged",
|
|
31051
|
+
"auto",
|
|
31052
|
+
"none"
|
|
31053
|
+
]);
|
|
31054
|
+
/** One single-valued role: the flagged address, the one in use, and why. */
|
|
31055
|
+
var HubAddressRoleResolutionSchema = object({
|
|
31056
|
+
/** The address carrying the flag, or null = automatic. */
|
|
31057
|
+
flagged: string().nullable(),
|
|
31058
|
+
/** The address in use, or null when nothing qualifies (never loopback). */
|
|
31059
|
+
resolved: string().nullable(),
|
|
31060
|
+
source: RoleSourceSchema
|
|
31061
|
+
});
|
|
31062
|
+
var HubAppConnectionsResolutionSchema = object({
|
|
31063
|
+
/** The addresses flagged `appConnections`, or empty = automatic. */
|
|
31064
|
+
flagged: array(string()).readonly(),
|
|
31065
|
+
/** What the apps race right now. */
|
|
31066
|
+
resolved: array(string()).readonly(),
|
|
31067
|
+
source: RoleSourceSchema
|
|
31068
|
+
});
|
|
31069
|
+
var HubAddressBookSchema = object({
|
|
31070
|
+
rows: array(ConnectionEndpointSchema.extend({
|
|
31071
|
+
/** In the hub's detected palette right now. */
|
|
31072
|
+
detected: boolean(),
|
|
31073
|
+
/** Entered by the operator (removable). */
|
|
31074
|
+
custom: boolean(),
|
|
31075
|
+
/** The roles explicitly flagged on this address. */
|
|
31076
|
+
flags: array(HubAddressRoleSchema).readonly(),
|
|
31077
|
+
/** The roles this address serves right now — flagged or automatic. */
|
|
31078
|
+
inUse: array(HubAddressRoleSchema).readonly(),
|
|
31079
|
+
/** The roles this address may be flagged with. */
|
|
31080
|
+
eligible: array(HubAddressRoleSchema).readonly()
|
|
31081
|
+
})).readonly(),
|
|
31082
|
+
roles: object({
|
|
31083
|
+
appConnections: HubAppConnectionsResolutionSchema,
|
|
31084
|
+
notificationTaps: HubAddressRoleResolutionSchema,
|
|
31085
|
+
internal: HubAddressRoleResolutionSchema,
|
|
31086
|
+
publicEndpoint: HubAddressRoleResolutionSchema
|
|
31087
|
+
}),
|
|
31088
|
+
/** The `hub=` an app link carries (D709), or null. */
|
|
31089
|
+
appLinkHub: string().nullable()
|
|
31090
|
+
});
|
|
30636
31091
|
var AllowedAddressesSchema = object({
|
|
30637
31092
|
/**
|
|
30638
31093
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -30674,8 +31129,29 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
30674
31129
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
30675
31130
|
* to avoid mixed-content blocks in the browser. The public
|
|
30676
31131
|
* tunnel always emits `https://` regardless. */
|
|
30677
|
-
scheme: _enum(["http", "https"]).optional()
|
|
30678
|
-
|
|
31132
|
+
scheme: _enum(["http", "https"]).optional(),
|
|
31133
|
+
/** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
|
|
31134
|
+
* an address notification taps can be flagged with, not a hub
|
|
31135
|
+
* route. Default `false`: racers and rankers never see it. */
|
|
31136
|
+
includeAppLink: boolean().optional()
|
|
31137
|
+
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(object({ scheme: _enum(["http", "https"]).optional() }).optional(), HubAddressBookSchema), method(object({
|
|
31138
|
+
baseUrl: string(),
|
|
31139
|
+
label: string().max(80)
|
|
31140
|
+
}), HubAddressBookSchema, {
|
|
31141
|
+
kind: "mutation",
|
|
31142
|
+
auth: "admin"
|
|
31143
|
+
}), method(object({ baseUrl: string() }), HubAddressBookSchema, {
|
|
31144
|
+
kind: "mutation",
|
|
31145
|
+
auth: "admin"
|
|
31146
|
+
}), method(object({
|
|
31147
|
+
role: HubAddressRoleSchema,
|
|
31148
|
+
baseUrl: string().nullable(),
|
|
31149
|
+
enabled: boolean(),
|
|
31150
|
+
acceptInsecureHttp: boolean().optional()
|
|
31151
|
+
}), HubAddressBookSchema, {
|
|
31152
|
+
kind: "mutation",
|
|
31153
|
+
auth: "admin"
|
|
31154
|
+
}), method(_void(), HubAddressesSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
|
|
30679
31155
|
kind: "mutation",
|
|
30680
31156
|
auth: "admin"
|
|
30681
31157
|
}), method(object({
|
|
@@ -30935,7 +31411,7 @@ var MeshPeerSchema = object({
|
|
|
30935
31411
|
tags: array(string()).readonly(),
|
|
30936
31412
|
/** True when the peer advertises itself as an exit-node candidate. */
|
|
30937
31413
|
exitNodeOption: boolean(),
|
|
30938
|
-
/** Subnet routes the peer advertises (e.g. `["192.168.
|
|
31414
|
+
/** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
|
|
30939
31415
|
advertisedRoutes: array(string()).readonly(),
|
|
30940
31416
|
/** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
|
|
30941
31417
|
userLogin: string().nullable(),
|
|
@@ -31065,7 +31541,8 @@ object({
|
|
|
31065
31541
|
* writes this slice (not even a boot seed), and neither does the runner's
|
|
31066
31542
|
* phase machine: its phase is "a session is attached" for every on-motion
|
|
31067
31543
|
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
31068
|
-
* A provider's `isDetected`
|
|
31544
|
+
* A provider's `isDetected` and `getStatus` answer from the hub's slice
|
|
31545
|
+
* (`hubMotionNativeProvider`).
|
|
31069
31546
|
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
31070
31547
|
* imports) — the provider pushes both edges itself, from the upstream
|
|
31071
31548
|
* system's own state change. That is the sensor's own fact.
|
|
@@ -31879,7 +32356,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
31879
32356
|
*/
|
|
31880
32357
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
31881
32358
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
31882
|
-
readOnly: boolean().optional()
|
|
32359
|
+
readOnly: boolean().optional(),
|
|
32360
|
+
/**
|
|
32361
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32362
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32363
|
+
* copy on the binding.
|
|
32364
|
+
*/
|
|
32365
|
+
maxCharacters: number().int().positive().optional()
|
|
31883
32366
|
})).readonly() });
|
|
31884
32367
|
var OsdOverlayPatchSchema = object({
|
|
31885
32368
|
enabled: boolean().optional(),
|
|
@@ -31930,7 +32413,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
31930
32413
|
valuePath: string().min(1).max(64),
|
|
31931
32414
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
31932
32415
|
displayUnit: string().max(16).optional(),
|
|
31933
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32416
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32417
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32418
|
+
valueMap: record(string(), string()).optional()
|
|
31934
32419
|
});
|
|
31935
32420
|
/**
|
|
31936
32421
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -31980,18 +32465,11 @@ var OsdSlotBindingSchema = object({
|
|
|
31980
32465
|
source: OsdSourceSchema,
|
|
31981
32466
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
31982
32467
|
template: string().max(96).default("${value}"),
|
|
31983
|
-
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
31984
|
-
maxCharacters: number().int().min(4).max(64).optional(),
|
|
31985
32468
|
/**
|
|
31986
32469
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
31987
32470
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
31988
32471
|
*/
|
|
31989
32472
|
maxDecimals: number().int().min(0).max(4).default(1),
|
|
31990
|
-
/**
|
|
31991
|
-
* Appended via `${unit}` when no unit conversion produces a label.
|
|
31992
|
-
* Prefer `source.displayUnit` for converted device-state values.
|
|
31993
|
-
*/
|
|
31994
|
-
unitLabel: string().max(8).optional(),
|
|
31995
32473
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
31996
32474
|
valueMap: record(string(), string()).optional(),
|
|
31997
32475
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -32000,9 +32478,7 @@ var OsdSlotBindingSchema = object({
|
|
|
32000
32478
|
* Display gate, in the notification centre's condition vocabulary.
|
|
32001
32479
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
32002
32480
|
*/
|
|
32003
|
-
conditions: NcConditionsSchema.optional()
|
|
32004
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32005
|
-
fallbackText: string().max(64).default("")
|
|
32481
|
+
conditions: NcConditionsSchema.optional()
|
|
32006
32482
|
});
|
|
32007
32483
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32008
32484
|
var OsdSlotViewSchema = object({
|
|
@@ -36197,6 +36673,16 @@ DeviceType.Camera, method(object({
|
|
|
36197
36673
|
package: array(ZoneRuleSchema).readonly()
|
|
36198
36674
|
});
|
|
36199
36675
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
36676
|
+
Object.freeze({
|
|
36677
|
+
person: .5,
|
|
36678
|
+
vehicle: .5,
|
|
36679
|
+
animal: .5
|
|
36680
|
+
});
|
|
36681
|
+
Object.freeze({
|
|
36682
|
+
person: .65,
|
|
36683
|
+
vehicle: .65,
|
|
36684
|
+
animal: .65
|
|
36685
|
+
});
|
|
36200
36686
|
/**
|
|
36201
36687
|
* Accessory device helpers — shared across drivers.
|
|
36202
36688
|
*
|
|
@@ -37553,90 +38039,18 @@ Object.freeze({
|
|
|
37553
38039
|
addonId: null,
|
|
37554
38040
|
access: "create"
|
|
37555
38041
|
},
|
|
37556
|
-
"decoder.createSession": {
|
|
37557
|
-
capName: "decoder",
|
|
37558
|
-
capScope: "system",
|
|
37559
|
-
addonId: null,
|
|
37560
|
-
access: "view"
|
|
37561
|
-
},
|
|
37562
|
-
"decoder.destroySession": {
|
|
37563
|
-
capName: "decoder",
|
|
37564
|
-
capScope: "system",
|
|
37565
|
-
addonId: null,
|
|
37566
|
-
access: "delete"
|
|
37567
|
-
},
|
|
37568
|
-
"decoder.getFrame": {
|
|
37569
|
-
capName: "decoder",
|
|
37570
|
-
capScope: "system",
|
|
37571
|
-
addonId: null,
|
|
37572
|
-
access: "view"
|
|
37573
|
-
},
|
|
37574
38042
|
"decoder.getInfo": {
|
|
37575
38043
|
capName: "decoder",
|
|
37576
38044
|
capScope: "system",
|
|
37577
38045
|
addonId: null,
|
|
37578
38046
|
access: "view"
|
|
37579
38047
|
},
|
|
37580
|
-
"decoder.getShmStats": {
|
|
37581
|
-
capName: "decoder",
|
|
37582
|
-
capScope: "system",
|
|
37583
|
-
addonId: null,
|
|
37584
|
-
access: "view"
|
|
37585
|
-
},
|
|
37586
|
-
"decoder.getStats": {
|
|
37587
|
-
capName: "decoder",
|
|
37588
|
-
capScope: "system",
|
|
37589
|
-
addonId: null,
|
|
37590
|
-
access: "view"
|
|
37591
|
-
},
|
|
37592
|
-
"decoder.listActiveSessions": {
|
|
37593
|
-
capName: "decoder",
|
|
37594
|
-
capScope: "system",
|
|
37595
|
-
addonId: null,
|
|
37596
|
-
access: "view"
|
|
37597
|
-
},
|
|
37598
|
-
"decoder.openStream": {
|
|
37599
|
-
capName: "decoder",
|
|
37600
|
-
capScope: "system",
|
|
37601
|
-
addonId: null,
|
|
37602
|
-
access: "view"
|
|
37603
|
-
},
|
|
37604
|
-
"decoder.pullFrames": {
|
|
37605
|
-
capName: "decoder",
|
|
37606
|
-
capScope: "system",
|
|
37607
|
-
addonId: null,
|
|
37608
|
-
access: "view"
|
|
37609
|
-
},
|
|
37610
|
-
"decoder.pullHandles": {
|
|
37611
|
-
capName: "decoder",
|
|
37612
|
-
capScope: "system",
|
|
37613
|
-
addonId: null,
|
|
37614
|
-
access: "view"
|
|
37615
|
-
},
|
|
37616
|
-
"decoder.pushPacket": {
|
|
37617
|
-
capName: "decoder",
|
|
37618
|
-
capScope: "system",
|
|
37619
|
-
addonId: null,
|
|
37620
|
-
access: "view"
|
|
37621
|
-
},
|
|
37622
38048
|
"decoder.reprobeHwaccel": {
|
|
37623
38049
|
capName: "decoder",
|
|
37624
38050
|
capScope: "system",
|
|
37625
38051
|
addonId: null,
|
|
37626
38052
|
access: "create"
|
|
37627
38053
|
},
|
|
37628
|
-
"decoder.supportsCodec": {
|
|
37629
|
-
capName: "decoder",
|
|
37630
|
-
capScope: "system",
|
|
37631
|
-
addonId: null,
|
|
37632
|
-
access: "view"
|
|
37633
|
-
},
|
|
37634
|
-
"decoder.updateConfig": {
|
|
37635
|
-
capName: "decoder",
|
|
37636
|
-
capScope: "system",
|
|
37637
|
-
addonId: null,
|
|
37638
|
-
access: "view"
|
|
37639
|
-
},
|
|
37640
38054
|
"detectionPipeline.applyDeviceSettingsPatch": {
|
|
37641
38055
|
capName: "detection-pipeline",
|
|
37642
38056
|
capScope: "device",
|
|
@@ -39125,12 +39539,24 @@ Object.freeze({
|
|
|
39125
39539
|
addonId: null,
|
|
39126
39540
|
access: "view"
|
|
39127
39541
|
},
|
|
39542
|
+
"localNetwork.addCustomAddress": {
|
|
39543
|
+
capName: "local-network",
|
|
39544
|
+
capScope: "system",
|
|
39545
|
+
addonId: null,
|
|
39546
|
+
access: "create"
|
|
39547
|
+
},
|
|
39128
39548
|
"localNetwork.downloadCa": {
|
|
39129
39549
|
capName: "local-network",
|
|
39130
39550
|
capScope: "system",
|
|
39131
39551
|
addonId: null,
|
|
39132
39552
|
access: "view"
|
|
39133
39553
|
},
|
|
39554
|
+
"localNetwork.getAddresses": {
|
|
39555
|
+
capName: "local-network",
|
|
39556
|
+
capScope: "system",
|
|
39557
|
+
addonId: null,
|
|
39558
|
+
access: "view"
|
|
39559
|
+
},
|
|
39134
39560
|
"localNetwork.getAllowedAddresses": {
|
|
39135
39561
|
capName: "local-network",
|
|
39136
39562
|
capScope: "system",
|
|
@@ -39143,6 +39569,12 @@ Object.freeze({
|
|
|
39143
39569
|
addonId: null,
|
|
39144
39570
|
access: "view"
|
|
39145
39571
|
},
|
|
39572
|
+
"localNetwork.getHubAddresses": {
|
|
39573
|
+
capName: "local-network",
|
|
39574
|
+
capScope: "system",
|
|
39575
|
+
addonId: null,
|
|
39576
|
+
access: "view"
|
|
39577
|
+
},
|
|
39146
39578
|
"localNetwork.getNotificationEndpoint": {
|
|
39147
39579
|
capName: "local-network",
|
|
39148
39580
|
capScope: "system",
|
|
@@ -39179,6 +39611,12 @@ Object.freeze({
|
|
|
39179
39611
|
addonId: null,
|
|
39180
39612
|
access: "create"
|
|
39181
39613
|
},
|
|
39614
|
+
"localNetwork.removeCustomAddress": {
|
|
39615
|
+
capName: "local-network",
|
|
39616
|
+
capScope: "system",
|
|
39617
|
+
addonId: null,
|
|
39618
|
+
access: "delete"
|
|
39619
|
+
},
|
|
39182
39620
|
"localNetwork.resetAllowlistToBestMatch": {
|
|
39183
39621
|
capName: "local-network",
|
|
39184
39622
|
capScope: "system",
|
|
@@ -39191,6 +39629,12 @@ Object.freeze({
|
|
|
39191
39629
|
addonId: null,
|
|
39192
39630
|
access: "create"
|
|
39193
39631
|
},
|
|
39632
|
+
"localNetwork.setAddressRole": {
|
|
39633
|
+
capName: "local-network",
|
|
39634
|
+
capScope: "system",
|
|
39635
|
+
addonId: null,
|
|
39636
|
+
access: "create"
|
|
39637
|
+
},
|
|
39194
39638
|
"localNetwork.setAllowedAddresses": {
|
|
39195
39639
|
capName: "local-network",
|
|
39196
39640
|
capScope: "system",
|
|
@@ -40685,13 +41129,13 @@ Object.freeze({
|
|
|
40685
41129
|
addonId: null,
|
|
40686
41130
|
access: "delete"
|
|
40687
41131
|
},
|
|
40688
|
-
"pipelineExecutor.
|
|
41132
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": {
|
|
40689
41133
|
capName: "pipeline-executor",
|
|
40690
41134
|
capScope: "system",
|
|
40691
41135
|
addonId: null,
|
|
40692
41136
|
access: "delete"
|
|
40693
41137
|
},
|
|
40694
|
-
"pipelineExecutor.
|
|
41138
|
+
"pipelineExecutor.deleteModel": {
|
|
40695
41139
|
capName: "pipeline-executor",
|
|
40696
41140
|
capScope: "system",
|
|
40697
41141
|
addonId: null,
|
|
@@ -40835,12 +41279,6 @@ Object.freeze({
|
|
|
40835
41279
|
addonId: null,
|
|
40836
41280
|
access: "view"
|
|
40837
41281
|
},
|
|
40838
|
-
"pipelineExecutor.listTemplates": {
|
|
40839
|
-
capName: "pipeline-executor",
|
|
40840
|
-
capScope: "system",
|
|
40841
|
-
addonId: null,
|
|
40842
|
-
access: "view"
|
|
40843
|
-
},
|
|
40844
41282
|
"pipelineExecutor.rearmInferenceDevice": {
|
|
40845
41283
|
capName: "pipeline-executor",
|
|
40846
41284
|
capScope: "system",
|
|
@@ -40865,12 +41303,6 @@ Object.freeze({
|
|
|
40865
41303
|
addonId: null,
|
|
40866
41304
|
access: "create"
|
|
40867
41305
|
},
|
|
40868
|
-
"pipelineExecutor.saveTemplate": {
|
|
40869
|
-
capName: "pipeline-executor",
|
|
40870
|
-
capScope: "system",
|
|
40871
|
-
addonId: null,
|
|
40872
|
-
access: "create"
|
|
40873
|
-
},
|
|
40874
41306
|
"pipelineExecutor.setVideoPipelineSteps": {
|
|
40875
41307
|
capName: "pipeline-executor",
|
|
40876
41308
|
capScope: "system",
|
|
@@ -40889,41 +41321,35 @@ Object.freeze({
|
|
|
40889
41321
|
addonId: null,
|
|
40890
41322
|
access: "create"
|
|
40891
41323
|
},
|
|
40892
|
-
"pipelineExecutor.updateTemplate": {
|
|
40893
|
-
capName: "pipeline-executor",
|
|
40894
|
-
capScope: "system",
|
|
40895
|
-
addonId: null,
|
|
40896
|
-
access: "create"
|
|
40897
|
-
},
|
|
40898
41324
|
"pipelineExecutor.validatePipeline": {
|
|
40899
41325
|
capName: "pipeline-executor",
|
|
40900
41326
|
capScope: "system",
|
|
40901
41327
|
addonId: null,
|
|
40902
41328
|
access: "view"
|
|
40903
41329
|
},
|
|
40904
|
-
"pipelineOrchestrator.
|
|
41330
|
+
"pipelineOrchestrator.applyAutoTune": {
|
|
40905
41331
|
capName: "pipeline-orchestrator",
|
|
40906
41332
|
capScope: "system",
|
|
40907
41333
|
addonId: null,
|
|
40908
41334
|
access: "create"
|
|
40909
41335
|
},
|
|
40910
|
-
"pipelineOrchestrator.
|
|
41336
|
+
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
40911
41337
|
capName: "pipeline-orchestrator",
|
|
40912
41338
|
capScope: "system",
|
|
40913
41339
|
addonId: null,
|
|
40914
41340
|
access: "create"
|
|
40915
41341
|
},
|
|
40916
|
-
"pipelineOrchestrator.
|
|
41342
|
+
"pipelineOrchestrator.assignAudio": {
|
|
40917
41343
|
capName: "pipeline-orchestrator",
|
|
40918
41344
|
capScope: "system",
|
|
40919
41345
|
addonId: null,
|
|
40920
41346
|
access: "create"
|
|
40921
41347
|
},
|
|
40922
|
-
"pipelineOrchestrator.
|
|
41348
|
+
"pipelineOrchestrator.assignPipeline": {
|
|
40923
41349
|
capName: "pipeline-orchestrator",
|
|
40924
41350
|
capScope: "system",
|
|
40925
41351
|
addonId: null,
|
|
40926
|
-
access: "
|
|
41352
|
+
access: "create"
|
|
40927
41353
|
},
|
|
40928
41354
|
"pipelineOrchestrator.getAgentLoad": {
|
|
40929
41355
|
capName: "pipeline-orchestrator",
|
|
@@ -41015,6 +41441,12 @@ Object.freeze({
|
|
|
41015
41441
|
addonId: null,
|
|
41016
41442
|
access: "view"
|
|
41017
41443
|
},
|
|
41444
|
+
"pipelineOrchestrator.getEffectiveShares": {
|
|
41445
|
+
capName: "pipeline-orchestrator",
|
|
41446
|
+
capScope: "system",
|
|
41447
|
+
addonId: null,
|
|
41448
|
+
access: "view"
|
|
41449
|
+
},
|
|
41018
41450
|
"pipelineOrchestrator.getGlobalMetrics": {
|
|
41019
41451
|
capName: "pipeline-orchestrator",
|
|
41020
41452
|
capScope: "system",
|
|
@@ -41051,19 +41483,19 @@ Object.freeze({
|
|
|
41051
41483
|
addonId: null,
|
|
41052
41484
|
access: "view"
|
|
41053
41485
|
},
|
|
41054
|
-
"pipelineOrchestrator.
|
|
41486
|
+
"pipelineOrchestrator.getPlacementStats": {
|
|
41055
41487
|
capName: "pipeline-orchestrator",
|
|
41056
41488
|
capScope: "system",
|
|
41057
41489
|
addonId: null,
|
|
41058
41490
|
access: "view"
|
|
41059
41491
|
},
|
|
41060
|
-
"pipelineOrchestrator.
|
|
41492
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
41061
41493
|
capName: "pipeline-orchestrator",
|
|
41062
41494
|
capScope: "system",
|
|
41063
41495
|
addonId: null,
|
|
41064
41496
|
access: "view"
|
|
41065
41497
|
},
|
|
41066
|
-
"pipelineOrchestrator.
|
|
41498
|
+
"pipelineOrchestrator.listAgentSettings": {
|
|
41067
41499
|
capName: "pipeline-orchestrator",
|
|
41068
41500
|
capScope: "system",
|
|
41069
41501
|
addonId: null,
|
|
@@ -41075,6 +41507,12 @@ Object.freeze({
|
|
|
41075
41507
|
addonId: null,
|
|
41076
41508
|
access: "create"
|
|
41077
41509
|
},
|
|
41510
|
+
"pipelineOrchestrator.planAutoTune": {
|
|
41511
|
+
capName: "pipeline-orchestrator",
|
|
41512
|
+
capScope: "system",
|
|
41513
|
+
addonId: null,
|
|
41514
|
+
access: "view"
|
|
41515
|
+
},
|
|
41078
41516
|
"pipelineOrchestrator.rebalance": {
|
|
41079
41517
|
capName: "pipeline-orchestrator",
|
|
41080
41518
|
capScope: "system",
|
|
@@ -41117,12 +41555,6 @@ Object.freeze({
|
|
|
41117
41555
|
addonId: null,
|
|
41118
41556
|
access: "create"
|
|
41119
41557
|
},
|
|
41120
|
-
"pipelineOrchestrator.saveTemplate": {
|
|
41121
|
-
capName: "pipeline-orchestrator",
|
|
41122
|
-
capScope: "system",
|
|
41123
|
-
addonId: null,
|
|
41124
|
-
access: "create"
|
|
41125
|
-
},
|
|
41126
41558
|
"pipelineOrchestrator.setAgentCapabilities": {
|
|
41127
41559
|
capName: "pipeline-orchestrator",
|
|
41128
41560
|
capScope: "system",
|
|
@@ -41135,12 +41567,6 @@ Object.freeze({
|
|
|
41135
41567
|
addonId: null,
|
|
41136
41568
|
access: "create"
|
|
41137
41569
|
},
|
|
41138
|
-
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
41139
|
-
capName: "pipeline-orchestrator",
|
|
41140
|
-
capScope: "system",
|
|
41141
|
-
addonId: null,
|
|
41142
|
-
access: "create"
|
|
41143
|
-
},
|
|
41144
41570
|
"pipelineOrchestrator.setAgentInferenceDevices": {
|
|
41145
41571
|
capName: "pipeline-orchestrator",
|
|
41146
41572
|
capScope: "system",
|
|
@@ -41177,6 +41603,12 @@ Object.freeze({
|
|
|
41177
41603
|
addonId: null,
|
|
41178
41604
|
access: "create"
|
|
41179
41605
|
},
|
|
41606
|
+
"pipelineOrchestrator.setCameraStepSettings": {
|
|
41607
|
+
capName: "pipeline-orchestrator",
|
|
41608
|
+
capScope: "system",
|
|
41609
|
+
addonId: null,
|
|
41610
|
+
access: "create"
|
|
41611
|
+
},
|
|
41180
41612
|
"pipelineOrchestrator.setCameraStepToggle": {
|
|
41181
41613
|
capName: "pipeline-orchestrator",
|
|
41182
41614
|
capScope: "system",
|
|
@@ -41195,25 +41627,37 @@ Object.freeze({
|
|
|
41195
41627
|
addonId: null,
|
|
41196
41628
|
access: "create"
|
|
41197
41629
|
},
|
|
41630
|
+
"pipelineOrchestrator.setLoadBalancingShares": {
|
|
41631
|
+
capName: "pipeline-orchestrator",
|
|
41632
|
+
capScope: "system",
|
|
41633
|
+
addonId: null,
|
|
41634
|
+
access: "create"
|
|
41635
|
+
},
|
|
41198
41636
|
"pipelineOrchestrator.setPipelineDevicePin": {
|
|
41199
41637
|
capName: "pipeline-orchestrator",
|
|
41200
41638
|
capScope: "system",
|
|
41201
41639
|
addonId: null,
|
|
41202
41640
|
access: "create"
|
|
41203
41641
|
},
|
|
41204
|
-
"pipelineOrchestrator.
|
|
41642
|
+
"pipelineOrchestrator.setShareSource": {
|
|
41205
41643
|
capName: "pipeline-orchestrator",
|
|
41206
41644
|
capScope: "system",
|
|
41207
41645
|
addonId: null,
|
|
41208
41646
|
access: "create"
|
|
41209
41647
|
},
|
|
41210
|
-
"pipelineOrchestrator.
|
|
41648
|
+
"pipelineOrchestrator.suggestShares": {
|
|
41649
|
+
capName: "pipeline-orchestrator",
|
|
41650
|
+
capScope: "system",
|
|
41651
|
+
addonId: null,
|
|
41652
|
+
access: "view"
|
|
41653
|
+
},
|
|
41654
|
+
"pipelineOrchestrator.unassignAudio": {
|
|
41211
41655
|
capName: "pipeline-orchestrator",
|
|
41212
41656
|
capScope: "system",
|
|
41213
41657
|
addonId: null,
|
|
41214
41658
|
access: "create"
|
|
41215
41659
|
},
|
|
41216
|
-
"pipelineOrchestrator.
|
|
41660
|
+
"pipelineOrchestrator.unassignPipeline": {
|
|
41217
41661
|
capName: "pipeline-orchestrator",
|
|
41218
41662
|
capScope: "system",
|
|
41219
41663
|
addonId: null,
|
|
@@ -42653,12 +43097,6 @@ Object.freeze({
|
|
|
42653
43097
|
addonId: null,
|
|
42654
43098
|
access: "view"
|
|
42655
43099
|
},
|
|
42656
|
-
"streamBroker.pullFrameHandles": {
|
|
42657
|
-
capName: "stream-broker",
|
|
42658
|
-
capScope: "system",
|
|
42659
|
-
addonId: null,
|
|
42660
|
-
access: "view"
|
|
42661
|
-
},
|
|
42662
43100
|
"streamBroker.regenerateRtspToken": {
|
|
42663
43101
|
capName: "stream-broker",
|
|
42664
43102
|
capScope: "system",
|
|
@@ -42725,12 +43163,6 @@ Object.freeze({
|
|
|
42725
43163
|
addonId: null,
|
|
42726
43164
|
access: "create"
|
|
42727
43165
|
},
|
|
42728
|
-
"streamBroker.subscribeFrames": {
|
|
42729
|
-
capName: "stream-broker",
|
|
42730
|
-
capScope: "system",
|
|
42731
|
-
addonId: null,
|
|
42732
|
-
access: "create"
|
|
42733
|
-
},
|
|
42734
43166
|
"streamBroker.unassignProfile": {
|
|
42735
43167
|
capName: "stream-broker",
|
|
42736
43168
|
capScope: "system",
|
|
@@ -42743,12 +43175,6 @@ Object.freeze({
|
|
|
42743
43175
|
addonId: null,
|
|
42744
43176
|
access: "create"
|
|
42745
43177
|
},
|
|
42746
|
-
"streamBroker.unsubscribeFrames": {
|
|
42747
|
-
capName: "stream-broker",
|
|
42748
|
-
capScope: "system",
|
|
42749
|
-
addonId: null,
|
|
42750
|
-
access: "create"
|
|
42751
|
-
},
|
|
42752
43178
|
"streamCatalog.getCatalog": {
|
|
42753
43179
|
capName: "stream-catalog",
|
|
42754
43180
|
capScope: "device",
|
|
@@ -43445,6 +43871,12 @@ Object.freeze({
|
|
|
43445
43871
|
addonId: null,
|
|
43446
43872
|
access: "create"
|
|
43447
43873
|
},
|
|
43874
|
+
"webrtcSession.applyDownlinkProbe": {
|
|
43875
|
+
capName: "webrtc-session",
|
|
43876
|
+
capScope: "device",
|
|
43877
|
+
addonId: null,
|
|
43878
|
+
access: "create"
|
|
43879
|
+
},
|
|
43448
43880
|
"webrtcSession.closeSession": {
|
|
43449
43881
|
capName: "webrtc-session",
|
|
43450
43882
|
capScope: "device",
|
|
@@ -45256,11 +45688,6 @@ Object.freeze({
|
|
|
45256
45688
|
form: "single",
|
|
45257
45689
|
optional: false
|
|
45258
45690
|
}],
|
|
45259
|
-
"decoder.createSession": [{
|
|
45260
|
-
name: "deviceId",
|
|
45261
|
-
form: "single",
|
|
45262
|
-
optional: true
|
|
45263
|
-
}],
|
|
45264
45691
|
"detectionPipeline.applyDeviceSettingsPatch": [{
|
|
45265
45692
|
name: "deviceId",
|
|
45266
45693
|
form: "single",
|
|
@@ -46491,6 +46918,11 @@ Object.freeze({
|
|
|
46491
46918
|
form: "single",
|
|
46492
46919
|
optional: false
|
|
46493
46920
|
}],
|
|
46921
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": [{
|
|
46922
|
+
name: "deviceIds",
|
|
46923
|
+
form: "array",
|
|
46924
|
+
optional: true
|
|
46925
|
+
}],
|
|
46494
46926
|
"pipelineExecutor.runPipeline": [{
|
|
46495
46927
|
name: "deviceId",
|
|
46496
46928
|
form: "single",
|
|
@@ -46596,6 +47028,11 @@ Object.freeze({
|
|
|
46596
47028
|
form: "single",
|
|
46597
47029
|
optional: false
|
|
46598
47030
|
}],
|
|
47031
|
+
"pipelineOrchestrator.setCameraStepSettings": [{
|
|
47032
|
+
name: "deviceId",
|
|
47033
|
+
form: "single",
|
|
47034
|
+
optional: false
|
|
47035
|
+
}],
|
|
46599
47036
|
"pipelineOrchestrator.setCameraStepToggle": [{
|
|
46600
47037
|
name: "deviceId",
|
|
46601
47038
|
form: "single",
|
|
@@ -47367,6 +47804,11 @@ Object.freeze({
|
|
|
47367
47804
|
form: "single",
|
|
47368
47805
|
optional: false
|
|
47369
47806
|
}],
|
|
47807
|
+
"webrtcSession.applyDownlinkProbe": [{
|
|
47808
|
+
name: "deviceId",
|
|
47809
|
+
form: "single",
|
|
47810
|
+
optional: false
|
|
47811
|
+
}],
|
|
47370
47812
|
"webrtcSession.closeSession": [{
|
|
47371
47813
|
name: "deviceId",
|
|
47372
47814
|
form: "single",
|
|
@@ -47596,6 +48038,8 @@ new Map(Object.entries({
|
|
|
47596
48038
|
"detection-blind": "device-events",
|
|
47597
48039
|
"node-online": "system",
|
|
47598
48040
|
"node-offline": "system",
|
|
48041
|
+
"node-restarting": "system",
|
|
48042
|
+
"node-updating": "system",
|
|
47599
48043
|
"node-inference-unavailable": "system",
|
|
47600
48044
|
"addon-crash-loop": "system",
|
|
47601
48045
|
"addon-update-available": "system",
|
|
@@ -47673,18 +48117,21 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47673
48117
|
kinds: [
|
|
47674
48118
|
"node-offline",
|
|
47675
48119
|
"node-online",
|
|
48120
|
+
"node-restarting",
|
|
47676
48121
|
"node-inference-unavailable"
|
|
47677
48122
|
]
|
|
47678
48123
|
},
|
|
47679
48124
|
{
|
|
47680
48125
|
id: "addons",
|
|
47681
48126
|
family: "system",
|
|
47682
|
-
label: "Addons",
|
|
47683
|
-
kinds: [
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
|
|
47687
|
-
|
|
48127
|
+
label: "Addons stopped",
|
|
48128
|
+
kinds: ["addon-crash-loop"]
|
|
48129
|
+
},
|
|
48130
|
+
{
|
|
48131
|
+
id: "addon-updates",
|
|
48132
|
+
family: "system",
|
|
48133
|
+
label: "Addon updates",
|
|
48134
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47688
48135
|
},
|
|
47689
48136
|
{
|
|
47690
48137
|
id: "updates",
|
|
@@ -47692,6 +48139,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47692
48139
|
label: "Server & container updates",
|
|
47693
48140
|
kinds: [
|
|
47694
48141
|
"server-update-available",
|
|
48142
|
+
"node-updating",
|
|
47695
48143
|
"server-updated",
|
|
47696
48144
|
"wrapper-update-available"
|
|
47697
48145
|
]
|
|
@@ -47839,18 +48287,21 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47839
48287
|
kinds: [
|
|
47840
48288
|
"node-offline",
|
|
47841
48289
|
"node-online",
|
|
48290
|
+
"node-restarting",
|
|
47842
48291
|
"node-inference-unavailable"
|
|
47843
48292
|
]
|
|
47844
48293
|
},
|
|
47845
48294
|
{
|
|
47846
48295
|
name: "summaryAddons",
|
|
47847
|
-
label: "Summary: addons",
|
|
47848
|
-
example: "
|
|
47849
|
-
kinds: [
|
|
47850
|
-
|
|
47851
|
-
|
|
47852
|
-
|
|
47853
|
-
|
|
48296
|
+
label: "Summary: addons stopped",
|
|
48297
|
+
example: "hub — recorder stopped after repeated crashes",
|
|
48298
|
+
kinds: ["addon-crash-loop"]
|
|
48299
|
+
},
|
|
48300
|
+
{
|
|
48301
|
+
name: "summaryAddonUpdates",
|
|
48302
|
+
label: "Summary: addon updates",
|
|
48303
|
+
example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
|
|
48304
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47854
48305
|
},
|
|
47855
48306
|
{
|
|
47856
48307
|
name: "summaryUpdates",
|
|
@@ -47858,6 +48309,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47858
48309
|
example: "server 1.2.377 → 1.2.378",
|
|
47859
48310
|
kinds: [
|
|
47860
48311
|
"server-update-available",
|
|
48312
|
+
"node-updating",
|
|
47861
48313
|
"server-updated",
|
|
47862
48314
|
"wrapper-update-available"
|
|
47863
48315
|
]
|
|
@@ -47890,7 +48342,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47890
48342
|
{
|
|
47891
48343
|
name: "summarySection",
|
|
47892
48344
|
label: "Summary: this section",
|
|
47893
|
-
description: "The section a per-section summary row is about
|
|
48345
|
+
description: "The section a per-section summary row is about; on a summary sent as one message, every section it reports, comma-separated.",
|
|
47894
48346
|
example: "Batterie scariche",
|
|
47895
48347
|
group: "system",
|
|
47896
48348
|
families: ["rule"],
|
|
@@ -47934,6 +48386,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47934
48386
|
[...RULE_BODY_ONLY],
|
|
47935
48387
|
[...RULE_TEXT],
|
|
47936
48388
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48389
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
47937
48390
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
47938
48391
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
47939
48392
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -47974,6 +48427,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
47974
48427
|
[...RULE_TEXT],
|
|
47975
48428
|
[...RULE_TEXT],
|
|
47976
48429
|
[...RULE_TEXT],
|
|
48430
|
+
[...RULE_TEXT],
|
|
48431
|
+
[...RULE_TEXT],
|
|
47977
48432
|
...STATE_SUMMARY_VARS
|
|
47978
48433
|
];
|
|
47979
48434
|
new Set([
|
|
@@ -48493,45 +48948,17 @@ DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
|
48493
48948
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
48494
48949
|
var MB = 1024 * 1024;
|
|
48495
48950
|
1024 * MB, 3072 * MB;
|
|
48496
|
-
/**
|
|
48497
|
-
* Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
|
|
48498
|
-
* drain() returns up to maxCount items in FIFO order and removes them.
|
|
48499
|
-
*/
|
|
48500
|
-
var RingBuffer = class {
|
|
48501
|
-
capacity;
|
|
48502
|
-
items;
|
|
48503
|
-
head = 0;
|
|
48504
|
-
tail = 0;
|
|
48505
|
-
count = 0;
|
|
48506
|
-
constructor(capacity) {
|
|
48507
|
-
this.capacity = capacity;
|
|
48508
|
-
this.items = Array.from({ length: capacity });
|
|
48509
|
-
}
|
|
48510
|
-
get size() {
|
|
48511
|
-
return this.count;
|
|
48512
|
-
}
|
|
48513
|
-
push(item) {
|
|
48514
|
-
this.items[this.tail] = item;
|
|
48515
|
-
this.tail = (this.tail + 1) % this.capacity;
|
|
48516
|
-
if (this.count < this.capacity) this.count++;
|
|
48517
|
-
else this.head = (this.head + 1) % this.capacity;
|
|
48518
|
-
}
|
|
48519
|
-
drain(maxCount) {
|
|
48520
|
-
const result = [];
|
|
48521
|
-
const n = Math.min(maxCount, this.count);
|
|
48522
|
-
for (let i = 0; i < n; i++) {
|
|
48523
|
-
result.push(this.items[this.head]);
|
|
48524
|
-
this.items[this.head] = void 0;
|
|
48525
|
-
this.head = (this.head + 1) % this.capacity;
|
|
48526
|
-
}
|
|
48527
|
-
this.count -= n;
|
|
48528
|
-
return result;
|
|
48529
|
-
}
|
|
48530
|
-
};
|
|
48531
48951
|
//#endregion
|
|
48532
|
-
//#region src/
|
|
48952
|
+
//#region src/decode-hwaccel-rank.ts
|
|
48953
|
+
/**
|
|
48954
|
+
* Decode-hwaccel ranking for `reprobeHwaccel`: the kernel resolver's preferred
|
|
48955
|
+
* order, re-ranked so the backend this node reports is the one that actually
|
|
48956
|
+
* decodes (vaapi above qsv on Intel). A copy of the broker's
|
|
48957
|
+
* `packages/addon-pipeline/src/shared/ffmpeg-decode-args.ts` ranking — addons
|
|
48958
|
+
* do not import each other.
|
|
48959
|
+
*/
|
|
48533
48960
|
/**
|
|
48534
|
-
* Canonical decode-hwaccel preference order
|
|
48961
|
+
* Canonical decode-hwaccel preference order.
|
|
48535
48962
|
*
|
|
48536
48963
|
* `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
|
|
48537
48964
|
* child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
|
|
@@ -48596,1234 +49023,6 @@ function rankDecodeHwAccels(preferred, supportedMethods) {
|
|
|
48596
49023
|
return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
|
|
48597
49024
|
}
|
|
48598
49025
|
//#endregion
|
|
48599
|
-
//#region src/shared/notifying-ring-buffer.ts
|
|
48600
|
-
/**
|
|
48601
|
-
* A {@link RingBuffer} that can notify a single blocked consumer the moment an
|
|
48602
|
-
* item commits — the server side of the `pullFrames` long-poll.
|
|
48603
|
-
*
|
|
48604
|
-
* The decoder's `onFrame` callback drives `push()`; a consumer
|
|
48605
|
-
* that finds the ring momentarily empty calls {@link waitForItem} and is woken
|
|
48606
|
-
* on the very next `push()` (a real commit signal) rather than spinning at
|
|
48607
|
-
* ~1kHz on `setTimeout(…,1)`. If nothing commits it resolves after `waitMs`,
|
|
48608
|
-
* and {@link cancel} (session teardown) resolves any in-flight wait promptly so
|
|
48609
|
-
* a blocked long-poll never waits out `waitMs` on destroy.
|
|
48610
|
-
*
|
|
48611
|
-
* Semantics are otherwise identical to `RingBuffer`: fixed capacity,
|
|
48612
|
-
* latest-wins overwrite when full, FIFO `drain`.
|
|
48613
|
-
*/
|
|
48614
|
-
var NotifyingRingBuffer = class {
|
|
48615
|
-
ring;
|
|
48616
|
-
waiters = /* @__PURE__ */ new Set();
|
|
48617
|
-
constructor(capacity) {
|
|
48618
|
-
this.ring = new RingBuffer(capacity);
|
|
48619
|
-
}
|
|
48620
|
-
get size() {
|
|
48621
|
-
return this.ring.size;
|
|
48622
|
-
}
|
|
48623
|
-
push(item) {
|
|
48624
|
-
this.ring.push(item);
|
|
48625
|
-
this.wakeAll();
|
|
48626
|
-
}
|
|
48627
|
-
drain(maxCount) {
|
|
48628
|
-
return this.ring.drain(maxCount);
|
|
48629
|
-
}
|
|
48630
|
-
/**
|
|
48631
|
-
* Resolve as soon as an item commits to the ring, or after `waitMs`,
|
|
48632
|
-
* whichever comes first. Returns immediately when items are already present
|
|
48633
|
-
* or `waitMs <= 0` (legacy immediate-return). {@link cancel} also resolves an
|
|
48634
|
-
* in-flight wait.
|
|
48635
|
-
*/
|
|
48636
|
-
async waitForItem(waitMs) {
|
|
48637
|
-
if (this.ring.size > 0 || waitMs <= 0) return;
|
|
48638
|
-
await new Promise((resolve) => {
|
|
48639
|
-
let settled = false;
|
|
48640
|
-
const finish = () => {
|
|
48641
|
-
if (settled) return;
|
|
48642
|
-
settled = true;
|
|
48643
|
-
this.waiters.delete(wake);
|
|
48644
|
-
clearTimeout(timer);
|
|
48645
|
-
resolve();
|
|
48646
|
-
};
|
|
48647
|
-
const wake = () => finish();
|
|
48648
|
-
const timer = setTimeout(finish, waitMs);
|
|
48649
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
48650
|
-
this.waiters.add(wake);
|
|
48651
|
-
});
|
|
48652
|
-
}
|
|
48653
|
-
/**
|
|
48654
|
-
* Cancel every in-flight {@link waitForItem} — called on session teardown so
|
|
48655
|
-
* a blocked long-poll returns promptly instead of waiting out `waitMs`.
|
|
48656
|
-
*/
|
|
48657
|
-
cancel() {
|
|
48658
|
-
this.wakeAll();
|
|
48659
|
-
}
|
|
48660
|
-
wakeAll() {
|
|
48661
|
-
if (this.waiters.size === 0) return;
|
|
48662
|
-
const pending = [...this.waiters];
|
|
48663
|
-
this.waiters.clear();
|
|
48664
|
-
for (const wake of pending) wake();
|
|
48665
|
-
}
|
|
48666
|
-
};
|
|
48667
|
-
//#endregion
|
|
48668
|
-
//#region src/shared/decoder-backend-keys.ts
|
|
48669
|
-
/** Built-in default when a node has no selection — the in-process node-av
|
|
48670
|
-
* decoder. node-av is the standing default everywhere; the ffmpeg subprocess
|
|
48671
|
-
* decoder is an explicit opt-in fallback only. A stale/failed settings read at
|
|
48672
|
-
* boot therefore resolves to node-av (never leaves the node with no decoder). */
|
|
48673
|
-
var DEFAULT_DECODER_BACKEND = "nodeav";
|
|
48674
|
-
//#endregion
|
|
48675
|
-
//#region src/shared/decoder-backend.ts
|
|
48676
|
-
/**
|
|
48677
|
-
* Resolve the decoder backend this addon should run. Owner settings are
|
|
48678
|
-
* ignored: persisted `backend@<nodeId>` rows stay in the store and are not
|
|
48679
|
-
* consulted. The runtime default is always {@link DEFAULT_DECODER_BACKEND}.
|
|
48680
|
-
*/
|
|
48681
|
-
async function resolveDecoderBackend(_api, _nodeId, _logger) {
|
|
48682
|
-
return DEFAULT_DECODER_BACKEND;
|
|
48683
|
-
}
|
|
48684
|
-
//#endregion
|
|
48685
|
-
//#region src/pull-demuxer-options.ts
|
|
48686
|
-
/**
|
|
48687
|
-
* libav demuxer options for the node-av PULL decode path — RTP sources
|
|
48688
|
-
* (`isRtpSource()`) dialing the broker's RTSP restream (`rtsp://…/muted`).
|
|
48689
|
-
*
|
|
48690
|
-
* Without low-latency flags libav buffers up to `analyzeduration` (default
|
|
48691
|
-
* **5s**) of the live stream during `avformat_find_stream_info`, and that buffer
|
|
48692
|
-
* becomes a fixed ~5s offset on the decoded-frame timeline — detection overlays
|
|
48693
|
-
* then lag the WebRTC raw-RTP passthrough (which never decodes) by ~5s. Adding
|
|
48694
|
-
* `fflags: nobuffer` + a short `analyzeduration` collapses that offset to
|
|
48695
|
-
* sub-second, mirroring the ffmpeg backend's `-fflags +nobuffer` on its pull
|
|
48696
|
-
* path (decoder-ffmpeg/ffmpeg-args.ts).
|
|
48697
|
-
*
|
|
48698
|
-
* ‼ `analyzeduration`/`probesize` stay SMALL BUT NON-ZERO. Probe-zeroing
|
|
48699
|
-
* (`analyzeduration 0`) on the pull RTSP path STARVES the Reolink rfc4571
|
|
48700
|
-
* restream demuxer (garbage / zero frames — looked like a ~3000fps churn
|
|
48701
|
-
* runaway); that optimisation is push-mode-only. Keep them > 0.
|
|
48702
|
-
*/
|
|
48703
|
-
function buildPullDemuxerOptions() {
|
|
48704
|
-
return {
|
|
48705
|
-
rtsp_transport: "tcp",
|
|
48706
|
-
fflags: "nobuffer",
|
|
48707
|
-
analyzeduration: "1000000",
|
|
48708
|
-
probesize: "1000000",
|
|
48709
|
-
max_delay: "0",
|
|
48710
|
-
reorder_queue_size: "0",
|
|
48711
|
-
user_agent: "decoder"
|
|
48712
|
-
};
|
|
48713
|
-
}
|
|
48714
|
-
//#endregion
|
|
48715
|
-
//#region src/scaler-geometry.ts
|
|
48716
|
-
/**
|
|
48717
|
-
* Decide the scaler action for an incoming frame's source geometry.
|
|
48718
|
-
*
|
|
48719
|
-
* @param current The scaler's configured source geometry, or `null` when no
|
|
48720
|
-
* scaler has been built yet.
|
|
48721
|
-
* @param incoming The source geometry of the frame about to be scaled.
|
|
48722
|
-
*/
|
|
48723
|
-
function resolveScalerAction(current, incoming) {
|
|
48724
|
-
if (current === null) return "init";
|
|
48725
|
-
if (current.width === incoming.width && current.height === incoming.height && current.format === incoming.format) return "reuse";
|
|
48726
|
-
return "rebuild";
|
|
48727
|
-
}
|
|
48728
|
-
//#endregion
|
|
48729
|
-
//#region src/nodeav-decoder-session.ts
|
|
48730
|
-
/**
|
|
48731
|
-
* Pull-mode re-dial backoff. On a pull input error/EOF the loop waits this long
|
|
48732
|
-
* before re-opening the URL — mirrors the ffmpeg pull decoder's 3s re-dial so a
|
|
48733
|
-
* transient RTSP restream blip (source suspended on idle, keyframe wait) does
|
|
48734
|
-
* not hot-loop `Demuxer.open`.
|
|
48735
|
-
*/
|
|
48736
|
-
var PULL_REDIAL_MS = 3e3;
|
|
48737
|
-
/**
|
|
48738
|
-
* Short re-dial backoff for the adaptive "drop-to-live" path. When the decoder
|
|
48739
|
-
* has fallen hopelessly behind the live edge ({@link DROP_TO_LIVE_LAG_MS}) the
|
|
48740
|
-
* loop tears the input down and re-dials fast so the restream's burst-prime
|
|
48741
|
-
* re-seats it at the live edge, instead of waiting the full {@link PULL_REDIAL_MS}
|
|
48742
|
-
* (which is for transient stream blips, not a deliberate re-seat).
|
|
48743
|
-
*/
|
|
48744
|
-
var PULL_FAST_REDIAL_MS = 250;
|
|
48745
|
-
/** Adaptive controller cadence — recompute emit-fps from lag at most this often. */
|
|
48746
|
-
var ADAPTIVE_TICK_MS = 1e3;
|
|
48747
|
-
/** Lag EWMA above this (ms) → halve the adaptive emit-fps. */
|
|
48748
|
-
var ADAPTIVE_LAG_HIGH_MS = 750;
|
|
48749
|
-
/** Lag EWMA below this (ms), sustained {@link ADAPTIVE_RECOVER_SUSTAIN_MS} → grow emit-fps. */
|
|
48750
|
-
var ADAPTIVE_LAG_LOW_MS = 250;
|
|
48751
|
-
/** How long lag must stay below {@link ADAPTIVE_LAG_LOW_MS} before growing emit-fps. */
|
|
48752
|
-
var ADAPTIVE_RECOVER_SUSTAIN_MS = 5e3;
|
|
48753
|
-
/** Floor the adaptive controller never sheds below (fps). */
|
|
48754
|
-
var ADAPTIVE_MIN_FPS = 2;
|
|
48755
|
-
/**
|
|
48756
|
-
* Ceiling the adaptive controller recovers toward when the subscriber requested
|
|
48757
|
-
* an UNLIMITED rate (`maxFps <= 0`). Source cameras run ≤ this, so the clamp is
|
|
48758
|
-
* a no-op in practice while keeping the /2 shed + *1.5 recover math bounded.
|
|
48759
|
-
*/
|
|
48760
|
-
var ADAPTIVE_FALLBACK_CEILING_FPS = 30;
|
|
48761
|
-
/**
|
|
48762
|
-
* Lag EWMA above this (ms) = hopelessly behind → drop-to-live re-seat.
|
|
48763
|
-
*
|
|
48764
|
-
* This bounds the MAX glass-to-box media staleness: `lagMsEwma` is how far the
|
|
48765
|
-
* decoder's media clock trails live, i.e. how old the *content* of an emitted
|
|
48766
|
-
* frame is (distinct from `frameAge`, which only measures decode→pick pipeline
|
|
48767
|
-
* delay and does NOT capture this media lag). On a host that decodes marginally
|
|
48768
|
-
* below real-time the lag sawtooths up to this ceiling, then re-seats to live —
|
|
48769
|
-
* so this constant directly caps how stale the boxes can get. 1200 ms (was
|
|
48770
|
-
* 2000) trades slightly more frequent 250 ms re-seats for fresher overlays; the
|
|
48771
|
-
* re-seat cost is negligible (~1.4 drops/cam/min at the observed drift). The
|
|
48772
|
-
* real fix — keeping decode at real-time so the lag never builds — is the
|
|
48773
|
-
* co-located / subprocess-decode epic.
|
|
48774
|
-
*/
|
|
48775
|
-
var DROP_TO_LIVE_LAG_MS = 1200;
|
|
48776
|
-
/** Never trigger drop-to-live more than once per this window (ms). */
|
|
48777
|
-
var DROP_TO_LIVE_MIN_INTERVAL_MS = 3e3;
|
|
48778
|
-
/** Decoder DEBUG facility flush cadence (~1Hz). */
|
|
48779
|
-
var DEBUG_FLUSH_MS = 1e3;
|
|
48780
|
-
/** Map our canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
|
|
48781
|
-
function backendToHwDeviceConst(backend, consts) {
|
|
48782
|
-
switch (backend) {
|
|
48783
|
-
case "videotoolbox": return consts.AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
|
|
48784
|
-
case "cuda": return consts.AV_HWDEVICE_TYPE_CUDA;
|
|
48785
|
-
case "nvdec": return consts.AV_HWDEVICE_TYPE_CUDA;
|
|
48786
|
-
case "vaapi": return consts.AV_HWDEVICE_TYPE_VAAPI;
|
|
48787
|
-
case "qsv": return consts.AV_HWDEVICE_TYPE_QSV;
|
|
48788
|
-
case "d3d11va": return consts.AV_HWDEVICE_TYPE_D3D11VA;
|
|
48789
|
-
case "dxva2": return consts.AV_HWDEVICE_TYPE_DXVA2;
|
|
48790
|
-
case "amf": return consts.AV_HWDEVICE_TYPE_AMF;
|
|
48791
|
-
case "vdpau": return consts.AV_HWDEVICE_TYPE_VDPAU;
|
|
48792
|
-
case "drm": return consts.AV_HWDEVICE_TYPE_DRM;
|
|
48793
|
-
default: return null;
|
|
48794
|
-
}
|
|
48795
|
-
}
|
|
48796
|
-
/** Pick the hw pixel format for a given backend — used in get_format callback. */
|
|
48797
|
-
function backendToHwPixFmt(backend, consts) {
|
|
48798
|
-
switch (backend) {
|
|
48799
|
-
case "videotoolbox": return consts.AV_PIX_FMT_VIDEOTOOLBOX;
|
|
48800
|
-
case "cuda": return consts.AV_PIX_FMT_CUDA;
|
|
48801
|
-
case "nvdec": return consts.AV_PIX_FMT_CUDA;
|
|
48802
|
-
case "vaapi": return consts.AV_PIX_FMT_VAAPI;
|
|
48803
|
-
case "qsv": return consts.AV_PIX_FMT_QSV;
|
|
48804
|
-
case "d3d11va": return consts.AV_PIX_FMT_D3D11;
|
|
48805
|
-
case "dxva2": return consts.AV_PIX_FMT_DXVA2_VLD;
|
|
48806
|
-
case "amf": return consts.AV_PIX_FMT_D3D11;
|
|
48807
|
-
case "vdpau": return consts.AV_PIX_FMT_VDPAU;
|
|
48808
|
-
case "drm": return consts.AV_PIX_FMT_DRM_PRIME;
|
|
48809
|
-
default: return null;
|
|
48810
|
-
}
|
|
48811
|
-
}
|
|
48812
|
-
var _nav = null;
|
|
48813
|
-
var _consts = null;
|
|
48814
|
-
var _sharp = null;
|
|
48815
|
-
async function getNodeAv() {
|
|
48816
|
-
if (!_nav) _nav = await import("node-av");
|
|
48817
|
-
return _nav;
|
|
48818
|
-
}
|
|
48819
|
-
async function getConstants() {
|
|
48820
|
-
if (!_consts) _consts = await import("node-av/constants");
|
|
48821
|
-
return _consts;
|
|
48822
|
-
}
|
|
48823
|
-
async function getSharp() {
|
|
48824
|
-
if (!_sharp) _sharp = (await import("sharp")).default;
|
|
48825
|
-
return _sharp;
|
|
48826
|
-
}
|
|
48827
|
-
var noopLogger = {
|
|
48828
|
-
debug() {},
|
|
48829
|
-
info() {},
|
|
48830
|
-
warn() {},
|
|
48831
|
-
error() {},
|
|
48832
|
-
child() {
|
|
48833
|
-
return noopLogger;
|
|
48834
|
-
},
|
|
48835
|
-
withTags() {
|
|
48836
|
-
return noopLogger;
|
|
48837
|
-
}
|
|
48838
|
-
};
|
|
48839
|
-
var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
48840
|
-
config;
|
|
48841
|
-
logger;
|
|
48842
|
-
frameCallbacks = /* @__PURE__ */ new Set();
|
|
48843
|
-
destroyed = false;
|
|
48844
|
-
parser = null;
|
|
48845
|
-
codecCtx = null;
|
|
48846
|
-
scaler = null;
|
|
48847
|
-
avPacket = null;
|
|
48848
|
-
avFrame = null;
|
|
48849
|
-
dstFrame = null;
|
|
48850
|
-
EAGAIN = -11;
|
|
48851
|
-
PIX_FMT_GRAY8 = 8;
|
|
48852
|
-
PIX_FMT_RGB24 = 2;
|
|
48853
|
-
SWS_FAST_BILINEAR = 1;
|
|
48854
|
-
/**
|
|
48855
|
-
* Decoder output mode. Drives both the scaler's destination pixel
|
|
48856
|
-
* format and whether sharp runs the JPEG encode at the end:
|
|
48857
|
-
*
|
|
48858
|
-
* - `'jpeg'` — scaler→RGB24 → sharp encode → emit JPEG bytes
|
|
48859
|
-
* - `'rgb'` — scaler→RGB24 → emit raw RGB24 (no sharp)
|
|
48860
|
-
* - `'gray'` — scaler→GRAY8 → emit raw GRAY8 (no sharp)
|
|
48861
|
-
*
|
|
48862
|
-
* The broker holds the policy decision on which mode to request based
|
|
48863
|
-
* on its active subscribers; on-the-fly conversion (e.g. RGB→JPEG for
|
|
48864
|
-
* a WebRTC consumer that joined while detection holds the decoder in
|
|
48865
|
-
* RGB mode) happens broker-side via the per-frame conversion cache.
|
|
48866
|
-
*/
|
|
48867
|
-
outputMode;
|
|
48868
|
-
sharpFn = null;
|
|
48869
|
-
/**
|
|
48870
|
-
* Backpressure for the sharp JPEG encode pipeline. The broker
|
|
48871
|
-
* currently creates sessions with `maxFps: 0` (unlimited) and relies
|
|
48872
|
-
* on per-subscriber throttling, so without a bound the
|
|
48873
|
-
* fire-and-forget `sharp(...).toBuffer()` chain would accumulate
|
|
48874
|
-
* unboundedly whenever sharp falls behind the decoder. Cap at
|
|
48875
|
-
* `MAX_JPEG_INFLIGHT` pending encodes per session — any frame that
|
|
48876
|
-
* arrives while the cap is saturated is dropped and counted.
|
|
48877
|
-
*/
|
|
48878
|
-
static MAX_JPEG_INFLIGHT = 2;
|
|
48879
|
-
jpegEncodeInFlight = 0;
|
|
48880
|
-
/**
|
|
48881
|
-
* Map a `DecoderSessionConfig.outputFormat` value to one of the three
|
|
48882
|
-
* native scaler/encoder modes the session understands. The cap-level
|
|
48883
|
-
* format vocabulary is broader (it accepts `bgr`, `yuv420`) than what
|
|
48884
|
-
* libav's scaler is wired for here — anything else degrades to RGB
|
|
48885
|
-
* (the canonical raw mode) and the broker is expected to convert
|
|
48886
|
-
* downstream if a subscriber needs a different shape.
|
|
48887
|
-
*/
|
|
48888
|
-
static resolveOutputMode(format) {
|
|
48889
|
-
if (format === "jpeg" || format === void 0) return "jpeg";
|
|
48890
|
-
if (format === "gray") return "gray";
|
|
48891
|
-
return "rgb";
|
|
48892
|
-
}
|
|
48893
|
-
initialized = false;
|
|
48894
|
-
initializing = false;
|
|
48895
|
-
scalerInitializing = false;
|
|
48896
|
-
/**
|
|
48897
|
-
* Monotonic counter incremented by `updateConfig` whenever the
|
|
48898
|
-
* scaler + dstFrame get invalidated (e.g. output format toggle).
|
|
48899
|
-
* `initScaler` captures the current value at entry and aborts — or
|
|
48900
|
-
* disposes the locally-built scaler — if the epoch moved while
|
|
48901
|
-
* its async init was in flight. Without this, a toggle racing an
|
|
48902
|
-
* in-flight init could leave two scalers allocated natively while
|
|
48903
|
-
* `this.scaler` only holds a reference to one → libav leak.
|
|
48904
|
-
*/
|
|
48905
|
-
scalerEpoch = 0;
|
|
48906
|
-
/**
|
|
48907
|
-
* One-shot guard for the "first frame" diagnostic log + raw frame
|
|
48908
|
-
* dump. Setting this synchronously inside `emitDecodedFrame`
|
|
48909
|
-
* prevents re-entry — without it we were using `outputFrames === 0`
|
|
48910
|
-
* which stays true until the async sharp encode callback runs, so
|
|
48911
|
-
* several decoded frames could trigger the dump before the first
|
|
48912
|
-
* JPEG landed.
|
|
48913
|
-
*/
|
|
48914
|
-
firstFrameLogged = false;
|
|
48915
|
-
outWidth = 0;
|
|
48916
|
-
outHeight = 0;
|
|
48917
|
-
/**
|
|
48918
|
-
* The SOURCE geometry the current `scaler` was built for. `sws_scale` trusts
|
|
48919
|
-
* these dimensions blindly; if a later decoded frame arrives with different
|
|
48920
|
-
* width/height/format we MUST rebuild the scaler rather than feed it mismatched
|
|
48921
|
-
* planes (an out-of-bounds read/write → uncatchable SIGBUS). `-1` means "no
|
|
48922
|
-
* scaler built yet" (mirrors `this.scaler === null`).
|
|
48923
|
-
*/
|
|
48924
|
-
scalerSrcW = -1;
|
|
48925
|
-
scalerSrcH = -1;
|
|
48926
|
-
scalerSrcFmt = -1;
|
|
48927
|
-
lastEmitTime = 0;
|
|
48928
|
-
minIntervalMs;
|
|
48929
|
-
/** Gates {@link emitDebugFlash}; set from `config.debug === true`. Off = zero cost. */
|
|
48930
|
-
debugEnabled;
|
|
48931
|
-
/** Wall-clock of the last `decoder debug` flush — throttles the facility to ~1Hz. */
|
|
48932
|
-
lastDebugFlush = 0;
|
|
48933
|
-
/** `droppedFrames` snapshot at the last debug flush — window base for the drop delta. */
|
|
48934
|
-
debugWindowDropped = 0;
|
|
48935
|
-
/** Subscriber-requested emit ceiling (fps). `maxFps<=0` clamps to the fallback ceiling. */
|
|
48936
|
-
ceilingFps;
|
|
48937
|
-
/** Current lag-throttled emit rate (fps) — starts at the ceiling, floored at ADAPTIVE_MIN_FPS. */
|
|
48938
|
-
adaptiveFps;
|
|
48939
|
-
/** Frames actually emitted/sec over the last adaptive-tick window (feeds getStats + debug). */
|
|
48940
|
-
effectiveFps = 0;
|
|
48941
|
-
/** `outputFrames` snapshot at the last adaptive tick — window base for effective-fps. */
|
|
48942
|
-
adaptiveWindowFrames = 0;
|
|
48943
|
-
/** First sampled frame's pts (timebase units); `null` until the current dial seeds it. */
|
|
48944
|
-
basePts = null;
|
|
48945
|
-
/** Wall-clock at `basePts` — the real-time anchor for the drift computation. */
|
|
48946
|
-
baseWall = 0;
|
|
48947
|
-
/** EWMA of real-time drift (ms). Rising = decoder falling behind the live edge. */
|
|
48948
|
-
lagMsEwma = 0;
|
|
48949
|
-
/** Wall-clock the lag EWMA first dropped below ADAPTIVE_LAG_LOW_MS (0 = not low). */
|
|
48950
|
-
lowLagSince = 0;
|
|
48951
|
-
/** Wall-clock of the last adaptive-controller tick. */
|
|
48952
|
-
lastAdaptiveCheck = 0;
|
|
48953
|
-
/** Wall-clock of the last drop-to-live re-seat — rate-limits the re-dial. */
|
|
48954
|
-
lastDropToLive = 0;
|
|
48955
|
-
/** Set by drop-to-live so `runPullLoop` uses the SHORT re-dial backoff for one iteration. */
|
|
48956
|
-
pullFastRedial = false;
|
|
48957
|
-
inputPackets = 0;
|
|
48958
|
-
outputFrames = 0;
|
|
48959
|
-
droppedFrames = 0;
|
|
48960
|
-
totalDecodeTimeMs = 0;
|
|
48961
|
-
startTime = Date.now();
|
|
48962
|
-
hwaccelPref;
|
|
48963
|
-
hwaccelResolver;
|
|
48964
|
-
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
48965
|
-
activeHwAccel = "none";
|
|
48966
|
-
hwDevice = null;
|
|
48967
|
-
swTransferFrame = null;
|
|
48968
|
-
/** True once `openStream` has armed the pull loop; drives `isPullMode`. */
|
|
48969
|
-
pullActive = false;
|
|
48970
|
-
/** One-shot guard so a stray push into a pull session logs once, not per-packet. */
|
|
48971
|
-
pullStrayPushLogged = false;
|
|
48972
|
-
/** The URL the pull loop (re)dials — updatable while active. */
|
|
48973
|
-
pullUrl = null;
|
|
48974
|
-
/** Pending re-dial timer + its resolver, so `destroy` can wake it instantly. */
|
|
48975
|
-
pullRedialTimer = null;
|
|
48976
|
-
pullRedialResolve = null;
|
|
48977
|
-
/** High-level demuxer/decoder for the CURRENT dial — recreated per re-dial. */
|
|
48978
|
-
pullDemuxer = null;
|
|
48979
|
-
pullDecoder = null;
|
|
48980
|
-
/** HW context — resolved once, reused across re-dials, freed in `destroy`. */
|
|
48981
|
-
pullHwContext = null;
|
|
48982
|
-
constructor(config, logger = noopLogger, options) {
|
|
48983
|
-
this.config = { ...config };
|
|
48984
|
-
const sessionTags = {};
|
|
48985
|
-
if (typeof config.deviceId === "number") sessionTags["deviceId"] = config.deviceId;
|
|
48986
|
-
if (typeof config.tag === "string" && config.tag.length > 0) sessionTags["tag"] = config.tag;
|
|
48987
|
-
this.logger = Object.keys(sessionTags).length > 0 ? logger.withTags(sessionTags) : logger;
|
|
48988
|
-
this.minIntervalMs = config.maxFps > 0 ? 1e3 / config.maxFps : 0;
|
|
48989
|
-
this.debugEnabled = config.debug === true;
|
|
48990
|
-
this.ceilingFps = config.maxFps > 0 ? config.maxFps : ADAPTIVE_FALLBACK_CEILING_FPS;
|
|
48991
|
-
this.adaptiveFps = this.ceilingFps;
|
|
48992
|
-
this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
|
|
48993
|
-
this.hwaccelPref = options?.hwaccel ?? "auto";
|
|
48994
|
-
this.hwaccelResolver = options?.hwaccelResolver ?? null;
|
|
48995
|
-
}
|
|
48996
|
-
/**
|
|
48997
|
-
* Resolve the backend preference list and try each one against
|
|
48998
|
-
* node-av's HW context APIs. The first backend whose
|
|
48999
|
-
* `HardwareDeviceContext.create()` succeeds gets attached to
|
|
49000
|
-
* `codecCtx.hwDeviceCtx` + its hw pixel format registered via
|
|
49001
|
-
* `setHardwarePixelFormat`. On any failure, falls through to the
|
|
49002
|
-
* next backend; if all fail, returns with `activeHwAccel='none'`
|
|
49003
|
-
* and the decoder runs in software on the same context.
|
|
49004
|
-
*/
|
|
49005
|
-
async tryAttachHwAccel(nav, C) {
|
|
49006
|
-
if (!this.codecCtx) return;
|
|
49007
|
-
if (this.hwaccelPref === "none") {
|
|
49008
|
-
this.activeHwAccel = "none";
|
|
49009
|
-
return;
|
|
49010
|
-
}
|
|
49011
|
-
const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
|
|
49012
|
-
const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
|
|
49013
|
-
preferred: [explicit],
|
|
49014
|
-
rationale: "explicit (no resolver)"
|
|
49015
|
-
} : {
|
|
49016
|
-
preferred: [],
|
|
49017
|
-
rationale: "auto + no resolver → software"
|
|
49018
|
-
};
|
|
49019
|
-
if (resolution.preferred.length === 0) {
|
|
49020
|
-
this.activeHwAccel = "none";
|
|
49021
|
-
return;
|
|
49022
|
-
}
|
|
49023
|
-
for (const backend of resolution.preferred) {
|
|
49024
|
-
const deviceType = backendToHwDeviceConst(backend, C);
|
|
49025
|
-
const hwPixFmt = backendToHwPixFmt(backend, C);
|
|
49026
|
-
if (!deviceType || !hwPixFmt) continue;
|
|
49027
|
-
const device = new nav.HardwareDeviceContext();
|
|
49028
|
-
const rc = device.create(deviceType);
|
|
49029
|
-
if (rc < 0) {
|
|
49030
|
-
this.logger.warn("node-av: hwaccel device create failed — trying next", { meta: {
|
|
49031
|
-
backend,
|
|
49032
|
-
rc
|
|
49033
|
-
} });
|
|
49034
|
-
device.free();
|
|
49035
|
-
continue;
|
|
49036
|
-
}
|
|
49037
|
-
try {
|
|
49038
|
-
this.codecCtx.hwDeviceCtx = device;
|
|
49039
|
-
this.codecCtx.setHardwarePixelFormat(hwPixFmt);
|
|
49040
|
-
} catch (err) {
|
|
49041
|
-
this.logger.warn("node-av: hwaccel context attach failed — trying next", { meta: {
|
|
49042
|
-
backend,
|
|
49043
|
-
error: errMsg(err)
|
|
49044
|
-
} });
|
|
49045
|
-
device.free();
|
|
49046
|
-
continue;
|
|
49047
|
-
}
|
|
49048
|
-
this.hwDevice = device;
|
|
49049
|
-
this.activeHwAccel = backend;
|
|
49050
|
-
return;
|
|
49051
|
-
}
|
|
49052
|
-
this.logger.warn("node-av: no hwaccel backend initialised — using software", { meta: {
|
|
49053
|
-
attempted: resolution.preferred.join(","),
|
|
49054
|
-
rationale: resolution.rationale
|
|
49055
|
-
} });
|
|
49056
|
-
this.activeHwAccel = "none";
|
|
49057
|
-
}
|
|
49058
|
-
/**
|
|
49059
|
-
* Download a HW frame (format == hw pix fmt) into a SW frame so the
|
|
49060
|
-
* rest of the pipeline (scaler, JPEG encoder, grayscale passthrough)
|
|
49061
|
-
* handles it identically to the pure-software path. Uses the sync
|
|
49062
|
-
* variant so the synchronous receive loop below doesn't need to be
|
|
49063
|
-
* async-ified. Returns `null` on transfer failure, meaning the
|
|
49064
|
-
* caller should drop the frame.
|
|
49065
|
-
*/
|
|
49066
|
-
transferHwFrame(hwFrame) {
|
|
49067
|
-
if (this.activeHwAccel === "none" || !this.swTransferFrame) return hwFrame;
|
|
49068
|
-
const rc = hwFrame.hwframeTransferDataSync(this.swTransferFrame, 0);
|
|
49069
|
-
if (rc < 0) {
|
|
49070
|
-
this.logger.warn("node-av: hwframeTransferData failed", { meta: { rc } });
|
|
49071
|
-
return null;
|
|
49072
|
-
}
|
|
49073
|
-
return this.swTransferFrame;
|
|
49074
|
-
}
|
|
49075
|
-
/**
|
|
49076
|
-
* Initialize the decoder pipeline on the first keyframe.
|
|
49077
|
-
* After this returns, all hot-path methods are fully synchronous (except JPEG encode).
|
|
49078
|
-
*/
|
|
49079
|
-
async initDecoder() {
|
|
49080
|
-
if (this.initialized || this.initializing || this.destroyed) return;
|
|
49081
|
-
this.initializing = true;
|
|
49082
|
-
try {
|
|
49083
|
-
const nav = await getNodeAv();
|
|
49084
|
-
const C = await getConstants();
|
|
49085
|
-
this.EAGAIN = C.AVERROR_EAGAIN;
|
|
49086
|
-
this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
|
|
49087
|
-
this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
|
|
49088
|
-
this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
|
|
49089
|
-
if (this.outputMode === "jpeg") this.sharpFn = await getSharp();
|
|
49090
|
-
nav.Log.setLevel(C.AV_LOG_FATAL);
|
|
49091
|
-
const codecId = this.config.codec === "h265" || this.config.codec === "hevc" ? C.AV_CODEC_ID_HEVC : C.AV_CODEC_ID_H264;
|
|
49092
|
-
this.parser = new nav.CodecParser();
|
|
49093
|
-
this.parser.init(codecId);
|
|
49094
|
-
const codec = nav.Codec.findDecoder(codecId);
|
|
49095
|
-
if (!codec) {
|
|
49096
|
-
this.logger.error("node-av: no decoder found", { meta: { codec: this.config.codec } });
|
|
49097
|
-
return;
|
|
49098
|
-
}
|
|
49099
|
-
this.codecCtx = new nav.CodecContext();
|
|
49100
|
-
this.codecCtx.allocContext3(codec);
|
|
49101
|
-
if (this.config.width && this.config.height) {
|
|
49102
|
-
this.codecCtx.width = this.config.width;
|
|
49103
|
-
this.codecCtx.height = this.config.height;
|
|
49104
|
-
}
|
|
49105
|
-
this.codecCtx.threadCount = 1;
|
|
49106
|
-
await this.tryAttachHwAccel(nav, C);
|
|
49107
|
-
const ret = await this.codecCtx.open2(codec);
|
|
49108
|
-
if (ret < 0) if (this.activeHwAccel !== "none") {
|
|
49109
|
-
this.logger.warn("node-av: open2 failed with hwaccel — retrying in software", { meta: {
|
|
49110
|
-
ret,
|
|
49111
|
-
hwAccel: this.activeHwAccel
|
|
49112
|
-
} });
|
|
49113
|
-
this.hwDevice?.free();
|
|
49114
|
-
this.hwDevice = null;
|
|
49115
|
-
this.activeHwAccel = "none";
|
|
49116
|
-
this.codecCtx = new nav.CodecContext();
|
|
49117
|
-
this.codecCtx.allocContext3(codec);
|
|
49118
|
-
if (this.config.width && this.config.height) {
|
|
49119
|
-
this.codecCtx.width = this.config.width;
|
|
49120
|
-
this.codecCtx.height = this.config.height;
|
|
49121
|
-
}
|
|
49122
|
-
this.codecCtx.threadCount = 1;
|
|
49123
|
-
const retry = await this.codecCtx.open2(codec);
|
|
49124
|
-
if (retry < 0) {
|
|
49125
|
-
this.logger.error("node-av: failed to open decoder (sw fallback)", { meta: { ret: retry } });
|
|
49126
|
-
return;
|
|
49127
|
-
}
|
|
49128
|
-
} else {
|
|
49129
|
-
this.logger.error("node-av: failed to open decoder", { meta: { ret } });
|
|
49130
|
-
return;
|
|
49131
|
-
}
|
|
49132
|
-
if (this.activeHwAccel !== "none") {
|
|
49133
|
-
this.swTransferFrame = new nav.Frame();
|
|
49134
|
-
this.swTransferFrame.alloc();
|
|
49135
|
-
}
|
|
49136
|
-
this.avPacket = new nav.Packet();
|
|
49137
|
-
this.avPacket.alloc();
|
|
49138
|
-
this.avFrame = new nav.Frame();
|
|
49139
|
-
this.avFrame.alloc();
|
|
49140
|
-
this.initialized = true;
|
|
49141
|
-
this.logger.info("node-av push decoder initialized", { meta: {
|
|
49142
|
-
codec: this.config.codec,
|
|
49143
|
-
output: this.outputMode,
|
|
49144
|
-
hwAccel: this.activeHwAccel
|
|
49145
|
-
} });
|
|
49146
|
-
} catch (err) {
|
|
49147
|
-
this.logger.error("node-av init error", { meta: { error: errMsg(err) } });
|
|
49148
|
-
} finally {
|
|
49149
|
-
this.initializing = false;
|
|
49150
|
-
}
|
|
49151
|
-
}
|
|
49152
|
-
/**
|
|
49153
|
-
* Initialize the scaler after the first frame tells us the actual
|
|
49154
|
-
* dimensions. Output pixel format: RGB24 for JPEG encoding, GRAY8
|
|
49155
|
-
* for raw motion.
|
|
49156
|
-
*
|
|
49157
|
-
* Builds `scaler` + `dstFrame` on local variables and publishes
|
|
49158
|
-
* them onto `this` in a single atomic step at the end. Captures
|
|
49159
|
-
* `scalerEpoch` at entry; if `updateConfig` invalidated the scaler
|
|
49160
|
-
* while this init was in flight (epoch mismatch), the locally
|
|
49161
|
-
* built pair is disposed and discarded so the later init wins.
|
|
49162
|
-
* Without the local-first approach, partial state (scaler set,
|
|
49163
|
-
* dstFrame still null) could be observed by a concurrent
|
|
49164
|
-
* `emitDecodedFrame` call.
|
|
49165
|
-
*/
|
|
49166
|
-
async initScaler(srcW, srcH, srcFmt) {
|
|
49167
|
-
if (this.scalerInitializing) return;
|
|
49168
|
-
this.scalerInitializing = true;
|
|
49169
|
-
const myEpoch = this.scalerEpoch;
|
|
49170
|
-
let localScaler = null;
|
|
49171
|
-
let localDstFrame = null;
|
|
49172
|
-
try {
|
|
49173
|
-
const nav = await getNodeAv();
|
|
49174
|
-
if (this.destroyed || myEpoch !== this.scalerEpoch) return;
|
|
49175
|
-
const scale = this.config.scale > 1 ? this.config.scale : 1;
|
|
49176
|
-
const maxW = Math.floor(640 / scale);
|
|
49177
|
-
const outWidth = Math.min(srcW, maxW);
|
|
49178
|
-
const outHeight = Math.round(outWidth * srcH / srcW);
|
|
49179
|
-
const dstFmt = this.outputMode === "gray" ? this.PIX_FMT_GRAY8 : this.PIX_FMT_RGB24;
|
|
49180
|
-
const fmtName = this.outputMode === "gray" ? "gray8" : "rgb24";
|
|
49181
|
-
localScaler = new nav.SoftwareScaleContext();
|
|
49182
|
-
localScaler.getContext(srcW, srcH, srcFmt, outWidth, outHeight, dstFmt, this.SWS_FAST_BILINEAR);
|
|
49183
|
-
const ret = localScaler.initContext();
|
|
49184
|
-
if (ret < 0) {
|
|
49185
|
-
this.logger.error("node-av: sws_init_context failed", { meta: { ret } });
|
|
49186
|
-
return;
|
|
49187
|
-
}
|
|
49188
|
-
localDstFrame = new nav.Frame();
|
|
49189
|
-
localDstFrame.alloc();
|
|
49190
|
-
localDstFrame.width = outWidth;
|
|
49191
|
-
localDstFrame.height = outHeight;
|
|
49192
|
-
localDstFrame.format = dstFmt;
|
|
49193
|
-
const allocRet = localDstFrame.allocBuffer();
|
|
49194
|
-
if (allocRet < 0) {
|
|
49195
|
-
this.logger.error("node-av: dst frame allocBuffer failed", { meta: { ret: allocRet } });
|
|
49196
|
-
return;
|
|
49197
|
-
}
|
|
49198
|
-
if (this.destroyed || myEpoch !== this.scalerEpoch) return;
|
|
49199
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49200
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49201
|
-
this.scaler = localScaler;
|
|
49202
|
-
this.dstFrame = localDstFrame;
|
|
49203
|
-
this.outWidth = outWidth;
|
|
49204
|
-
this.outHeight = outHeight;
|
|
49205
|
-
this.scalerSrcW = srcW;
|
|
49206
|
-
this.scalerSrcH = srcH;
|
|
49207
|
-
this.scalerSrcFmt = srcFmt;
|
|
49208
|
-
localScaler = null;
|
|
49209
|
-
localDstFrame = null;
|
|
49210
|
-
this.logger.info("node-av scaler initialized", { meta: {
|
|
49211
|
-
srcWidth: srcW,
|
|
49212
|
-
srcHeight: srcH,
|
|
49213
|
-
outWidth,
|
|
49214
|
-
outHeight,
|
|
49215
|
-
format: fmtName
|
|
49216
|
-
} });
|
|
49217
|
-
} catch (err) {
|
|
49218
|
-
this.logger.error("Scaler init failed", { meta: { error: errMsg(err) } });
|
|
49219
|
-
} finally {
|
|
49220
|
-
localScaler?.[Symbol.dispose]?.();
|
|
49221
|
-
localDstFrame?.[Symbol.dispose]?.();
|
|
49222
|
-
this.scalerInitializing = false;
|
|
49223
|
-
}
|
|
49224
|
-
}
|
|
49225
|
-
/**
|
|
49226
|
-
* Start PULL mode: libav dials `url` and demuxes it in-process, decoding
|
|
49227
|
-
* every frame through the same emit path as push mode. Idempotent while
|
|
49228
|
-
* active (updates the re-dial target). Throws if the session already took a
|
|
49229
|
-
* push packet — a session is push-mode or pull-mode for its whole life.
|
|
49230
|
-
*/
|
|
49231
|
-
openStream(url) {
|
|
49232
|
-
if (this.destroyed) return Promise.resolve();
|
|
49233
|
-
if (this.inputPackets > 0 || this.initialized || this.initializing) throw new Error("node-av decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
|
|
49234
|
-
if (this.pullActive) {
|
|
49235
|
-
this.pullUrl = url;
|
|
49236
|
-
return Promise.resolve();
|
|
49237
|
-
}
|
|
49238
|
-
this.pullActive = true;
|
|
49239
|
-
this.pullUrl = url;
|
|
49240
|
-
this.logger.info("node-av decoder: pull-mode openStream — dialing input URL", { meta: { url } });
|
|
49241
|
-
this.runPullLoop().catch((err) => {
|
|
49242
|
-
this.logger.error("node-av decoder: pull loop crashed", { meta: { error: errMsg(err) } });
|
|
49243
|
-
});
|
|
49244
|
-
return Promise.resolve();
|
|
49245
|
-
}
|
|
49246
|
-
/**
|
|
49247
|
-
* The pull-mode supervisor loop: resolve HW once, then repeatedly dial +
|
|
49248
|
-
* decode the URL, re-dialing after {@link PULL_REDIAL_MS} on any exit/error
|
|
49249
|
-
* until the session is destroyed. Each iteration rebuilds a fresh
|
|
49250
|
-
* demuxer/decoder (the previous one is disposed in `closePullInput`).
|
|
49251
|
-
*/
|
|
49252
|
-
async runPullLoop() {
|
|
49253
|
-
const nav = await getNodeAv();
|
|
49254
|
-
const C = await getConstants();
|
|
49255
|
-
this.EAGAIN = C.AVERROR_EAGAIN;
|
|
49256
|
-
this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
|
|
49257
|
-
this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
|
|
49258
|
-
this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
|
|
49259
|
-
nav.Log.setLevel(C.AV_LOG_FATAL);
|
|
49260
|
-
if (this.outputMode === "jpeg" && !this.sharpFn) this.sharpFn = await getSharp();
|
|
49261
|
-
await this.ensurePullHwContext(nav, C);
|
|
49262
|
-
while (!this.destroyed && this.pullActive) {
|
|
49263
|
-
const url = this.pullUrl;
|
|
49264
|
-
if (!url) break;
|
|
49265
|
-
try {
|
|
49266
|
-
await this.pullDialAndDecode(nav, C, url);
|
|
49267
|
-
} catch (err) {
|
|
49268
|
-
if (this.destroyed) break;
|
|
49269
|
-
this.logger.warn("node-av decoder: pull-mode dial ended — re-dial scheduled", { meta: {
|
|
49270
|
-
error: errMsg(err),
|
|
49271
|
-
redialInMs: PULL_REDIAL_MS
|
|
49272
|
-
} });
|
|
49273
|
-
} finally {
|
|
49274
|
-
this.closePullInput();
|
|
49275
|
-
}
|
|
49276
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49277
|
-
const backoffMs = this.pullFastRedial ? PULL_FAST_REDIAL_MS : PULL_REDIAL_MS;
|
|
49278
|
-
this.pullFastRedial = false;
|
|
49279
|
-
await this.pullSleep(backoffMs);
|
|
49280
|
-
}
|
|
49281
|
-
}
|
|
49282
|
-
/**
|
|
49283
|
-
* One dial: open the demuxer, pick the video stream, create a HW/SW decoder
|
|
49284
|
-
* that downloads + normalises frames to YUV420P (so the shared scaler in
|
|
49285
|
-
* `emitDecodedFrame` reads plain software planes regardless of hwaccel), and
|
|
49286
|
-
* pump `demuxer.packets(video) → decoder.frames → emitDecodedFrame` until the
|
|
49287
|
-
* stream ends or the session is torn down.
|
|
49288
|
-
*/
|
|
49289
|
-
async pullDialAndDecode(nav, C, url) {
|
|
49290
|
-
const demuxer = await nav.Demuxer.open(url, { options: buildPullDemuxerOptions() });
|
|
49291
|
-
if (this.destroyed || !this.pullActive) {
|
|
49292
|
-
demuxer[Symbol.dispose]?.();
|
|
49293
|
-
return;
|
|
49294
|
-
}
|
|
49295
|
-
this.pullDemuxer = demuxer;
|
|
49296
|
-
const videoStream = demuxer.video();
|
|
49297
|
-
if (!videoStream) throw new Error("node-av decoder: pull input has no video stream");
|
|
49298
|
-
const decoder = await nav.Decoder.create(videoStream, {
|
|
49299
|
-
...this.pullHwContext ? { hardware: this.pullHwContext } : {},
|
|
49300
|
-
rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
|
|
49301
|
-
exitOnError: false
|
|
49302
|
-
});
|
|
49303
|
-
if (this.destroyed || !this.pullActive) {
|
|
49304
|
-
decoder[Symbol.dispose]?.();
|
|
49305
|
-
return;
|
|
49306
|
-
}
|
|
49307
|
-
this.pullDecoder = decoder;
|
|
49308
|
-
this.logger.info("node-av pull decoder initialized", { meta: {
|
|
49309
|
-
codec: this.config.codec,
|
|
49310
|
-
output: this.outputMode,
|
|
49311
|
-
hwAccel: this.activeHwAccel,
|
|
49312
|
-
streamIndex: videoStream.index
|
|
49313
|
-
} });
|
|
49314
|
-
const probeTb = videoStream.timeBase;
|
|
49315
|
-
const tbNum = probeTb?.num ?? 0;
|
|
49316
|
-
const tbDen = probeTb?.den ?? 0;
|
|
49317
|
-
this.resetLagTracker(Date.now());
|
|
49318
|
-
await this.consumePullFrames(decoder.frames(demuxer.packets(videoStream.index)), tbNum, tbDen);
|
|
49319
|
-
}
|
|
49320
|
-
/**
|
|
49321
|
-
* Consume decoded pull frames until the stream ends or the session is torn
|
|
49322
|
-
* down. Each yielded frame is a node-av `Frame` — for the HW path a `clone`
|
|
49323
|
-
* that refs a VAAPI surface + the decoder's whole `hw_frames_ctx`, so a SINGLE
|
|
49324
|
-
* unfreed frame pins the dial's entire GPU surface pool (i915 GEM/shmem) until
|
|
49325
|
-
* the process dies. Therefore EVERY exit path (teardown break, drop-to-live
|
|
49326
|
-
* re-seat, normal emit, or a throw from the lag/emit calls) MUST `frame.free()`
|
|
49327
|
-
* — the `try/finally` below is the single release point that guarantees it.
|
|
49328
|
-
*
|
|
49329
|
-
* Extracted from {@link pullDialAndDecode} so this free-on-all-paths contract
|
|
49330
|
-
* is unit-testable with a stub async iterable (no live libav / GPU needed).
|
|
49331
|
-
*/
|
|
49332
|
-
async consumePullFrames(frames, tbNum, tbDen) {
|
|
49333
|
-
for await (const frame of frames) {
|
|
49334
|
-
if (!frame) {
|
|
49335
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49336
|
-
continue;
|
|
49337
|
-
}
|
|
49338
|
-
try {
|
|
49339
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49340
|
-
this.inputPackets++;
|
|
49341
|
-
const wallNow = Date.now();
|
|
49342
|
-
this.updateLagTracker(frame.pts, tbNum, tbDen, wallNow);
|
|
49343
|
-
this.runAdaptiveController(wallNow);
|
|
49344
|
-
this.emitDebugFlash(wallNow);
|
|
49345
|
-
if (this.shouldDropToLive(wallNow)) {
|
|
49346
|
-
this.logger.warn("node-av decoder: lag beyond recovery — dropping to live edge", { meta: {
|
|
49347
|
-
lagMs: Math.round(this.lagMsEwma),
|
|
49348
|
-
redialInMs: PULL_FAST_REDIAL_MS
|
|
49349
|
-
} });
|
|
49350
|
-
this.pullFastRedial = true;
|
|
49351
|
-
break;
|
|
49352
|
-
}
|
|
49353
|
-
this.emitDecodedFrame(frame);
|
|
49354
|
-
} finally {
|
|
49355
|
-
frame.free();
|
|
49356
|
-
}
|
|
49357
|
-
}
|
|
49358
|
-
}
|
|
49359
|
-
/**
|
|
49360
|
-
* Resolve and build the HW context for pull mode ONCE (reused across every
|
|
49361
|
-
* re-dial, freed in `destroy`). Explicit backend → single try; `'auto'` → the
|
|
49362
|
-
* kernel resolver's ordered list; no resolver → software. The first
|
|
49363
|
-
* `HardwareContext.create` that succeeds wins; all failures fall through to
|
|
49364
|
-
* software decode (`hardware: null`).
|
|
49365
|
-
*/
|
|
49366
|
-
async ensurePullHwContext(nav, C) {
|
|
49367
|
-
if (this.hwaccelPref === "none") {
|
|
49368
|
-
this.activeHwAccel = "none";
|
|
49369
|
-
return;
|
|
49370
|
-
}
|
|
49371
|
-
const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
|
|
49372
|
-
const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
|
|
49373
|
-
preferred: [explicit],
|
|
49374
|
-
rationale: "explicit (no resolver)"
|
|
49375
|
-
} : {
|
|
49376
|
-
preferred: [],
|
|
49377
|
-
rationale: "auto + no resolver → software"
|
|
49378
|
-
};
|
|
49379
|
-
for (const backend of resolution.preferred) {
|
|
49380
|
-
const deviceType = backendToHwDeviceConst(backend, C);
|
|
49381
|
-
if (!deviceType) continue;
|
|
49382
|
-
const hw = nav.HardwareContext.create(deviceType);
|
|
49383
|
-
if (!hw) {
|
|
49384
|
-
this.logger.debug("node-av: pull hwaccel context create failed — trying next", { meta: { backend } });
|
|
49385
|
-
continue;
|
|
49386
|
-
}
|
|
49387
|
-
this.pullHwContext = hw;
|
|
49388
|
-
this.activeHwAccel = backend;
|
|
49389
|
-
return;
|
|
49390
|
-
}
|
|
49391
|
-
this.logger.warn("node-av: no pull hwaccel backend initialised — using software", { meta: {
|
|
49392
|
-
attempted: resolution.preferred.join(","),
|
|
49393
|
-
rationale: resolution.rationale
|
|
49394
|
-
} });
|
|
49395
|
-
this.activeHwAccel = "none";
|
|
49396
|
-
}
|
|
49397
|
-
/** Dispose the CURRENT dial's demuxer + decoder (HW context is reused). */
|
|
49398
|
-
closePullInput() {
|
|
49399
|
-
if (this.pullDecoder) {
|
|
49400
|
-
this.pullDecoder[Symbol.dispose]?.();
|
|
49401
|
-
this.pullDecoder = null;
|
|
49402
|
-
}
|
|
49403
|
-
if (this.pullDemuxer) {
|
|
49404
|
-
this.pullDemuxer[Symbol.dispose]?.();
|
|
49405
|
-
this.pullDemuxer = null;
|
|
49406
|
-
}
|
|
49407
|
-
}
|
|
49408
|
-
/** Interruptible sleep for the re-dial backoff — `destroy` resolves it early. */
|
|
49409
|
-
pullSleep(ms) {
|
|
49410
|
-
return new Promise((resolve) => {
|
|
49411
|
-
this.pullRedialResolve = resolve;
|
|
49412
|
-
this.pullRedialTimer = setTimeout(() => {
|
|
49413
|
-
this.pullRedialTimer = null;
|
|
49414
|
-
this.pullRedialResolve = null;
|
|
49415
|
-
resolve();
|
|
49416
|
-
}, ms);
|
|
49417
|
-
});
|
|
49418
|
-
}
|
|
49419
|
-
/**
|
|
49420
|
-
* Re-anchor the real-time lag tracker + per-dial controllers at the start of
|
|
49421
|
-
* each dial. A fresh dial (including a drop-to-live re-seat) begins at the
|
|
49422
|
-
* live edge, so the drift EWMA resets to 0 — otherwise the just-torn dial's
|
|
49423
|
-
* accumulated lag would re-trigger drop-to-live immediately. The learned
|
|
49424
|
-
* `adaptiveFps` is intentionally NOT reset: a re-seat should keep the shed
|
|
49425
|
-
* emit-rate and recover from there once lag stays low.
|
|
49426
|
-
*/
|
|
49427
|
-
resetLagTracker(wallNow) {
|
|
49428
|
-
this.basePts = null;
|
|
49429
|
-
this.baseWall = 0;
|
|
49430
|
-
this.lagMsEwma = 0;
|
|
49431
|
-
this.lowLagSince = 0;
|
|
49432
|
-
this.lastAdaptiveCheck = wallNow;
|
|
49433
|
-
this.adaptiveWindowFrames = this.outputFrames;
|
|
49434
|
-
this.lastDebugFlush = wallNow;
|
|
49435
|
-
this.debugWindowDropped = this.droppedFrames;
|
|
49436
|
-
}
|
|
49437
|
-
/**
|
|
49438
|
-
* Update the EWMA of the decoder's real-time drift from a decoded frame's
|
|
49439
|
-
* pts. The first frame of a dial anchors `basePts`/`baseWall`; every later
|
|
49440
|
-
* frame compares elapsed wall-clock against elapsed media time —
|
|
49441
|
-
* `lagMs = wallElapsed - mediaElapsed`. A rising EWMA means the decoder is
|
|
49442
|
-
* falling behind the live edge (wall time outrunning the media timeline).
|
|
49443
|
-
*/
|
|
49444
|
-
updateLagTracker(pts, tbNum, tbDen, wallNow) {
|
|
49445
|
-
if (tbDen === 0) return;
|
|
49446
|
-
if (this.basePts === null) {
|
|
49447
|
-
this.basePts = pts;
|
|
49448
|
-
this.baseWall = wallNow;
|
|
49449
|
-
return;
|
|
49450
|
-
}
|
|
49451
|
-
const sample = wallNow - this.baseWall - Number(pts - this.basePts) * 1e3 * tbNum / tbDen;
|
|
49452
|
-
this.lagMsEwma = .9 * this.lagMsEwma + .1 * sample;
|
|
49453
|
-
}
|
|
49454
|
-
/**
|
|
49455
|
-
* Lag-driven adaptive emit-fps controller (P1), self-throttled to
|
|
49456
|
-
* {@link ADAPTIVE_TICK_MS}. Under load (lag EWMA high) it halves the emit-fps
|
|
49457
|
-
* — fewer frames transferred + GPU-scaled + emitted, so the decoder stays
|
|
49458
|
-
* real-time instead of accumulating lag; once lag stays low for a sustained
|
|
49459
|
-
* window it grows the emit-fps back toward the subscriber's ceiling.
|
|
49460
|
-
* `minIntervalMs` (the throttle both emit paths honour) is recomputed from
|
|
49461
|
-
* `adaptiveFps`, NOT the raw ceiling.
|
|
49462
|
-
*/
|
|
49463
|
-
runAdaptiveController(wallNow) {
|
|
49464
|
-
const windowMs = wallNow - this.lastAdaptiveCheck;
|
|
49465
|
-
if (windowMs < ADAPTIVE_TICK_MS) return;
|
|
49466
|
-
this.lastAdaptiveCheck = wallNow;
|
|
49467
|
-
const framesDelta = this.outputFrames - this.adaptiveWindowFrames;
|
|
49468
|
-
this.adaptiveWindowFrames = this.outputFrames;
|
|
49469
|
-
this.effectiveFps = windowMs > 0 ? framesDelta * 1e3 / windowMs : 0;
|
|
49470
|
-
const lag = this.lagMsEwma;
|
|
49471
|
-
if (lag > ADAPTIVE_LAG_HIGH_MS) {
|
|
49472
|
-
this.adaptiveFps = Math.max(ADAPTIVE_MIN_FPS, this.adaptiveFps / 2);
|
|
49473
|
-
this.lowLagSince = 0;
|
|
49474
|
-
} else if (lag < ADAPTIVE_LAG_LOW_MS) {
|
|
49475
|
-
if (this.lowLagSince === 0) this.lowLagSince = wallNow;
|
|
49476
|
-
else if (wallNow - this.lowLagSince >= ADAPTIVE_RECOVER_SUSTAIN_MS) {
|
|
49477
|
-
this.adaptiveFps = Math.min(this.ceilingFps, this.adaptiveFps * 1.5);
|
|
49478
|
-
this.lowLagSince = wallNow;
|
|
49479
|
-
}
|
|
49480
|
-
} else this.lowLagSince = 0;
|
|
49481
|
-
this.minIntervalMs = this.adaptiveFps > 0 ? 1e3 / this.adaptiveFps : 0;
|
|
49482
|
-
}
|
|
49483
|
-
/**
|
|
49484
|
-
* Whether the decoder is hopelessly behind the live edge and should tear the
|
|
49485
|
-
* input down for a fast re-seat. Rate-limited to at most once per
|
|
49486
|
-
* {@link DROP_TO_LIVE_MIN_INTERVAL_MS} so a bad stretch can't hot-loop the
|
|
49487
|
-
* dial. Records the trigger time as a side effect when it returns `true`.
|
|
49488
|
-
*/
|
|
49489
|
-
shouldDropToLive(wallNow) {
|
|
49490
|
-
if (this.lagMsEwma <= DROP_TO_LIVE_LAG_MS) return false;
|
|
49491
|
-
if (wallNow - this.lastDropToLive < DROP_TO_LIVE_MIN_INTERVAL_MS) return false;
|
|
49492
|
-
this.lastDropToLive = wallNow;
|
|
49493
|
-
return true;
|
|
49494
|
-
}
|
|
49495
|
-
/**
|
|
49496
|
-
* Per-camera DEBUG facility (mirrors the stream-broker's `streamingDebug`
|
|
49497
|
-
* gate). When `config.debug` is set, flush a single structured `decoder
|
|
49498
|
-
* debug` line at ~1Hz — effective/adaptive fps, real-time lag, dropped-frame
|
|
49499
|
-
* delta, avg decode time, hwaccel. `deviceId`/`tag` ride on the logger tags.
|
|
49500
|
-
* Cheap: all values come from running counters, and the whole method is a
|
|
49501
|
-
* no-op (one comparison) when debug is disabled or the window hasn't elapsed.
|
|
49502
|
-
*/
|
|
49503
|
-
emitDebugFlash(wallNow) {
|
|
49504
|
-
if (!this.debugEnabled) return;
|
|
49505
|
-
if (wallNow - this.lastDebugFlush < DEBUG_FLUSH_MS) return;
|
|
49506
|
-
this.lastDebugFlush = wallNow;
|
|
49507
|
-
const droppedDelta = this.droppedFrames - this.debugWindowDropped;
|
|
49508
|
-
this.debugWindowDropped = this.droppedFrames;
|
|
49509
|
-
const decodeMsAvg = this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0;
|
|
49510
|
-
this.logger.info("decoder debug", { meta: {
|
|
49511
|
-
effectiveFps: Number(this.effectiveFps.toFixed(1)),
|
|
49512
|
-
adaptiveFps: Number(this.adaptiveFps.toFixed(1)),
|
|
49513
|
-
ceilingFps: this.ceilingFps,
|
|
49514
|
-
lagMs: Math.round(this.lagMsEwma),
|
|
49515
|
-
droppedFrames: droppedDelta,
|
|
49516
|
-
decodeMs: Number(decodeMsAvg.toFixed(2)),
|
|
49517
|
-
hwAccel: this.activeHwAccel
|
|
49518
|
-
} });
|
|
49519
|
-
}
|
|
49520
|
-
pushPacket(packet) {
|
|
49521
|
-
if (this.destroyed) return;
|
|
49522
|
-
if (this.pullActive) {
|
|
49523
|
-
if (!this.pullStrayPushLogged) {
|
|
49524
|
-
this.pullStrayPushLogged = true;
|
|
49525
|
-
this.logger.debug("node-av decoder: pushPacket on a pull-mode session — ignored");
|
|
49526
|
-
}
|
|
49527
|
-
return;
|
|
49528
|
-
}
|
|
49529
|
-
this.inputPackets++;
|
|
49530
|
-
if (!this.initialized && !this.initializing && packet.keyframe) {
|
|
49531
|
-
this.initDecoder().then(() => {
|
|
49532
|
-
if (this.initialized) this.decodeRawData(packet.data, packet.pts ?? Date.now());
|
|
49533
|
-
}).catch((err) => {
|
|
49534
|
-
this.logger.error("node-av init failed", { meta: { error: errMsg(err) } });
|
|
49535
|
-
});
|
|
49536
|
-
return;
|
|
49537
|
-
}
|
|
49538
|
-
if (!this.initialized) return;
|
|
49539
|
-
this.decodeRawData(packet.data, packet.pts ?? Date.now());
|
|
49540
|
-
}
|
|
49541
|
-
decodeRawData(data, pts) {
|
|
49542
|
-
if (!this.parser || !this.codecCtx || !this.avPacket || !this.avFrame) return;
|
|
49543
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
49544
|
-
const bigPts = BigInt(pts);
|
|
49545
|
-
let offset = 0;
|
|
49546
|
-
while (offset < buf.length) {
|
|
49547
|
-
const remaining = buf.subarray(offset);
|
|
49548
|
-
const consumed = this.parser.parse2(this.codecCtx, this.avPacket, remaining, bigPts, bigPts, offset);
|
|
49549
|
-
if (consumed < 0) {
|
|
49550
|
-
this.logger.warn("node-av parser error", { meta: { ret: consumed } });
|
|
49551
|
-
break;
|
|
49552
|
-
}
|
|
49553
|
-
offset += consumed;
|
|
49554
|
-
if (this.avPacket.size > 0) {
|
|
49555
|
-
this.decodePacket();
|
|
49556
|
-
this.avPacket.unref();
|
|
49557
|
-
}
|
|
49558
|
-
}
|
|
49559
|
-
}
|
|
49560
|
-
decodePacket() {
|
|
49561
|
-
if (!this.codecCtx || !this.avFrame) return;
|
|
49562
|
-
const sendRet = this.codecCtx.sendPacketSync(this.avPacket);
|
|
49563
|
-
if (sendRet < 0 && sendRet !== this.EAGAIN) {
|
|
49564
|
-
this.logger.warn("node-av sendPacket error", { meta: { ret: sendRet } });
|
|
49565
|
-
return;
|
|
49566
|
-
}
|
|
49567
|
-
while (true) {
|
|
49568
|
-
if (this.codecCtx.receiveFrameSync(this.avFrame) < 0) break;
|
|
49569
|
-
const frame = this.transferHwFrame(this.avFrame);
|
|
49570
|
-
if (!frame) continue;
|
|
49571
|
-
this.emitDecodedFrame(frame);
|
|
49572
|
-
}
|
|
49573
|
-
}
|
|
49574
|
-
emitDecodedFrame(frame) {
|
|
49575
|
-
const now = performance.now();
|
|
49576
|
-
if (frame.isHwFrame()) {
|
|
49577
|
-
this.droppedFrames++;
|
|
49578
|
-
return;
|
|
49579
|
-
}
|
|
49580
|
-
if (this.minIntervalMs > 0 && now - this.lastEmitTime < this.minIntervalMs) {
|
|
49581
|
-
this.droppedFrames++;
|
|
49582
|
-
return;
|
|
49583
|
-
}
|
|
49584
|
-
if (this.scalerInitializing) return;
|
|
49585
|
-
const incoming = {
|
|
49586
|
-
width: frame.width,
|
|
49587
|
-
height: frame.height,
|
|
49588
|
-
format: frame.format
|
|
49589
|
-
};
|
|
49590
|
-
const current = this.scaler !== null ? {
|
|
49591
|
-
width: this.scalerSrcW,
|
|
49592
|
-
height: this.scalerSrcH,
|
|
49593
|
-
format: this.scalerSrcFmt
|
|
49594
|
-
} : null;
|
|
49595
|
-
const action = resolveScalerAction(current, incoming);
|
|
49596
|
-
if (action === "init") {
|
|
49597
|
-
this.initScaler(incoming.width, incoming.height, incoming.format);
|
|
49598
|
-
return;
|
|
49599
|
-
}
|
|
49600
|
-
if (action === "rebuild") {
|
|
49601
|
-
this.logger.info("node-av: decoded source geometry changed — rebuilding scaler", { meta: {
|
|
49602
|
-
from: current,
|
|
49603
|
-
to: incoming
|
|
49604
|
-
} });
|
|
49605
|
-
this.scalerEpoch++;
|
|
49606
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49607
|
-
this.scaler = null;
|
|
49608
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49609
|
-
this.dstFrame = null;
|
|
49610
|
-
this.scalerSrcW = -1;
|
|
49611
|
-
this.scalerSrcH = -1;
|
|
49612
|
-
this.scalerSrcFmt = -1;
|
|
49613
|
-
this.initScaler(incoming.width, incoming.height, incoming.format);
|
|
49614
|
-
return;
|
|
49615
|
-
}
|
|
49616
|
-
if (!this.dstFrame || !this.scaler) return;
|
|
49617
|
-
const decodeStart = performance.now();
|
|
49618
|
-
try {
|
|
49619
|
-
this.dstFrame.makeWritable();
|
|
49620
|
-
this.scaler.scaleFrameSync(this.dstFrame, frame);
|
|
49621
|
-
} catch (err) {
|
|
49622
|
-
this.logger.warn("node-av scale error", { meta: { error: errMsg(err) } });
|
|
49623
|
-
return;
|
|
49624
|
-
}
|
|
49625
|
-
if (!this.firstFrameLogged) {
|
|
49626
|
-
this.firstFrameLogged = true;
|
|
49627
|
-
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
49628
|
-
const ls = this.dstFrame.linesize;
|
|
49629
|
-
const buf = this.dstFrame.toBuffer();
|
|
49630
|
-
this.logger.info("dstFrame after scale", { meta: {
|
|
49631
|
-
phase: "frame-debug",
|
|
49632
|
-
width: this.dstFrame.width,
|
|
49633
|
-
height: this.dstFrame.height,
|
|
49634
|
-
linesize: [
|
|
49635
|
-
ls[0],
|
|
49636
|
-
ls[1],
|
|
49637
|
-
ls[2]
|
|
49638
|
-
],
|
|
49639
|
-
expectedStride: this.dstFrame.width * channels,
|
|
49640
|
-
bufLen: buf.length,
|
|
49641
|
-
expectedPacked: this.dstFrame.width * channels * this.dstFrame.height,
|
|
49642
|
-
srcFormat: frame.format ?? "?"
|
|
49643
|
-
} });
|
|
49644
|
-
if (this.outputMode !== "gray") import("node:fs").then((fsModule) => {
|
|
49645
|
-
import("node:path").then((pathModule) => {
|
|
49646
|
-
const dumpPath = pathModule.join(process.cwd(), "camstack-data", "debug-frame-rgb24.raw");
|
|
49647
|
-
fsModule.writeFileSync(dumpPath, buf);
|
|
49648
|
-
this.logger.info("Dumped first RGB24 frame", { meta: {
|
|
49649
|
-
phase: "frame-debug",
|
|
49650
|
-
path: dumpPath,
|
|
49651
|
-
bytes: buf.length
|
|
49652
|
-
} });
|
|
49653
|
-
}).catch(() => {});
|
|
49654
|
-
}).catch(() => {});
|
|
49655
|
-
}
|
|
49656
|
-
const rawBuf = this.extractPackedBuffer(this.dstFrame);
|
|
49657
|
-
if (this.outputMode === "jpeg") this.encodeAndEmitJpeg(rawBuf, decodeStart);
|
|
49658
|
-
else if (this.outputMode === "rgb") this.emitRawFrame(rawBuf, "rgb", decodeStart);
|
|
49659
|
-
else this.emitRawFrame(rawBuf, "gray", decodeStart);
|
|
49660
|
-
}
|
|
49661
|
-
/**
|
|
49662
|
-
* Extract packed pixel buffer from a decoded frame.
|
|
49663
|
-
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
49664
|
-
* Sharp and WASM consumers expect tightly-packed rows (stride = width * channels).
|
|
49665
|
-
* If linesize matches expected stride, return the buffer directly (zero-copy).
|
|
49666
|
-
*/
|
|
49667
|
-
extractPackedBuffer(frame) {
|
|
49668
|
-
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
49669
|
-
const expectedStride = frame.width * channels;
|
|
49670
|
-
const actualStride = frame.linesize[0] ?? expectedStride;
|
|
49671
|
-
const src = frame.data?.[0];
|
|
49672
|
-
if (!src) return frame.toBuffer();
|
|
49673
|
-
if (actualStride === expectedStride) return Buffer.from(src.buffer, src.byteOffset, expectedStride * frame.height);
|
|
49674
|
-
const dst = Buffer.allocUnsafe(expectedStride * frame.height);
|
|
49675
|
-
for (let y = 0; y < frame.height; y++) src.copy(dst, y * expectedStride, y * actualStride, y * actualStride + expectedStride);
|
|
49676
|
-
return dst;
|
|
49677
|
-
}
|
|
49678
|
-
/**
|
|
49679
|
-
* Encode RGB24 raw buffer as JPEG and emit.
|
|
49680
|
-
*
|
|
49681
|
-
* Drops the frame (and counts it) when `MAX_JPEG_INFLIGHT` encodes
|
|
49682
|
-
* are already pending — prevents unbounded growth of the
|
|
49683
|
-
* fire-and-forget promise chain when sharp cannot keep up with the
|
|
49684
|
-
* decode rate.
|
|
49685
|
-
*/
|
|
49686
|
-
encodeAndEmitJpeg(rgb, decodeStart) {
|
|
49687
|
-
if (!this.sharpFn) return;
|
|
49688
|
-
if (this.jpegEncodeInFlight >= NodeAvDecoderSession.MAX_JPEG_INFLIGHT) {
|
|
49689
|
-
this.droppedFrames++;
|
|
49690
|
-
return;
|
|
49691
|
-
}
|
|
49692
|
-
this.jpegEncodeInFlight++;
|
|
49693
|
-
this.sharpFn(rgb, { raw: {
|
|
49694
|
-
width: this.outWidth,
|
|
49695
|
-
height: this.outHeight,
|
|
49696
|
-
channels: 3
|
|
49697
|
-
} }).jpeg({
|
|
49698
|
-
quality: 80,
|
|
49699
|
-
mozjpeg: false
|
|
49700
|
-
}).toBuffer().then((jpegBuf) => {
|
|
49701
|
-
if (this.destroyed) return;
|
|
49702
|
-
this.emitRawFrame(jpegBuf, "jpeg", decodeStart);
|
|
49703
|
-
}).catch((err) => {
|
|
49704
|
-
this.logger.warn("sharp jpeg encode error", { meta: { error: errMsg(err) } });
|
|
49705
|
-
}).finally(() => {
|
|
49706
|
-
this.jpegEncodeInFlight--;
|
|
49707
|
-
});
|
|
49708
|
-
}
|
|
49709
|
-
emitRawFrame(data, format, decodeStart) {
|
|
49710
|
-
const decodeMs = performance.now() - decodeStart;
|
|
49711
|
-
this.totalDecodeTimeMs += decodeMs;
|
|
49712
|
-
this.outputFrames++;
|
|
49713
|
-
this.lastEmitTime = performance.now();
|
|
49714
|
-
if (this.outputFrames === 1 || this.outputFrames % 500 === 0) this.logger.info("node-av frame emitted", { meta: {
|
|
49715
|
-
frameNumber: this.outputFrames,
|
|
49716
|
-
width: this.outWidth,
|
|
49717
|
-
height: this.outHeight,
|
|
49718
|
-
format,
|
|
49719
|
-
decodeMs,
|
|
49720
|
-
subs: this.frameCallbacks.size
|
|
49721
|
-
} });
|
|
49722
|
-
const decodedFrame = {
|
|
49723
|
-
data,
|
|
49724
|
-
width: this.outWidth,
|
|
49725
|
-
height: this.outHeight,
|
|
49726
|
-
format,
|
|
49727
|
-
timestamp: Date.now()
|
|
49728
|
-
};
|
|
49729
|
-
for (const cb of this.frameCallbacks) cb(decodedFrame);
|
|
49730
|
-
}
|
|
49731
|
-
onFrame(callback) {
|
|
49732
|
-
this.frameCallbacks.add(callback);
|
|
49733
|
-
return () => {
|
|
49734
|
-
this.frameCallbacks.delete(callback);
|
|
49735
|
-
};
|
|
49736
|
-
}
|
|
49737
|
-
updateConfig(update) {
|
|
49738
|
-
const prevFormat = this.config.outputFormat;
|
|
49739
|
-
this.config = {
|
|
49740
|
-
...this.config,
|
|
49741
|
-
...update
|
|
49742
|
-
};
|
|
49743
|
-
if (update.maxFps !== void 0) this.minIntervalMs = update.maxFps > 0 ? 1e3 / update.maxFps : 0;
|
|
49744
|
-
if (update.outputFormat !== void 0 && update.outputFormat !== prevFormat) {
|
|
49745
|
-
const prevMode = this.outputMode;
|
|
49746
|
-
this.outputMode = NodeAvDecoderSession.resolveOutputMode(update.outputFormat);
|
|
49747
|
-
if (this.outputMode === prevMode) return;
|
|
49748
|
-
this.scalerEpoch++;
|
|
49749
|
-
if (this.scaler) {
|
|
49750
|
-
this.scaler[Symbol.dispose]?.();
|
|
49751
|
-
this.scaler = null;
|
|
49752
|
-
}
|
|
49753
|
-
if (this.dstFrame) {
|
|
49754
|
-
this.dstFrame[Symbol.dispose]?.();
|
|
49755
|
-
this.dstFrame = null;
|
|
49756
|
-
}
|
|
49757
|
-
this.scalerSrcW = -1;
|
|
49758
|
-
this.scalerSrcH = -1;
|
|
49759
|
-
this.scalerSrcFmt = -1;
|
|
49760
|
-
if (this.outputMode === "jpeg" && !this.sharpFn) getSharp().then((fn) => {
|
|
49761
|
-
this.sharpFn = fn;
|
|
49762
|
-
}).catch(() => {});
|
|
49763
|
-
this.logger.info("node-av: output format changed — scaler will reinit", { meta: {
|
|
49764
|
-
from: prevFormat,
|
|
49765
|
-
to: update.outputFormat,
|
|
49766
|
-
mode: this.outputMode
|
|
49767
|
-
} });
|
|
49768
|
-
}
|
|
49769
|
-
}
|
|
49770
|
-
async destroy() {
|
|
49771
|
-
if (this.destroyed) return;
|
|
49772
|
-
this.destroyed = true;
|
|
49773
|
-
this.frameCallbacks.clear();
|
|
49774
|
-
this.pullActive = false;
|
|
49775
|
-
if (this.pullRedialTimer) {
|
|
49776
|
-
clearTimeout(this.pullRedialTimer);
|
|
49777
|
-
this.pullRedialTimer = null;
|
|
49778
|
-
}
|
|
49779
|
-
if (this.pullRedialResolve) {
|
|
49780
|
-
const resolve = this.pullRedialResolve;
|
|
49781
|
-
this.pullRedialResolve = null;
|
|
49782
|
-
resolve();
|
|
49783
|
-
}
|
|
49784
|
-
this.closePullInput();
|
|
49785
|
-
if (this.pullHwContext) {
|
|
49786
|
-
if (!this.pullHwContext.isDisposed) this.pullHwContext.dispose();
|
|
49787
|
-
this.pullHwContext = null;
|
|
49788
|
-
}
|
|
49789
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49790
|
-
this.avFrame?.[Symbol.dispose]?.();
|
|
49791
|
-
this.avPacket?.[Symbol.dispose]?.();
|
|
49792
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49793
|
-
this.parser?.[Symbol.dispose]?.();
|
|
49794
|
-
this.swTransferFrame?.[Symbol.dispose]?.();
|
|
49795
|
-
this.codecCtx?.[Symbol.dispose]?.();
|
|
49796
|
-
this.hwDevice?.free();
|
|
49797
|
-
this.dstFrame = null;
|
|
49798
|
-
this.avFrame = null;
|
|
49799
|
-
this.avPacket = null;
|
|
49800
|
-
this.scaler = null;
|
|
49801
|
-
this.parser = null;
|
|
49802
|
-
this.codecCtx = null;
|
|
49803
|
-
this.swTransferFrame = null;
|
|
49804
|
-
this.hwDevice = null;
|
|
49805
|
-
}
|
|
49806
|
-
getStats() {
|
|
49807
|
-
const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
|
|
49808
|
-
const base = {
|
|
49809
|
-
inputFps: this.inputPackets / uptimeSec,
|
|
49810
|
-
outputFps: this.outputFrames / uptimeSec,
|
|
49811
|
-
avgDecodeTimeMs: this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0,
|
|
49812
|
-
droppedFrames: this.droppedFrames
|
|
49813
|
-
};
|
|
49814
|
-
if (!this.pullActive) return base;
|
|
49815
|
-
return {
|
|
49816
|
-
...base,
|
|
49817
|
-
lagMs: this.lagMsEwma,
|
|
49818
|
-
effectiveFps: this.effectiveFps,
|
|
49819
|
-
adaptiveFps: this.adaptiveFps
|
|
49820
|
-
};
|
|
49821
|
-
}
|
|
49822
|
-
get isPullMode() {
|
|
49823
|
-
return this.pullActive;
|
|
49824
|
-
}
|
|
49825
|
-
};
|
|
49826
|
-
//#endregion
|
|
49827
49026
|
//#region src/audio-codec/codec-catalog.ts
|
|
49828
49027
|
/**
|
|
49829
49028
|
* Normalise an SDP-reported codec name to the libav/ffmpeg name. Mirrors
|
|
@@ -50639,21 +49838,22 @@ function peekNodeAvRuntime() {
|
|
|
50639
49838
|
}
|
|
50640
49839
|
//#endregion
|
|
50641
49840
|
//#region src/addon/index.ts
|
|
50642
|
-
|
|
49841
|
+
/**
|
|
49842
|
+
* The node's `decoder` + `audio-codec` provider.
|
|
49843
|
+
*
|
|
49844
|
+
* The `decoder` cap is per-node hwaccel only (D763): `getInfo` reports this
|
|
49845
|
+
* node's operator choice and probed best backend, `reprobeHwaccel` re-runs the
|
|
49846
|
+
* probe. Both are per-node settings (`hwaccel@<node>`,
|
|
49847
|
+
* `probedBestHwaccel@<node>`); the egress transcode, HomeKit and the load view
|
|
49848
|
+
* read them through `getInfo`, pinned to the node. Video decoding itself runs in the pipeline runner's session-decode
|
|
49849
|
+
* workers, which use node-av directly. The `audio-codec` cap is the node-av
|
|
49850
|
+
* codec (encode/decode + resample) every audio session on the node runs on.
|
|
49851
|
+
*/
|
|
50643
49852
|
var DecoderNodeAvAddon = class extends BaseAddon {
|
|
50644
49853
|
/**
|
|
50645
|
-
*
|
|
50646
|
-
* this addon
|
|
50647
|
-
|
|
50648
|
-
sessions = /* @__PURE__ */ new Map();
|
|
50649
|
-
/** Pixel-frame buffers drained by `pullFrames`. */
|
|
50650
|
-
frameBuffers = /* @__PURE__ */ new Map();
|
|
50651
|
-
unsubscribers = /* @__PURE__ */ new Map();
|
|
50652
|
-
sessionMeta = /* @__PURE__ */ new Map();
|
|
50653
|
-
/**
|
|
50654
|
-
* node-av audio-codec provider — registered independently of the video
|
|
50655
|
-
* decoder backend gate whenever node-av loads on this node, so this single
|
|
50656
|
-
* addon provides both the `decoder` and `audio-codec` caps.
|
|
49854
|
+
* node-av audio-codec provider — registered whenever node-av loads on this
|
|
49855
|
+
* node, so this single addon provides both the `decoder` and `audio-codec`
|
|
49856
|
+
* caps.
|
|
50657
49857
|
*/
|
|
50658
49858
|
audioProvider = null;
|
|
50659
49859
|
constructor() {
|
|
@@ -50664,7 +49864,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50664
49864
|
id: "hwaccel",
|
|
50665
49865
|
title: "Hardware acceleration",
|
|
50666
49866
|
tab: "decoder",
|
|
50667
|
-
description: "
|
|
49867
|
+
description: "Decode hwaccel this node reports through decoder.getInfo — read by the egress transcode and HomeKit. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW transcodes.",
|
|
50668
49868
|
fields: [this.field({
|
|
50669
49869
|
type: "select",
|
|
50670
49870
|
key: "hwaccel",
|
|
@@ -50709,12 +49909,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50709
49909
|
provider: this.audioProvider
|
|
50710
49910
|
});
|
|
50711
49911
|
}
|
|
50712
|
-
|
|
50713
|
-
if (backend !== "nodeav") {
|
|
50714
|
-
this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
50715
|
-
return registrations;
|
|
50716
|
-
}
|
|
50717
|
-
this.ctx.logger.info("node-av decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
49912
|
+
this.ctx.logger.info("node-av decoder addon initialized");
|
|
50718
49913
|
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
50719
49914
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50720
49915
|
});
|
|
@@ -50725,16 +49920,6 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50725
49920
|
return registrations;
|
|
50726
49921
|
}
|
|
50727
49922
|
/**
|
|
50728
|
-
* Resolve the effective hwaccel backend for a new session — from THIS addon's
|
|
50729
|
-
* OWN per-node `hwaccel` config (edited via the Pipeline UI when node-av is
|
|
50730
|
-
* the active decoder). No cross-read of another addon's store. `'auto'`
|
|
50731
|
-
* defers to the session's local resolver (`ctx.kernel.hwaccel`); fails safe
|
|
50732
|
-
* to `'auto'`.
|
|
50733
|
-
*/
|
|
50734
|
-
resolveHwAccelPref() {
|
|
50735
|
-
return Promise.resolve(this.config.hwaccel ?? "auto");
|
|
50736
|
-
}
|
|
50737
|
-
/**
|
|
50738
49923
|
* Re-run the platform probe on this host and persist the detected
|
|
50739
49924
|
* backend as `probedBestHwaccel`. The operator's `hwaccel` setting
|
|
50740
49925
|
* is intentionally left alone — the probe only updates the hint.
|
|
@@ -50762,190 +49947,29 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50762
49947
|
return { backend: "none" };
|
|
50763
49948
|
}
|
|
50764
49949
|
}
|
|
50765
|
-
async supportsCodec(input) {
|
|
50766
|
-
return [
|
|
50767
|
-
"h264",
|
|
50768
|
-
"h265",
|
|
50769
|
-
"hevc"
|
|
50770
|
-
].includes(input.codec.toLowerCase());
|
|
50771
|
-
}
|
|
50772
49950
|
async getInfo() {
|
|
50773
49951
|
return {
|
|
50774
49952
|
id: "decoder-nodeav",
|
|
50775
49953
|
name: "Decoder (node-av)",
|
|
50776
|
-
isPullMode: true,
|
|
50777
|
-
priority: 10,
|
|
50778
49954
|
hwaccel: this.config.hwaccel,
|
|
50779
49955
|
probedBestHwaccel: this.config.probedBestHwaccel || void 0
|
|
50780
49956
|
};
|
|
50781
49957
|
}
|
|
50782
49958
|
/**
|
|
50783
|
-
* The cluster node id of this
|
|
50784
|
-
* `dev-agent-0`, …), not the addon id.
|
|
50785
|
-
*
|
|
50786
|
-
*
|
|
50787
|
-
*
|
|
50788
|
-
* resolution `PipelineRunnerAddon.onInitialize` performs.
|
|
49959
|
+
* The cluster node id of this addon — the Moleculer nodeID (`hub`,
|
|
49960
|
+
* `dev-agent-0`, …), not the addon id. A hierarchical broker nodeID
|
|
49961
|
+
* (`hub/...`) is collapsed to the cluster-visible parent; in-process boot is
|
|
49962
|
+
* left as-is. Mirrors the resolution `PipelineRunnerAddon.onInitialize`
|
|
49963
|
+
* performs.
|
|
50789
49964
|
*/
|
|
50790
49965
|
resolveLocalNodeId() {
|
|
50791
49966
|
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
50792
49967
|
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
50793
49968
|
}
|
|
50794
|
-
async createSession(config) {
|
|
50795
|
-
const sessionId = (0, node_crypto.randomUUID)();
|
|
50796
|
-
const hwaccel = await this.resolveHwAccelPref();
|
|
50797
|
-
const nodeId = this.resolveLocalNodeId();
|
|
50798
|
-
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
50799
|
-
hwaccel,
|
|
50800
|
-
hwaccelResolver: this.ctx.kernel.hwaccel
|
|
50801
|
-
});
|
|
50802
|
-
const unsub = this.wireCallbackSink(sessionId, session);
|
|
50803
|
-
this.sessions.set(sessionId, session);
|
|
50804
|
-
this.unsubscribers.set(sessionId, unsub);
|
|
50805
|
-
this.sessionMeta.set(sessionId, {
|
|
50806
|
-
codec: config.codec,
|
|
50807
|
-
outputFormat: config.outputFormat,
|
|
50808
|
-
createdAtMs: Date.now(),
|
|
50809
|
-
deviceId: config.deviceId
|
|
50810
|
-
});
|
|
50811
|
-
this.ctx.logger.info("node-av: created session", {
|
|
50812
|
-
tags: typeof config.deviceId === "number" ? { deviceId: config.deviceId } : void 0,
|
|
50813
|
-
meta: {
|
|
50814
|
-
sessionId,
|
|
50815
|
-
codec: config.codec,
|
|
50816
|
-
hwaccelPref: hwaccel,
|
|
50817
|
-
nodeId
|
|
50818
|
-
}
|
|
50819
|
-
});
|
|
50820
|
-
return {
|
|
50821
|
-
sessionId,
|
|
50822
|
-
nodeId
|
|
50823
|
-
};
|
|
50824
|
-
}
|
|
50825
|
-
/**
|
|
50826
|
-
* Subscribe a `'callback'` session's pixel frames into a per-session
|
|
50827
|
-
* ring buffer drained by `pullFrames`.
|
|
50828
|
-
*/
|
|
50829
|
-
wireCallbackSink(sessionId, session) {
|
|
50830
|
-
const ringBuffer = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
50831
|
-
this.frameBuffers.set(sessionId, ringBuffer);
|
|
50832
|
-
return session.onFrame((frame) => {
|
|
50833
|
-
const { format } = frame;
|
|
50834
|
-
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
50835
|
-
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
50836
|
-
new Uint8Array(arrayBuf).set(frame.data);
|
|
50837
|
-
const capFrame = {
|
|
50838
|
-
data: new Uint8Array(arrayBuf),
|
|
50839
|
-
width: frame.width,
|
|
50840
|
-
height: frame.height,
|
|
50841
|
-
format,
|
|
50842
|
-
timestamp: frame.timestamp
|
|
50843
|
-
};
|
|
50844
|
-
ringBuffer.push(capFrame);
|
|
50845
|
-
});
|
|
50846
|
-
}
|
|
50847
|
-
async destroySession(input) {
|
|
50848
|
-
const { sessionId } = input;
|
|
50849
|
-
const session = this.sessions.get(sessionId);
|
|
50850
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
|
|
50851
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
50852
|
-
if (unsub) unsub();
|
|
50853
|
-
this.frameBuffers.get(sessionId)?.cancel();
|
|
50854
|
-
const deviceId = this.sessionMeta.get(sessionId)?.deviceId;
|
|
50855
|
-
await session.destroy();
|
|
50856
|
-
this.sessions.delete(sessionId);
|
|
50857
|
-
this.frameBuffers.delete(sessionId);
|
|
50858
|
-
this.unsubscribers.delete(sessionId);
|
|
50859
|
-
this.sessionMeta.delete(sessionId);
|
|
50860
|
-
this.ctx.logger.info("node-av: destroyed session", {
|
|
50861
|
-
tags: typeof deviceId === "number" ? { deviceId } : void 0,
|
|
50862
|
-
meta: { sessionId }
|
|
50863
|
-
});
|
|
50864
|
-
}
|
|
50865
|
-
async listActiveSessions() {
|
|
50866
|
-
const out = [];
|
|
50867
|
-
for (const [sessionId, meta] of this.sessionMeta) out.push({
|
|
50868
|
-
sessionId,
|
|
50869
|
-
codec: meta.codec,
|
|
50870
|
-
outputFormat: meta.outputFormat,
|
|
50871
|
-
createdAtMs: meta.createdAtMs
|
|
50872
|
-
});
|
|
50873
|
-
return out;
|
|
50874
|
-
}
|
|
50875
|
-
async pushPacket(input) {
|
|
50876
|
-
const session = this.sessions.get(input.sessionId);
|
|
50877
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50878
|
-
const rawData = input.packet.data;
|
|
50879
|
-
const data = Buffer.isBuffer(rawData) ? rawData : rawData instanceof Uint8Array ? Buffer.from(rawData.buffer, rawData.byteOffset, rawData.byteLength) : Buffer.from(rawData);
|
|
50880
|
-
session.pushPacket({
|
|
50881
|
-
...input.packet,
|
|
50882
|
-
data
|
|
50883
|
-
});
|
|
50884
|
-
}
|
|
50885
|
-
async openStream(input) {
|
|
50886
|
-
const session = this.sessions.get(input.sessionId);
|
|
50887
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50888
|
-
await session.openStream(input.url);
|
|
50889
|
-
}
|
|
50890
|
-
async pullFrames(input) {
|
|
50891
|
-
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50892
|
-
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
50893
|
-
if (!ringBuffer) return [];
|
|
50894
|
-
if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
|
|
50895
|
-
return ringBuffer.drain(input.maxCount);
|
|
50896
|
-
}
|
|
50897
|
-
/**
|
|
50898
|
-
* DORMANT — the shm `FrameHandle` frame plane is retired. Every session now
|
|
50899
|
-
* delivers pixels over the callback path drained by `pullFrames`, so no
|
|
50900
|
-
* handles are ever produced. Kept to satisfy the (still-defined) cap method;
|
|
50901
|
-
* always returns an empty list. Removal rides a later framework train.
|
|
50902
|
-
*/
|
|
50903
|
-
async pullHandles() {
|
|
50904
|
-
return [];
|
|
50905
|
-
}
|
|
50906
|
-
async updateConfig(input) {
|
|
50907
|
-
const session = this.sessions.get(input.sessionId);
|
|
50908
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50909
|
-
session.updateConfig(input.config);
|
|
50910
|
-
}
|
|
50911
|
-
async getStats(input) {
|
|
50912
|
-
const session = this.sessions.get(input.sessionId);
|
|
50913
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50914
|
-
return session.getStats();
|
|
50915
|
-
}
|
|
50916
|
-
/**
|
|
50917
|
-
* DORMANT — shm handle read-back is retired (the decoder no longer owns a
|
|
50918
|
-
* `/dev/shm` ring). Kept to satisfy the (still-defined) cap method; always
|
|
50919
|
-
* returns `null`. Full-frame retrieval by handle now flows through the
|
|
50920
|
-
* runner's RAM-backed `pipelineRunner.getNativeCrop`. Removal rides a later
|
|
50921
|
-
* framework train.
|
|
50922
|
-
*/
|
|
50923
|
-
async getFrame() {
|
|
50924
|
-
return null;
|
|
50925
|
-
}
|
|
50926
|
-
/**
|
|
50927
|
-
* DORMANT — no shm ring exists to report stats for. Kept to satisfy the
|
|
50928
|
-
* (still-defined) cap method; always returns `null`. Removal rides a later
|
|
50929
|
-
* framework train.
|
|
50930
|
-
*/
|
|
50931
|
-
async getShmStats() {
|
|
50932
|
-
return null;
|
|
50933
|
-
}
|
|
50934
49969
|
async onShutdown() {
|
|
50935
49970
|
this.audioProvider?.stop();
|
|
50936
49971
|
this.audioProvider = null;
|
|
50937
|
-
this.ctx.logger.info("node-av decoder addon shutdown
|
|
50938
|
-
const destroyPromises = [];
|
|
50939
|
-
for (const [sessionId, session] of this.sessions) {
|
|
50940
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
50941
|
-
if (unsub) unsub();
|
|
50942
|
-
destroyPromises.push(session.destroy());
|
|
50943
|
-
}
|
|
50944
|
-
await Promise.all(destroyPromises);
|
|
50945
|
-
this.sessions.clear();
|
|
50946
|
-
this.frameBuffers.clear();
|
|
50947
|
-
this.unsubscribers.clear();
|
|
50948
|
-
this.sessionMeta.clear();
|
|
49972
|
+
this.ctx.logger.info("node-av decoder addon shutdown");
|
|
50949
49973
|
}
|
|
50950
49974
|
};
|
|
50951
49975
|
//#endregion
|
|
@@ -50953,7 +49977,6 @@ exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
|
|
|
50953
49977
|
exports.NodeAvAudioCodecProvider = NodeAvAudioCodecProvider;
|
|
50954
49978
|
exports.NodeAvAudioDecodeSession = NodeAvAudioDecodeSession;
|
|
50955
49979
|
exports.NodeAvAudioEncodeSession = NodeAvAudioEncodeSession;
|
|
50956
|
-
exports.NodeAvDecoderSession = NodeAvDecoderSession;
|
|
50957
49980
|
exports.default = DecoderNodeAvAddon;
|
|
50958
49981
|
exports.loadNodeAvRuntime = loadNodeAvRuntime;
|
|
50959
49982
|
exports.peekNodeAvRuntime = peekNodeAvRuntime;
|