@camstack/addon-decoder-nodeav 1.2.119 → 1.2.121
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 +1117 -2031
- package/dist/index.mjs +1118 -2031
- 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" });
|
|
@@ -28108,6 +28483,59 @@ var AddonAutoUpdateSchema = ChannelWithInheritSchema;
|
|
|
28108
28483
|
var RestartAddonResultSchema = unknown();
|
|
28109
28484
|
var InstallPackageResultSchema = unknown();
|
|
28110
28485
|
var ReloadPackagesResultSchema = unknown();
|
|
28486
|
+
/**
|
|
28487
|
+
* What an uninstall did on ONE node.
|
|
28488
|
+
* - `uninstalled` — removed: runner(s) stopped, caps gone, code deleted.
|
|
28489
|
+
* - `not-installed` — the node did not have it (nothing to do).
|
|
28490
|
+
* - `refused` — protected / bootstrap package; nothing touched.
|
|
28491
|
+
* - `failed` — attempted and did not complete; `error` says why.
|
|
28492
|
+
* - `pending-reconnect` — the node is OFFLINE: its record was pruned so the
|
|
28493
|
+
* back-fill never returns the package, and the stale-addon sweep removes the
|
|
28494
|
+
* copy when the node registers again (only once the hub no longer has it).
|
|
28495
|
+
*/
|
|
28496
|
+
var UninstallNodeOutcomeSchema = _enum([
|
|
28497
|
+
"uninstalled",
|
|
28498
|
+
"not-installed",
|
|
28499
|
+
"refused",
|
|
28500
|
+
"failed",
|
|
28501
|
+
"pending-reconnect"
|
|
28502
|
+
]);
|
|
28503
|
+
var UninstallNodeResultSchema = object({
|
|
28504
|
+
nodeId: string(),
|
|
28505
|
+
outcome: UninstallNodeOutcomeSchema,
|
|
28506
|
+
error: string().optional(),
|
|
28507
|
+
addonIds: array(string()).readonly(),
|
|
28508
|
+
runnersStopped: array(string()).readonly(),
|
|
28509
|
+
warnings: array(string()).readonly()
|
|
28510
|
+
});
|
|
28511
|
+
var UninstallPackageResultSchema = object({
|
|
28512
|
+
/** Every targeted node ended `uninstalled`, `not-installed` or `pending-reconnect`. */
|
|
28513
|
+
success: boolean(),
|
|
28514
|
+
packageName: string(),
|
|
28515
|
+
results: array(UninstallNodeResultSchema).readonly()
|
|
28516
|
+
});
|
|
28517
|
+
/**
|
|
28518
|
+
* `unreachable` lists online agents whose status read FAILED — absence from
|
|
28519
|
+
* `installations` is only "not installed" for a node that answered (D315).
|
|
28520
|
+
*/
|
|
28521
|
+
var PackageInstallationsSchema = object({
|
|
28522
|
+
installations: array(object({
|
|
28523
|
+
packageName: string(),
|
|
28524
|
+
nodeId: string(),
|
|
28525
|
+
nodeName: string(),
|
|
28526
|
+
isHub: boolean(),
|
|
28527
|
+
isOnline: boolean(),
|
|
28528
|
+
version: string().nullable(),
|
|
28529
|
+
addonIds: array(string()).readonly(),
|
|
28530
|
+
source: _enum(["live", "recorded"]),
|
|
28531
|
+
/** The hub installs this package too. `false` on an agent row = an orphan. */
|
|
28532
|
+
onHub: boolean()
|
|
28533
|
+
})).readonly(),
|
|
28534
|
+
unreachable: array(object({
|
|
28535
|
+
nodeId: string(),
|
|
28536
|
+
error: string()
|
|
28537
|
+
})).readonly()
|
|
28538
|
+
});
|
|
28111
28539
|
var BulkUpdateItemStatusSchema = _enum([
|
|
28112
28540
|
"queued",
|
|
28113
28541
|
"updating",
|
|
@@ -28190,10 +28618,14 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
|
|
|
28190
28618
|
}), method(object({ packageName: string() }), InstallPackageResultSchema, {
|
|
28191
28619
|
kind: "mutation",
|
|
28192
28620
|
auth: "admin"
|
|
28193
|
-
}), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
|
|
28621
|
+
}), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
|
|
28622
|
+
packageName: string().min(1),
|
|
28623
|
+
nodeId: string().optional(),
|
|
28624
|
+
everywhere: boolean().optional()
|
|
28625
|
+
}), UninstallPackageResultSchema, {
|
|
28194
28626
|
kind: "mutation",
|
|
28195
28627
|
auth: "admin"
|
|
28196
|
-
}), method(_void(), ReloadPackagesResultSchema, {
|
|
28628
|
+
}), method(object({ packageName: string().optional() }), PackageInstallationsSchema, { auth: "admin" }), method(_void(), ReloadPackagesResultSchema, {
|
|
28197
28629
|
kind: "mutation",
|
|
28198
28630
|
auth: "admin"
|
|
28199
28631
|
}), method(object({ query: string().optional() }), array(SearchResultSchema)), method(object({ nodeId: string().optional() }), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
|
|
@@ -30489,7 +30921,7 @@ var LocalInterfaceSchema = object({
|
|
|
30489
30921
|
name: string(),
|
|
30490
30922
|
family: _enum(["IPv4", "IPv6"]),
|
|
30491
30923
|
address: string(),
|
|
30492
|
-
/** CIDR shorthand (e.g. `192.168.
|
|
30924
|
+
/** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
|
|
30493
30925
|
cidr: string(),
|
|
30494
30926
|
/** OS netmask. Stable across calls so the SDK can hash on it. */
|
|
30495
30927
|
netmask: string(),
|
|
@@ -30543,7 +30975,9 @@ var ConnectionEndpointSchema = object({
|
|
|
30543
30975
|
"lan-ipv4",
|
|
30544
30976
|
"lan-ipv6",
|
|
30545
30977
|
"public",
|
|
30546
|
-
"loopback"
|
|
30978
|
+
"loopback",
|
|
30979
|
+
"app-link",
|
|
30980
|
+
"custom"
|
|
30547
30981
|
]),
|
|
30548
30982
|
/**
|
|
30549
30983
|
* Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
|
|
@@ -30612,15 +31046,27 @@ var GetConnectionEndpointsResultSchema = object({
|
|
|
30612
31046
|
localPort: AdvertisedLocalPortSchema
|
|
30613
31047
|
});
|
|
30614
31048
|
/**
|
|
30615
|
-
* The
|
|
30616
|
-
*
|
|
30617
|
-
*
|
|
31049
|
+
* The address flagged for notification taps (`notificationTaps`). `baseUrl: null` = AUTO, which
|
|
31050
|
+
* since D709 is the CamStack app link; `resolved` reports the effective value,
|
|
31051
|
+
* so the UI can show it either way.
|
|
30618
31052
|
*/
|
|
30619
31053
|
var NotificationEndpointSchema = object({
|
|
30620
31054
|
/** The operator's explicit choice, or null for AUTO. */
|
|
30621
31055
|
baseUrl: string().nullable(),
|
|
30622
|
-
/**
|
|
30623
|
-
|
|
31056
|
+
/**
|
|
31057
|
+
* The effective flagged address: the explicit choice, else (AUTO) the
|
|
31058
|
+
* CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
|
|
31059
|
+
* answered the public tunnel / mesh here, or null.
|
|
31060
|
+
*/
|
|
31061
|
+
resolved: string().nullable(),
|
|
31062
|
+
/**
|
|
31063
|
+
* The hub origin an app link carries as `hub=` (D709): the hub's best
|
|
31064
|
+
* public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
|
|
31065
|
+
* picked from this cap's own palette by `pickAppLinkHub`. `null` when the
|
|
31066
|
+
* hub advertises neither; the app link is then minted without `hub=`.
|
|
31067
|
+
* Absent on a pre-D709 hub.
|
|
31068
|
+
*/
|
|
31069
|
+
appLinkHub: string().nullable()
|
|
30624
31070
|
});
|
|
30625
31071
|
/**
|
|
30626
31072
|
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
@@ -30633,6 +31079,72 @@ var ViewerEndpointsSchema = object({
|
|
|
30633
31079
|
/** What the ranking currently resolves to (may be empty if nothing is up). */
|
|
30634
31080
|
resolved: array(string()).readonly()
|
|
30635
31081
|
});
|
|
31082
|
+
/**
|
|
31083
|
+
* DEPRECATED compatibility shape of `getHubAddresses` (D729 amendment, kept
|
|
31084
|
+
* for addons built against it): `cluster` is the `internal` role, `browser`
|
|
31085
|
+
* the `publicEndpoint` role; `override` is the flagged address.
|
|
31086
|
+
*/
|
|
31087
|
+
var HubAddressSnapshotSchema = object({
|
|
31088
|
+
override: string().nullable(),
|
|
31089
|
+
resolved: string().nullable(),
|
|
31090
|
+
source: _enum([
|
|
31091
|
+
"override",
|
|
31092
|
+
"auto",
|
|
31093
|
+
"none"
|
|
31094
|
+
])
|
|
31095
|
+
});
|
|
31096
|
+
var HubAddressesSchema = object({
|
|
31097
|
+
cluster: HubAddressSnapshotSchema,
|
|
31098
|
+
browser: HubAddressSnapshotSchema
|
|
31099
|
+
});
|
|
31100
|
+
/**
|
|
31101
|
+
* What an address is used for (`hub-address.ts`, D734). `appConnections` is
|
|
31102
|
+
* many-valued; `notificationTaps`, `publicEndpoint` and `internal` are carried
|
|
31103
|
+
* by at most one address.
|
|
31104
|
+
*/
|
|
31105
|
+
var HubAddressRoleSchema = _enum(HUB_ADDRESS_ROLES);
|
|
31106
|
+
var RoleSourceSchema = _enum([
|
|
31107
|
+
"flagged",
|
|
31108
|
+
"auto",
|
|
31109
|
+
"none"
|
|
31110
|
+
]);
|
|
31111
|
+
/** One single-valued role: the flagged address, the one in use, and why. */
|
|
31112
|
+
var HubAddressRoleResolutionSchema = object({
|
|
31113
|
+
/** The address carrying the flag, or null = automatic. */
|
|
31114
|
+
flagged: string().nullable(),
|
|
31115
|
+
/** The address in use, or null when nothing qualifies (never loopback). */
|
|
31116
|
+
resolved: string().nullable(),
|
|
31117
|
+
source: RoleSourceSchema
|
|
31118
|
+
});
|
|
31119
|
+
var HubAppConnectionsResolutionSchema = object({
|
|
31120
|
+
/** The addresses flagged `appConnections`, or empty = automatic. */
|
|
31121
|
+
flagged: array(string()).readonly(),
|
|
31122
|
+
/** What the apps race right now. */
|
|
31123
|
+
resolved: array(string()).readonly(),
|
|
31124
|
+
source: RoleSourceSchema
|
|
31125
|
+
});
|
|
31126
|
+
var HubAddressBookSchema = object({
|
|
31127
|
+
rows: array(ConnectionEndpointSchema.extend({
|
|
31128
|
+
/** In the hub's detected palette right now. */
|
|
31129
|
+
detected: boolean(),
|
|
31130
|
+
/** Entered by the operator (removable). */
|
|
31131
|
+
custom: boolean(),
|
|
31132
|
+
/** The roles explicitly flagged on this address. */
|
|
31133
|
+
flags: array(HubAddressRoleSchema).readonly(),
|
|
31134
|
+
/** The roles this address serves right now — flagged or automatic. */
|
|
31135
|
+
inUse: array(HubAddressRoleSchema).readonly(),
|
|
31136
|
+
/** The roles this address may be flagged with. */
|
|
31137
|
+
eligible: array(HubAddressRoleSchema).readonly()
|
|
31138
|
+
})).readonly(),
|
|
31139
|
+
roles: object({
|
|
31140
|
+
appConnections: HubAppConnectionsResolutionSchema,
|
|
31141
|
+
notificationTaps: HubAddressRoleResolutionSchema,
|
|
31142
|
+
internal: HubAddressRoleResolutionSchema,
|
|
31143
|
+
publicEndpoint: HubAddressRoleResolutionSchema
|
|
31144
|
+
}),
|
|
31145
|
+
/** The `hub=` an app link carries (D709), or null. */
|
|
31146
|
+
appLinkHub: string().nullable()
|
|
31147
|
+
});
|
|
30636
31148
|
var AllowedAddressesSchema = object({
|
|
30637
31149
|
/**
|
|
30638
31150
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -30674,8 +31186,29 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
30674
31186
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
30675
31187
|
* to avoid mixed-content blocks in the browser. The public
|
|
30676
31188
|
* tunnel always emits `https://` regardless. */
|
|
30677
|
-
scheme: _enum(["http", "https"]).optional()
|
|
30678
|
-
|
|
31189
|
+
scheme: _enum(["http", "https"]).optional(),
|
|
31190
|
+
/** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
|
|
31191
|
+
* an address notification taps can be flagged with, not a hub
|
|
31192
|
+
* route. Default `false`: racers and rankers never see it. */
|
|
31193
|
+
includeAppLink: boolean().optional()
|
|
31194
|
+
}), 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({
|
|
31195
|
+
baseUrl: string(),
|
|
31196
|
+
label: string().max(80)
|
|
31197
|
+
}), HubAddressBookSchema, {
|
|
31198
|
+
kind: "mutation",
|
|
31199
|
+
auth: "admin"
|
|
31200
|
+
}), method(object({ baseUrl: string() }), HubAddressBookSchema, {
|
|
31201
|
+
kind: "mutation",
|
|
31202
|
+
auth: "admin"
|
|
31203
|
+
}), method(object({
|
|
31204
|
+
role: HubAddressRoleSchema,
|
|
31205
|
+
baseUrl: string().nullable(),
|
|
31206
|
+
enabled: boolean(),
|
|
31207
|
+
acceptInsecureHttp: boolean().optional()
|
|
31208
|
+
}), HubAddressBookSchema, {
|
|
31209
|
+
kind: "mutation",
|
|
31210
|
+
auth: "admin"
|
|
31211
|
+
}), 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
31212
|
kind: "mutation",
|
|
30680
31213
|
auth: "admin"
|
|
30681
31214
|
}), method(object({
|
|
@@ -30935,7 +31468,7 @@ var MeshPeerSchema = object({
|
|
|
30935
31468
|
tags: array(string()).readonly(),
|
|
30936
31469
|
/** True when the peer advertises itself as an exit-node candidate. */
|
|
30937
31470
|
exitNodeOption: boolean(),
|
|
30938
|
-
/** Subnet routes the peer advertises (e.g. `["192.168.
|
|
31471
|
+
/** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
|
|
30939
31472
|
advertisedRoutes: array(string()).readonly(),
|
|
30940
31473
|
/** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
|
|
30941
31474
|
userLogin: string().nullable(),
|
|
@@ -31065,7 +31598,8 @@ object({
|
|
|
31065
31598
|
* writes this slice (not even a boot seed), and neither does the runner's
|
|
31066
31599
|
* phase machine: its phase is "a session is attached" for every on-motion
|
|
31067
31600
|
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
31068
|
-
* A provider's `isDetected`
|
|
31601
|
+
* A provider's `isDetected` and `getStatus` answer from the hub's slice
|
|
31602
|
+
* (`hubMotionNativeProvider`).
|
|
31069
31603
|
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
31070
31604
|
* imports) — the provider pushes both edges itself, from the upstream
|
|
31071
31605
|
* system's own state change. That is the sensor's own fact.
|
|
@@ -31879,7 +32413,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
31879
32413
|
*/
|
|
31880
32414
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
31881
32415
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
31882
|
-
readOnly: boolean().optional()
|
|
32416
|
+
readOnly: boolean().optional(),
|
|
32417
|
+
/**
|
|
32418
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32419
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32420
|
+
* copy on the binding.
|
|
32421
|
+
*/
|
|
32422
|
+
maxCharacters: number().int().positive().optional()
|
|
31883
32423
|
})).readonly() });
|
|
31884
32424
|
var OsdOverlayPatchSchema = object({
|
|
31885
32425
|
enabled: boolean().optional(),
|
|
@@ -31930,7 +32470,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
31930
32470
|
valuePath: string().min(1).max(64),
|
|
31931
32471
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
31932
32472
|
displayUnit: string().max(16).optional(),
|
|
31933
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32473
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32474
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32475
|
+
valueMap: record(string(), string()).optional()
|
|
31934
32476
|
});
|
|
31935
32477
|
/**
|
|
31936
32478
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -31980,18 +32522,11 @@ var OsdSlotBindingSchema = object({
|
|
|
31980
32522
|
source: OsdSourceSchema,
|
|
31981
32523
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
31982
32524
|
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
32525
|
/**
|
|
31986
32526
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
31987
32527
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
31988
32528
|
*/
|
|
31989
32529
|
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
32530
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
31996
32531
|
valueMap: record(string(), string()).optional(),
|
|
31997
32532
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -32000,9 +32535,7 @@ var OsdSlotBindingSchema = object({
|
|
|
32000
32535
|
* Display gate, in the notification centre's condition vocabulary.
|
|
32001
32536
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
32002
32537
|
*/
|
|
32003
|
-
conditions: NcConditionsSchema.optional()
|
|
32004
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32005
|
-
fallbackText: string().max(64).default("")
|
|
32538
|
+
conditions: NcConditionsSchema.optional()
|
|
32006
32539
|
});
|
|
32007
32540
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32008
32541
|
var OsdSlotViewSchema = object({
|
|
@@ -36197,6 +36730,16 @@ DeviceType.Camera, method(object({
|
|
|
36197
36730
|
package: array(ZoneRuleSchema).readonly()
|
|
36198
36731
|
});
|
|
36199
36732
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
36733
|
+
Object.freeze({
|
|
36734
|
+
person: .5,
|
|
36735
|
+
vehicle: .5,
|
|
36736
|
+
animal: .5
|
|
36737
|
+
});
|
|
36738
|
+
Object.freeze({
|
|
36739
|
+
person: .65,
|
|
36740
|
+
vehicle: .65,
|
|
36741
|
+
animal: .65
|
|
36742
|
+
});
|
|
36200
36743
|
/**
|
|
36201
36744
|
* Accessory device helpers — shared across drivers.
|
|
36202
36745
|
*
|
|
@@ -36371,6 +36914,12 @@ Object.freeze({
|
|
|
36371
36914
|
addonId: null,
|
|
36372
36915
|
access: "view"
|
|
36373
36916
|
},
|
|
36917
|
+
"addons.listInstallations": {
|
|
36918
|
+
capName: "addons",
|
|
36919
|
+
capScope: "system",
|
|
36920
|
+
addonId: null,
|
|
36921
|
+
access: "view"
|
|
36922
|
+
},
|
|
36374
36923
|
"addons.listJobs": {
|
|
36375
36924
|
capName: "addons",
|
|
36376
36925
|
capScope: "system",
|
|
@@ -37553,90 +38102,18 @@ Object.freeze({
|
|
|
37553
38102
|
addonId: null,
|
|
37554
38103
|
access: "create"
|
|
37555
38104
|
},
|
|
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
38105
|
"decoder.getInfo": {
|
|
37575
38106
|
capName: "decoder",
|
|
37576
38107
|
capScope: "system",
|
|
37577
38108
|
addonId: null,
|
|
37578
38109
|
access: "view"
|
|
37579
38110
|
},
|
|
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
38111
|
"decoder.reprobeHwaccel": {
|
|
37623
38112
|
capName: "decoder",
|
|
37624
38113
|
capScope: "system",
|
|
37625
38114
|
addonId: null,
|
|
37626
38115
|
access: "create"
|
|
37627
38116
|
},
|
|
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
38117
|
"detectionPipeline.applyDeviceSettingsPatch": {
|
|
37641
38118
|
capName: "detection-pipeline",
|
|
37642
38119
|
capScope: "device",
|
|
@@ -39125,12 +39602,24 @@ Object.freeze({
|
|
|
39125
39602
|
addonId: null,
|
|
39126
39603
|
access: "view"
|
|
39127
39604
|
},
|
|
39605
|
+
"localNetwork.addCustomAddress": {
|
|
39606
|
+
capName: "local-network",
|
|
39607
|
+
capScope: "system",
|
|
39608
|
+
addonId: null,
|
|
39609
|
+
access: "create"
|
|
39610
|
+
},
|
|
39128
39611
|
"localNetwork.downloadCa": {
|
|
39129
39612
|
capName: "local-network",
|
|
39130
39613
|
capScope: "system",
|
|
39131
39614
|
addonId: null,
|
|
39132
39615
|
access: "view"
|
|
39133
39616
|
},
|
|
39617
|
+
"localNetwork.getAddresses": {
|
|
39618
|
+
capName: "local-network",
|
|
39619
|
+
capScope: "system",
|
|
39620
|
+
addonId: null,
|
|
39621
|
+
access: "view"
|
|
39622
|
+
},
|
|
39134
39623
|
"localNetwork.getAllowedAddresses": {
|
|
39135
39624
|
capName: "local-network",
|
|
39136
39625
|
capScope: "system",
|
|
@@ -39143,6 +39632,12 @@ Object.freeze({
|
|
|
39143
39632
|
addonId: null,
|
|
39144
39633
|
access: "view"
|
|
39145
39634
|
},
|
|
39635
|
+
"localNetwork.getHubAddresses": {
|
|
39636
|
+
capName: "local-network",
|
|
39637
|
+
capScope: "system",
|
|
39638
|
+
addonId: null,
|
|
39639
|
+
access: "view"
|
|
39640
|
+
},
|
|
39146
39641
|
"localNetwork.getNotificationEndpoint": {
|
|
39147
39642
|
capName: "local-network",
|
|
39148
39643
|
capScope: "system",
|
|
@@ -39179,6 +39674,12 @@ Object.freeze({
|
|
|
39179
39674
|
addonId: null,
|
|
39180
39675
|
access: "create"
|
|
39181
39676
|
},
|
|
39677
|
+
"localNetwork.removeCustomAddress": {
|
|
39678
|
+
capName: "local-network",
|
|
39679
|
+
capScope: "system",
|
|
39680
|
+
addonId: null,
|
|
39681
|
+
access: "delete"
|
|
39682
|
+
},
|
|
39182
39683
|
"localNetwork.resetAllowlistToBestMatch": {
|
|
39183
39684
|
capName: "local-network",
|
|
39184
39685
|
capScope: "system",
|
|
@@ -39191,6 +39692,12 @@ Object.freeze({
|
|
|
39191
39692
|
addonId: null,
|
|
39192
39693
|
access: "create"
|
|
39193
39694
|
},
|
|
39695
|
+
"localNetwork.setAddressRole": {
|
|
39696
|
+
capName: "local-network",
|
|
39697
|
+
capScope: "system",
|
|
39698
|
+
addonId: null,
|
|
39699
|
+
access: "create"
|
|
39700
|
+
},
|
|
39194
39701
|
"localNetwork.setAllowedAddresses": {
|
|
39195
39702
|
capName: "local-network",
|
|
39196
39703
|
capScope: "system",
|
|
@@ -40685,13 +41192,13 @@ Object.freeze({
|
|
|
40685
41192
|
addonId: null,
|
|
40686
41193
|
access: "delete"
|
|
40687
41194
|
},
|
|
40688
|
-
"pipelineExecutor.
|
|
41195
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": {
|
|
40689
41196
|
capName: "pipeline-executor",
|
|
40690
41197
|
capScope: "system",
|
|
40691
41198
|
addonId: null,
|
|
40692
41199
|
access: "delete"
|
|
40693
41200
|
},
|
|
40694
|
-
"pipelineExecutor.
|
|
41201
|
+
"pipelineExecutor.deleteModel": {
|
|
40695
41202
|
capName: "pipeline-executor",
|
|
40696
41203
|
capScope: "system",
|
|
40697
41204
|
addonId: null,
|
|
@@ -40835,12 +41342,6 @@ Object.freeze({
|
|
|
40835
41342
|
addonId: null,
|
|
40836
41343
|
access: "view"
|
|
40837
41344
|
},
|
|
40838
|
-
"pipelineExecutor.listTemplates": {
|
|
40839
|
-
capName: "pipeline-executor",
|
|
40840
|
-
capScope: "system",
|
|
40841
|
-
addonId: null,
|
|
40842
|
-
access: "view"
|
|
40843
|
-
},
|
|
40844
41345
|
"pipelineExecutor.rearmInferenceDevice": {
|
|
40845
41346
|
capName: "pipeline-executor",
|
|
40846
41347
|
capScope: "system",
|
|
@@ -40865,12 +41366,6 @@ Object.freeze({
|
|
|
40865
41366
|
addonId: null,
|
|
40866
41367
|
access: "create"
|
|
40867
41368
|
},
|
|
40868
|
-
"pipelineExecutor.saveTemplate": {
|
|
40869
|
-
capName: "pipeline-executor",
|
|
40870
|
-
capScope: "system",
|
|
40871
|
-
addonId: null,
|
|
40872
|
-
access: "create"
|
|
40873
|
-
},
|
|
40874
41369
|
"pipelineExecutor.setVideoPipelineSteps": {
|
|
40875
41370
|
capName: "pipeline-executor",
|
|
40876
41371
|
capScope: "system",
|
|
@@ -40889,41 +41384,35 @@ Object.freeze({
|
|
|
40889
41384
|
addonId: null,
|
|
40890
41385
|
access: "create"
|
|
40891
41386
|
},
|
|
40892
|
-
"pipelineExecutor.updateTemplate": {
|
|
40893
|
-
capName: "pipeline-executor",
|
|
40894
|
-
capScope: "system",
|
|
40895
|
-
addonId: null,
|
|
40896
|
-
access: "create"
|
|
40897
|
-
},
|
|
40898
41387
|
"pipelineExecutor.validatePipeline": {
|
|
40899
41388
|
capName: "pipeline-executor",
|
|
40900
41389
|
capScope: "system",
|
|
40901
41390
|
addonId: null,
|
|
40902
41391
|
access: "view"
|
|
40903
41392
|
},
|
|
40904
|
-
"pipelineOrchestrator.
|
|
41393
|
+
"pipelineOrchestrator.applyAutoTune": {
|
|
40905
41394
|
capName: "pipeline-orchestrator",
|
|
40906
41395
|
capScope: "system",
|
|
40907
41396
|
addonId: null,
|
|
40908
41397
|
access: "create"
|
|
40909
41398
|
},
|
|
40910
|
-
"pipelineOrchestrator.
|
|
41399
|
+
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
40911
41400
|
capName: "pipeline-orchestrator",
|
|
40912
41401
|
capScope: "system",
|
|
40913
41402
|
addonId: null,
|
|
40914
41403
|
access: "create"
|
|
40915
41404
|
},
|
|
40916
|
-
"pipelineOrchestrator.
|
|
41405
|
+
"pipelineOrchestrator.assignAudio": {
|
|
40917
41406
|
capName: "pipeline-orchestrator",
|
|
40918
41407
|
capScope: "system",
|
|
40919
41408
|
addonId: null,
|
|
40920
41409
|
access: "create"
|
|
40921
41410
|
},
|
|
40922
|
-
"pipelineOrchestrator.
|
|
41411
|
+
"pipelineOrchestrator.assignPipeline": {
|
|
40923
41412
|
capName: "pipeline-orchestrator",
|
|
40924
41413
|
capScope: "system",
|
|
40925
41414
|
addonId: null,
|
|
40926
|
-
access: "
|
|
41415
|
+
access: "create"
|
|
40927
41416
|
},
|
|
40928
41417
|
"pipelineOrchestrator.getAgentLoad": {
|
|
40929
41418
|
capName: "pipeline-orchestrator",
|
|
@@ -41015,6 +41504,12 @@ Object.freeze({
|
|
|
41015
41504
|
addonId: null,
|
|
41016
41505
|
access: "view"
|
|
41017
41506
|
},
|
|
41507
|
+
"pipelineOrchestrator.getEffectiveShares": {
|
|
41508
|
+
capName: "pipeline-orchestrator",
|
|
41509
|
+
capScope: "system",
|
|
41510
|
+
addonId: null,
|
|
41511
|
+
access: "view"
|
|
41512
|
+
},
|
|
41018
41513
|
"pipelineOrchestrator.getGlobalMetrics": {
|
|
41019
41514
|
capName: "pipeline-orchestrator",
|
|
41020
41515
|
capScope: "system",
|
|
@@ -41051,19 +41546,19 @@ Object.freeze({
|
|
|
41051
41546
|
addonId: null,
|
|
41052
41547
|
access: "view"
|
|
41053
41548
|
},
|
|
41054
|
-
"pipelineOrchestrator.
|
|
41549
|
+
"pipelineOrchestrator.getPlacementStats": {
|
|
41055
41550
|
capName: "pipeline-orchestrator",
|
|
41056
41551
|
capScope: "system",
|
|
41057
41552
|
addonId: null,
|
|
41058
41553
|
access: "view"
|
|
41059
41554
|
},
|
|
41060
|
-
"pipelineOrchestrator.
|
|
41555
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
41061
41556
|
capName: "pipeline-orchestrator",
|
|
41062
41557
|
capScope: "system",
|
|
41063
41558
|
addonId: null,
|
|
41064
41559
|
access: "view"
|
|
41065
41560
|
},
|
|
41066
|
-
"pipelineOrchestrator.
|
|
41561
|
+
"pipelineOrchestrator.listAgentSettings": {
|
|
41067
41562
|
capName: "pipeline-orchestrator",
|
|
41068
41563
|
capScope: "system",
|
|
41069
41564
|
addonId: null,
|
|
@@ -41075,6 +41570,12 @@ Object.freeze({
|
|
|
41075
41570
|
addonId: null,
|
|
41076
41571
|
access: "create"
|
|
41077
41572
|
},
|
|
41573
|
+
"pipelineOrchestrator.planAutoTune": {
|
|
41574
|
+
capName: "pipeline-orchestrator",
|
|
41575
|
+
capScope: "system",
|
|
41576
|
+
addonId: null,
|
|
41577
|
+
access: "view"
|
|
41578
|
+
},
|
|
41078
41579
|
"pipelineOrchestrator.rebalance": {
|
|
41079
41580
|
capName: "pipeline-orchestrator",
|
|
41080
41581
|
capScope: "system",
|
|
@@ -41117,12 +41618,6 @@ Object.freeze({
|
|
|
41117
41618
|
addonId: null,
|
|
41118
41619
|
access: "create"
|
|
41119
41620
|
},
|
|
41120
|
-
"pipelineOrchestrator.saveTemplate": {
|
|
41121
|
-
capName: "pipeline-orchestrator",
|
|
41122
|
-
capScope: "system",
|
|
41123
|
-
addonId: null,
|
|
41124
|
-
access: "create"
|
|
41125
|
-
},
|
|
41126
41621
|
"pipelineOrchestrator.setAgentCapabilities": {
|
|
41127
41622
|
capName: "pipeline-orchestrator",
|
|
41128
41623
|
capScope: "system",
|
|
@@ -41135,12 +41630,6 @@ Object.freeze({
|
|
|
41135
41630
|
addonId: null,
|
|
41136
41631
|
access: "create"
|
|
41137
41632
|
},
|
|
41138
|
-
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
41139
|
-
capName: "pipeline-orchestrator",
|
|
41140
|
-
capScope: "system",
|
|
41141
|
-
addonId: null,
|
|
41142
|
-
access: "create"
|
|
41143
|
-
},
|
|
41144
41633
|
"pipelineOrchestrator.setAgentInferenceDevices": {
|
|
41145
41634
|
capName: "pipeline-orchestrator",
|
|
41146
41635
|
capScope: "system",
|
|
@@ -41177,6 +41666,12 @@ Object.freeze({
|
|
|
41177
41666
|
addonId: null,
|
|
41178
41667
|
access: "create"
|
|
41179
41668
|
},
|
|
41669
|
+
"pipelineOrchestrator.setCameraStepSettings": {
|
|
41670
|
+
capName: "pipeline-orchestrator",
|
|
41671
|
+
capScope: "system",
|
|
41672
|
+
addonId: null,
|
|
41673
|
+
access: "create"
|
|
41674
|
+
},
|
|
41180
41675
|
"pipelineOrchestrator.setCameraStepToggle": {
|
|
41181
41676
|
capName: "pipeline-orchestrator",
|
|
41182
41677
|
capScope: "system",
|
|
@@ -41195,25 +41690,37 @@ Object.freeze({
|
|
|
41195
41690
|
addonId: null,
|
|
41196
41691
|
access: "create"
|
|
41197
41692
|
},
|
|
41693
|
+
"pipelineOrchestrator.setLoadBalancingShares": {
|
|
41694
|
+
capName: "pipeline-orchestrator",
|
|
41695
|
+
capScope: "system",
|
|
41696
|
+
addonId: null,
|
|
41697
|
+
access: "create"
|
|
41698
|
+
},
|
|
41198
41699
|
"pipelineOrchestrator.setPipelineDevicePin": {
|
|
41199
41700
|
capName: "pipeline-orchestrator",
|
|
41200
41701
|
capScope: "system",
|
|
41201
41702
|
addonId: null,
|
|
41202
41703
|
access: "create"
|
|
41203
41704
|
},
|
|
41204
|
-
"pipelineOrchestrator.
|
|
41705
|
+
"pipelineOrchestrator.setShareSource": {
|
|
41205
41706
|
capName: "pipeline-orchestrator",
|
|
41206
41707
|
capScope: "system",
|
|
41207
41708
|
addonId: null,
|
|
41208
41709
|
access: "create"
|
|
41209
41710
|
},
|
|
41210
|
-
"pipelineOrchestrator.
|
|
41711
|
+
"pipelineOrchestrator.suggestShares": {
|
|
41712
|
+
capName: "pipeline-orchestrator",
|
|
41713
|
+
capScope: "system",
|
|
41714
|
+
addonId: null,
|
|
41715
|
+
access: "view"
|
|
41716
|
+
},
|
|
41717
|
+
"pipelineOrchestrator.unassignAudio": {
|
|
41211
41718
|
capName: "pipeline-orchestrator",
|
|
41212
41719
|
capScope: "system",
|
|
41213
41720
|
addonId: null,
|
|
41214
41721
|
access: "create"
|
|
41215
41722
|
},
|
|
41216
|
-
"pipelineOrchestrator.
|
|
41723
|
+
"pipelineOrchestrator.unassignPipeline": {
|
|
41217
41724
|
capName: "pipeline-orchestrator",
|
|
41218
41725
|
capScope: "system",
|
|
41219
41726
|
addonId: null,
|
|
@@ -42653,12 +43160,6 @@ Object.freeze({
|
|
|
42653
43160
|
addonId: null,
|
|
42654
43161
|
access: "view"
|
|
42655
43162
|
},
|
|
42656
|
-
"streamBroker.pullFrameHandles": {
|
|
42657
|
-
capName: "stream-broker",
|
|
42658
|
-
capScope: "system",
|
|
42659
|
-
addonId: null,
|
|
42660
|
-
access: "view"
|
|
42661
|
-
},
|
|
42662
43163
|
"streamBroker.regenerateRtspToken": {
|
|
42663
43164
|
capName: "stream-broker",
|
|
42664
43165
|
capScope: "system",
|
|
@@ -42725,12 +43226,6 @@ Object.freeze({
|
|
|
42725
43226
|
addonId: null,
|
|
42726
43227
|
access: "create"
|
|
42727
43228
|
},
|
|
42728
|
-
"streamBroker.subscribeFrames": {
|
|
42729
|
-
capName: "stream-broker",
|
|
42730
|
-
capScope: "system",
|
|
42731
|
-
addonId: null,
|
|
42732
|
-
access: "create"
|
|
42733
|
-
},
|
|
42734
43229
|
"streamBroker.unassignProfile": {
|
|
42735
43230
|
capName: "stream-broker",
|
|
42736
43231
|
capScope: "system",
|
|
@@ -42743,12 +43238,6 @@ Object.freeze({
|
|
|
42743
43238
|
addonId: null,
|
|
42744
43239
|
access: "create"
|
|
42745
43240
|
},
|
|
42746
|
-
"streamBroker.unsubscribeFrames": {
|
|
42747
|
-
capName: "stream-broker",
|
|
42748
|
-
capScope: "system",
|
|
42749
|
-
addonId: null,
|
|
42750
|
-
access: "create"
|
|
42751
|
-
},
|
|
42752
43241
|
"streamCatalog.getCatalog": {
|
|
42753
43242
|
capName: "stream-catalog",
|
|
42754
43243
|
capScope: "device",
|
|
@@ -43445,6 +43934,12 @@ Object.freeze({
|
|
|
43445
43934
|
addonId: null,
|
|
43446
43935
|
access: "create"
|
|
43447
43936
|
},
|
|
43937
|
+
"webrtcSession.applyDownlinkProbe": {
|
|
43938
|
+
capName: "webrtc-session",
|
|
43939
|
+
capScope: "device",
|
|
43940
|
+
addonId: null,
|
|
43941
|
+
access: "create"
|
|
43942
|
+
},
|
|
43448
43943
|
"webrtcSession.closeSession": {
|
|
43449
43944
|
capName: "webrtc-session",
|
|
43450
43945
|
capScope: "device",
|
|
@@ -45256,11 +45751,6 @@ Object.freeze({
|
|
|
45256
45751
|
form: "single",
|
|
45257
45752
|
optional: false
|
|
45258
45753
|
}],
|
|
45259
|
-
"decoder.createSession": [{
|
|
45260
|
-
name: "deviceId",
|
|
45261
|
-
form: "single",
|
|
45262
|
-
optional: true
|
|
45263
|
-
}],
|
|
45264
45754
|
"detectionPipeline.applyDeviceSettingsPatch": [{
|
|
45265
45755
|
name: "deviceId",
|
|
45266
45756
|
form: "single",
|
|
@@ -46491,6 +46981,11 @@ Object.freeze({
|
|
|
46491
46981
|
form: "single",
|
|
46492
46982
|
optional: false
|
|
46493
46983
|
}],
|
|
46984
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": [{
|
|
46985
|
+
name: "deviceIds",
|
|
46986
|
+
form: "array",
|
|
46987
|
+
optional: true
|
|
46988
|
+
}],
|
|
46494
46989
|
"pipelineExecutor.runPipeline": [{
|
|
46495
46990
|
name: "deviceId",
|
|
46496
46991
|
form: "single",
|
|
@@ -46596,6 +47091,11 @@ Object.freeze({
|
|
|
46596
47091
|
form: "single",
|
|
46597
47092
|
optional: false
|
|
46598
47093
|
}],
|
|
47094
|
+
"pipelineOrchestrator.setCameraStepSettings": [{
|
|
47095
|
+
name: "deviceId",
|
|
47096
|
+
form: "single",
|
|
47097
|
+
optional: false
|
|
47098
|
+
}],
|
|
46599
47099
|
"pipelineOrchestrator.setCameraStepToggle": [{
|
|
46600
47100
|
name: "deviceId",
|
|
46601
47101
|
form: "single",
|
|
@@ -47367,6 +47867,11 @@ Object.freeze({
|
|
|
47367
47867
|
form: "single",
|
|
47368
47868
|
optional: false
|
|
47369
47869
|
}],
|
|
47870
|
+
"webrtcSession.applyDownlinkProbe": [{
|
|
47871
|
+
name: "deviceId",
|
|
47872
|
+
form: "single",
|
|
47873
|
+
optional: false
|
|
47874
|
+
}],
|
|
47370
47875
|
"webrtcSession.closeSession": [{
|
|
47371
47876
|
name: "deviceId",
|
|
47372
47877
|
form: "single",
|
|
@@ -47596,6 +48101,8 @@ new Map(Object.entries({
|
|
|
47596
48101
|
"detection-blind": "device-events",
|
|
47597
48102
|
"node-online": "system",
|
|
47598
48103
|
"node-offline": "system",
|
|
48104
|
+
"node-restarting": "system",
|
|
48105
|
+
"node-updating": "system",
|
|
47599
48106
|
"node-inference-unavailable": "system",
|
|
47600
48107
|
"addon-crash-loop": "system",
|
|
47601
48108
|
"addon-update-available": "system",
|
|
@@ -47673,18 +48180,21 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47673
48180
|
kinds: [
|
|
47674
48181
|
"node-offline",
|
|
47675
48182
|
"node-online",
|
|
48183
|
+
"node-restarting",
|
|
47676
48184
|
"node-inference-unavailable"
|
|
47677
48185
|
]
|
|
47678
48186
|
},
|
|
47679
48187
|
{
|
|
47680
48188
|
id: "addons",
|
|
47681
48189
|
family: "system",
|
|
47682
|
-
label: "Addons",
|
|
47683
|
-
kinds: [
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
|
|
47687
|
-
|
|
48190
|
+
label: "Addons stopped",
|
|
48191
|
+
kinds: ["addon-crash-loop"]
|
|
48192
|
+
},
|
|
48193
|
+
{
|
|
48194
|
+
id: "addon-updates",
|
|
48195
|
+
family: "system",
|
|
48196
|
+
label: "Addon updates",
|
|
48197
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47688
48198
|
},
|
|
47689
48199
|
{
|
|
47690
48200
|
id: "updates",
|
|
@@ -47692,6 +48202,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47692
48202
|
label: "Server & container updates",
|
|
47693
48203
|
kinds: [
|
|
47694
48204
|
"server-update-available",
|
|
48205
|
+
"node-updating",
|
|
47695
48206
|
"server-updated",
|
|
47696
48207
|
"wrapper-update-available"
|
|
47697
48208
|
]
|
|
@@ -47839,18 +48350,21 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47839
48350
|
kinds: [
|
|
47840
48351
|
"node-offline",
|
|
47841
48352
|
"node-online",
|
|
48353
|
+
"node-restarting",
|
|
47842
48354
|
"node-inference-unavailable"
|
|
47843
48355
|
]
|
|
47844
48356
|
},
|
|
47845
48357
|
{
|
|
47846
48358
|
name: "summaryAddons",
|
|
47847
|
-
label: "Summary: addons",
|
|
47848
|
-
example: "
|
|
47849
|
-
kinds: [
|
|
47850
|
-
|
|
47851
|
-
|
|
47852
|
-
|
|
47853
|
-
|
|
48359
|
+
label: "Summary: addons stopped",
|
|
48360
|
+
example: "hub — recorder stopped after repeated crashes",
|
|
48361
|
+
kinds: ["addon-crash-loop"]
|
|
48362
|
+
},
|
|
48363
|
+
{
|
|
48364
|
+
name: "summaryAddonUpdates",
|
|
48365
|
+
label: "Summary: addon updates",
|
|
48366
|
+
example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
|
|
48367
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47854
48368
|
},
|
|
47855
48369
|
{
|
|
47856
48370
|
name: "summaryUpdates",
|
|
@@ -47858,6 +48372,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47858
48372
|
example: "server 1.2.377 → 1.2.378",
|
|
47859
48373
|
kinds: [
|
|
47860
48374
|
"server-update-available",
|
|
48375
|
+
"node-updating",
|
|
47861
48376
|
"server-updated",
|
|
47862
48377
|
"wrapper-update-available"
|
|
47863
48378
|
]
|
|
@@ -47890,7 +48405,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47890
48405
|
{
|
|
47891
48406
|
name: "summarySection",
|
|
47892
48407
|
label: "Summary: this section",
|
|
47893
|
-
description: "The section a per-section summary row is about
|
|
48408
|
+
description: "The section a per-section summary row is about; on a summary sent as one message, every section it reports, comma-separated.",
|
|
47894
48409
|
example: "Batterie scariche",
|
|
47895
48410
|
group: "system",
|
|
47896
48411
|
families: ["rule"],
|
|
@@ -47934,6 +48449,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47934
48449
|
[...RULE_BODY_ONLY],
|
|
47935
48450
|
[...RULE_TEXT],
|
|
47936
48451
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48452
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
47937
48453
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
47938
48454
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
47939
48455
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -47974,6 +48490,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
47974
48490
|
[...RULE_TEXT],
|
|
47975
48491
|
[...RULE_TEXT],
|
|
47976
48492
|
[...RULE_TEXT],
|
|
48493
|
+
[...RULE_TEXT],
|
|
48494
|
+
[...RULE_TEXT],
|
|
47977
48495
|
...STATE_SUMMARY_VARS
|
|
47978
48496
|
];
|
|
47979
48497
|
new Set([
|
|
@@ -48493,45 +49011,17 @@ DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
|
48493
49011
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
48494
49012
|
var MB = 1024 * 1024;
|
|
48495
49013
|
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
49014
|
//#endregion
|
|
48532
|
-
//#region src/
|
|
49015
|
+
//#region src/decode-hwaccel-rank.ts
|
|
48533
49016
|
/**
|
|
48534
|
-
*
|
|
49017
|
+
* Decode-hwaccel ranking for `reprobeHwaccel`: the kernel resolver's preferred
|
|
49018
|
+
* order, re-ranked so the backend this node reports is the one that actually
|
|
49019
|
+
* decodes (vaapi above qsv on Intel). A copy of the broker's
|
|
49020
|
+
* `packages/addon-pipeline/src/shared/ffmpeg-decode-args.ts` ranking — addons
|
|
49021
|
+
* do not import each other.
|
|
49022
|
+
*/
|
|
49023
|
+
/**
|
|
49024
|
+
* Canonical decode-hwaccel preference order.
|
|
48535
49025
|
*
|
|
48536
49026
|
* `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
|
|
48537
49027
|
* child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
|
|
@@ -48596,1234 +49086,6 @@ function rankDecodeHwAccels(preferred, supportedMethods) {
|
|
|
48596
49086
|
return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
|
|
48597
49087
|
}
|
|
48598
49088
|
//#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
49089
|
//#region src/audio-codec/codec-catalog.ts
|
|
49828
49090
|
/**
|
|
49829
49091
|
* Normalise an SDP-reported codec name to the libav/ffmpeg name. Mirrors
|
|
@@ -50639,21 +49901,22 @@ function peekNodeAvRuntime() {
|
|
|
50639
49901
|
}
|
|
50640
49902
|
//#endregion
|
|
50641
49903
|
//#region src/addon/index.ts
|
|
50642
|
-
|
|
49904
|
+
/**
|
|
49905
|
+
* The node's `decoder` + `audio-codec` provider.
|
|
49906
|
+
*
|
|
49907
|
+
* The `decoder` cap is per-node hwaccel only (D763): `getInfo` reports this
|
|
49908
|
+
* node's operator choice and probed best backend, `reprobeHwaccel` re-runs the
|
|
49909
|
+
* probe. Both are per-node settings (`hwaccel@<node>`,
|
|
49910
|
+
* `probedBestHwaccel@<node>`); the egress transcode, HomeKit and the load view
|
|
49911
|
+
* read them through `getInfo`, pinned to the node. Video decoding itself runs in the pipeline runner's session-decode
|
|
49912
|
+
* workers, which use node-av directly. The `audio-codec` cap is the node-av
|
|
49913
|
+
* codec (encode/decode + resample) every audio session on the node runs on.
|
|
49914
|
+
*/
|
|
50643
49915
|
var DecoderNodeAvAddon = class extends BaseAddon {
|
|
50644
49916
|
/**
|
|
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.
|
|
49917
|
+
* node-av audio-codec provider — registered whenever node-av loads on this
|
|
49918
|
+
* node, so this single addon provides both the `decoder` and `audio-codec`
|
|
49919
|
+
* caps.
|
|
50657
49920
|
*/
|
|
50658
49921
|
audioProvider = null;
|
|
50659
49922
|
constructor() {
|
|
@@ -50664,7 +49927,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50664
49927
|
id: "hwaccel",
|
|
50665
49928
|
title: "Hardware acceleration",
|
|
50666
49929
|
tab: "decoder",
|
|
50667
|
-
description: "
|
|
49930
|
+
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
49931
|
fields: [this.field({
|
|
50669
49932
|
type: "select",
|
|
50670
49933
|
key: "hwaccel",
|
|
@@ -50709,12 +49972,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50709
49972
|
provider: this.audioProvider
|
|
50710
49973
|
});
|
|
50711
49974
|
}
|
|
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 } });
|
|
49975
|
+
this.ctx.logger.info("node-av decoder addon initialized");
|
|
50718
49976
|
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
50719
49977
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50720
49978
|
});
|
|
@@ -50725,16 +49983,6 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50725
49983
|
return registrations;
|
|
50726
49984
|
}
|
|
50727
49985
|
/**
|
|
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
49986
|
* Re-run the platform probe on this host and persist the detected
|
|
50739
49987
|
* backend as `probedBestHwaccel`. The operator's `hwaccel` setting
|
|
50740
49988
|
* is intentionally left alone — the probe only updates the hint.
|
|
@@ -50762,190 +50010,29 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50762
50010
|
return { backend: "none" };
|
|
50763
50011
|
}
|
|
50764
50012
|
}
|
|
50765
|
-
async supportsCodec(input) {
|
|
50766
|
-
return [
|
|
50767
|
-
"h264",
|
|
50768
|
-
"h265",
|
|
50769
|
-
"hevc"
|
|
50770
|
-
].includes(input.codec.toLowerCase());
|
|
50771
|
-
}
|
|
50772
50013
|
async getInfo() {
|
|
50773
50014
|
return {
|
|
50774
50015
|
id: "decoder-nodeav",
|
|
50775
50016
|
name: "Decoder (node-av)",
|
|
50776
|
-
isPullMode: true,
|
|
50777
|
-
priority: 10,
|
|
50778
50017
|
hwaccel: this.config.hwaccel,
|
|
50779
50018
|
probedBestHwaccel: this.config.probedBestHwaccel || void 0
|
|
50780
50019
|
};
|
|
50781
50020
|
}
|
|
50782
50021
|
/**
|
|
50783
|
-
* The cluster node id of this
|
|
50784
|
-
* `dev-agent-0`, …), not the addon id.
|
|
50785
|
-
*
|
|
50786
|
-
*
|
|
50787
|
-
*
|
|
50788
|
-
* resolution `PipelineRunnerAddon.onInitialize` performs.
|
|
50022
|
+
* The cluster node id of this addon — the Moleculer nodeID (`hub`,
|
|
50023
|
+
* `dev-agent-0`, …), not the addon id. A hierarchical broker nodeID
|
|
50024
|
+
* (`hub/...`) is collapsed to the cluster-visible parent; in-process boot is
|
|
50025
|
+
* left as-is. Mirrors the resolution `PipelineRunnerAddon.onInitialize`
|
|
50026
|
+
* performs.
|
|
50789
50027
|
*/
|
|
50790
50028
|
resolveLocalNodeId() {
|
|
50791
50029
|
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
50792
50030
|
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
50793
50031
|
}
|
|
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
50032
|
async onShutdown() {
|
|
50935
50033
|
this.audioProvider?.stop();
|
|
50936
50034
|
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();
|
|
50035
|
+
this.ctx.logger.info("node-av decoder addon shutdown");
|
|
50949
50036
|
}
|
|
50950
50037
|
};
|
|
50951
50038
|
//#endregion
|
|
@@ -50953,7 +50040,6 @@ exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
|
|
|
50953
50040
|
exports.NodeAvAudioCodecProvider = NodeAvAudioCodecProvider;
|
|
50954
50041
|
exports.NodeAvAudioDecodeSession = NodeAvAudioDecodeSession;
|
|
50955
50042
|
exports.NodeAvAudioEncodeSession = NodeAvAudioEncodeSession;
|
|
50956
|
-
exports.NodeAvDecoderSession = NodeAvDecoderSession;
|
|
50957
50043
|
exports.default = DecoderNodeAvAddon;
|
|
50958
50044
|
exports.loadNodeAvRuntime = loadNodeAvRuntime;
|
|
50959
50045
|
exports.peekNodeAvRuntime = peekNodeAvRuntime;
|