@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-B_0XEcLF.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -500,15 +500,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
500
500
|
EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
|
|
501
501
|
EventCategory["AgentBackupActivated"] = "agent.backup.activated";
|
|
502
502
|
EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
|
|
503
|
-
/**
|
|
504
|
-
* Per-agent hwaccel preference changed (user override set, cleared, or
|
|
505
|
-
* re-probed). Observability event — decoders pull the current pref at
|
|
506
|
-
* `createSession`, so running sessions keep their current backend until
|
|
507
|
-
* they rotate naturally (camera add/remove, stream restart). Future
|
|
508
|
-
* work can wire a listener in stream-broker that force-rotates live
|
|
509
|
-
* sessions; for now this event powers logs + admin-UI toast feedback.
|
|
510
|
-
*/
|
|
511
|
-
EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
|
|
512
503
|
EventCategory["MotionAnalysis"] = "detection.motion-analysis";
|
|
513
504
|
/** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
|
|
514
505
|
EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
|
|
@@ -3771,7 +3762,7 @@ function initializeContext(params) {
|
|
|
3771
3762
|
external: params?.external ?? void 0
|
|
3772
3763
|
};
|
|
3773
3764
|
}
|
|
3774
|
-
function process
|
|
3765
|
+
function process(schema, ctx, _params = {
|
|
3775
3766
|
path: [],
|
|
3776
3767
|
schemaPath: []
|
|
3777
3768
|
}) {
|
|
@@ -3808,7 +3799,7 @@ function process$1(schema, ctx, _params = {
|
|
|
3808
3799
|
const parent = schema._zod.parent;
|
|
3809
3800
|
if (parent) {
|
|
3810
3801
|
if (!result.ref) result.ref = parent;
|
|
3811
|
-
process
|
|
3802
|
+
process(parent, ctx, params);
|
|
3812
3803
|
ctx.seen.get(parent).isParent = true;
|
|
3813
3804
|
}
|
|
3814
3805
|
}
|
|
@@ -4028,7 +4019,7 @@ var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
|
|
|
4028
4019
|
...params,
|
|
4029
4020
|
processors
|
|
4030
4021
|
});
|
|
4031
|
-
process
|
|
4022
|
+
process(schema, ctx);
|
|
4032
4023
|
extractDefs(ctx, schema);
|
|
4033
4024
|
return finalize(ctx, schema);
|
|
4034
4025
|
};
|
|
@@ -4040,7 +4031,7 @@ var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) =
|
|
|
4040
4031
|
io,
|
|
4041
4032
|
processors
|
|
4042
4033
|
});
|
|
4043
|
-
process
|
|
4034
|
+
process(schema, ctx);
|
|
4044
4035
|
extractDefs(ctx, schema);
|
|
4045
4036
|
return finalize(ctx, schema);
|
|
4046
4037
|
};
|
|
@@ -4154,7 +4145,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
|
|
|
4154
4145
|
if (typeof minimum === "number") json.minItems = minimum;
|
|
4155
4146
|
if (typeof maximum === "number") json.maxItems = maximum;
|
|
4156
4147
|
json.type = "array";
|
|
4157
|
-
json.items = process
|
|
4148
|
+
json.items = process(def.element, ctx, {
|
|
4158
4149
|
...params,
|
|
4159
4150
|
path: [...params.path, "items"]
|
|
4160
4151
|
});
|
|
@@ -4165,7 +4156,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4165
4156
|
json.type = "object";
|
|
4166
4157
|
json.properties = {};
|
|
4167
4158
|
const shape = def.shape;
|
|
4168
|
-
for (const key in shape) json.properties[key] = process
|
|
4159
|
+
for (const key in shape) json.properties[key] = process(shape[key], ctx, {
|
|
4169
4160
|
...params,
|
|
4170
4161
|
path: [
|
|
4171
4162
|
...params.path,
|
|
@@ -4183,7 +4174,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4183
4174
|
if (def.catchall?._zod.def.type === "never") json.additionalProperties = false;
|
|
4184
4175
|
else if (!def.catchall) {
|
|
4185
4176
|
if (ctx.io === "output") json.additionalProperties = false;
|
|
4186
|
-
} else if (def.catchall) json.additionalProperties = process
|
|
4177
|
+
} else if (def.catchall) json.additionalProperties = process(def.catchall, ctx, {
|
|
4187
4178
|
...params,
|
|
4188
4179
|
path: [...params.path, "additionalProperties"]
|
|
4189
4180
|
});
|
|
@@ -4191,7 +4182,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
4191
4182
|
var unionProcessor = (schema, ctx, json, params) => {
|
|
4192
4183
|
const def = schema._zod.def;
|
|
4193
4184
|
const isExclusive = def.inclusive === false;
|
|
4194
|
-
const options = def.options.map((x, i) => process
|
|
4185
|
+
const options = def.options.map((x, i) => process(x, ctx, {
|
|
4195
4186
|
...params,
|
|
4196
4187
|
path: [
|
|
4197
4188
|
...params.path,
|
|
@@ -4204,7 +4195,7 @@ var unionProcessor = (schema, ctx, json, params) => {
|
|
|
4204
4195
|
};
|
|
4205
4196
|
var intersectionProcessor = (schema, ctx, json, params) => {
|
|
4206
4197
|
const def = schema._zod.def;
|
|
4207
|
-
const a = process
|
|
4198
|
+
const a = process(def.left, ctx, {
|
|
4208
4199
|
...params,
|
|
4209
4200
|
path: [
|
|
4210
4201
|
...params.path,
|
|
@@ -4212,7 +4203,7 @@ var intersectionProcessor = (schema, ctx, json, params) => {
|
|
|
4212
4203
|
0
|
|
4213
4204
|
]
|
|
4214
4205
|
});
|
|
4215
|
-
const b = process
|
|
4206
|
+
const b = process(def.right, ctx, {
|
|
4216
4207
|
...params,
|
|
4217
4208
|
path: [
|
|
4218
4209
|
...params.path,
|
|
@@ -4229,7 +4220,7 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
4229
4220
|
json.type = "array";
|
|
4230
4221
|
const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
4231
4222
|
const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
4232
|
-
const prefixItems = def.items.map((x, i) => process
|
|
4223
|
+
const prefixItems = def.items.map((x, i) => process(x, ctx, {
|
|
4233
4224
|
...params,
|
|
4234
4225
|
path: [
|
|
4235
4226
|
...params.path,
|
|
@@ -4237,7 +4228,7 @@ var tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
4237
4228
|
i
|
|
4238
4229
|
]
|
|
4239
4230
|
}));
|
|
4240
|
-
const rest = def.rest ? process
|
|
4231
|
+
const rest = def.rest ? process(def.rest, ctx, {
|
|
4241
4232
|
...params,
|
|
4242
4233
|
path: [
|
|
4243
4234
|
...params.path,
|
|
@@ -4268,7 +4259,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4268
4259
|
const keyType = def.keyType;
|
|
4269
4260
|
const patterns = keyType._zod.bag?.patterns;
|
|
4270
4261
|
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
4271
|
-
const valueSchema = process
|
|
4262
|
+
const valueSchema = process(def.valueType, ctx, {
|
|
4272
4263
|
...params,
|
|
4273
4264
|
path: [
|
|
4274
4265
|
...params.path,
|
|
@@ -4279,11 +4270,11 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4279
4270
|
json.patternProperties = {};
|
|
4280
4271
|
for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
|
|
4281
4272
|
} else {
|
|
4282
|
-
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process
|
|
4273
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process(def.keyType, ctx, {
|
|
4283
4274
|
...params,
|
|
4284
4275
|
path: [...params.path, "propertyNames"]
|
|
4285
4276
|
});
|
|
4286
|
-
json.additionalProperties = process
|
|
4277
|
+
json.additionalProperties = process(def.valueType, ctx, {
|
|
4287
4278
|
...params,
|
|
4288
4279
|
path: [...params.path, "additionalProperties"]
|
|
4289
4280
|
});
|
|
@@ -4296,7 +4287,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
|
|
|
4296
4287
|
};
|
|
4297
4288
|
var nullableProcessor = (schema, ctx, json, params) => {
|
|
4298
4289
|
const def = schema._zod.def;
|
|
4299
|
-
const inner = process
|
|
4290
|
+
const inner = process(def.innerType, ctx, params);
|
|
4300
4291
|
const seen = ctx.seen.get(schema);
|
|
4301
4292
|
if (ctx.target === "openapi-3.0") {
|
|
4302
4293
|
seen.ref = def.innerType;
|
|
@@ -4305,27 +4296,27 @@ var nullableProcessor = (schema, ctx, json, params) => {
|
|
|
4305
4296
|
};
|
|
4306
4297
|
var nonoptionalProcessor = (schema, ctx, _json, params) => {
|
|
4307
4298
|
const def = schema._zod.def;
|
|
4308
|
-
process
|
|
4299
|
+
process(def.innerType, ctx, params);
|
|
4309
4300
|
const seen = ctx.seen.get(schema);
|
|
4310
4301
|
seen.ref = def.innerType;
|
|
4311
4302
|
};
|
|
4312
4303
|
var defaultProcessor = (schema, ctx, json, params) => {
|
|
4313
4304
|
const def = schema._zod.def;
|
|
4314
|
-
process
|
|
4305
|
+
process(def.innerType, ctx, params);
|
|
4315
4306
|
const seen = ctx.seen.get(schema);
|
|
4316
4307
|
seen.ref = def.innerType;
|
|
4317
4308
|
json.default = JSON.parse(JSON.stringify(def.defaultValue));
|
|
4318
4309
|
};
|
|
4319
4310
|
var prefaultProcessor = (schema, ctx, json, params) => {
|
|
4320
4311
|
const def = schema._zod.def;
|
|
4321
|
-
process
|
|
4312
|
+
process(def.innerType, ctx, params);
|
|
4322
4313
|
const seen = ctx.seen.get(schema);
|
|
4323
4314
|
seen.ref = def.innerType;
|
|
4324
4315
|
if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
|
|
4325
4316
|
};
|
|
4326
4317
|
var catchProcessor = (schema, ctx, json, params) => {
|
|
4327
4318
|
const def = schema._zod.def;
|
|
4328
|
-
process
|
|
4319
|
+
process(def.innerType, ctx, params);
|
|
4329
4320
|
const seen = ctx.seen.get(schema);
|
|
4330
4321
|
seen.ref = def.innerType;
|
|
4331
4322
|
let catchValue;
|
|
@@ -4340,26 +4331,26 @@ var pipeProcessor = (schema, ctx, _json, params) => {
|
|
|
4340
4331
|
const def = schema._zod.def;
|
|
4341
4332
|
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
|
|
4342
4333
|
const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
|
|
4343
|
-
process
|
|
4334
|
+
process(innerType, ctx, params);
|
|
4344
4335
|
const seen = ctx.seen.get(schema);
|
|
4345
4336
|
seen.ref = innerType;
|
|
4346
4337
|
};
|
|
4347
4338
|
var readonlyProcessor = (schema, ctx, json, params) => {
|
|
4348
4339
|
const def = schema._zod.def;
|
|
4349
|
-
process
|
|
4340
|
+
process(def.innerType, ctx, params);
|
|
4350
4341
|
const seen = ctx.seen.get(schema);
|
|
4351
4342
|
seen.ref = def.innerType;
|
|
4352
4343
|
json.readOnly = true;
|
|
4353
4344
|
};
|
|
4354
4345
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4355
4346
|
const def = schema._zod.def;
|
|
4356
|
-
process
|
|
4347
|
+
process(def.innerType, ctx, params);
|
|
4357
4348
|
const seen = ctx.seen.get(schema);
|
|
4358
4349
|
seen.ref = def.innerType;
|
|
4359
4350
|
};
|
|
4360
4351
|
var lazyProcessor = (schema, ctx, _json, params) => {
|
|
4361
4352
|
const innerType = schema._zod.innerType;
|
|
4362
|
-
process
|
|
4353
|
+
process(innerType, ctx, params);
|
|
4363
4354
|
const seen = ctx.seen.get(schema);
|
|
4364
4355
|
seen.ref = innerType;
|
|
4365
4356
|
};
|
|
@@ -5379,7 +5370,7 @@ var ZodIssueCode = {
|
|
|
5379
5370
|
var ZodFirstPartyTypeKind;
|
|
5380
5371
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5381
5372
|
//#endregion
|
|
5382
|
-
//#region ../types/dist/sleep-
|
|
5373
|
+
//#region ../types/dist/sleep-BNeXenvr.mjs
|
|
5383
5374
|
/**
|
|
5384
5375
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5385
5376
|
* window to float samples (D455).
|
|
@@ -6745,32 +6736,12 @@ object({
|
|
|
6745
6736
|
audioCodec: string().optional(),
|
|
6746
6737
|
metadata: record(string(), unknown()).readonly().optional()
|
|
6747
6738
|
});
|
|
6748
|
-
var EncodedPacketSchema = object({
|
|
6749
|
-
type: _enum(["video", "audio"]),
|
|
6750
|
-
data: _instanceof(Uint8Array),
|
|
6751
|
-
pts: number(),
|
|
6752
|
-
dts: number(),
|
|
6753
|
-
keyframe: boolean(),
|
|
6754
|
-
codec: string()
|
|
6755
|
-
});
|
|
6756
|
-
var DecodedFrameSchema = object({
|
|
6757
|
-
data: _instanceof(Uint8Array),
|
|
6758
|
-
width: number(),
|
|
6759
|
-
height: number(),
|
|
6760
|
-
format: _enum([
|
|
6761
|
-
"jpeg",
|
|
6762
|
-
"rgb",
|
|
6763
|
-
"bgr",
|
|
6764
|
-
"yuv420",
|
|
6765
|
-
"gray"
|
|
6766
|
-
]),
|
|
6767
|
-
timestamp: number()
|
|
6768
|
-
});
|
|
6769
6739
|
/**
|
|
6770
6740
|
* Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
|
|
6771
6741
|
* frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
|
|
6772
|
-
* interface in `interfaces/frame-handle.ts` field-for-field so a
|
|
6773
|
-
*
|
|
6742
|
+
* interface in `interfaces/frame-handle.ts` field-for-field so a cap method
|
|
6743
|
+
* (the runner's native-crop and pipeline-executor inputs) can carry handles
|
|
6744
|
+
* over tRPC / Moleculer.
|
|
6774
6745
|
*
|
|
6775
6746
|
* The `satisfies` assertion below pins this schema to the interface: if a
|
|
6776
6747
|
* field is added to / removed from `FrameHandle` without a matching schema
|
|
@@ -6796,50 +6767,6 @@ var FrameHandleSchema = object({
|
|
|
6796
6767
|
slotCount: number().int().positive()
|
|
6797
6768
|
});
|
|
6798
6769
|
/**
|
|
6799
|
-
* Pixel format a frame-handle subscriber can request from the broker
|
|
6800
|
-
* (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
|
|
6801
|
-
* `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
|
|
6802
|
-
* `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
|
|
6803
|
-
* payload, and no decoded-frame consumer requests it over the shm plane.
|
|
6804
|
-
*/
|
|
6805
|
-
var FrameHandleFormatSchema = _enum([
|
|
6806
|
-
"rgb",
|
|
6807
|
-
"bgr",
|
|
6808
|
-
"yuv420",
|
|
6809
|
-
"gray"
|
|
6810
|
-
]);
|
|
6811
|
-
/**
|
|
6812
|
-
* Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
|
|
6813
|
-
* broker for a `FrameHandle` stream of one `format`; the broker maintains one
|
|
6814
|
-
* shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
|
|
6815
|
-
* subscriber (motion) and an `rgb` subscriber (detection) each read the format
|
|
6816
|
-
* they asked for with no broker-side conversion.
|
|
6817
|
-
*/
|
|
6818
|
-
var SubscribeFramesInputSchema = object({
|
|
6819
|
-
brokerId: string(),
|
|
6820
|
-
format: FrameHandleFormatSchema,
|
|
6821
|
-
/**
|
|
6822
|
-
* Optional reader-side cadence hint in frames per second. The broker does
|
|
6823
|
-
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
6824
|
-
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
6825
|
-
* the consumer can pace its own `pullFrameHandles` polling.
|
|
6826
|
-
*/
|
|
6827
|
-
maxFps: number().positive().optional(),
|
|
6828
|
-
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
6829
|
-
tag: string().optional()
|
|
6830
|
-
});
|
|
6831
|
-
/**
|
|
6832
|
-
* Result of `stream-broker.subscribeFrames`. The consumer then polls
|
|
6833
|
-
* `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
|
|
6834
|
-
* `FrameHandle` to a `FrameRingReader`.
|
|
6835
|
-
*/
|
|
6836
|
-
var SubscribeFramesResultSchema = object({
|
|
6837
|
-
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
6838
|
-
subscriptionId: string(),
|
|
6839
|
-
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
6840
|
-
maxFps: number().nonnegative()
|
|
6841
|
-
});
|
|
6842
|
-
/**
|
|
6843
6770
|
* Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
|
|
6844
6771
|
* `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
|
|
6845
6772
|
* sample bytes plus the track parameters a consumer needs to interpret them.
|
|
@@ -6847,8 +6774,8 @@ var SubscribeFramesResultSchema = object({
|
|
|
6847
6774
|
* Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
|
|
6848
6775
|
* Float32 PCM), so unlike video frames they ship their bytes INLINE over
|
|
6849
6776
|
* tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
|
|
6850
|
-
* (the wire-serialisable supertype of `Buffer`)
|
|
6851
|
-
*
|
|
6777
|
+
* (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
|
|
6778
|
+
* it.
|
|
6852
6779
|
*/
|
|
6853
6780
|
var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
|
|
6854
6781
|
var DecodedAudioChunkSchema = object({
|
|
@@ -6866,11 +6793,10 @@ var DecodedAudioChunkSchema = object({
|
|
|
6866
6793
|
format: AudioChunkFormatSchema.optional()
|
|
6867
6794
|
});
|
|
6868
6795
|
/**
|
|
6869
|
-
* Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9).
|
|
6870
|
-
*
|
|
6871
|
-
*
|
|
6872
|
-
*
|
|
6873
|
-
* `pullAudioChunks`.
|
|
6796
|
+
* Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
|
|
6797
|
+
* its bytes inline: a consumer asks the broker for the decoded audio-chunk
|
|
6798
|
+
* stream of one broker; the broker keeps a per-subscription bounded FIFO queue
|
|
6799
|
+
* the consumer drains via `pullAudioChunks`.
|
|
6874
6800
|
*/
|
|
6875
6801
|
var SubscribeAudioChunksInputSchema = object({
|
|
6876
6802
|
brokerId: string(),
|
|
@@ -6901,9 +6827,7 @@ var BrokerStatsSchema = object({
|
|
|
6901
6827
|
"stopped"
|
|
6902
6828
|
]),
|
|
6903
6829
|
inputFps: number(),
|
|
6904
|
-
decodeFps: number(),
|
|
6905
6830
|
encodedSubscribers: number(),
|
|
6906
|
-
decodedSubscribers: number(),
|
|
6907
6831
|
uptimeMs: number(),
|
|
6908
6832
|
bitrateKbps: number(),
|
|
6909
6833
|
idrIntervalMs: number(),
|
|
@@ -6916,14 +6840,6 @@ var BrokerStatsSchema = object({
|
|
|
6916
6840
|
preBufferMs: number(),
|
|
6917
6841
|
preBufferPackets: number(),
|
|
6918
6842
|
/**
|
|
6919
|
-
* Moleculer node id of the decoder provider currently servicing this
|
|
6920
|
-
* stream's decoded subscribers. `null` until the deferred decoder is
|
|
6921
|
-
* created (no decoded clients yet, or codec not detected). Surfaces the
|
|
6922
|
-
* runtime decoder placement so the UI can show "Decoder: <agent>" without
|
|
6923
|
-
* a separate cap call per broker.
|
|
6924
|
-
*/
|
|
6925
|
-
decoderNodeId: string().nullable(),
|
|
6926
|
-
/**
|
|
6927
6843
|
* Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
|
|
6928
6844
|
* when the stream has no audio track or the broker is in cold start.
|
|
6929
6845
|
* `supported = false` means the codec was detected but the local
|
|
@@ -10255,70 +10171,6 @@ var StorageLocationDeclarationSchema = object({
|
|
|
10255
10171
|
"backup"
|
|
10256
10172
|
]).optional()
|
|
10257
10173
|
});
|
|
10258
|
-
var DecoderStatsSchema = object({
|
|
10259
|
-
inputFps: number(),
|
|
10260
|
-
outputFps: number(),
|
|
10261
|
-
avgDecodeTimeMs: number(),
|
|
10262
|
-
droppedFrames: number(),
|
|
10263
|
-
/**
|
|
10264
|
-
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
10265
|
-
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
10266
|
-
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
10267
|
-
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
10268
|
-
*/
|
|
10269
|
-
lagMs: number().optional(),
|
|
10270
|
-
effectiveFps: number().optional(),
|
|
10271
|
-
adaptiveFps: number().optional()
|
|
10272
|
-
});
|
|
10273
|
-
var DecoderSessionConfigSchema = object({
|
|
10274
|
-
codec: string(),
|
|
10275
|
-
maxFps: number().default(0),
|
|
10276
|
-
outputFormat: _enum([
|
|
10277
|
-
"jpeg",
|
|
10278
|
-
"rgb",
|
|
10279
|
-
"bgr",
|
|
10280
|
-
"yuv420",
|
|
10281
|
-
"gray"
|
|
10282
|
-
]).default("jpeg"),
|
|
10283
|
-
scale: number().default(1),
|
|
10284
|
-
width: number().optional(),
|
|
10285
|
-
height: number().optional(),
|
|
10286
|
-
/**
|
|
10287
|
-
* Identifier of the camera this decoder session serves. Optional
|
|
10288
|
-
* because the cap is generic (any caller could request decode), but
|
|
10289
|
-
* stream-broker passes it so decoder logs include `deviceId` for
|
|
10290
|
-
* per-camera filtering when diagnosing failures (e.g. node-av
|
|
10291
|
-
* sendPacket errors on a single hung camera).
|
|
10292
|
-
*/
|
|
10293
|
-
deviceId: number().int().nonnegative().optional(),
|
|
10294
|
-
/**
|
|
10295
|
-
* Free-form tag for log scoping. Stream-broker uses
|
|
10296
|
-
* `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
|
|
10297
|
-
* on every line so `grep tag=broker:5/high` filters one camera
|
|
10298
|
-
* profile cleanly.
|
|
10299
|
-
*/
|
|
10300
|
-
tag: string().optional(),
|
|
10301
|
-
/**
|
|
10302
|
-
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
10303
|
-
*
|
|
10304
|
-
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
10305
|
-
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
10306
|
-
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
10307
|
-
* into an OS shared-memory ring and drained as zero-pixel
|
|
10308
|
-
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
10309
|
-
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
10310
|
-
* `pullHandles` returns nothing for a `'callback'` session.
|
|
10311
|
-
*/
|
|
10312
|
-
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
10313
|
-
/**
|
|
10314
|
-
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
10315
|
-
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
10316
|
-
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
10317
|
-
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
10318
|
-
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
10319
|
-
*/
|
|
10320
|
-
debug: boolean().optional()
|
|
10321
|
-
});
|
|
10322
10174
|
/**
|
|
10323
10175
|
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
10324
10176
|
* an addon declares its channels in.
|
|
@@ -10461,6 +10313,26 @@ var ClipSearchStrictnessSchema = _enum([
|
|
|
10461
10313
|
"balanced",
|
|
10462
10314
|
"strict"
|
|
10463
10315
|
]);
|
|
10316
|
+
/** Steps whose effect a root label can BE. */
|
|
10317
|
+
var LABEL_SUPPLY_STEP_IDS = [
|
|
10318
|
+
"animal-classifier",
|
|
10319
|
+
"vehicle-classifier",
|
|
10320
|
+
"package-detection"
|
|
10321
|
+
];
|
|
10322
|
+
/** Steps whose box a root detection can BE. */
|
|
10323
|
+
var CROP_SUPPLY_STEP_IDS = ["plate-detection", "face-detection"];
|
|
10324
|
+
/**
|
|
10325
|
+
* Steps whose per-detection MASK a root detection can carry (D737). The live
|
|
10326
|
+
* mask step is `segmentation` — a frame-plane refiner that runs its own model
|
|
10327
|
+
* once per detection crop. (`instance-segmentation` is a dormant step id that
|
|
10328
|
+
* no camera tree carries.)
|
|
10329
|
+
*/
|
|
10330
|
+
var MASK_SUPPLY_STEP_IDS = ["segmentation"];
|
|
10331
|
+
[
|
|
10332
|
+
...LABEL_SUPPLY_STEP_IDS,
|
|
10333
|
+
...CROP_SUPPLY_STEP_IDS,
|
|
10334
|
+
...MASK_SUPPLY_STEP_IDS
|
|
10335
|
+
];
|
|
10464
10336
|
var MODEL_FORMATS = [
|
|
10465
10337
|
"onnx",
|
|
10466
10338
|
"coreml",
|
|
@@ -10489,7 +10361,53 @@ var ModelFormatEntrySchema = object({
|
|
|
10489
10361
|
/** Multi-file payload (directory members or sibling files). */
|
|
10490
10362
|
files: array(string()).readonly().optional(),
|
|
10491
10363
|
/** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
|
|
10492
|
-
runtimes: array(_enum(["python"])).readonly().optional()
|
|
10364
|
+
runtimes: array(_enum(["python"])).readonly().optional(),
|
|
10365
|
+
/**
|
|
10366
|
+
* Content revision of the artefact, for one hosted OFF the CamStack mirror.
|
|
10367
|
+
* A format on the mirror leaves it unset: its revision (the sha256 of its
|
|
10368
|
+
* weight file) comes from the downloader's generated table, kept in step
|
|
10369
|
+
* with the mirror by `scripts/check-hosted-model-revisions.ts` (D744). A
|
|
10370
|
+
* node records the revision it downloaded next to the file
|
|
10371
|
+
* (`<file>.revision`); a copy with no record or a different one is stale
|
|
10372
|
+
* unless its weight file hashes to the revision, and a stale copy is
|
|
10373
|
+
* downloaded again, the old one serving until the new one is complete. No
|
|
10374
|
+
* revision at all ⇒ a file on disk is current, whatever it holds (D743).
|
|
10375
|
+
*/
|
|
10376
|
+
revision: string().min(1).optional()
|
|
10377
|
+
});
|
|
10378
|
+
/**
|
|
10379
|
+
* What a node holds for one (model, format) (D746). `current` is a complete
|
|
10380
|
+
* copy at its catalog revision — exactly what `downloaded: true` means.
|
|
10381
|
+
* `stale` is bytes on disk that are not (yet) proven current under D743/D744:
|
|
10382
|
+
* no or a different revision record, or a missing sibling. `missing` is no
|
|
10383
|
+
* copy. `downloaded` alone folds `stale` into "not downloaded", which hid
|
|
10384
|
+
* reclaimable bytes and a copy about to be replaced.
|
|
10385
|
+
*/
|
|
10386
|
+
var ModelOnDiskStateSchema = _enum([
|
|
10387
|
+
"current",
|
|
10388
|
+
"stale",
|
|
10389
|
+
"missing"
|
|
10390
|
+
]);
|
|
10391
|
+
/**
|
|
10392
|
+
* The numeric precision of one format's BUILD (D746) — reported only when the
|
|
10393
|
+
* build states it (file-name token, an INT8 variant, an EdgeTPU compile, a
|
|
10394
|
+
* CamStack Core ML export). Absent means "not stated", never "fp32".
|
|
10395
|
+
*/
|
|
10396
|
+
var ModelFormatPrecisionSchema = _enum([
|
|
10397
|
+
"fp32",
|
|
10398
|
+
"fp16",
|
|
10399
|
+
"int8"
|
|
10400
|
+
]);
|
|
10401
|
+
/**
|
|
10402
|
+
* One format of one model as a node's `pipelineExecutor.getSchema` reports it.
|
|
10403
|
+
* `onDisk` / `precision` are optional for version skew: a runner that predates
|
|
10404
|
+
* them omits them, and a reader falls back to `downloaded`.
|
|
10405
|
+
*/
|
|
10406
|
+
var PipelineModelFormatStatusSchema = object({
|
|
10407
|
+
downloaded: boolean(),
|
|
10408
|
+
sizeMB: number(),
|
|
10409
|
+
onDisk: ModelOnDiskStateSchema.optional(),
|
|
10410
|
+
precision: ModelFormatPrecisionSchema.optional()
|
|
10493
10411
|
});
|
|
10494
10412
|
/**
|
|
10495
10413
|
* Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
|
|
@@ -10530,11 +10448,11 @@ var ModelVariantGroupSchema = object({
|
|
|
10530
10448
|
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
10531
10449
|
precision: _enum(["fp32", "int8"]).optional(),
|
|
10532
10450
|
/**
|
|
10533
|
-
*
|
|
10534
|
-
*
|
|
10535
|
-
*
|
|
10451
|
+
* Training / latency-variant axis. Omit ⇒ the standard build. `test` marks
|
|
10452
|
+
* an alternate training of the same family (Scrypted `relu_test`); chip
|
|
10453
|
+
* label is "Test".
|
|
10536
10454
|
*/
|
|
10537
|
-
optimization: _enum(["standard", "
|
|
10455
|
+
optimization: _enum(["standard", "test"]).optional(),
|
|
10538
10456
|
/**
|
|
10539
10457
|
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
10540
10458
|
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
@@ -10557,9 +10475,10 @@ var ModelProviderIdSchema = _enum([
|
|
|
10557
10475
|
* are per upstream. A persisted custom-model row written when this field was a
|
|
10558
10476
|
* string still parses — the string is lifted into `{ weights }`.
|
|
10559
10477
|
*
|
|
10560
|
-
* Every licence surface (the
|
|
10561
|
-
*
|
|
10562
|
-
*
|
|
10478
|
+
* Every machine licence surface (`THIRD_PARTY_MODELS.md`, the Hugging Face
|
|
10479
|
+
* model table) is GENERATED from these records by
|
|
10480
|
+
* `scripts/gen-model-licenses.ts` (D661). Operator-facing docs live in
|
|
10481
|
+
* `camstack/docs` → `/docs/detection/models`.
|
|
10563
10482
|
*/
|
|
10564
10483
|
var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
|
|
10565
10484
|
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
@@ -10592,6 +10511,46 @@ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { wei
|
|
|
10592
10511
|
/** True when the weights or their data forbid commercial use. */
|
|
10593
10512
|
nonCommercial: boolean().optional()
|
|
10594
10513
|
}));
|
|
10514
|
+
/**
|
|
10515
|
+
* One step a ROOT detector can supply from its own output (D730) — see
|
|
10516
|
+
* `ModelCatalogEntry.suppliesSteps`.
|
|
10517
|
+
*
|
|
10518
|
+
* - `label`: the root's raw class IS the step's answer. `labelToEffect` maps
|
|
10519
|
+
* the root's label to the step's vocabulary; `uncovered` is the vocabulary
|
|
10520
|
+
* the root cannot produce (shown as unavailable, never a blocker).
|
|
10521
|
+
* - `crop`: the root draws the box the step would have found (`license_plate`
|
|
10522
|
+
* for plate-detection, `face` for face-detection). The box becomes a detail
|
|
10523
|
+
* child of the `parentMacro` detection that contains it — never a track.
|
|
10524
|
+
* - `mask`: the root's decode emits an instance mask with every detection (a
|
|
10525
|
+
* YOLO26-seg root, D737) — the `segmentation` step's result, carried on the
|
|
10526
|
+
* detection in the shape that step produces. Derived from the model's
|
|
10527
|
+
* postprocessor on read, never stored by an import.
|
|
10528
|
+
*/
|
|
10529
|
+
var RootStepSupplySchema = discriminatedUnion("kind", [
|
|
10530
|
+
object({
|
|
10531
|
+
kind: literal("label"),
|
|
10532
|
+
stepId: _enum(LABEL_SUPPLY_STEP_IDS),
|
|
10533
|
+
labelToEffect: record(string(), string()),
|
|
10534
|
+
uncovered: array(string()).readonly()
|
|
10535
|
+
}),
|
|
10536
|
+
object({
|
|
10537
|
+
kind: literal("crop"),
|
|
10538
|
+
stepId: _enum(CROP_SUPPLY_STEP_IDS),
|
|
10539
|
+
rootLabels: array(string()).readonly(),
|
|
10540
|
+
parentMacro: _enum(["vehicle", "person"]),
|
|
10541
|
+
childClass: _enum(["plate", "face"]),
|
|
10542
|
+
/**
|
|
10543
|
+
* The root's smaller input side, stamped on read by `rootSuppliesOf` from
|
|
10544
|
+
* the model entry — what a step's `minRootInputPx` gate judges. Never
|
|
10545
|
+
* stored by an import.
|
|
10546
|
+
*/
|
|
10547
|
+
rootInputPx: number().int().positive().optional()
|
|
10548
|
+
}),
|
|
10549
|
+
object({
|
|
10550
|
+
kind: literal("mask"),
|
|
10551
|
+
stepId: _enum(MASK_SUPPLY_STEP_IDS)
|
|
10552
|
+
})
|
|
10553
|
+
]);
|
|
10595
10554
|
var ModelCatalogEntrySchema = object({
|
|
10596
10555
|
id: string(),
|
|
10597
10556
|
name: string(),
|
|
@@ -10657,6 +10616,35 @@ var ModelCatalogEntrySchema = object({
|
|
|
10657
10616
|
*/
|
|
10658
10617
|
defaultConfidence: number().min(0).max(1).optional(),
|
|
10659
10618
|
/**
|
|
10619
|
+
* Per-MODEL default of the per-class confidence floors (`minConfidence<Macro>`
|
|
10620
|
+
* on a detector step), keyed by macro class: `{ person: 0.45 }`. A class
|
|
10621
|
+
* absent here runs at the step slider's global default. The same row shape
|
|
10622
|
+
* the floor measurement produces (`model-floors.json`: modelId → floors), so
|
|
10623
|
+
* a measured table is copied in as-is. An operator override is bound to the
|
|
10624
|
+
* model it was made for and does not survive a model change (D738). A custom
|
|
10625
|
+
* / imported model may declare it; without it, the global defaults apply.
|
|
10626
|
+
*/
|
|
10627
|
+
defaultClassFloors: record(string(), number().min(0).max(1)).optional(),
|
|
10628
|
+
/**
|
|
10629
|
+
* The inference precision this model's OpenVINO IR needs on an Intel GPU.
|
|
10630
|
+
* Absent ⇒ the device default (f16 on a GPU). Set to `'f32'` for an FP32 IR
|
|
10631
|
+
* that produces invalid output at f16 — measured 2026-10-07 for YOLO26
|
|
10632
|
+
* base IRs (`yolo26s`: 0 true / 146 false positives; `yolo26m*`: NaN
|
|
10633
|
+
* scores) on the Core Ultra iGPU and the N100. Threaded to the pool as
|
|
10634
|
+
* `PoolModelConfig.gpuInferencePrecision`; the pool sets
|
|
10635
|
+
* INFERENCE_PRECISION_HINT on a GPU only. Costs ~60% latency on the GPU.
|
|
10636
|
+
*/
|
|
10637
|
+
gpuInferencePrecision: _enum(["f32", "f16"]).optional(),
|
|
10638
|
+
/**
|
|
10639
|
+
* ROOT detectors only: per supplied step (`animal-classifier`, …), the
|
|
10640
|
+
* minimum DETECTION score from which this root's own fine label (species,
|
|
10641
|
+
* vehicle type — D736 root supply) is taken (D741). Below it the detection
|
|
10642
|
+
* keeps its macro class without the fine label. `1` = never take it from
|
|
10643
|
+
* this root. Absent ⇒ no floor beyond the macro floor. The supplied step's
|
|
10644
|
+
* `minConfidence` slider, bound to the ROOT model, overrides it.
|
|
10645
|
+
*/
|
|
10646
|
+
suppliedLabelFloors: record(string(), number().min(0).max(1)).optional(),
|
|
10647
|
+
/**
|
|
10660
10648
|
* When true, the executor produces a landmark-aligned crop (similarity warp
|
|
10661
10649
|
* onto the canonical template) before this step runs, instead of a plain
|
|
10662
10650
|
* axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
|
|
@@ -10718,6 +10706,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
10718
10706
|
*/
|
|
10719
10707
|
classMap: DetectionCatalogClassMapSchema.optional(),
|
|
10720
10708
|
/**
|
|
10709
|
+
* The pipeline steps this ROOT detector can supply from its own output
|
|
10710
|
+
* (D730): its labels name a step's effect (`deer` for `animal-classifier`)
|
|
10711
|
+
* or draw a step's box (`license_plate` for `plate-detection`). Computed at
|
|
10712
|
+
* import from THIS model's labelmap by `deriveRootSupplies`, never written
|
|
10713
|
+
* by hand and never assumed per family; a row without it is derived on read
|
|
10714
|
+
* (`rootSuppliesOf`). Whether a camera uses it is the step's own
|
|
10715
|
+
* `useRootModel` flag (`resolveRootSupply`).
|
|
10716
|
+
*/
|
|
10717
|
+
suppliesSteps: array(RootStepSupplySchema).readonly().optional(),
|
|
10718
|
+
/**
|
|
10721
10719
|
* The model's CLIP feature-space contract (text encoder, dimension, vector
|
|
10722
10720
|
* index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
|
|
10723
10721
|
* CLIP image encoder; its absence is what "not a CLIP model" means to the
|
|
@@ -12356,13 +12354,6 @@ var BrokerRtspClientSchema = object({
|
|
|
12356
12354
|
lastRtpAt: number(),
|
|
12357
12355
|
bytesSent: number()
|
|
12358
12356
|
});
|
|
12359
|
-
var BrokerDecodedClientSchema = object({
|
|
12360
|
-
tag: string(),
|
|
12361
|
-
subscribedAt: number(),
|
|
12362
|
-
maxFps: number(),
|
|
12363
|
-
framesDelivered: number(),
|
|
12364
|
-
framesDropped: number()
|
|
12365
|
-
});
|
|
12366
12357
|
var BrokerAudioClientSchema = object({
|
|
12367
12358
|
tag: string(),
|
|
12368
12359
|
subscribedAt: number(),
|
|
@@ -12454,7 +12445,6 @@ var BrokerEncodedClientSchema = object({
|
|
|
12454
12445
|
});
|
|
12455
12446
|
var BrokerClientsSchema = object({
|
|
12456
12447
|
rtsp: array(BrokerRtspClientSchema).readonly(),
|
|
12457
|
-
decoded: array(BrokerDecodedClientSchema).readonly(),
|
|
12458
12448
|
audio: array(BrokerAudioClientSchema).readonly(),
|
|
12459
12449
|
encoded: array(BrokerEncodedClientSchema).readonly(),
|
|
12460
12450
|
pipeClients: number(),
|
|
@@ -12823,11 +12813,7 @@ method(object({
|
|
|
12823
12813
|
auth: "admin"
|
|
12824
12814
|
}), method(object({ brokerId: string() }), BrokerClientsSchema), method(object({
|
|
12825
12815
|
brokerId: string(),
|
|
12826
|
-
channel: _enum([
|
|
12827
|
-
"rtsp",
|
|
12828
|
-
"decoded",
|
|
12829
|
-
"audio"
|
|
12830
|
-
]),
|
|
12816
|
+
channel: _enum(["rtsp", "audio"]),
|
|
12831
12817
|
handle: string()
|
|
12832
12818
|
}), object({ killed: boolean() }), {
|
|
12833
12819
|
kind: "mutation",
|
|
@@ -12862,10 +12848,7 @@ method(object({
|
|
|
12862
12848
|
}), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
|
|
12863
12849
|
subscriptionId: string(),
|
|
12864
12850
|
maxCount: number().int().positive().default(8)
|
|
12865
|
-
}), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(
|
|
12866
|
-
subscriptionId: string(),
|
|
12867
|
-
maxCount: number().int().positive().default(4)
|
|
12868
|
-
}), array(FrameHandleSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
|
|
12851
|
+
}), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
|
|
12869
12852
|
brokerId: string(),
|
|
12870
12853
|
seconds: number().min(0).max(30)
|
|
12871
12854
|
}), _void(), {
|
|
@@ -14082,72 +14065,18 @@ var DEFAULT_DECODER_HWACCEL_CONFIG = {
|
|
|
14082
14065
|
hwaccel: "auto",
|
|
14083
14066
|
probedBestHwaccel: ""
|
|
14084
14067
|
};
|
|
14085
|
-
/**
|
|
14086
|
-
* shm ring usage stats for a `frameSink: 'shm'` decoder session —
|
|
14087
|
-
* exposed via `decoder.getShmStats` so downstream consumers can
|
|
14088
|
-
* observe ring pressure (slot count, byte budget, hit/miss ratio).
|
|
14089
|
-
*/
|
|
14090
|
-
var ShmRingStatsSchema = object({
|
|
14091
|
-
sessionId: string(),
|
|
14092
|
-
slotCount: number().int(),
|
|
14093
|
-
slotByteLength: number().int(),
|
|
14094
|
-
segmentBytes: number().int(),
|
|
14095
|
-
budgetMb: number().int(),
|
|
14096
|
-
framesWritten: number().int(),
|
|
14097
|
-
getFrameHits: number().int(),
|
|
14098
|
-
getFrameMisses: number().int()
|
|
14099
|
-
});
|
|
14100
14068
|
var decoderCapability = {
|
|
14101
14069
|
name: "decoder",
|
|
14102
14070
|
scope: "system",
|
|
14103
14071
|
mode: "singleton",
|
|
14104
14072
|
preferredProvider: "decoder-nodeav",
|
|
14105
14073
|
methods: {
|
|
14106
|
-
supportsCodec: method(object({ codec: string() }), boolean()),
|
|
14107
14074
|
getInfo: method(_void(), object({
|
|
14108
14075
|
id: string(),
|
|
14109
14076
|
name: string(),
|
|
14110
|
-
isPullMode: boolean().optional(),
|
|
14111
|
-
priority: number().optional(),
|
|
14112
14077
|
hwaccel: string().optional(),
|
|
14113
14078
|
probedBestHwaccel: string().optional()
|
|
14114
14079
|
})),
|
|
14115
|
-
createSession: method(DecoderSessionConfigSchema, object({
|
|
14116
|
-
sessionId: string(),
|
|
14117
|
-
nodeId: string()
|
|
14118
|
-
})),
|
|
14119
|
-
destroySession: method(object({ sessionId: string() }), _void()),
|
|
14120
|
-
pushPacket: method(object({
|
|
14121
|
-
sessionId: string(),
|
|
14122
|
-
packet: EncodedPacketSchema
|
|
14123
|
-
}), _void()),
|
|
14124
|
-
openStream: method(object({
|
|
14125
|
-
sessionId: string(),
|
|
14126
|
-
url: string()
|
|
14127
|
-
}), _void()),
|
|
14128
|
-
pullFrames: method(object({
|
|
14129
|
-
sessionId: string(),
|
|
14130
|
-
maxCount: number().default(1),
|
|
14131
|
-
waitMs: number().optional()
|
|
14132
|
-
}), array(DecodedFrameSchema)),
|
|
14133
|
-
pullHandles: method(object({
|
|
14134
|
-
sessionId: string(),
|
|
14135
|
-
maxCount: number().default(1),
|
|
14136
|
-
waitMs: number().optional()
|
|
14137
|
-
}), array(FrameHandleSchema)),
|
|
14138
|
-
getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
|
|
14139
|
-
getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
|
|
14140
|
-
updateConfig: method(object({
|
|
14141
|
-
sessionId: string(),
|
|
14142
|
-
config: DecoderSessionConfigSchema.partial()
|
|
14143
|
-
}), _void()),
|
|
14144
|
-
getStats: method(object({ sessionId: string() }), DecoderStatsSchema),
|
|
14145
|
-
listActiveSessions: method(_void(), array(object({
|
|
14146
|
-
sessionId: string(),
|
|
14147
|
-
codec: string(),
|
|
14148
|
-
outputFormat: string(),
|
|
14149
|
-
createdAtMs: number()
|
|
14150
|
-
})).readonly()),
|
|
14151
14080
|
/**
|
|
14152
14081
|
* Re-run the host platform-probe for the decoder's hwaccel backend
|
|
14153
14082
|
* and write the detected value into the addon's own global
|
|
@@ -14218,7 +14147,7 @@ DeviceType.Camera;
|
|
|
14218
14147
|
* This matters here more than anywhere else in the repo, because every provider
|
|
14219
14148
|
* that knows a device's host knows its PASSWORD too: `{ host, port, username,
|
|
14220
14149
|
* password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
|
|
14221
|
-
* and `http://admin:hunter2@192.168.
|
|
14150
|
+
* and `http://admin:hunter2@192.168.60.139/` is a URL a browser accepts. The
|
|
14222
14151
|
* camera's own page will ask for its own login. That is correct, and pre-
|
|
14223
14152
|
* filling it is the operator's business, not ours.
|
|
14224
14153
|
*
|
|
@@ -16006,6 +15935,12 @@ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({
|
|
|
16006
15935
|
kind: "mutation",
|
|
16007
15936
|
auth: "admin"
|
|
16008
15937
|
});
|
|
15938
|
+
var HUB_ADDRESS_ROLES = [
|
|
15939
|
+
"appConnections",
|
|
15940
|
+
"notificationTaps",
|
|
15941
|
+
"internal",
|
|
15942
|
+
"publicEndpoint"
|
|
15943
|
+
];
|
|
16009
15944
|
/**
|
|
16010
15945
|
* Transport bounds for AI capability methods.
|
|
16011
15946
|
*
|
|
@@ -17202,7 +17137,7 @@ method(object({
|
|
|
17202
17137
|
timeoutMs: 6e5
|
|
17203
17138
|
});
|
|
17204
17139
|
method(object({
|
|
17205
|
-
|
|
17140
|
+
targetNodeId: string(),
|
|
17206
17141
|
modelId: string(),
|
|
17207
17142
|
format: _enum(MODEL_FORMATS),
|
|
17208
17143
|
entry: ModelCatalogEntrySchema
|
|
@@ -18570,6 +18505,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
18570
18505
|
"stream-offline",
|
|
18571
18506
|
"node-online",
|
|
18572
18507
|
"node-offline",
|
|
18508
|
+
"node-restarting",
|
|
18509
|
+
"node-updating",
|
|
18573
18510
|
"node-inference-unavailable",
|
|
18574
18511
|
"detection-blind",
|
|
18575
18512
|
"addon-crash-loop",
|
|
@@ -19668,8 +19605,9 @@ var NcRuleSchema = NcRuleInputSchema.extend({
|
|
|
19668
19605
|
disabledTargetIds: array(string()).default([])
|
|
19669
19606
|
});
|
|
19670
19607
|
/**
|
|
19671
|
-
* The state-summary SECTIONS (D647 / D698): the D637 kind groups that
|
|
19672
|
-
* still-true state. `
|
|
19608
|
+
* The state-summary SECTIONS (D647 / D698 / D708): the D637 kind groups that
|
|
19609
|
+
* report a still-true state. `addons` is the crash-looped addons only and
|
|
19610
|
+
* `addon-updates` the addons that are behind (D708 split them). `backups-exports` and `alarm` are not sections — a completed
|
|
19673
19611
|
* job is not an open state, and the alarm stays instant.
|
|
19674
19612
|
*/
|
|
19675
19613
|
var NcStateSummarySectionIdSchema = _enum([
|
|
@@ -19681,6 +19619,7 @@ var NcStateSummarySectionIdSchema = _enum([
|
|
|
19681
19619
|
"detection-blind",
|
|
19682
19620
|
"nodes",
|
|
19683
19621
|
"addons",
|
|
19622
|
+
"addon-updates",
|
|
19684
19623
|
"updates"
|
|
19685
19624
|
]);
|
|
19686
19625
|
/** One section of a previewed state summary. `unavailable` = its state could not be read. */
|
|
@@ -20161,7 +20100,25 @@ var NcAlarmModeDelaysListSchema = array(object({
|
|
|
20161
20100
|
* When this mode may be armed. Absent / null = always allowed (same as a
|
|
20162
20101
|
* rule with no `schedule`). Evaluated with {@link isScheduleActive}.
|
|
20163
20102
|
*/
|
|
20164
|
-
schedule: NcScheduleSchema.optional()
|
|
20103
|
+
schedule: NcScheduleSchema.optional(),
|
|
20104
|
+
/**
|
|
20105
|
+
* D706: while this mode's ENTRY DELAY runs, do the alarm rules' own pushes go out (as an
|
|
20106
|
+
* intrusion naming the locations)? Absent = false — the entry-delay notification is the
|
|
20107
|
+
* only one, and the matches ride the triggered notification's mosaic (D705).
|
|
20108
|
+
*/
|
|
20109
|
+
notifyDuringEntryDelay: boolean().optional(),
|
|
20110
|
+
/**
|
|
20111
|
+
* D706: notify "watched again" when a sensor this arm bypassed closes and opens again.
|
|
20112
|
+
* Absent = false (logged, not sent). Even when true it is not sent when that same sensor
|
|
20113
|
+
* is the one that starts the entry delay — the entry-delay notification already names it.
|
|
20114
|
+
*/
|
|
20115
|
+
notifySensorRewatched: boolean().optional(),
|
|
20116
|
+
/**
|
|
20117
|
+
* D735: notify "ignored" when a sensor this arm bypassed really MOVED while armed (its own
|
|
20118
|
+
* change time is after the arm) and set nothing off. Absent = false (logged, not sent). A
|
|
20119
|
+
* re-sync, a recovery from `unavailable` or a duplicate is never announced, switch or not.
|
|
20120
|
+
*/
|
|
20121
|
+
notifySensorIgnored: boolean().optional()
|
|
20165
20122
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
|
|
20166
20123
|
var NcAlarmModeDelaysEditListSchema = array(object({
|
|
20167
20124
|
mode: AlarmArmModeSchema,
|
|
@@ -20170,7 +20127,13 @@ var NcAlarmModeDelaysEditListSchema = array(object({
|
|
|
20170
20127
|
triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
|
|
20171
20128
|
postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
|
|
20172
20129
|
/** Set a schedule, or `null` to clear (mode always allowed again). */
|
|
20173
|
-
schedule: NcScheduleSchema.nullable().optional()
|
|
20130
|
+
schedule: NcScheduleSchema.nullable().optional(),
|
|
20131
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20132
|
+
notifyDuringEntryDelay: boolean().nullable().optional(),
|
|
20133
|
+
/** D706: set the switch, or `null` to clear it (back to the default, false). */
|
|
20134
|
+
notifySensorRewatched: boolean().nullable().optional(),
|
|
20135
|
+
/** D735: set the switch, or `null` to clear it (back to the default, false). */
|
|
20136
|
+
notifySensorIgnored: boolean().nullable().optional()
|
|
20174
20137
|
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
|
|
20175
20138
|
/** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
|
|
20176
20139
|
var NcAlarmNonBlockingEditSchema = object({
|
|
@@ -21131,45 +21094,25 @@ Object.freeze(Object.fromEntries([{
|
|
|
21131
21094
|
label: "Face recognition model",
|
|
21132
21095
|
defaultModelId: "auraface-r100",
|
|
21133
21096
|
indexName: "the enrolled face gallery",
|
|
21134
|
-
options: [
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
},
|
|
21139
|
-
{
|
|
21140
|
-
id: "arcface-r100",
|
|
21141
|
-
label: "ArcFace (ResNet34)"
|
|
21142
|
-
},
|
|
21143
|
-
{
|
|
21144
|
-
id: "inception-resnet-v1",
|
|
21145
|
-
label: "Inception ResNet V1"
|
|
21146
|
-
}
|
|
21147
|
-
]
|
|
21097
|
+
options: [{
|
|
21098
|
+
id: "auraface-r100",
|
|
21099
|
+
label: "AuraFace R100"
|
|
21100
|
+
}]
|
|
21148
21101
|
}, {
|
|
21149
21102
|
stepId: "clip-embedding",
|
|
21150
21103
|
label: "Semantic search model",
|
|
21151
|
-
defaultModelId: "
|
|
21104
|
+
defaultModelId: "siglip2-b16-224",
|
|
21152
21105
|
indexName: "the object semantic-search index",
|
|
21153
|
-
options: [
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21157
|
-
},
|
|
21158
|
-
{
|
|
21159
|
-
id: "mobileclip-s2",
|
|
21160
|
-
label: "MobileCLIP S2"
|
|
21161
|
-
},
|
|
21162
|
-
{
|
|
21163
|
-
id: "siglip2-b16-224",
|
|
21164
|
-
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21165
|
-
}
|
|
21166
|
-
]
|
|
21106
|
+
options: [{
|
|
21107
|
+
id: "siglip2-b16-224",
|
|
21108
|
+
label: "SigLIP2 B/16 (Apache-2.0)"
|
|
21109
|
+
}]
|
|
21167
21110
|
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
21168
21111
|
var CLUSTER_STEP_SETTING_FIELDS = [{
|
|
21169
21112
|
stepId: "face-embedding",
|
|
21170
21113
|
key: "minLandmarkFaceSize",
|
|
21171
|
-
label: "Min face size
|
|
21172
|
-
description: "
|
|
21114
|
+
label: "Min face size to embed (detection-frame px)",
|
|
21115
|
+
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.",
|
|
21173
21116
|
type: "slider",
|
|
21174
21117
|
min: 0,
|
|
21175
21118
|
max: 64,
|
|
@@ -23089,6 +23032,34 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
23089
23032
|
timestamp: number()
|
|
23090
23033
|
});
|
|
23091
23034
|
/**
|
|
23035
|
+
* A root model's boxes standing in for one crop-kind step's own inference
|
|
23036
|
+
* (D730) — see `runPipeline.suppliedRoot`.
|
|
23037
|
+
*/
|
|
23038
|
+
var SuppliedRootOutputSchema = object({
|
|
23039
|
+
stepId: string(),
|
|
23040
|
+
/** The root model that drew the boxes — stamped on the result. */
|
|
23041
|
+
modelId: string(),
|
|
23042
|
+
detections: array(object({
|
|
23043
|
+
/** `[x1, y1, x2, y2]` in the pixels of the call's image. */
|
|
23044
|
+
bbox: tuple([
|
|
23045
|
+
number(),
|
|
23046
|
+
number(),
|
|
23047
|
+
number(),
|
|
23048
|
+
number()
|
|
23049
|
+
]),
|
|
23050
|
+
score: number(),
|
|
23051
|
+
/**
|
|
23052
|
+
* Five facial landmarks in the same pixels — a face the step's own
|
|
23053
|
+
* detector already re-found inside a root face box (D730), so its
|
|
23054
|
+
* embedder child aligns it exactly as it aligns any detected face.
|
|
23055
|
+
*/
|
|
23056
|
+
landmarks: array(object({
|
|
23057
|
+
x: number(),
|
|
23058
|
+
y: number()
|
|
23059
|
+
})).optional()
|
|
23060
|
+
})).min(1)
|
|
23061
|
+
});
|
|
23062
|
+
/**
|
|
23092
23063
|
* Reference to the frame's retained NATIVE surface + the parent crop's placement
|
|
23093
23064
|
* within the frame, so the executor can re-cut a leaf child ROI at native
|
|
23094
23065
|
* resolution on the detail plane. See the `runPipeline` `nativeCropRef` field.
|
|
@@ -23136,7 +23107,7 @@ var FrameRefSchema = object({
|
|
|
23136
23107
|
timestamp: number(),
|
|
23137
23108
|
capturedAt: number().optional()
|
|
23138
23109
|
});
|
|
23139
|
-
var ModelFormatSchema$
|
|
23110
|
+
var ModelFormatSchema$2 = _enum([
|
|
23140
23111
|
"onnx",
|
|
23141
23112
|
"coreml",
|
|
23142
23113
|
"openvino",
|
|
@@ -23154,7 +23125,7 @@ var PipelineSlotSchema = _enum([
|
|
|
23154
23125
|
var PipelineEngineChoiceSchema = object({
|
|
23155
23126
|
runtime: literal("python"),
|
|
23156
23127
|
backend: string(),
|
|
23157
|
-
format: ModelFormatSchema$
|
|
23128
|
+
format: ModelFormatSchema$2,
|
|
23158
23129
|
device: string().optional()
|
|
23159
23130
|
});
|
|
23160
23131
|
/**
|
|
@@ -23193,32 +23164,15 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
23193
23164
|
group: string().optional(),
|
|
23194
23165
|
settings: record(string(), unknown()).optional()
|
|
23195
23166
|
}));
|
|
23196
|
-
var PipelineTemplateStepSchema = lazy(() => object({
|
|
23197
|
-
addonId: string(),
|
|
23198
|
-
enabled: boolean(),
|
|
23199
|
-
modelId: string(),
|
|
23200
|
-
children: array(PipelineTemplateStepSchema).readonly(),
|
|
23201
|
-
settings: record(string(), unknown()).optional()
|
|
23202
|
-
}));
|
|
23203
|
-
var PipelineTemplateSchema$1 = object({
|
|
23204
|
-
id: string(),
|
|
23205
|
-
name: string(),
|
|
23206
|
-
createdAt: string(),
|
|
23207
|
-
updatedAt: string(),
|
|
23208
|
-
engine: PipelineEngineChoiceSchema,
|
|
23209
|
-
steps: array(PipelineTemplateStepSchema).readonly()
|
|
23210
|
-
});
|
|
23211
23167
|
var PipelineModelOptionSchema = object({
|
|
23212
23168
|
id: string(),
|
|
23213
23169
|
name: string(),
|
|
23214
|
-
formats: record(string(),
|
|
23215
|
-
downloaded: boolean(),
|
|
23216
|
-
sizeMB: number()
|
|
23217
|
-
})),
|
|
23170
|
+
formats: record(string(), PipelineModelFormatStatusSchema),
|
|
23218
23171
|
group: ModelVariantGroupSchema.optional(),
|
|
23219
23172
|
legacy: boolean().optional(),
|
|
23220
23173
|
provider: ModelProviderIdSchema.optional(),
|
|
23221
|
-
defaultConfidence: number().optional()
|
|
23174
|
+
defaultConfidence: number().optional(),
|
|
23175
|
+
suppliesSteps: array(RootStepSupplySchema).readonly().optional()
|
|
23222
23176
|
});
|
|
23223
23177
|
var ConfigFieldBridge = custom();
|
|
23224
23178
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -23380,6 +23334,22 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23380
23334
|
}), {
|
|
23381
23335
|
kind: "mutation",
|
|
23382
23336
|
auth: "admin"
|
|
23337
|
+
}), method(object({
|
|
23338
|
+
nodeId: string(),
|
|
23339
|
+
deviceIds: array(number()).readonly().optional(),
|
|
23340
|
+
dryRun: boolean()
|
|
23341
|
+
}), object({
|
|
23342
|
+
entries: array(object({
|
|
23343
|
+
deviceId: number(),
|
|
23344
|
+
keys: array(string()).readonly()
|
|
23345
|
+
})).readonly(),
|
|
23346
|
+
refused: array(object({
|
|
23347
|
+
deviceId: number(),
|
|
23348
|
+
reason: string()
|
|
23349
|
+
})).readonly()
|
|
23350
|
+
}), {
|
|
23351
|
+
kind: "mutation",
|
|
23352
|
+
auth: "admin"
|
|
23383
23353
|
}), method(object({ nodeId: string() }), object({ unhealthy: array(object({
|
|
23384
23354
|
/** `<backend>:<device>`, e.g. `openvino:gpu`. */
|
|
23385
23355
|
deviceKey: string(),
|
|
@@ -23405,22 +23375,14 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23405
23375
|
}), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({
|
|
23406
23376
|
steps: array(PipelineStepInputSchema),
|
|
23407
23377
|
deviceKey: string().optional()
|
|
23408
|
-
}), PipelineValidationResultSchema), method(_void(),
|
|
23409
|
-
name: string(),
|
|
23410
|
-
steps: array(PipelineTemplateStepSchema).readonly(),
|
|
23411
|
-
engine: PipelineEngineChoiceSchema
|
|
23412
|
-
}), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({
|
|
23413
|
-
id: string(),
|
|
23414
|
-
name: string().optional(),
|
|
23415
|
-
steps: array(PipelineTemplateStepSchema).readonly().optional()
|
|
23416
|
-
}), PipelineTemplateSchema$1, { kind: "mutation" }), method(object({ id: string() }), _void(), { kind: "mutation" }), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
|
|
23378
|
+
}), PipelineValidationResultSchema), method(_void(), InferenceCapabilitiesBridge), method(object({ addonId: string() }), ModelAvailabilityListBridge), method(object({
|
|
23417
23379
|
addonId: string(),
|
|
23418
23380
|
modelId: string(),
|
|
23419
|
-
format: ModelFormatSchema$
|
|
23381
|
+
format: ModelFormatSchema$2
|
|
23420
23382
|
}), DownloadModelResultSchema, { kind: "mutation" }), method(object({
|
|
23421
23383
|
addonId: string(),
|
|
23422
23384
|
modelId: string(),
|
|
23423
|
-
format: ModelFormatSchema$
|
|
23385
|
+
format: ModelFormatSchema$2
|
|
23424
23386
|
}), object({ success: literal(true) }), { kind: "mutation" }), method(object({
|
|
23425
23387
|
steps: array(PipelineStepInputSchema).min(1),
|
|
23426
23388
|
frame: FrameInputSchema.optional(),
|
|
@@ -23486,7 +23448,16 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23486
23448
|
* `device-not-servable`), and nothing reaches the camera's live
|
|
23487
23449
|
* trace stream. Absent ⇒ unchanged behaviour.
|
|
23488
23450
|
*/
|
|
23489
|
-
replay: literal(true).optional()
|
|
23451
|
+
replay: literal(true).optional(),
|
|
23452
|
+
/**
|
|
23453
|
+
* Root-supplied detections for the tree's ROOT step (D730). Set by
|
|
23454
|
+
* `runDetailSubtree` when the camera's root model supplies a crop-kind
|
|
23455
|
+
* step (a Frigate+ `license_plate` for `plate-detection`): the step is
|
|
23456
|
+
* NOT inferred, these boxes are its output, and its children (the plate
|
|
23457
|
+
* reader) run on them through their own crop policy. Boxes are in the
|
|
23458
|
+
* PIXELS of the image this call carries. Absent ⇒ the root step runs.
|
|
23459
|
+
*/
|
|
23460
|
+
suppliedRoot: SuppliedRootOutputSchema.optional()
|
|
23490
23461
|
}), PipelineRunResultBridge, { kind: "mutation" }), method(object({
|
|
23491
23462
|
steps: array(PipelineStepInputSchema).min(1),
|
|
23492
23463
|
frames: array(FrameInputSchema).min(1).max(255),
|
|
@@ -23568,6 +23539,375 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
23568
23539
|
filename: string().optional(),
|
|
23569
23540
|
settings: record(string(), unknown()).optional()
|
|
23570
23541
|
}), AudioTestResultSchema, { kind: "mutation" }), method(_void(), ConfigUISchemaNullableBridge);
|
|
23542
|
+
/**
|
|
23543
|
+
* The wire shapes of the Auto tune plan and apply (D745), used by the
|
|
23544
|
+
* `pipeline-orchestrator` cap. The recommender's own types
|
|
23545
|
+
* (`recommend.ts`) are the TS face of the same data; these
|
|
23546
|
+
* schemas are what crosses the cap boundary.
|
|
23547
|
+
*/
|
|
23548
|
+
var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
|
|
23549
|
+
/** The operator's range for it. */
|
|
23550
|
+
var MIN_SESSIONS_PER_DEVICE_RANGE = {
|
|
23551
|
+
min: 1,
|
|
23552
|
+
max: 8
|
|
23553
|
+
};
|
|
23554
|
+
var HardwareClassIdSchema = _enum([
|
|
23555
|
+
"intel-core-ultra",
|
|
23556
|
+
"intel-n-series",
|
|
23557
|
+
"apple-m-base",
|
|
23558
|
+
"apple-m-pro",
|
|
23559
|
+
"unknown"
|
|
23560
|
+
]);
|
|
23561
|
+
var AcceleratorKindSchema = _enum([
|
|
23562
|
+
"intel-npu",
|
|
23563
|
+
"intel-igpu",
|
|
23564
|
+
"x86-cpu",
|
|
23565
|
+
"arm-cpu",
|
|
23566
|
+
"apple-ane",
|
|
23567
|
+
"coral"
|
|
23568
|
+
]);
|
|
23569
|
+
var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
23570
|
+
object({
|
|
23571
|
+
kind: literal("capacity"),
|
|
23572
|
+
sessions: number(),
|
|
23573
|
+
minSessions: number()
|
|
23574
|
+
}),
|
|
23575
|
+
object({
|
|
23576
|
+
kind: literal("tie-faster"),
|
|
23577
|
+
over: string(),
|
|
23578
|
+
recallDelta: number()
|
|
23579
|
+
}),
|
|
23580
|
+
object({
|
|
23581
|
+
kind: literal("stepped-down"),
|
|
23582
|
+
from: string(),
|
|
23583
|
+
to: string(),
|
|
23584
|
+
limitedBy: string()
|
|
23585
|
+
}),
|
|
23586
|
+
object({
|
|
23587
|
+
kind: literal("no-fit-lightest"),
|
|
23588
|
+
sessions: number(),
|
|
23589
|
+
minSessions: number()
|
|
23590
|
+
}),
|
|
23591
|
+
object({
|
|
23592
|
+
kind: literal("derived-latency"),
|
|
23593
|
+
modelId: string()
|
|
23594
|
+
}),
|
|
23595
|
+
object({ kind: literal("cpu-device") }),
|
|
23596
|
+
object({ kind: literal("coral-untouched") }),
|
|
23597
|
+
object({ kind: literal("device-disabled") }),
|
|
23598
|
+
object({ kind: literal("device-unavailable") }),
|
|
23599
|
+
object({ kind: literal("node-offline") }),
|
|
23600
|
+
object({ kind: literal("no-hardware-facts") }),
|
|
23601
|
+
object({
|
|
23602
|
+
kind: literal("no-measured-data"),
|
|
23603
|
+
hardwareClass: HardwareClassIdSchema
|
|
23604
|
+
}),
|
|
23605
|
+
object({
|
|
23606
|
+
kind: literal("no-family-build"),
|
|
23607
|
+
format: ModelFormatSchema$1
|
|
23608
|
+
})
|
|
23609
|
+
]);
|
|
23610
|
+
var DeviceModelChoiceSchema = object({
|
|
23611
|
+
modelId: string(),
|
|
23612
|
+
p50Ms: number(),
|
|
23613
|
+
fps: number(),
|
|
23614
|
+
measured: boolean(),
|
|
23615
|
+
/** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
|
|
23616
|
+
capacitySessions: number(),
|
|
23617
|
+
personRecall: number()
|
|
23618
|
+
});
|
|
23619
|
+
var DeviceRecommendationSchema = object({
|
|
23620
|
+
deviceKey: string(),
|
|
23621
|
+
accelerator: AcceleratorKindSchema.nullable(),
|
|
23622
|
+
enabled: boolean(),
|
|
23623
|
+
available: boolean(),
|
|
23624
|
+
chosen: DeviceModelChoiceSchema.nullable(),
|
|
23625
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
23626
|
+
});
|
|
23627
|
+
var NodeRecommendationSchema = object({
|
|
23628
|
+
nodeId: string(),
|
|
23629
|
+
reachable: boolean(),
|
|
23630
|
+
hardwareClass: HardwareClassIdSchema.nullable(),
|
|
23631
|
+
cpuModel: string().nullable(),
|
|
23632
|
+
skipped: boolean(),
|
|
23633
|
+
acceleratorModelId: string().nullable(),
|
|
23634
|
+
devices: array(DeviceRecommendationSchema).readonly(),
|
|
23635
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
23636
|
+
});
|
|
23637
|
+
var ModelPinChangeSchema = object({
|
|
23638
|
+
stepId: string(),
|
|
23639
|
+
before: string().nullable(),
|
|
23640
|
+
after: string().nullable()
|
|
23641
|
+
});
|
|
23642
|
+
var NodeDeviceChangeSchema = object({
|
|
23643
|
+
nodeId: string(),
|
|
23644
|
+
deviceKey: string(),
|
|
23645
|
+
madeExplicit: boolean(),
|
|
23646
|
+
modelPins: array(ModelPinChangeSchema).readonly(),
|
|
23647
|
+
thresholds: array(object({
|
|
23648
|
+
stepId: string(),
|
|
23649
|
+
keys: array(string()).readonly()
|
|
23650
|
+
})).readonly()
|
|
23651
|
+
});
|
|
23652
|
+
var CameraOverrideClearSchema = object({
|
|
23653
|
+
deviceId: number(),
|
|
23654
|
+
source: _enum(["step-override", "wholesale"]),
|
|
23655
|
+
nodeId: string(),
|
|
23656
|
+
deviceKey: string().nullable(),
|
|
23657
|
+
stepId: string(),
|
|
23658
|
+
modelIdBefore: string().nullable(),
|
|
23659
|
+
keys: array(string()).readonly()
|
|
23660
|
+
});
|
|
23661
|
+
var DetectorStoreClearSchema = object({
|
|
23662
|
+
deviceId: number(),
|
|
23663
|
+
keys: array(string()).readonly()
|
|
23664
|
+
});
|
|
23665
|
+
var AutoTuneWarningSchema = discriminatedUnion("kind", [
|
|
23666
|
+
object({
|
|
23667
|
+
kind: literal("detector-store-cluster-wide"),
|
|
23668
|
+
skippedNodes: array(string()).readonly()
|
|
23669
|
+
}),
|
|
23670
|
+
object({
|
|
23671
|
+
kind: literal("detector-store-unreadable"),
|
|
23672
|
+
error: string()
|
|
23673
|
+
}),
|
|
23674
|
+
object({
|
|
23675
|
+
kind: literal("coral-enabled"),
|
|
23676
|
+
nodeId: string(),
|
|
23677
|
+
deviceKey: string()
|
|
23678
|
+
})
|
|
23679
|
+
]);
|
|
23680
|
+
var AutoTuneOptionsSchema = object({
|
|
23681
|
+
/** Share of each accelerator's throughput kept free (0.3–0.5). */
|
|
23682
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23683
|
+
/**
|
|
23684
|
+
* The least number of parallel sessions every device must sustain at the
|
|
23685
|
+
* camera rate with the headroom kept free (D755). The most accurate model
|
|
23686
|
+
* that reaches it wins.
|
|
23687
|
+
*/
|
|
23688
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
23689
|
+
});
|
|
23690
|
+
var AutoTunePlanSchema = object({
|
|
23691
|
+
/** Hash of everything the apply will write — the apply refuses a different one. */
|
|
23692
|
+
planHash: string(),
|
|
23693
|
+
family: literal("yolov9"),
|
|
23694
|
+
headroom: number(),
|
|
23695
|
+
minSessionsPerDevice: number(),
|
|
23696
|
+
/** The detection rate a session is counted at (the fastest camera running the pipeline root). */
|
|
23697
|
+
cameraFps: number(),
|
|
23698
|
+
generatedAt: number(),
|
|
23699
|
+
nodes: array(NodeRecommendationSchema).readonly(),
|
|
23700
|
+
nodeChanges: array(NodeDeviceChangeSchema).readonly(),
|
|
23701
|
+
cameraClears: array(CameraOverrideClearSchema).readonly(),
|
|
23702
|
+
detectorStoreClears: array(DetectorStoreClearSchema).readonly(),
|
|
23703
|
+
warnings: array(AutoTuneWarningSchema).readonly()
|
|
23704
|
+
});
|
|
23705
|
+
var AutoTuneApplyInputSchema = object({
|
|
23706
|
+
planHash: string(),
|
|
23707
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23708
|
+
minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
|
|
23709
|
+
});
|
|
23710
|
+
var AutoTunePhaseResultSchema = object({
|
|
23711
|
+
phase: _enum([
|
|
23712
|
+
"download",
|
|
23713
|
+
"camera-overrides",
|
|
23714
|
+
"detector-thresholds",
|
|
23715
|
+
"node-models",
|
|
23716
|
+
"redispatch"
|
|
23717
|
+
]),
|
|
23718
|
+
ok: boolean(),
|
|
23719
|
+
detail: string()
|
|
23720
|
+
});
|
|
23721
|
+
var AutoTuneApplyResultSchema = discriminatedUnion("kind", [
|
|
23722
|
+
object({
|
|
23723
|
+
kind: literal("applied"),
|
|
23724
|
+
planHash: string(),
|
|
23725
|
+
phases: array(AutoTunePhaseResultSchema).readonly()
|
|
23726
|
+
}),
|
|
23727
|
+
object({
|
|
23728
|
+
kind: literal("aborted"),
|
|
23729
|
+
phases: array(AutoTunePhaseResultSchema).readonly()
|
|
23730
|
+
}),
|
|
23731
|
+
object({
|
|
23732
|
+
kind: literal("stale"),
|
|
23733
|
+
plan: AutoTunePlanSchema
|
|
23734
|
+
}),
|
|
23735
|
+
object({
|
|
23736
|
+
kind: literal("refused"),
|
|
23737
|
+
reason: _enum(["apply-in-progress"])
|
|
23738
|
+
})
|
|
23739
|
+
]);
|
|
23740
|
+
/**
|
|
23741
|
+
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
23742
|
+
* policy, the atomic share write, the observed placement statistics, the
|
|
23743
|
+
* hardware share suggestion and the stale device pins.
|
|
23744
|
+
*/
|
|
23745
|
+
/**
|
|
23746
|
+
* How a new detection session picks its node and device.
|
|
23747
|
+
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
23748
|
+
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
23749
|
+
* weight only bites once a node already holds a session.
|
|
23750
|
+
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
23751
|
+
* full or drowning node yields to a free one (D748).
|
|
23752
|
+
*/
|
|
23753
|
+
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
23754
|
+
var LoadBalancingSharesInputSchema = object({
|
|
23755
|
+
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
23756
|
+
nodes: record(string(), number().positive()).default({}),
|
|
23757
|
+
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
23758
|
+
devices: record(string(), record(string(), number().positive())).default({})
|
|
23759
|
+
});
|
|
23760
|
+
var PlacementStatsInputSchema = object({
|
|
23761
|
+
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
23762
|
+
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
23763
|
+
var PlacementDeviceStatsSchema = object({
|
|
23764
|
+
deviceKey: string(),
|
|
23765
|
+
/** Sessions running on this device now. */
|
|
23766
|
+
sessionsNow: number(),
|
|
23767
|
+
/** Sessions started on this device inside the window. */
|
|
23768
|
+
sessionsStarted: number(),
|
|
23769
|
+
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
23770
|
+
observedShare: number().nullable()
|
|
23771
|
+
});
|
|
23772
|
+
var PlacementNodeStatsSchema = object({
|
|
23773
|
+
nodeId: string(),
|
|
23774
|
+
sessionsNow: number(),
|
|
23775
|
+
sessionsStarted: number(),
|
|
23776
|
+
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
23777
|
+
observedShare: number().nullable(),
|
|
23778
|
+
/** Most sessions this node ran at once inside the window. */
|
|
23779
|
+
peakSessions: number(),
|
|
23780
|
+
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
23781
|
+
yieldsAway: number(),
|
|
23782
|
+
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
23783
|
+
});
|
|
23784
|
+
/** A camera's device pin that the session path cannot honour. */
|
|
23785
|
+
var StaleDevicePinSchema = object({
|
|
23786
|
+
deviceId: number(),
|
|
23787
|
+
deviceKey: string(),
|
|
23788
|
+
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
23789
|
+
reason: _enum(["disabled", "absent"])
|
|
23790
|
+
});
|
|
23791
|
+
var PlacementStatsSchema = object({
|
|
23792
|
+
policy: SessionPlacementPolicySchema,
|
|
23793
|
+
windowMs: number(),
|
|
23794
|
+
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
23795
|
+
observedForMs: number(),
|
|
23796
|
+
sessionsNow: number(),
|
|
23797
|
+
/** Most parallel sessions across the cluster inside the window. */
|
|
23798
|
+
clusterPeakSessions: number(),
|
|
23799
|
+
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
23800
|
+
stalePins: array(StaleDevicePinSchema).readonly()
|
|
23801
|
+
});
|
|
23802
|
+
/**
|
|
23803
|
+
* Where an automatic share's capacity comes from (D755).
|
|
23804
|
+
* - `hardware` — the static capacity of each device at its Auto tune model:
|
|
23805
|
+
* the measured latency table for the node's hardware class.
|
|
23806
|
+
* - `benchmark` — the node's own measured capacity (node benchmark results).
|
|
23807
|
+
* A node with no results falls back to `hardware`, and says so.
|
|
23808
|
+
*/
|
|
23809
|
+
var CapacitySourceSchema = _enum(["hardware", "benchmark"]);
|
|
23810
|
+
/** Why a node under the `benchmark` source was sized from hardware instead. */
|
|
23811
|
+
var CapacityFallbackSchema = _enum(["no-benchmark-results", "partial-benchmark-results"]);
|
|
23812
|
+
var SuggestSharesInputSchema = object({
|
|
23813
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
23814
|
+
/** Default `hardware`. */
|
|
23815
|
+
source: CapacitySourceSchema.default("hardware")
|
|
23816
|
+
});
|
|
23817
|
+
var SuggestedDeviceShareSchema = object({
|
|
23818
|
+
deviceKey: string(),
|
|
23819
|
+
/** Share of the node (0–100, rounded). */
|
|
23820
|
+
share: number(),
|
|
23821
|
+
modelId: string().nullable(),
|
|
23822
|
+
/** Inferences per second at that model, from the measured table; null when sized by a benchmark. */
|
|
23823
|
+
fps: number().nullable(),
|
|
23824
|
+
/** False for a table cell derived from another chip; true for a measured cell or a benchmark. */
|
|
23825
|
+
measured: boolean(),
|
|
23826
|
+
/**
|
|
23827
|
+
* Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋ from the table,
|
|
23828
|
+
* or the benchmark's measured max sessions. Never enforced.
|
|
23829
|
+
*/
|
|
23830
|
+
suggestedMaxSessions: number().nullable()
|
|
23831
|
+
});
|
|
23832
|
+
var SuggestedNodeShareSchema = object({
|
|
23833
|
+
nodeId: string(),
|
|
23834
|
+
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
23835
|
+
share: number().nullable(),
|
|
23836
|
+
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
23837
|
+
capacitySessions: number(),
|
|
23838
|
+
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
23839
|
+
skippedReason: string().nullable(),
|
|
23840
|
+
/** What this node's capacity was read from; null when the node is skipped. */
|
|
23841
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
23842
|
+
/** Set when the `benchmark` source was asked for and this node used `hardware`. */
|
|
23843
|
+
capacityFallback: CapacityFallbackSchema.nullable()
|
|
23844
|
+
});
|
|
23845
|
+
var SuggestSharesResultSchema = object({
|
|
23846
|
+
headroom: number(),
|
|
23847
|
+
/** The source asked for; each node says what it actually used. */
|
|
23848
|
+
source: CapacitySourceSchema,
|
|
23849
|
+
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
23850
|
+
cameraFps: number(),
|
|
23851
|
+
/** The Auto tune capacity floor the models were picked at. */
|
|
23852
|
+
minSessionsPerDevice: number(),
|
|
23853
|
+
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
23854
|
+
});
|
|
23855
|
+
/**
|
|
23856
|
+
* Where the node and device shares come from (D750, D755).
|
|
23857
|
+
* - `hardware` (default) — computed by the system from each node's static
|
|
23858
|
+
* capacity (its accelerators at their Auto tune models, from the measured
|
|
23859
|
+
* latency table), recomputed whenever a node or device joins, leaves, is
|
|
23860
|
+
* switched on or off, or changes model. No load input.
|
|
23861
|
+
* - `benchmark` — the same, from each node's measured capacity (node
|
|
23862
|
+
* benchmark results); a node without results falls back to `hardware`.
|
|
23863
|
+
* - `manual` — the operator's stored sliders.
|
|
23864
|
+
* Either way a node or device that cannot take sessions gives its share to
|
|
23865
|
+
* the others in proportion; the stored values are never rewritten for that.
|
|
23866
|
+
*/
|
|
23867
|
+
var ShareSourceSchema = _enum([
|
|
23868
|
+
"hardware",
|
|
23869
|
+
"benchmark",
|
|
23870
|
+
"manual"
|
|
23871
|
+
]);
|
|
23872
|
+
var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
|
|
23873
|
+
object({
|
|
23874
|
+
nodes: record(string(), number()),
|
|
23875
|
+
devices: record(string(), record(string(), number()))
|
|
23876
|
+
});
|
|
23877
|
+
var EffectiveDeviceShareSchema = object({
|
|
23878
|
+
deviceKey: string(),
|
|
23879
|
+
/** Can take sessions now (enabled, available, can run the root). */
|
|
23880
|
+
eligible: boolean(),
|
|
23881
|
+
/** The share the source assigns, as a percentage of the node's devices. */
|
|
23882
|
+
configuredPct: number(),
|
|
23883
|
+
/** The share actually used, after an ineligible device's share went to the others. */
|
|
23884
|
+
effectivePct: number()
|
|
23885
|
+
});
|
|
23886
|
+
var EffectiveNodeShareSchema = object({
|
|
23887
|
+
nodeId: string(),
|
|
23888
|
+
eligible: boolean(),
|
|
23889
|
+
configuredPct: number(),
|
|
23890
|
+
effectivePct: number(),
|
|
23891
|
+
/** Automatic sources: what this node's share was computed from; null under `manual` or before the first answer. */
|
|
23892
|
+
capacitySource: CapacitySourceSchema.nullable(),
|
|
23893
|
+
/** Automatic `benchmark`: why this node used `hardware` instead. */
|
|
23894
|
+
capacityFallback: CapacityFallbackSchema.nullable(),
|
|
23895
|
+
devices: array(EffectiveDeviceShareSchema).readonly()
|
|
23896
|
+
});
|
|
23897
|
+
var EffectiveSharesSchema = object({
|
|
23898
|
+
source: ShareSourceSchema,
|
|
23899
|
+
policy: SessionPlacementPolicySchema,
|
|
23900
|
+
/** An automatic source with no computed answer yet: the stored sliders are used meanwhile. */
|
|
23901
|
+
autoPending: boolean(),
|
|
23902
|
+
/** When the automatic shares were last computed (ms epoch); null = never. */
|
|
23903
|
+
computedAt: number().nullable(),
|
|
23904
|
+
/** What made the system recompute them last. */
|
|
23905
|
+
trigger: string().nullable(),
|
|
23906
|
+
nodes: array(EffectiveNodeShareSchema).readonly(),
|
|
23907
|
+
/** The relative weights the election reads (before eligibility). */
|
|
23908
|
+
nodeWeights: record(string(), number()),
|
|
23909
|
+
deviceWeights: record(string(), record(string(), number()))
|
|
23910
|
+
});
|
|
23571
23911
|
object({
|
|
23572
23912
|
activeCameras: number(),
|
|
23573
23913
|
throttledCameras: number(),
|
|
@@ -24011,6 +24351,22 @@ var MotionSourceEnum = _enum([
|
|
|
24011
24351
|
"device-activity"
|
|
24012
24352
|
]);
|
|
24013
24353
|
/**
|
|
24354
|
+
* What opened an on-motion detection SESSION: the motion source that passed the
|
|
24355
|
+
* camera's `motionSources` gate, and what raised it (the camera itself, or a
|
|
24356
|
+
* sensor the operator attached — D672). `sensorName` is the attached sensor's
|
|
24357
|
+
* device name as the orchestrator's camera-sensors resolution last read it;
|
|
24358
|
+
* absent when unknown or when the camera itself triggered.
|
|
24359
|
+
*
|
|
24360
|
+
* Carried on the session attach ONLY so the runner's `DetectionPhaseTransition`
|
|
24361
|
+
* can say what started the session (D672 rule 11, amended 2026-10-06). The
|
|
24362
|
+
* runner never decides anything on it.
|
|
24363
|
+
*/
|
|
24364
|
+
var SessionTriggerSchema = object({
|
|
24365
|
+
source: MotionSourceEnum,
|
|
24366
|
+
triggeredBy: MotionTriggeredBySchema,
|
|
24367
|
+
sensorName: string().optional()
|
|
24368
|
+
});
|
|
24369
|
+
/**
|
|
24014
24370
|
* List of motion sources active on a camera. Empty array is valid:
|
|
24015
24371
|
* "no source" — happens for battery cams without firmware motion when
|
|
24016
24372
|
* the operator hasn't opted into the SW analyzer (would hold the
|
|
@@ -24262,6 +24618,14 @@ var RunnerCameraConfigSchema = object({
|
|
|
24262
24618
|
*/
|
|
24263
24619
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
24264
24620
|
/**
|
|
24621
|
+
* What opened this attach, on a SESSION attach only (see
|
|
24622
|
+
* {@link SessionTriggerSchema}). OPTIONAL with no default: absent on a
|
|
24623
|
+
* standing attach, an occupancy burst, a re-dispatch with no motion event
|
|
24624
|
+
* behind it, and every attach from an orchestrator older than the field — the
|
|
24625
|
+
* runner then names no source on the phase event.
|
|
24626
|
+
*/
|
|
24627
|
+
sessionTrigger: SessionTriggerSchema.optional(),
|
|
24628
|
+
/**
|
|
24265
24629
|
* WHICH root detector runs for this camera. The detection counterpart of
|
|
24266
24630
|
* {@link motionSources}, same shape and same discipline: a per-camera list
|
|
24267
24631
|
* of sources the system understands, vendor-agnostic, owned here.
|
|
@@ -24421,14 +24785,15 @@ var RunnerLocalLoadSchema = object({
|
|
|
24421
24785
|
* Per-inference-device live load (multi-device C4). One entry per deviceKey
|
|
24422
24786
|
* this runner currently has attached cameras on, so the orchestrator's second
|
|
24423
24787
|
* `balance()` pass (over a node's devices) weights on real per-pool session
|
|
24424
|
-
* counts. Empty on single-device / pre-multi-device runners.
|
|
24425
|
-
* per
|
|
24788
|
+
* counts. Empty on single-device / pre-multi-device runners. There is no
|
|
24789
|
+
* per-device queue depth: the inference pool's backlog has no accessor, and
|
|
24790
|
+
* a field that always read 0 was removed rather than shown as a measurement
|
|
24791
|
+
* (D762).
|
|
24426
24792
|
*/
|
|
24427
24793
|
devices: array(object({
|
|
24428
24794
|
deviceKey: string(),
|
|
24429
24795
|
backend: string(),
|
|
24430
|
-
attachedCameras: number()
|
|
24431
|
-
queueDepthTotal: number()
|
|
24796
|
+
attachedCameras: number()
|
|
24432
24797
|
})).default([]),
|
|
24433
24798
|
/** Hardware capability flags reported by this node. */
|
|
24434
24799
|
hardware: object({
|
|
@@ -24564,7 +24929,22 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
24564
24929
|
*/
|
|
24565
24930
|
cropJpeg: string().optional(),
|
|
24566
24931
|
parent: DetailParentSchema,
|
|
24567
|
-
steps: array(string()).optional()
|
|
24932
|
+
steps: array(string()).optional(),
|
|
24933
|
+
/**
|
|
24934
|
+
* The camera's root model already drew this step's boxes (D730): a
|
|
24935
|
+
* `license_plate` inside the parent vehicle. The named step is NOT
|
|
24936
|
+
* inferred — these FRAME-space boxes are its output — and its
|
|
24937
|
+
* requested children (the plate reader) run on them. Absent ⇒ every
|
|
24938
|
+
* requested step infers as before.
|
|
24939
|
+
*/
|
|
24940
|
+
supplied: object({
|
|
24941
|
+
stepId: string(),
|
|
24942
|
+
modelId: string(),
|
|
24943
|
+
boxes: array(object({
|
|
24944
|
+
bbox: NativeCropBboxSchema,
|
|
24945
|
+
score: number()
|
|
24946
|
+
})).min(1)
|
|
24947
|
+
}).optional()
|
|
24568
24948
|
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" }), method(object({
|
|
24569
24949
|
/** Catalog step id, e.g. `clip-embedding`. */
|
|
24570
24950
|
stepId: string(),
|
|
@@ -24627,14 +25007,6 @@ var CameraPipelineConfigSchema = object({
|
|
|
24627
25007
|
settings: record(string(), unknown()).readonly().optional()
|
|
24628
25008
|
}).nullable().optional()
|
|
24629
25009
|
});
|
|
24630
|
-
var PipelineTemplateSchema = object({
|
|
24631
|
-
id: string(),
|
|
24632
|
-
name: string(),
|
|
24633
|
-
description: string().optional(),
|
|
24634
|
-
config: CameraPipelineConfigSchema,
|
|
24635
|
-
createdAt: string(),
|
|
24636
|
-
updatedAt: string()
|
|
24637
|
-
});
|
|
24638
25010
|
var DeviceStepConfigSchema = object({
|
|
24639
25011
|
modelId: string().optional(),
|
|
24640
25012
|
settings: record(string(), unknown()).optional(),
|
|
@@ -24708,9 +25080,10 @@ var CameraStepOverridePatchSchema = object({
|
|
|
24708
25080
|
modelId: string().optional(),
|
|
24709
25081
|
settings: record(string(), unknown()).readonly().optional()
|
|
24710
25082
|
});
|
|
25083
|
+
var CameraStepSettingsSchema = record(string(), record(string(), unknown()).readonly());
|
|
24711
25084
|
var CameraPipelineSettingsSchema = object({
|
|
24712
|
-
pinnedAgentNodeId: string().optional(),
|
|
24713
25085
|
stepToggles: record(string(), boolean()).optional(),
|
|
25086
|
+
stepSettings: CameraStepSettingsSchema.optional(),
|
|
24714
25087
|
stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
|
|
24715
25088
|
pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
|
|
24716
25089
|
});
|
|
@@ -24743,7 +25116,10 @@ var PipelineAssignmentSchema = object({
|
|
|
24743
25116
|
});
|
|
24744
25117
|
/**
|
|
24745
25118
|
* Per-agent load summary surfaced to the load balancer + dashboards.
|
|
24746
|
-
* Aggregated from each runner's `getLocalLoad` cap call.
|
|
25119
|
+
* Aggregated from each runner's `getLocalLoad` cap call. In the
|
|
25120
|
+
* `getAgentLoad` report, a single-device node's default-pool sessions (no
|
|
25121
|
+
* `deviceKey`) are credited to its one eligible device in `load.devices` —
|
|
25122
|
+
* the default pool IS that device (D745); the balancer's census stays raw.
|
|
24747
25123
|
*/
|
|
24748
25124
|
var AgentLoadSummarySchema = object({
|
|
24749
25125
|
nodeId: string(),
|
|
@@ -24832,26 +25208,27 @@ var CameraBrokerStatusSchema = object({
|
|
|
24832
25208
|
width: number(),
|
|
24833
25209
|
height: number(),
|
|
24834
25210
|
subscribers: number(),
|
|
24835
|
-
|
|
24836
|
-
|
|
25211
|
+
/**
|
|
25212
|
+
* Encoded packets/s the broker receives from the camera. null when the
|
|
25213
|
+
* broker's stats read failed — unknown, never 0 (a 0 here reads as a stall).
|
|
25214
|
+
* There is no output rate: decoding runs in the session decode workers, not
|
|
25215
|
+
* in the broker, so the broker's own decoded-frame rate is 0 on every live
|
|
25216
|
+
* camera and is not served (D758).
|
|
25217
|
+
*/
|
|
25218
|
+
inFps: number().nullable()
|
|
24837
25219
|
})).readonly(),
|
|
24838
25220
|
webrtcSessions: number(),
|
|
24839
25221
|
rtspRestream: boolean()
|
|
24840
25222
|
});
|
|
24841
|
-
/**
|
|
24842
|
-
|
|
24843
|
-
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
24848
|
-
|
|
24849
|
-
var CameraDecoderStatusSchema = object({
|
|
24850
|
-
nodeId: string(),
|
|
24851
|
-
formats: array(string()).readonly(),
|
|
24852
|
-
sessionCount: number(),
|
|
24853
|
-
shm: CameraDecoderShmSchema
|
|
24854
|
-
});
|
|
25223
|
+
/**
|
|
25224
|
+
* Decoder block — WHERE decode runs (co-located with the detection node by
|
|
25225
|
+
* construction); null when the camera has no detection node. It carries no
|
|
25226
|
+
* frame counter: the shared-memory plane whose counters it used to serve is
|
|
25227
|
+
* gone, and those fields read 0 forever. Whether frames actually flow is read
|
|
25228
|
+
* from `detection.actualFps` (frames delivered to and processed by the
|
|
25229
|
+
* detector), never from a counter nothing feeds.
|
|
25230
|
+
*/
|
|
25231
|
+
var CameraDecoderStatusSchema = object({ nodeId: string() });
|
|
24855
25232
|
/** Motion block — null when motion detection is not active for this device. */
|
|
24856
25233
|
var CameraMotionStatusSchema = object({
|
|
24857
25234
|
enabled: boolean(),
|
|
@@ -25208,12 +25585,6 @@ method(object({
|
|
|
25208
25585
|
}), object({ success: literal(true) }), {
|
|
25209
25586
|
kind: "mutation",
|
|
25210
25587
|
auth: "admin"
|
|
25211
|
-
}), method(object({
|
|
25212
|
-
agentNodeId: string(),
|
|
25213
|
-
detectWeight: number().positive().nullable()
|
|
25214
|
-
}), object({ success: literal(true) }), {
|
|
25215
|
-
kind: "mutation",
|
|
25216
|
-
auth: "admin"
|
|
25217
25588
|
}), method(object({
|
|
25218
25589
|
agentNodeId: string(),
|
|
25219
25590
|
detect: boolean().nullable().optional(),
|
|
@@ -25255,6 +25626,15 @@ method(object({
|
|
|
25255
25626
|
}), {
|
|
25256
25627
|
kind: "mutation",
|
|
25257
25628
|
auth: "admin"
|
|
25629
|
+
}), method(AutoTuneOptionsSchema, AutoTunePlanSchema, { auth: "admin" }), method(AutoTuneApplyInputSchema, AutoTuneApplyResultSchema, {
|
|
25630
|
+
kind: "mutation",
|
|
25631
|
+
auth: "admin"
|
|
25632
|
+
}), method(LoadBalancingSharesInputSchema, object({ success: literal(true) }), {
|
|
25633
|
+
kind: "mutation",
|
|
25634
|
+
auth: "admin"
|
|
25635
|
+
}), method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }), method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }), method(_void(), EffectiveSharesSchema, { auth: "admin" }), method(SetShareSourceInputSchema, object({ success: literal(true) }), {
|
|
25636
|
+
kind: "mutation",
|
|
25637
|
+
auth: "admin"
|
|
25258
25638
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
25259
25639
|
deviceId: number(),
|
|
25260
25640
|
addonId: string(),
|
|
@@ -25271,6 +25651,13 @@ method(object({
|
|
|
25271
25651
|
}), object({ success: literal(true) }), {
|
|
25272
25652
|
kind: "mutation",
|
|
25273
25653
|
auth: "admin"
|
|
25654
|
+
}), method(object({
|
|
25655
|
+
deviceId: number(),
|
|
25656
|
+
addonId: string(),
|
|
25657
|
+
settings: record(string(), unknown()).nullable()
|
|
25658
|
+
}), object({ success: literal(true) }), {
|
|
25659
|
+
kind: "mutation",
|
|
25660
|
+
auth: "admin"
|
|
25274
25661
|
}), method(object({
|
|
25275
25662
|
deviceId: number(),
|
|
25276
25663
|
agentNodeId: string(),
|
|
@@ -25294,25 +25681,7 @@ method(object({
|
|
|
25294
25681
|
}), method(_void(), DiskReconcileJobSchema, {
|
|
25295
25682
|
kind: "mutation",
|
|
25296
25683
|
auth: "admin"
|
|
25297
|
-
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" })
|
|
25298
|
-
name: string(),
|
|
25299
|
-
description: string().optional(),
|
|
25300
|
-
config: CameraPipelineConfigSchema
|
|
25301
|
-
}), PipelineTemplateSchema, {
|
|
25302
|
-
kind: "mutation",
|
|
25303
|
-
auth: "admin"
|
|
25304
|
-
}), method(object({
|
|
25305
|
-
id: string(),
|
|
25306
|
-
name: string().optional(),
|
|
25307
|
-
description: string().optional(),
|
|
25308
|
-
config: CameraPipelineConfigSchema.optional()
|
|
25309
|
-
}), PipelineTemplateSchema, {
|
|
25310
|
-
kind: "mutation",
|
|
25311
|
-
auth: "admin"
|
|
25312
|
-
}), method(object({ id: string() }), object({ success: literal(true) }), {
|
|
25313
|
-
kind: "mutation",
|
|
25314
|
-
auth: "admin"
|
|
25315
|
-
});
|
|
25684
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" });
|
|
25316
25685
|
/**
|
|
25317
25686
|
* Cap → event-kind mapping for the SENSOR / CONTROL cap families — the table
|
|
25318
25687
|
* `pipeline-analytics.listEventKinds` uses to turn a linked device's bound
|
|
@@ -27532,8 +27901,6 @@ var WebrtcStreamChoiceSchema = object({
|
|
|
27532
27901
|
]).nullable(),
|
|
27533
27902
|
/** Input FPS from the camera (null for adaptive). */
|
|
27534
27903
|
inputFps: number().nullable(),
|
|
27535
|
-
/** Decoded FPS (null for adaptive). */
|
|
27536
|
-
decodeFps: number().nullable(),
|
|
27537
27904
|
/** Bitrate in kbps (null for adaptive). */
|
|
27538
27905
|
bitrateKbps: number().nullable()
|
|
27539
27906
|
});
|
|
@@ -27865,6 +28232,14 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
27865
28232
|
*/
|
|
27866
28233
|
alive: boolean()
|
|
27867
28234
|
})), method(object({
|
|
28235
|
+
deviceId: number().int().nonnegative(),
|
|
28236
|
+
sessionId: string(),
|
|
28237
|
+
downlinkMbps: number().nonnegative()
|
|
28238
|
+
}), object({
|
|
28239
|
+
outcome: _enum(["switching", "held"]),
|
|
28240
|
+
tier: CamProfileSchema.nullable(),
|
|
28241
|
+
reason: string()
|
|
28242
|
+
}), { kind: "mutation" }), method(object({
|
|
27868
28243
|
deviceId: number().int().nonnegative(),
|
|
27869
28244
|
sessionId: string()
|
|
27870
28245
|
}), WebrtcSessionDebugSchema, { kind: "query" });
|
|
@@ -28104,6 +28479,59 @@ var AddonAutoUpdateSchema = ChannelWithInheritSchema;
|
|
|
28104
28479
|
var RestartAddonResultSchema = unknown();
|
|
28105
28480
|
var InstallPackageResultSchema = unknown();
|
|
28106
28481
|
var ReloadPackagesResultSchema = unknown();
|
|
28482
|
+
/**
|
|
28483
|
+
* What an uninstall did on ONE node.
|
|
28484
|
+
* - `uninstalled` — removed: runner(s) stopped, caps gone, code deleted.
|
|
28485
|
+
* - `not-installed` — the node did not have it (nothing to do).
|
|
28486
|
+
* - `refused` — protected / bootstrap package; nothing touched.
|
|
28487
|
+
* - `failed` — attempted and did not complete; `error` says why.
|
|
28488
|
+
* - `pending-reconnect` — the node is OFFLINE: its record was pruned so the
|
|
28489
|
+
* back-fill never returns the package, and the stale-addon sweep removes the
|
|
28490
|
+
* copy when the node registers again (only once the hub no longer has it).
|
|
28491
|
+
*/
|
|
28492
|
+
var UninstallNodeOutcomeSchema = _enum([
|
|
28493
|
+
"uninstalled",
|
|
28494
|
+
"not-installed",
|
|
28495
|
+
"refused",
|
|
28496
|
+
"failed",
|
|
28497
|
+
"pending-reconnect"
|
|
28498
|
+
]);
|
|
28499
|
+
var UninstallNodeResultSchema = object({
|
|
28500
|
+
nodeId: string(),
|
|
28501
|
+
outcome: UninstallNodeOutcomeSchema,
|
|
28502
|
+
error: string().optional(),
|
|
28503
|
+
addonIds: array(string()).readonly(),
|
|
28504
|
+
runnersStopped: array(string()).readonly(),
|
|
28505
|
+
warnings: array(string()).readonly()
|
|
28506
|
+
});
|
|
28507
|
+
var UninstallPackageResultSchema = object({
|
|
28508
|
+
/** Every targeted node ended `uninstalled`, `not-installed` or `pending-reconnect`. */
|
|
28509
|
+
success: boolean(),
|
|
28510
|
+
packageName: string(),
|
|
28511
|
+
results: array(UninstallNodeResultSchema).readonly()
|
|
28512
|
+
});
|
|
28513
|
+
/**
|
|
28514
|
+
* `unreachable` lists online agents whose status read FAILED — absence from
|
|
28515
|
+
* `installations` is only "not installed" for a node that answered (D315).
|
|
28516
|
+
*/
|
|
28517
|
+
var PackageInstallationsSchema = object({
|
|
28518
|
+
installations: array(object({
|
|
28519
|
+
packageName: string(),
|
|
28520
|
+
nodeId: string(),
|
|
28521
|
+
nodeName: string(),
|
|
28522
|
+
isHub: boolean(),
|
|
28523
|
+
isOnline: boolean(),
|
|
28524
|
+
version: string().nullable(),
|
|
28525
|
+
addonIds: array(string()).readonly(),
|
|
28526
|
+
source: _enum(["live", "recorded"]),
|
|
28527
|
+
/** The hub installs this package too. `false` on an agent row = an orphan. */
|
|
28528
|
+
onHub: boolean()
|
|
28529
|
+
})).readonly(),
|
|
28530
|
+
unreachable: array(object({
|
|
28531
|
+
nodeId: string(),
|
|
28532
|
+
error: string()
|
|
28533
|
+
})).readonly()
|
|
28534
|
+
});
|
|
28107
28535
|
var BulkUpdateItemStatusSchema = _enum([
|
|
28108
28536
|
"queued",
|
|
28109
28537
|
"updating",
|
|
@@ -28186,10 +28614,14 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
|
|
|
28186
28614
|
}), method(object({ packageName: string() }), InstallPackageResultSchema, {
|
|
28187
28615
|
kind: "mutation",
|
|
28188
28616
|
auth: "admin"
|
|
28189
|
-
}), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
|
|
28617
|
+
}), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
|
|
28618
|
+
packageName: string().min(1),
|
|
28619
|
+
nodeId: string().optional(),
|
|
28620
|
+
everywhere: boolean().optional()
|
|
28621
|
+
}), UninstallPackageResultSchema, {
|
|
28190
28622
|
kind: "mutation",
|
|
28191
28623
|
auth: "admin"
|
|
28192
|
-
}), method(_void(), ReloadPackagesResultSchema, {
|
|
28624
|
+
}), method(object({ packageName: string().optional() }), PackageInstallationsSchema, { auth: "admin" }), method(_void(), ReloadPackagesResultSchema, {
|
|
28193
28625
|
kind: "mutation",
|
|
28194
28626
|
auth: "admin"
|
|
28195
28627
|
}), method(object({ query: string().optional() }), array(SearchResultSchema)), method(object({ nodeId: string().optional() }), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
|
|
@@ -30485,7 +30917,7 @@ var LocalInterfaceSchema = object({
|
|
|
30485
30917
|
name: string(),
|
|
30486
30918
|
family: _enum(["IPv4", "IPv6"]),
|
|
30487
30919
|
address: string(),
|
|
30488
|
-
/** CIDR shorthand (e.g. `192.168.
|
|
30920
|
+
/** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
|
|
30489
30921
|
cidr: string(),
|
|
30490
30922
|
/** OS netmask. Stable across calls so the SDK can hash on it. */
|
|
30491
30923
|
netmask: string(),
|
|
@@ -30539,7 +30971,9 @@ var ConnectionEndpointSchema = object({
|
|
|
30539
30971
|
"lan-ipv4",
|
|
30540
30972
|
"lan-ipv6",
|
|
30541
30973
|
"public",
|
|
30542
|
-
"loopback"
|
|
30974
|
+
"loopback",
|
|
30975
|
+
"app-link",
|
|
30976
|
+
"custom"
|
|
30543
30977
|
]),
|
|
30544
30978
|
/**
|
|
30545
30979
|
* Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
|
|
@@ -30608,15 +31042,27 @@ var GetConnectionEndpointsResultSchema = object({
|
|
|
30608
31042
|
localPort: AdvertisedLocalPortSchema
|
|
30609
31043
|
});
|
|
30610
31044
|
/**
|
|
30611
|
-
* The
|
|
30612
|
-
*
|
|
30613
|
-
*
|
|
31045
|
+
* The address flagged for notification taps (`notificationTaps`). `baseUrl: null` = AUTO, which
|
|
31046
|
+
* since D709 is the CamStack app link; `resolved` reports the effective value,
|
|
31047
|
+
* so the UI can show it either way.
|
|
30614
31048
|
*/
|
|
30615
31049
|
var NotificationEndpointSchema = object({
|
|
30616
31050
|
/** The operator's explicit choice, or null for AUTO. */
|
|
30617
31051
|
baseUrl: string().nullable(),
|
|
30618
|
-
/**
|
|
30619
|
-
|
|
31052
|
+
/**
|
|
31053
|
+
* The effective flagged address: the explicit choice, else (AUTO) the
|
|
31054
|
+
* CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
|
|
31055
|
+
* answered the public tunnel / mesh here, or null.
|
|
31056
|
+
*/
|
|
31057
|
+
resolved: string().nullable(),
|
|
31058
|
+
/**
|
|
31059
|
+
* The hub origin an app link carries as `hub=` (D709): the hub's best
|
|
31060
|
+
* public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
|
|
31061
|
+
* picked from this cap's own palette by `pickAppLinkHub`. `null` when the
|
|
31062
|
+
* hub advertises neither; the app link is then minted without `hub=`.
|
|
31063
|
+
* Absent on a pre-D709 hub.
|
|
31064
|
+
*/
|
|
31065
|
+
appLinkHub: string().nullable()
|
|
30620
31066
|
});
|
|
30621
31067
|
/**
|
|
30622
31068
|
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
@@ -30629,6 +31075,72 @@ var ViewerEndpointsSchema = object({
|
|
|
30629
31075
|
/** What the ranking currently resolves to (may be empty if nothing is up). */
|
|
30630
31076
|
resolved: array(string()).readonly()
|
|
30631
31077
|
});
|
|
31078
|
+
/**
|
|
31079
|
+
* DEPRECATED compatibility shape of `getHubAddresses` (D729 amendment, kept
|
|
31080
|
+
* for addons built against it): `cluster` is the `internal` role, `browser`
|
|
31081
|
+
* the `publicEndpoint` role; `override` is the flagged address.
|
|
31082
|
+
*/
|
|
31083
|
+
var HubAddressSnapshotSchema = object({
|
|
31084
|
+
override: string().nullable(),
|
|
31085
|
+
resolved: string().nullable(),
|
|
31086
|
+
source: _enum([
|
|
31087
|
+
"override",
|
|
31088
|
+
"auto",
|
|
31089
|
+
"none"
|
|
31090
|
+
])
|
|
31091
|
+
});
|
|
31092
|
+
var HubAddressesSchema = object({
|
|
31093
|
+
cluster: HubAddressSnapshotSchema,
|
|
31094
|
+
browser: HubAddressSnapshotSchema
|
|
31095
|
+
});
|
|
31096
|
+
/**
|
|
31097
|
+
* What an address is used for (`hub-address.ts`, D734). `appConnections` is
|
|
31098
|
+
* many-valued; `notificationTaps`, `publicEndpoint` and `internal` are carried
|
|
31099
|
+
* by at most one address.
|
|
31100
|
+
*/
|
|
31101
|
+
var HubAddressRoleSchema = _enum(HUB_ADDRESS_ROLES);
|
|
31102
|
+
var RoleSourceSchema = _enum([
|
|
31103
|
+
"flagged",
|
|
31104
|
+
"auto",
|
|
31105
|
+
"none"
|
|
31106
|
+
]);
|
|
31107
|
+
/** One single-valued role: the flagged address, the one in use, and why. */
|
|
31108
|
+
var HubAddressRoleResolutionSchema = object({
|
|
31109
|
+
/** The address carrying the flag, or null = automatic. */
|
|
31110
|
+
flagged: string().nullable(),
|
|
31111
|
+
/** The address in use, or null when nothing qualifies (never loopback). */
|
|
31112
|
+
resolved: string().nullable(),
|
|
31113
|
+
source: RoleSourceSchema
|
|
31114
|
+
});
|
|
31115
|
+
var HubAppConnectionsResolutionSchema = object({
|
|
31116
|
+
/** The addresses flagged `appConnections`, or empty = automatic. */
|
|
31117
|
+
flagged: array(string()).readonly(),
|
|
31118
|
+
/** What the apps race right now. */
|
|
31119
|
+
resolved: array(string()).readonly(),
|
|
31120
|
+
source: RoleSourceSchema
|
|
31121
|
+
});
|
|
31122
|
+
var HubAddressBookSchema = object({
|
|
31123
|
+
rows: array(ConnectionEndpointSchema.extend({
|
|
31124
|
+
/** In the hub's detected palette right now. */
|
|
31125
|
+
detected: boolean(),
|
|
31126
|
+
/** Entered by the operator (removable). */
|
|
31127
|
+
custom: boolean(),
|
|
31128
|
+
/** The roles explicitly flagged on this address. */
|
|
31129
|
+
flags: array(HubAddressRoleSchema).readonly(),
|
|
31130
|
+
/** The roles this address serves right now — flagged or automatic. */
|
|
31131
|
+
inUse: array(HubAddressRoleSchema).readonly(),
|
|
31132
|
+
/** The roles this address may be flagged with. */
|
|
31133
|
+
eligible: array(HubAddressRoleSchema).readonly()
|
|
31134
|
+
})).readonly(),
|
|
31135
|
+
roles: object({
|
|
31136
|
+
appConnections: HubAppConnectionsResolutionSchema,
|
|
31137
|
+
notificationTaps: HubAddressRoleResolutionSchema,
|
|
31138
|
+
internal: HubAddressRoleResolutionSchema,
|
|
31139
|
+
publicEndpoint: HubAddressRoleResolutionSchema
|
|
31140
|
+
}),
|
|
31141
|
+
/** The `hub=` an app link carries (D709), or null. */
|
|
31142
|
+
appLinkHub: string().nullable()
|
|
31143
|
+
});
|
|
30632
31144
|
var AllowedAddressesSchema = object({
|
|
30633
31145
|
/**
|
|
30634
31146
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -30670,8 +31182,29 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
30670
31182
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
30671
31183
|
* to avoid mixed-content blocks in the browser. The public
|
|
30672
31184
|
* tunnel always emits `https://` regardless. */
|
|
30673
|
-
scheme: _enum(["http", "https"]).optional()
|
|
30674
|
-
|
|
31185
|
+
scheme: _enum(["http", "https"]).optional(),
|
|
31186
|
+
/** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
|
|
31187
|
+
* an address notification taps can be flagged with, not a hub
|
|
31188
|
+
* route. Default `false`: racers and rankers never see it. */
|
|
31189
|
+
includeAppLink: boolean().optional()
|
|
31190
|
+
}), 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({
|
|
31191
|
+
baseUrl: string(),
|
|
31192
|
+
label: string().max(80)
|
|
31193
|
+
}), HubAddressBookSchema, {
|
|
31194
|
+
kind: "mutation",
|
|
31195
|
+
auth: "admin"
|
|
31196
|
+
}), method(object({ baseUrl: string() }), HubAddressBookSchema, {
|
|
31197
|
+
kind: "mutation",
|
|
31198
|
+
auth: "admin"
|
|
31199
|
+
}), method(object({
|
|
31200
|
+
role: HubAddressRoleSchema,
|
|
31201
|
+
baseUrl: string().nullable(),
|
|
31202
|
+
enabled: boolean(),
|
|
31203
|
+
acceptInsecureHttp: boolean().optional()
|
|
31204
|
+
}), HubAddressBookSchema, {
|
|
31205
|
+
kind: "mutation",
|
|
31206
|
+
auth: "admin"
|
|
31207
|
+
}), 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, {
|
|
30675
31208
|
kind: "mutation",
|
|
30676
31209
|
auth: "admin"
|
|
30677
31210
|
}), method(object({
|
|
@@ -30931,7 +31464,7 @@ var MeshPeerSchema = object({
|
|
|
30931
31464
|
tags: array(string()).readonly(),
|
|
30932
31465
|
/** True when the peer advertises itself as an exit-node candidate. */
|
|
30933
31466
|
exitNodeOption: boolean(),
|
|
30934
|
-
/** Subnet routes the peer advertises (e.g. `["192.168.
|
|
31467
|
+
/** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
|
|
30935
31468
|
advertisedRoutes: array(string()).readonly(),
|
|
30936
31469
|
/** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
|
|
30937
31470
|
userLogin: string().nullable(),
|
|
@@ -31061,7 +31594,8 @@ object({
|
|
|
31061
31594
|
* writes this slice (not even a boot seed), and neither does the runner's
|
|
31062
31595
|
* phase machine: its phase is "a session is attached" for every on-motion
|
|
31063
31596
|
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
31064
|
-
* A provider's `isDetected`
|
|
31597
|
+
* A provider's `isDetected` and `getStatus` answer from the hub's slice
|
|
31598
|
+
* (`hubMotionNativeProvider`).
|
|
31065
31599
|
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
31066
31600
|
* imports) — the provider pushes both edges itself, from the upstream
|
|
31067
31601
|
* system's own state change. That is the sensor's own fact.
|
|
@@ -31875,7 +32409,13 @@ var OsdStatusSchema = object({ overlays: array(object({
|
|
|
31875
32409
|
*/
|
|
31876
32410
|
availablePositions: array(OsdPositionEnum).readonly(),
|
|
31877
32411
|
/** Firmware does not allow editing the `text` field (e.g. timestamp). */
|
|
31878
|
-
readOnly: boolean().optional()
|
|
32412
|
+
readOnly: boolean().optional(),
|
|
32413
|
+
/**
|
|
32414
|
+
* Firmware text ceiling for this overlay. Absent = no known limit.
|
|
32415
|
+
* The OSD manager truncates here on every write — never stores its own
|
|
32416
|
+
* copy on the binding.
|
|
32417
|
+
*/
|
|
32418
|
+
maxCharacters: number().int().positive().optional()
|
|
31879
32419
|
})).readonly() });
|
|
31880
32420
|
var OsdOverlayPatchSchema = object({
|
|
31881
32421
|
enabled: boolean().optional(),
|
|
@@ -31926,7 +32466,9 @@ var OsdTemplateBindingSchema = object({
|
|
|
31926
32466
|
valuePath: string().min(1).max(64),
|
|
31927
32467
|
/** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
|
|
31928
32468
|
displayUnit: string().max(16).optional(),
|
|
31929
|
-
maxDecimals: number().int().min(0).max(4).default(1)
|
|
32469
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
32470
|
+
/** Raw value → display text (same shape as slot binding). Empty string hides. */
|
|
32471
|
+
valueMap: record(string(), string()).optional()
|
|
31930
32472
|
});
|
|
31931
32473
|
/**
|
|
31932
32474
|
* A named template: bindings (alias → leaf field) + a parser that
|
|
@@ -31976,18 +32518,11 @@ var OsdSlotBindingSchema = object({
|
|
|
31976
32518
|
source: OsdSourceSchema,
|
|
31977
32519
|
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
31978
32520
|
template: string().max(96).default("${value}"),
|
|
31979
|
-
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
31980
|
-
maxCharacters: number().int().min(4).max(64).optional(),
|
|
31981
32521
|
/**
|
|
31982
32522
|
* Decimal places for a numeric value. `0` yields an integer — the
|
|
31983
32523
|
* documented workaround for firmwares that reject `.` in overlay text.
|
|
31984
32524
|
*/
|
|
31985
32525
|
maxDecimals: number().int().min(0).max(4).default(1),
|
|
31986
|
-
/**
|
|
31987
|
-
* Appended via `${unit}` when no unit conversion produces a label.
|
|
31988
|
-
* Prefer `source.displayUnit` for converted device-state values.
|
|
31989
|
-
*/
|
|
31990
|
-
unitLabel: string().max(8).optional(),
|
|
31991
32526
|
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
31992
32527
|
valueMap: record(string(), string()).optional(),
|
|
31993
32528
|
/** Time windows in which the slot is shown. Absent = always. */
|
|
@@ -31996,9 +32531,7 @@ var OsdSlotBindingSchema = object({
|
|
|
31996
32531
|
* Display gate, in the notification centre's condition vocabulary.
|
|
31997
32532
|
* Only the keys reported by `getConditionSupport` are accepted.
|
|
31998
32533
|
*/
|
|
31999
|
-
conditions: NcConditionsSchema.optional()
|
|
32000
|
-
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
32001
|
-
fallbackText: string().max(64).default("")
|
|
32534
|
+
conditions: NcConditionsSchema.optional()
|
|
32002
32535
|
});
|
|
32003
32536
|
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
32004
32537
|
var OsdSlotViewSchema = object({
|
|
@@ -36193,6 +36726,16 @@ DeviceType.Camera, method(object({
|
|
|
36193
36726
|
package: array(ZoneRuleSchema).readonly()
|
|
36194
36727
|
});
|
|
36195
36728
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
36729
|
+
Object.freeze({
|
|
36730
|
+
person: .5,
|
|
36731
|
+
vehicle: .5,
|
|
36732
|
+
animal: .5
|
|
36733
|
+
});
|
|
36734
|
+
Object.freeze({
|
|
36735
|
+
person: .65,
|
|
36736
|
+
vehicle: .65,
|
|
36737
|
+
animal: .65
|
|
36738
|
+
});
|
|
36196
36739
|
/**
|
|
36197
36740
|
* Accessory device helpers — shared across drivers.
|
|
36198
36741
|
*
|
|
@@ -36367,6 +36910,12 @@ Object.freeze({
|
|
|
36367
36910
|
addonId: null,
|
|
36368
36911
|
access: "view"
|
|
36369
36912
|
},
|
|
36913
|
+
"addons.listInstallations": {
|
|
36914
|
+
capName: "addons",
|
|
36915
|
+
capScope: "system",
|
|
36916
|
+
addonId: null,
|
|
36917
|
+
access: "view"
|
|
36918
|
+
},
|
|
36370
36919
|
"addons.listJobs": {
|
|
36371
36920
|
capName: "addons",
|
|
36372
36921
|
capScope: "system",
|
|
@@ -37549,90 +38098,18 @@ Object.freeze({
|
|
|
37549
38098
|
addonId: null,
|
|
37550
38099
|
access: "create"
|
|
37551
38100
|
},
|
|
37552
|
-
"decoder.createSession": {
|
|
37553
|
-
capName: "decoder",
|
|
37554
|
-
capScope: "system",
|
|
37555
|
-
addonId: null,
|
|
37556
|
-
access: "view"
|
|
37557
|
-
},
|
|
37558
|
-
"decoder.destroySession": {
|
|
37559
|
-
capName: "decoder",
|
|
37560
|
-
capScope: "system",
|
|
37561
|
-
addonId: null,
|
|
37562
|
-
access: "delete"
|
|
37563
|
-
},
|
|
37564
|
-
"decoder.getFrame": {
|
|
37565
|
-
capName: "decoder",
|
|
37566
|
-
capScope: "system",
|
|
37567
|
-
addonId: null,
|
|
37568
|
-
access: "view"
|
|
37569
|
-
},
|
|
37570
38101
|
"decoder.getInfo": {
|
|
37571
38102
|
capName: "decoder",
|
|
37572
38103
|
capScope: "system",
|
|
37573
38104
|
addonId: null,
|
|
37574
38105
|
access: "view"
|
|
37575
38106
|
},
|
|
37576
|
-
"decoder.getShmStats": {
|
|
37577
|
-
capName: "decoder",
|
|
37578
|
-
capScope: "system",
|
|
37579
|
-
addonId: null,
|
|
37580
|
-
access: "view"
|
|
37581
|
-
},
|
|
37582
|
-
"decoder.getStats": {
|
|
37583
|
-
capName: "decoder",
|
|
37584
|
-
capScope: "system",
|
|
37585
|
-
addonId: null,
|
|
37586
|
-
access: "view"
|
|
37587
|
-
},
|
|
37588
|
-
"decoder.listActiveSessions": {
|
|
37589
|
-
capName: "decoder",
|
|
37590
|
-
capScope: "system",
|
|
37591
|
-
addonId: null,
|
|
37592
|
-
access: "view"
|
|
37593
|
-
},
|
|
37594
|
-
"decoder.openStream": {
|
|
37595
|
-
capName: "decoder",
|
|
37596
|
-
capScope: "system",
|
|
37597
|
-
addonId: null,
|
|
37598
|
-
access: "view"
|
|
37599
|
-
},
|
|
37600
|
-
"decoder.pullFrames": {
|
|
37601
|
-
capName: "decoder",
|
|
37602
|
-
capScope: "system",
|
|
37603
|
-
addonId: null,
|
|
37604
|
-
access: "view"
|
|
37605
|
-
},
|
|
37606
|
-
"decoder.pullHandles": {
|
|
37607
|
-
capName: "decoder",
|
|
37608
|
-
capScope: "system",
|
|
37609
|
-
addonId: null,
|
|
37610
|
-
access: "view"
|
|
37611
|
-
},
|
|
37612
|
-
"decoder.pushPacket": {
|
|
37613
|
-
capName: "decoder",
|
|
37614
|
-
capScope: "system",
|
|
37615
|
-
addonId: null,
|
|
37616
|
-
access: "view"
|
|
37617
|
-
},
|
|
37618
38107
|
"decoder.reprobeHwaccel": {
|
|
37619
38108
|
capName: "decoder",
|
|
37620
38109
|
capScope: "system",
|
|
37621
38110
|
addonId: null,
|
|
37622
38111
|
access: "create"
|
|
37623
38112
|
},
|
|
37624
|
-
"decoder.supportsCodec": {
|
|
37625
|
-
capName: "decoder",
|
|
37626
|
-
capScope: "system",
|
|
37627
|
-
addonId: null,
|
|
37628
|
-
access: "view"
|
|
37629
|
-
},
|
|
37630
|
-
"decoder.updateConfig": {
|
|
37631
|
-
capName: "decoder",
|
|
37632
|
-
capScope: "system",
|
|
37633
|
-
addonId: null,
|
|
37634
|
-
access: "view"
|
|
37635
|
-
},
|
|
37636
38113
|
"detectionPipeline.applyDeviceSettingsPatch": {
|
|
37637
38114
|
capName: "detection-pipeline",
|
|
37638
38115
|
capScope: "device",
|
|
@@ -39121,12 +39598,24 @@ Object.freeze({
|
|
|
39121
39598
|
addonId: null,
|
|
39122
39599
|
access: "view"
|
|
39123
39600
|
},
|
|
39601
|
+
"localNetwork.addCustomAddress": {
|
|
39602
|
+
capName: "local-network",
|
|
39603
|
+
capScope: "system",
|
|
39604
|
+
addonId: null,
|
|
39605
|
+
access: "create"
|
|
39606
|
+
},
|
|
39124
39607
|
"localNetwork.downloadCa": {
|
|
39125
39608
|
capName: "local-network",
|
|
39126
39609
|
capScope: "system",
|
|
39127
39610
|
addonId: null,
|
|
39128
39611
|
access: "view"
|
|
39129
39612
|
},
|
|
39613
|
+
"localNetwork.getAddresses": {
|
|
39614
|
+
capName: "local-network",
|
|
39615
|
+
capScope: "system",
|
|
39616
|
+
addonId: null,
|
|
39617
|
+
access: "view"
|
|
39618
|
+
},
|
|
39130
39619
|
"localNetwork.getAllowedAddresses": {
|
|
39131
39620
|
capName: "local-network",
|
|
39132
39621
|
capScope: "system",
|
|
@@ -39139,6 +39628,12 @@ Object.freeze({
|
|
|
39139
39628
|
addonId: null,
|
|
39140
39629
|
access: "view"
|
|
39141
39630
|
},
|
|
39631
|
+
"localNetwork.getHubAddresses": {
|
|
39632
|
+
capName: "local-network",
|
|
39633
|
+
capScope: "system",
|
|
39634
|
+
addonId: null,
|
|
39635
|
+
access: "view"
|
|
39636
|
+
},
|
|
39142
39637
|
"localNetwork.getNotificationEndpoint": {
|
|
39143
39638
|
capName: "local-network",
|
|
39144
39639
|
capScope: "system",
|
|
@@ -39175,6 +39670,12 @@ Object.freeze({
|
|
|
39175
39670
|
addonId: null,
|
|
39176
39671
|
access: "create"
|
|
39177
39672
|
},
|
|
39673
|
+
"localNetwork.removeCustomAddress": {
|
|
39674
|
+
capName: "local-network",
|
|
39675
|
+
capScope: "system",
|
|
39676
|
+
addonId: null,
|
|
39677
|
+
access: "delete"
|
|
39678
|
+
},
|
|
39178
39679
|
"localNetwork.resetAllowlistToBestMatch": {
|
|
39179
39680
|
capName: "local-network",
|
|
39180
39681
|
capScope: "system",
|
|
@@ -39187,6 +39688,12 @@ Object.freeze({
|
|
|
39187
39688
|
addonId: null,
|
|
39188
39689
|
access: "create"
|
|
39189
39690
|
},
|
|
39691
|
+
"localNetwork.setAddressRole": {
|
|
39692
|
+
capName: "local-network",
|
|
39693
|
+
capScope: "system",
|
|
39694
|
+
addonId: null,
|
|
39695
|
+
access: "create"
|
|
39696
|
+
},
|
|
39190
39697
|
"localNetwork.setAllowedAddresses": {
|
|
39191
39698
|
capName: "local-network",
|
|
39192
39699
|
capScope: "system",
|
|
@@ -40681,13 +41188,13 @@ Object.freeze({
|
|
|
40681
41188
|
addonId: null,
|
|
40682
41189
|
access: "delete"
|
|
40683
41190
|
},
|
|
40684
|
-
"pipelineExecutor.
|
|
41191
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": {
|
|
40685
41192
|
capName: "pipeline-executor",
|
|
40686
41193
|
capScope: "system",
|
|
40687
41194
|
addonId: null,
|
|
40688
41195
|
access: "delete"
|
|
40689
41196
|
},
|
|
40690
|
-
"pipelineExecutor.
|
|
41197
|
+
"pipelineExecutor.deleteModel": {
|
|
40691
41198
|
capName: "pipeline-executor",
|
|
40692
41199
|
capScope: "system",
|
|
40693
41200
|
addonId: null,
|
|
@@ -40831,12 +41338,6 @@ Object.freeze({
|
|
|
40831
41338
|
addonId: null,
|
|
40832
41339
|
access: "view"
|
|
40833
41340
|
},
|
|
40834
|
-
"pipelineExecutor.listTemplates": {
|
|
40835
|
-
capName: "pipeline-executor",
|
|
40836
|
-
capScope: "system",
|
|
40837
|
-
addonId: null,
|
|
40838
|
-
access: "view"
|
|
40839
|
-
},
|
|
40840
41341
|
"pipelineExecutor.rearmInferenceDevice": {
|
|
40841
41342
|
capName: "pipeline-executor",
|
|
40842
41343
|
capScope: "system",
|
|
@@ -40861,12 +41362,6 @@ Object.freeze({
|
|
|
40861
41362
|
addonId: null,
|
|
40862
41363
|
access: "create"
|
|
40863
41364
|
},
|
|
40864
|
-
"pipelineExecutor.saveTemplate": {
|
|
40865
|
-
capName: "pipeline-executor",
|
|
40866
|
-
capScope: "system",
|
|
40867
|
-
addonId: null,
|
|
40868
|
-
access: "create"
|
|
40869
|
-
},
|
|
40870
41365
|
"pipelineExecutor.setVideoPipelineSteps": {
|
|
40871
41366
|
capName: "pipeline-executor",
|
|
40872
41367
|
capScope: "system",
|
|
@@ -40885,41 +41380,35 @@ Object.freeze({
|
|
|
40885
41380
|
addonId: null,
|
|
40886
41381
|
access: "create"
|
|
40887
41382
|
},
|
|
40888
|
-
"pipelineExecutor.updateTemplate": {
|
|
40889
|
-
capName: "pipeline-executor",
|
|
40890
|
-
capScope: "system",
|
|
40891
|
-
addonId: null,
|
|
40892
|
-
access: "create"
|
|
40893
|
-
},
|
|
40894
41383
|
"pipelineExecutor.validatePipeline": {
|
|
40895
41384
|
capName: "pipeline-executor",
|
|
40896
41385
|
capScope: "system",
|
|
40897
41386
|
addonId: null,
|
|
40898
41387
|
access: "view"
|
|
40899
41388
|
},
|
|
40900
|
-
"pipelineOrchestrator.
|
|
41389
|
+
"pipelineOrchestrator.applyAutoTune": {
|
|
40901
41390
|
capName: "pipeline-orchestrator",
|
|
40902
41391
|
capScope: "system",
|
|
40903
41392
|
addonId: null,
|
|
40904
41393
|
access: "create"
|
|
40905
41394
|
},
|
|
40906
|
-
"pipelineOrchestrator.
|
|
41395
|
+
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
40907
41396
|
capName: "pipeline-orchestrator",
|
|
40908
41397
|
capScope: "system",
|
|
40909
41398
|
addonId: null,
|
|
40910
41399
|
access: "create"
|
|
40911
41400
|
},
|
|
40912
|
-
"pipelineOrchestrator.
|
|
41401
|
+
"pipelineOrchestrator.assignAudio": {
|
|
40913
41402
|
capName: "pipeline-orchestrator",
|
|
40914
41403
|
capScope: "system",
|
|
40915
41404
|
addonId: null,
|
|
40916
41405
|
access: "create"
|
|
40917
41406
|
},
|
|
40918
|
-
"pipelineOrchestrator.
|
|
41407
|
+
"pipelineOrchestrator.assignPipeline": {
|
|
40919
41408
|
capName: "pipeline-orchestrator",
|
|
40920
41409
|
capScope: "system",
|
|
40921
41410
|
addonId: null,
|
|
40922
|
-
access: "
|
|
41411
|
+
access: "create"
|
|
40923
41412
|
},
|
|
40924
41413
|
"pipelineOrchestrator.getAgentLoad": {
|
|
40925
41414
|
capName: "pipeline-orchestrator",
|
|
@@ -41011,6 +41500,12 @@ Object.freeze({
|
|
|
41011
41500
|
addonId: null,
|
|
41012
41501
|
access: "view"
|
|
41013
41502
|
},
|
|
41503
|
+
"pipelineOrchestrator.getEffectiveShares": {
|
|
41504
|
+
capName: "pipeline-orchestrator",
|
|
41505
|
+
capScope: "system",
|
|
41506
|
+
addonId: null,
|
|
41507
|
+
access: "view"
|
|
41508
|
+
},
|
|
41014
41509
|
"pipelineOrchestrator.getGlobalMetrics": {
|
|
41015
41510
|
capName: "pipeline-orchestrator",
|
|
41016
41511
|
capScope: "system",
|
|
@@ -41047,19 +41542,19 @@ Object.freeze({
|
|
|
41047
41542
|
addonId: null,
|
|
41048
41543
|
access: "view"
|
|
41049
41544
|
},
|
|
41050
|
-
"pipelineOrchestrator.
|
|
41545
|
+
"pipelineOrchestrator.getPlacementStats": {
|
|
41051
41546
|
capName: "pipeline-orchestrator",
|
|
41052
41547
|
capScope: "system",
|
|
41053
41548
|
addonId: null,
|
|
41054
41549
|
access: "view"
|
|
41055
41550
|
},
|
|
41056
|
-
"pipelineOrchestrator.
|
|
41551
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
41057
41552
|
capName: "pipeline-orchestrator",
|
|
41058
41553
|
capScope: "system",
|
|
41059
41554
|
addonId: null,
|
|
41060
41555
|
access: "view"
|
|
41061
41556
|
},
|
|
41062
|
-
"pipelineOrchestrator.
|
|
41557
|
+
"pipelineOrchestrator.listAgentSettings": {
|
|
41063
41558
|
capName: "pipeline-orchestrator",
|
|
41064
41559
|
capScope: "system",
|
|
41065
41560
|
addonId: null,
|
|
@@ -41071,6 +41566,12 @@ Object.freeze({
|
|
|
41071
41566
|
addonId: null,
|
|
41072
41567
|
access: "create"
|
|
41073
41568
|
},
|
|
41569
|
+
"pipelineOrchestrator.planAutoTune": {
|
|
41570
|
+
capName: "pipeline-orchestrator",
|
|
41571
|
+
capScope: "system",
|
|
41572
|
+
addonId: null,
|
|
41573
|
+
access: "view"
|
|
41574
|
+
},
|
|
41074
41575
|
"pipelineOrchestrator.rebalance": {
|
|
41075
41576
|
capName: "pipeline-orchestrator",
|
|
41076
41577
|
capScope: "system",
|
|
@@ -41113,12 +41614,6 @@ Object.freeze({
|
|
|
41113
41614
|
addonId: null,
|
|
41114
41615
|
access: "create"
|
|
41115
41616
|
},
|
|
41116
|
-
"pipelineOrchestrator.saveTemplate": {
|
|
41117
|
-
capName: "pipeline-orchestrator",
|
|
41118
|
-
capScope: "system",
|
|
41119
|
-
addonId: null,
|
|
41120
|
-
access: "create"
|
|
41121
|
-
},
|
|
41122
41617
|
"pipelineOrchestrator.setAgentCapabilities": {
|
|
41123
41618
|
capName: "pipeline-orchestrator",
|
|
41124
41619
|
capScope: "system",
|
|
@@ -41131,12 +41626,6 @@ Object.freeze({
|
|
|
41131
41626
|
addonId: null,
|
|
41132
41627
|
access: "create"
|
|
41133
41628
|
},
|
|
41134
|
-
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
41135
|
-
capName: "pipeline-orchestrator",
|
|
41136
|
-
capScope: "system",
|
|
41137
|
-
addonId: null,
|
|
41138
|
-
access: "create"
|
|
41139
|
-
},
|
|
41140
41629
|
"pipelineOrchestrator.setAgentInferenceDevices": {
|
|
41141
41630
|
capName: "pipeline-orchestrator",
|
|
41142
41631
|
capScope: "system",
|
|
@@ -41173,6 +41662,12 @@ Object.freeze({
|
|
|
41173
41662
|
addonId: null,
|
|
41174
41663
|
access: "create"
|
|
41175
41664
|
},
|
|
41665
|
+
"pipelineOrchestrator.setCameraStepSettings": {
|
|
41666
|
+
capName: "pipeline-orchestrator",
|
|
41667
|
+
capScope: "system",
|
|
41668
|
+
addonId: null,
|
|
41669
|
+
access: "create"
|
|
41670
|
+
},
|
|
41176
41671
|
"pipelineOrchestrator.setCameraStepToggle": {
|
|
41177
41672
|
capName: "pipeline-orchestrator",
|
|
41178
41673
|
capScope: "system",
|
|
@@ -41191,25 +41686,37 @@ Object.freeze({
|
|
|
41191
41686
|
addonId: null,
|
|
41192
41687
|
access: "create"
|
|
41193
41688
|
},
|
|
41689
|
+
"pipelineOrchestrator.setLoadBalancingShares": {
|
|
41690
|
+
capName: "pipeline-orchestrator",
|
|
41691
|
+
capScope: "system",
|
|
41692
|
+
addonId: null,
|
|
41693
|
+
access: "create"
|
|
41694
|
+
},
|
|
41194
41695
|
"pipelineOrchestrator.setPipelineDevicePin": {
|
|
41195
41696
|
capName: "pipeline-orchestrator",
|
|
41196
41697
|
capScope: "system",
|
|
41197
41698
|
addonId: null,
|
|
41198
41699
|
access: "create"
|
|
41199
41700
|
},
|
|
41200
|
-
"pipelineOrchestrator.
|
|
41701
|
+
"pipelineOrchestrator.setShareSource": {
|
|
41201
41702
|
capName: "pipeline-orchestrator",
|
|
41202
41703
|
capScope: "system",
|
|
41203
41704
|
addonId: null,
|
|
41204
41705
|
access: "create"
|
|
41205
41706
|
},
|
|
41206
|
-
"pipelineOrchestrator.
|
|
41707
|
+
"pipelineOrchestrator.suggestShares": {
|
|
41708
|
+
capName: "pipeline-orchestrator",
|
|
41709
|
+
capScope: "system",
|
|
41710
|
+
addonId: null,
|
|
41711
|
+
access: "view"
|
|
41712
|
+
},
|
|
41713
|
+
"pipelineOrchestrator.unassignAudio": {
|
|
41207
41714
|
capName: "pipeline-orchestrator",
|
|
41208
41715
|
capScope: "system",
|
|
41209
41716
|
addonId: null,
|
|
41210
41717
|
access: "create"
|
|
41211
41718
|
},
|
|
41212
|
-
"pipelineOrchestrator.
|
|
41719
|
+
"pipelineOrchestrator.unassignPipeline": {
|
|
41213
41720
|
capName: "pipeline-orchestrator",
|
|
41214
41721
|
capScope: "system",
|
|
41215
41722
|
addonId: null,
|
|
@@ -42649,12 +43156,6 @@ Object.freeze({
|
|
|
42649
43156
|
addonId: null,
|
|
42650
43157
|
access: "view"
|
|
42651
43158
|
},
|
|
42652
|
-
"streamBroker.pullFrameHandles": {
|
|
42653
|
-
capName: "stream-broker",
|
|
42654
|
-
capScope: "system",
|
|
42655
|
-
addonId: null,
|
|
42656
|
-
access: "view"
|
|
42657
|
-
},
|
|
42658
43159
|
"streamBroker.regenerateRtspToken": {
|
|
42659
43160
|
capName: "stream-broker",
|
|
42660
43161
|
capScope: "system",
|
|
@@ -42721,12 +43222,6 @@ Object.freeze({
|
|
|
42721
43222
|
addonId: null,
|
|
42722
43223
|
access: "create"
|
|
42723
43224
|
},
|
|
42724
|
-
"streamBroker.subscribeFrames": {
|
|
42725
|
-
capName: "stream-broker",
|
|
42726
|
-
capScope: "system",
|
|
42727
|
-
addonId: null,
|
|
42728
|
-
access: "create"
|
|
42729
|
-
},
|
|
42730
43225
|
"streamBroker.unassignProfile": {
|
|
42731
43226
|
capName: "stream-broker",
|
|
42732
43227
|
capScope: "system",
|
|
@@ -42739,12 +43234,6 @@ Object.freeze({
|
|
|
42739
43234
|
addonId: null,
|
|
42740
43235
|
access: "create"
|
|
42741
43236
|
},
|
|
42742
|
-
"streamBroker.unsubscribeFrames": {
|
|
42743
|
-
capName: "stream-broker",
|
|
42744
|
-
capScope: "system",
|
|
42745
|
-
addonId: null,
|
|
42746
|
-
access: "create"
|
|
42747
|
-
},
|
|
42748
43237
|
"streamCatalog.getCatalog": {
|
|
42749
43238
|
capName: "stream-catalog",
|
|
42750
43239
|
capScope: "device",
|
|
@@ -43441,6 +43930,12 @@ Object.freeze({
|
|
|
43441
43930
|
addonId: null,
|
|
43442
43931
|
access: "create"
|
|
43443
43932
|
},
|
|
43933
|
+
"webrtcSession.applyDownlinkProbe": {
|
|
43934
|
+
capName: "webrtc-session",
|
|
43935
|
+
capScope: "device",
|
|
43936
|
+
addonId: null,
|
|
43937
|
+
access: "create"
|
|
43938
|
+
},
|
|
43444
43939
|
"webrtcSession.closeSession": {
|
|
43445
43940
|
capName: "webrtc-session",
|
|
43446
43941
|
capScope: "device",
|
|
@@ -45252,11 +45747,6 @@ Object.freeze({
|
|
|
45252
45747
|
form: "single",
|
|
45253
45748
|
optional: false
|
|
45254
45749
|
}],
|
|
45255
|
-
"decoder.createSession": [{
|
|
45256
|
-
name: "deviceId",
|
|
45257
|
-
form: "single",
|
|
45258
|
-
optional: true
|
|
45259
|
-
}],
|
|
45260
45750
|
"detectionPipeline.applyDeviceSettingsPatch": [{
|
|
45261
45751
|
name: "deviceId",
|
|
45262
45752
|
form: "single",
|
|
@@ -46487,6 +46977,11 @@ Object.freeze({
|
|
|
46487
46977
|
form: "single",
|
|
46488
46978
|
optional: false
|
|
46489
46979
|
}],
|
|
46980
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": [{
|
|
46981
|
+
name: "deviceIds",
|
|
46982
|
+
form: "array",
|
|
46983
|
+
optional: true
|
|
46984
|
+
}],
|
|
46490
46985
|
"pipelineExecutor.runPipeline": [{
|
|
46491
46986
|
name: "deviceId",
|
|
46492
46987
|
form: "single",
|
|
@@ -46592,6 +47087,11 @@ Object.freeze({
|
|
|
46592
47087
|
form: "single",
|
|
46593
47088
|
optional: false
|
|
46594
47089
|
}],
|
|
47090
|
+
"pipelineOrchestrator.setCameraStepSettings": [{
|
|
47091
|
+
name: "deviceId",
|
|
47092
|
+
form: "single",
|
|
47093
|
+
optional: false
|
|
47094
|
+
}],
|
|
46595
47095
|
"pipelineOrchestrator.setCameraStepToggle": [{
|
|
46596
47096
|
name: "deviceId",
|
|
46597
47097
|
form: "single",
|
|
@@ -47363,6 +47863,11 @@ Object.freeze({
|
|
|
47363
47863
|
form: "single",
|
|
47364
47864
|
optional: false
|
|
47365
47865
|
}],
|
|
47866
|
+
"webrtcSession.applyDownlinkProbe": [{
|
|
47867
|
+
name: "deviceId",
|
|
47868
|
+
form: "single",
|
|
47869
|
+
optional: false
|
|
47870
|
+
}],
|
|
47366
47871
|
"webrtcSession.closeSession": [{
|
|
47367
47872
|
name: "deviceId",
|
|
47368
47873
|
form: "single",
|
|
@@ -47592,6 +48097,8 @@ new Map(Object.entries({
|
|
|
47592
48097
|
"detection-blind": "device-events",
|
|
47593
48098
|
"node-online": "system",
|
|
47594
48099
|
"node-offline": "system",
|
|
48100
|
+
"node-restarting": "system",
|
|
48101
|
+
"node-updating": "system",
|
|
47595
48102
|
"node-inference-unavailable": "system",
|
|
47596
48103
|
"addon-crash-loop": "system",
|
|
47597
48104
|
"addon-update-available": "system",
|
|
@@ -47669,18 +48176,21 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47669
48176
|
kinds: [
|
|
47670
48177
|
"node-offline",
|
|
47671
48178
|
"node-online",
|
|
48179
|
+
"node-restarting",
|
|
47672
48180
|
"node-inference-unavailable"
|
|
47673
48181
|
]
|
|
47674
48182
|
},
|
|
47675
48183
|
{
|
|
47676
48184
|
id: "addons",
|
|
47677
48185
|
family: "system",
|
|
47678
|
-
label: "Addons",
|
|
47679
|
-
kinds: [
|
|
47680
|
-
|
|
47681
|
-
|
|
47682
|
-
|
|
47683
|
-
|
|
48186
|
+
label: "Addons stopped",
|
|
48187
|
+
kinds: ["addon-crash-loop"]
|
|
48188
|
+
},
|
|
48189
|
+
{
|
|
48190
|
+
id: "addon-updates",
|
|
48191
|
+
family: "system",
|
|
48192
|
+
label: "Addon updates",
|
|
48193
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47684
48194
|
},
|
|
47685
48195
|
{
|
|
47686
48196
|
id: "updates",
|
|
@@ -47688,6 +48198,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
47688
48198
|
label: "Server & container updates",
|
|
47689
48199
|
kinds: [
|
|
47690
48200
|
"server-update-available",
|
|
48201
|
+
"node-updating",
|
|
47691
48202
|
"server-updated",
|
|
47692
48203
|
"wrapper-update-available"
|
|
47693
48204
|
]
|
|
@@ -47835,18 +48346,21 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47835
48346
|
kinds: [
|
|
47836
48347
|
"node-offline",
|
|
47837
48348
|
"node-online",
|
|
48349
|
+
"node-restarting",
|
|
47838
48350
|
"node-inference-unavailable"
|
|
47839
48351
|
]
|
|
47840
48352
|
},
|
|
47841
48353
|
{
|
|
47842
48354
|
name: "summaryAddons",
|
|
47843
|
-
label: "Summary: addons",
|
|
47844
|
-
example: "
|
|
47845
|
-
kinds: [
|
|
47846
|
-
|
|
47847
|
-
|
|
47848
|
-
|
|
47849
|
-
|
|
48355
|
+
label: "Summary: addons stopped",
|
|
48356
|
+
example: "hub — recorder stopped after repeated crashes",
|
|
48357
|
+
kinds: ["addon-crash-loop"]
|
|
48358
|
+
},
|
|
48359
|
+
{
|
|
48360
|
+
name: "summaryAddonUpdates",
|
|
48361
|
+
label: "Summary: addon updates",
|
|
48362
|
+
example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
|
|
48363
|
+
kinds: ["addon-update-available", "addon-updated"]
|
|
47850
48364
|
},
|
|
47851
48365
|
{
|
|
47852
48366
|
name: "summaryUpdates",
|
|
@@ -47854,6 +48368,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
47854
48368
|
example: "server 1.2.377 → 1.2.378",
|
|
47855
48369
|
kinds: [
|
|
47856
48370
|
"server-update-available",
|
|
48371
|
+
"node-updating",
|
|
47857
48372
|
"server-updated",
|
|
47858
48373
|
"wrapper-update-available"
|
|
47859
48374
|
]
|
|
@@ -47886,7 +48401,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47886
48401
|
{
|
|
47887
48402
|
name: "summarySection",
|
|
47888
48403
|
label: "Summary: this section",
|
|
47889
|
-
description: "The section a per-section summary row is about
|
|
48404
|
+
description: "The section a per-section summary row is about; on a summary sent as one message, every section it reports, comma-separated.",
|
|
47890
48405
|
example: "Batterie scariche",
|
|
47891
48406
|
group: "system",
|
|
47892
48407
|
families: ["rule"],
|
|
@@ -47930,6 +48445,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
47930
48445
|
[...RULE_BODY_ONLY],
|
|
47931
48446
|
[...RULE_TEXT],
|
|
47932
48447
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
48448
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
47933
48449
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
47934
48450
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
47935
48451
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -47970,6 +48486,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
47970
48486
|
[...RULE_TEXT],
|
|
47971
48487
|
[...RULE_TEXT],
|
|
47972
48488
|
[...RULE_TEXT],
|
|
48489
|
+
[...RULE_TEXT],
|
|
48490
|
+
[...RULE_TEXT],
|
|
47973
48491
|
...STATE_SUMMARY_VARS
|
|
47974
48492
|
];
|
|
47975
48493
|
new Set([
|
|
@@ -48489,45 +49007,17 @@ DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
|
48489
49007
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
48490
49008
|
var MB = 1024 * 1024;
|
|
48491
49009
|
1024 * MB, 3072 * MB;
|
|
48492
|
-
/**
|
|
48493
|
-
* Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
|
|
48494
|
-
* drain() returns up to maxCount items in FIFO order and removes them.
|
|
48495
|
-
*/
|
|
48496
|
-
var RingBuffer = class {
|
|
48497
|
-
capacity;
|
|
48498
|
-
items;
|
|
48499
|
-
head = 0;
|
|
48500
|
-
tail = 0;
|
|
48501
|
-
count = 0;
|
|
48502
|
-
constructor(capacity) {
|
|
48503
|
-
this.capacity = capacity;
|
|
48504
|
-
this.items = Array.from({ length: capacity });
|
|
48505
|
-
}
|
|
48506
|
-
get size() {
|
|
48507
|
-
return this.count;
|
|
48508
|
-
}
|
|
48509
|
-
push(item) {
|
|
48510
|
-
this.items[this.tail] = item;
|
|
48511
|
-
this.tail = (this.tail + 1) % this.capacity;
|
|
48512
|
-
if (this.count < this.capacity) this.count++;
|
|
48513
|
-
else this.head = (this.head + 1) % this.capacity;
|
|
48514
|
-
}
|
|
48515
|
-
drain(maxCount) {
|
|
48516
|
-
const result = [];
|
|
48517
|
-
const n = Math.min(maxCount, this.count);
|
|
48518
|
-
for (let i = 0; i < n; i++) {
|
|
48519
|
-
result.push(this.items[this.head]);
|
|
48520
|
-
this.items[this.head] = void 0;
|
|
48521
|
-
this.head = (this.head + 1) % this.capacity;
|
|
48522
|
-
}
|
|
48523
|
-
this.count -= n;
|
|
48524
|
-
return result;
|
|
48525
|
-
}
|
|
48526
|
-
};
|
|
48527
49010
|
//#endregion
|
|
48528
|
-
//#region src/
|
|
49011
|
+
//#region src/decode-hwaccel-rank.ts
|
|
48529
49012
|
/**
|
|
48530
|
-
*
|
|
49013
|
+
* Decode-hwaccel ranking for `reprobeHwaccel`: the kernel resolver's preferred
|
|
49014
|
+
* order, re-ranked so the backend this node reports is the one that actually
|
|
49015
|
+
* decodes (vaapi above qsv on Intel). A copy of the broker's
|
|
49016
|
+
* `packages/addon-pipeline/src/shared/ffmpeg-decode-args.ts` ranking — addons
|
|
49017
|
+
* do not import each other.
|
|
49018
|
+
*/
|
|
49019
|
+
/**
|
|
49020
|
+
* Canonical decode-hwaccel preference order.
|
|
48531
49021
|
*
|
|
48532
49022
|
* `vaapi` is ranked ABOVE `qsv` deliberately: on the Intel hub the `qsv` decode
|
|
48533
49023
|
* child exits early (`code=171`, no frames) while the `vaapi` path decodes 8MP
|
|
@@ -48592,1234 +49082,6 @@ function rankDecodeHwAccels(preferred, supportedMethods) {
|
|
|
48592
49082
|
return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
|
|
48593
49083
|
}
|
|
48594
49084
|
//#endregion
|
|
48595
|
-
//#region src/shared/notifying-ring-buffer.ts
|
|
48596
|
-
/**
|
|
48597
|
-
* A {@link RingBuffer} that can notify a single blocked consumer the moment an
|
|
48598
|
-
* item commits — the server side of the `pullFrames` long-poll.
|
|
48599
|
-
*
|
|
48600
|
-
* The decoder's `onFrame` callback drives `push()`; a consumer
|
|
48601
|
-
* that finds the ring momentarily empty calls {@link waitForItem} and is woken
|
|
48602
|
-
* on the very next `push()` (a real commit signal) rather than spinning at
|
|
48603
|
-
* ~1kHz on `setTimeout(…,1)`. If nothing commits it resolves after `waitMs`,
|
|
48604
|
-
* and {@link cancel} (session teardown) resolves any in-flight wait promptly so
|
|
48605
|
-
* a blocked long-poll never waits out `waitMs` on destroy.
|
|
48606
|
-
*
|
|
48607
|
-
* Semantics are otherwise identical to `RingBuffer`: fixed capacity,
|
|
48608
|
-
* latest-wins overwrite when full, FIFO `drain`.
|
|
48609
|
-
*/
|
|
48610
|
-
var NotifyingRingBuffer = class {
|
|
48611
|
-
ring;
|
|
48612
|
-
waiters = /* @__PURE__ */ new Set();
|
|
48613
|
-
constructor(capacity) {
|
|
48614
|
-
this.ring = new RingBuffer(capacity);
|
|
48615
|
-
}
|
|
48616
|
-
get size() {
|
|
48617
|
-
return this.ring.size;
|
|
48618
|
-
}
|
|
48619
|
-
push(item) {
|
|
48620
|
-
this.ring.push(item);
|
|
48621
|
-
this.wakeAll();
|
|
48622
|
-
}
|
|
48623
|
-
drain(maxCount) {
|
|
48624
|
-
return this.ring.drain(maxCount);
|
|
48625
|
-
}
|
|
48626
|
-
/**
|
|
48627
|
-
* Resolve as soon as an item commits to the ring, or after `waitMs`,
|
|
48628
|
-
* whichever comes first. Returns immediately when items are already present
|
|
48629
|
-
* or `waitMs <= 0` (legacy immediate-return). {@link cancel} also resolves an
|
|
48630
|
-
* in-flight wait.
|
|
48631
|
-
*/
|
|
48632
|
-
async waitForItem(waitMs) {
|
|
48633
|
-
if (this.ring.size > 0 || waitMs <= 0) return;
|
|
48634
|
-
await new Promise((resolve) => {
|
|
48635
|
-
let settled = false;
|
|
48636
|
-
const finish = () => {
|
|
48637
|
-
if (settled) return;
|
|
48638
|
-
settled = true;
|
|
48639
|
-
this.waiters.delete(wake);
|
|
48640
|
-
clearTimeout(timer);
|
|
48641
|
-
resolve();
|
|
48642
|
-
};
|
|
48643
|
-
const wake = () => finish();
|
|
48644
|
-
const timer = setTimeout(finish, waitMs);
|
|
48645
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
48646
|
-
this.waiters.add(wake);
|
|
48647
|
-
});
|
|
48648
|
-
}
|
|
48649
|
-
/**
|
|
48650
|
-
* Cancel every in-flight {@link waitForItem} — called on session teardown so
|
|
48651
|
-
* a blocked long-poll returns promptly instead of waiting out `waitMs`.
|
|
48652
|
-
*/
|
|
48653
|
-
cancel() {
|
|
48654
|
-
this.wakeAll();
|
|
48655
|
-
}
|
|
48656
|
-
wakeAll() {
|
|
48657
|
-
if (this.waiters.size === 0) return;
|
|
48658
|
-
const pending = [...this.waiters];
|
|
48659
|
-
this.waiters.clear();
|
|
48660
|
-
for (const wake of pending) wake();
|
|
48661
|
-
}
|
|
48662
|
-
};
|
|
48663
|
-
//#endregion
|
|
48664
|
-
//#region src/shared/decoder-backend-keys.ts
|
|
48665
|
-
/** Built-in default when a node has no selection — the in-process node-av
|
|
48666
|
-
* decoder. node-av is the standing default everywhere; the ffmpeg subprocess
|
|
48667
|
-
* decoder is an explicit opt-in fallback only. A stale/failed settings read at
|
|
48668
|
-
* boot therefore resolves to node-av (never leaves the node with no decoder). */
|
|
48669
|
-
var DEFAULT_DECODER_BACKEND = "nodeav";
|
|
48670
|
-
//#endregion
|
|
48671
|
-
//#region src/shared/decoder-backend.ts
|
|
48672
|
-
/**
|
|
48673
|
-
* Resolve the decoder backend this addon should run. Owner settings are
|
|
48674
|
-
* ignored: persisted `backend@<nodeId>` rows stay in the store and are not
|
|
48675
|
-
* consulted. The runtime default is always {@link DEFAULT_DECODER_BACKEND}.
|
|
48676
|
-
*/
|
|
48677
|
-
async function resolveDecoderBackend(_api, _nodeId, _logger) {
|
|
48678
|
-
return DEFAULT_DECODER_BACKEND;
|
|
48679
|
-
}
|
|
48680
|
-
//#endregion
|
|
48681
|
-
//#region src/pull-demuxer-options.ts
|
|
48682
|
-
/**
|
|
48683
|
-
* libav demuxer options for the node-av PULL decode path — RTP sources
|
|
48684
|
-
* (`isRtpSource()`) dialing the broker's RTSP restream (`rtsp://…/muted`).
|
|
48685
|
-
*
|
|
48686
|
-
* Without low-latency flags libav buffers up to `analyzeduration` (default
|
|
48687
|
-
* **5s**) of the live stream during `avformat_find_stream_info`, and that buffer
|
|
48688
|
-
* becomes a fixed ~5s offset on the decoded-frame timeline — detection overlays
|
|
48689
|
-
* then lag the WebRTC raw-RTP passthrough (which never decodes) by ~5s. Adding
|
|
48690
|
-
* `fflags: nobuffer` + a short `analyzeduration` collapses that offset to
|
|
48691
|
-
* sub-second, mirroring the ffmpeg backend's `-fflags +nobuffer` on its pull
|
|
48692
|
-
* path (decoder-ffmpeg/ffmpeg-args.ts).
|
|
48693
|
-
*
|
|
48694
|
-
* ‼ `analyzeduration`/`probesize` stay SMALL BUT NON-ZERO. Probe-zeroing
|
|
48695
|
-
* (`analyzeduration 0`) on the pull RTSP path STARVES the Reolink rfc4571
|
|
48696
|
-
* restream demuxer (garbage / zero frames — looked like a ~3000fps churn
|
|
48697
|
-
* runaway); that optimisation is push-mode-only. Keep them > 0.
|
|
48698
|
-
*/
|
|
48699
|
-
function buildPullDemuxerOptions() {
|
|
48700
|
-
return {
|
|
48701
|
-
rtsp_transport: "tcp",
|
|
48702
|
-
fflags: "nobuffer",
|
|
48703
|
-
analyzeduration: "1000000",
|
|
48704
|
-
probesize: "1000000",
|
|
48705
|
-
max_delay: "0",
|
|
48706
|
-
reorder_queue_size: "0",
|
|
48707
|
-
user_agent: "decoder"
|
|
48708
|
-
};
|
|
48709
|
-
}
|
|
48710
|
-
//#endregion
|
|
48711
|
-
//#region src/scaler-geometry.ts
|
|
48712
|
-
/**
|
|
48713
|
-
* Decide the scaler action for an incoming frame's source geometry.
|
|
48714
|
-
*
|
|
48715
|
-
* @param current The scaler's configured source geometry, or `null` when no
|
|
48716
|
-
* scaler has been built yet.
|
|
48717
|
-
* @param incoming The source geometry of the frame about to be scaled.
|
|
48718
|
-
*/
|
|
48719
|
-
function resolveScalerAction(current, incoming) {
|
|
48720
|
-
if (current === null) return "init";
|
|
48721
|
-
if (current.width === incoming.width && current.height === incoming.height && current.format === incoming.format) return "reuse";
|
|
48722
|
-
return "rebuild";
|
|
48723
|
-
}
|
|
48724
|
-
//#endregion
|
|
48725
|
-
//#region src/nodeav-decoder-session.ts
|
|
48726
|
-
/**
|
|
48727
|
-
* Pull-mode re-dial backoff. On a pull input error/EOF the loop waits this long
|
|
48728
|
-
* before re-opening the URL — mirrors the ffmpeg pull decoder's 3s re-dial so a
|
|
48729
|
-
* transient RTSP restream blip (source suspended on idle, keyframe wait) does
|
|
48730
|
-
* not hot-loop `Demuxer.open`.
|
|
48731
|
-
*/
|
|
48732
|
-
var PULL_REDIAL_MS = 3e3;
|
|
48733
|
-
/**
|
|
48734
|
-
* Short re-dial backoff for the adaptive "drop-to-live" path. When the decoder
|
|
48735
|
-
* has fallen hopelessly behind the live edge ({@link DROP_TO_LIVE_LAG_MS}) the
|
|
48736
|
-
* loop tears the input down and re-dials fast so the restream's burst-prime
|
|
48737
|
-
* re-seats it at the live edge, instead of waiting the full {@link PULL_REDIAL_MS}
|
|
48738
|
-
* (which is for transient stream blips, not a deliberate re-seat).
|
|
48739
|
-
*/
|
|
48740
|
-
var PULL_FAST_REDIAL_MS = 250;
|
|
48741
|
-
/** Adaptive controller cadence — recompute emit-fps from lag at most this often. */
|
|
48742
|
-
var ADAPTIVE_TICK_MS = 1e3;
|
|
48743
|
-
/** Lag EWMA above this (ms) → halve the adaptive emit-fps. */
|
|
48744
|
-
var ADAPTIVE_LAG_HIGH_MS = 750;
|
|
48745
|
-
/** Lag EWMA below this (ms), sustained {@link ADAPTIVE_RECOVER_SUSTAIN_MS} → grow emit-fps. */
|
|
48746
|
-
var ADAPTIVE_LAG_LOW_MS = 250;
|
|
48747
|
-
/** How long lag must stay below {@link ADAPTIVE_LAG_LOW_MS} before growing emit-fps. */
|
|
48748
|
-
var ADAPTIVE_RECOVER_SUSTAIN_MS = 5e3;
|
|
48749
|
-
/** Floor the adaptive controller never sheds below (fps). */
|
|
48750
|
-
var ADAPTIVE_MIN_FPS = 2;
|
|
48751
|
-
/**
|
|
48752
|
-
* Ceiling the adaptive controller recovers toward when the subscriber requested
|
|
48753
|
-
* an UNLIMITED rate (`maxFps <= 0`). Source cameras run ≤ this, so the clamp is
|
|
48754
|
-
* a no-op in practice while keeping the /2 shed + *1.5 recover math bounded.
|
|
48755
|
-
*/
|
|
48756
|
-
var ADAPTIVE_FALLBACK_CEILING_FPS = 30;
|
|
48757
|
-
/**
|
|
48758
|
-
* Lag EWMA above this (ms) = hopelessly behind → drop-to-live re-seat.
|
|
48759
|
-
*
|
|
48760
|
-
* This bounds the MAX glass-to-box media staleness: `lagMsEwma` is how far the
|
|
48761
|
-
* decoder's media clock trails live, i.e. how old the *content* of an emitted
|
|
48762
|
-
* frame is (distinct from `frameAge`, which only measures decode→pick pipeline
|
|
48763
|
-
* delay and does NOT capture this media lag). On a host that decodes marginally
|
|
48764
|
-
* below real-time the lag sawtooths up to this ceiling, then re-seats to live —
|
|
48765
|
-
* so this constant directly caps how stale the boxes can get. 1200 ms (was
|
|
48766
|
-
* 2000) trades slightly more frequent 250 ms re-seats for fresher overlays; the
|
|
48767
|
-
* re-seat cost is negligible (~1.4 drops/cam/min at the observed drift). The
|
|
48768
|
-
* real fix — keeping decode at real-time so the lag never builds — is the
|
|
48769
|
-
* co-located / subprocess-decode epic.
|
|
48770
|
-
*/
|
|
48771
|
-
var DROP_TO_LIVE_LAG_MS = 1200;
|
|
48772
|
-
/** Never trigger drop-to-live more than once per this window (ms). */
|
|
48773
|
-
var DROP_TO_LIVE_MIN_INTERVAL_MS = 3e3;
|
|
48774
|
-
/** Decoder DEBUG facility flush cadence (~1Hz). */
|
|
48775
|
-
var DEBUG_FLUSH_MS = 1e3;
|
|
48776
|
-
/** Map our canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
|
|
48777
|
-
function backendToHwDeviceConst(backend, consts) {
|
|
48778
|
-
switch (backend) {
|
|
48779
|
-
case "videotoolbox": return consts.AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
|
|
48780
|
-
case "cuda": return consts.AV_HWDEVICE_TYPE_CUDA;
|
|
48781
|
-
case "nvdec": return consts.AV_HWDEVICE_TYPE_CUDA;
|
|
48782
|
-
case "vaapi": return consts.AV_HWDEVICE_TYPE_VAAPI;
|
|
48783
|
-
case "qsv": return consts.AV_HWDEVICE_TYPE_QSV;
|
|
48784
|
-
case "d3d11va": return consts.AV_HWDEVICE_TYPE_D3D11VA;
|
|
48785
|
-
case "dxva2": return consts.AV_HWDEVICE_TYPE_DXVA2;
|
|
48786
|
-
case "amf": return consts.AV_HWDEVICE_TYPE_AMF;
|
|
48787
|
-
case "vdpau": return consts.AV_HWDEVICE_TYPE_VDPAU;
|
|
48788
|
-
case "drm": return consts.AV_HWDEVICE_TYPE_DRM;
|
|
48789
|
-
default: return null;
|
|
48790
|
-
}
|
|
48791
|
-
}
|
|
48792
|
-
/** Pick the hw pixel format for a given backend — used in get_format callback. */
|
|
48793
|
-
function backendToHwPixFmt(backend, consts) {
|
|
48794
|
-
switch (backend) {
|
|
48795
|
-
case "videotoolbox": return consts.AV_PIX_FMT_VIDEOTOOLBOX;
|
|
48796
|
-
case "cuda": return consts.AV_PIX_FMT_CUDA;
|
|
48797
|
-
case "nvdec": return consts.AV_PIX_FMT_CUDA;
|
|
48798
|
-
case "vaapi": return consts.AV_PIX_FMT_VAAPI;
|
|
48799
|
-
case "qsv": return consts.AV_PIX_FMT_QSV;
|
|
48800
|
-
case "d3d11va": return consts.AV_PIX_FMT_D3D11;
|
|
48801
|
-
case "dxva2": return consts.AV_PIX_FMT_DXVA2_VLD;
|
|
48802
|
-
case "amf": return consts.AV_PIX_FMT_D3D11;
|
|
48803
|
-
case "vdpau": return consts.AV_PIX_FMT_VDPAU;
|
|
48804
|
-
case "drm": return consts.AV_PIX_FMT_DRM_PRIME;
|
|
48805
|
-
default: return null;
|
|
48806
|
-
}
|
|
48807
|
-
}
|
|
48808
|
-
var _nav = null;
|
|
48809
|
-
var _consts = null;
|
|
48810
|
-
var _sharp = null;
|
|
48811
|
-
async function getNodeAv() {
|
|
48812
|
-
if (!_nav) _nav = await import("node-av");
|
|
48813
|
-
return _nav;
|
|
48814
|
-
}
|
|
48815
|
-
async function getConstants() {
|
|
48816
|
-
if (!_consts) _consts = await import("node-av/constants");
|
|
48817
|
-
return _consts;
|
|
48818
|
-
}
|
|
48819
|
-
async function getSharp() {
|
|
48820
|
-
if (!_sharp) _sharp = (await import("sharp")).default;
|
|
48821
|
-
return _sharp;
|
|
48822
|
-
}
|
|
48823
|
-
var noopLogger = {
|
|
48824
|
-
debug() {},
|
|
48825
|
-
info() {},
|
|
48826
|
-
warn() {},
|
|
48827
|
-
error() {},
|
|
48828
|
-
child() {
|
|
48829
|
-
return noopLogger;
|
|
48830
|
-
},
|
|
48831
|
-
withTags() {
|
|
48832
|
-
return noopLogger;
|
|
48833
|
-
}
|
|
48834
|
-
};
|
|
48835
|
-
var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
48836
|
-
config;
|
|
48837
|
-
logger;
|
|
48838
|
-
frameCallbacks = /* @__PURE__ */ new Set();
|
|
48839
|
-
destroyed = false;
|
|
48840
|
-
parser = null;
|
|
48841
|
-
codecCtx = null;
|
|
48842
|
-
scaler = null;
|
|
48843
|
-
avPacket = null;
|
|
48844
|
-
avFrame = null;
|
|
48845
|
-
dstFrame = null;
|
|
48846
|
-
EAGAIN = -11;
|
|
48847
|
-
PIX_FMT_GRAY8 = 8;
|
|
48848
|
-
PIX_FMT_RGB24 = 2;
|
|
48849
|
-
SWS_FAST_BILINEAR = 1;
|
|
48850
|
-
/**
|
|
48851
|
-
* Decoder output mode. Drives both the scaler's destination pixel
|
|
48852
|
-
* format and whether sharp runs the JPEG encode at the end:
|
|
48853
|
-
*
|
|
48854
|
-
* - `'jpeg'` — scaler→RGB24 → sharp encode → emit JPEG bytes
|
|
48855
|
-
* - `'rgb'` — scaler→RGB24 → emit raw RGB24 (no sharp)
|
|
48856
|
-
* - `'gray'` — scaler→GRAY8 → emit raw GRAY8 (no sharp)
|
|
48857
|
-
*
|
|
48858
|
-
* The broker holds the policy decision on which mode to request based
|
|
48859
|
-
* on its active subscribers; on-the-fly conversion (e.g. RGB→JPEG for
|
|
48860
|
-
* a WebRTC consumer that joined while detection holds the decoder in
|
|
48861
|
-
* RGB mode) happens broker-side via the per-frame conversion cache.
|
|
48862
|
-
*/
|
|
48863
|
-
outputMode;
|
|
48864
|
-
sharpFn = null;
|
|
48865
|
-
/**
|
|
48866
|
-
* Backpressure for the sharp JPEG encode pipeline. The broker
|
|
48867
|
-
* currently creates sessions with `maxFps: 0` (unlimited) and relies
|
|
48868
|
-
* on per-subscriber throttling, so without a bound the
|
|
48869
|
-
* fire-and-forget `sharp(...).toBuffer()` chain would accumulate
|
|
48870
|
-
* unboundedly whenever sharp falls behind the decoder. Cap at
|
|
48871
|
-
* `MAX_JPEG_INFLIGHT` pending encodes per session — any frame that
|
|
48872
|
-
* arrives while the cap is saturated is dropped and counted.
|
|
48873
|
-
*/
|
|
48874
|
-
static MAX_JPEG_INFLIGHT = 2;
|
|
48875
|
-
jpegEncodeInFlight = 0;
|
|
48876
|
-
/**
|
|
48877
|
-
* Map a `DecoderSessionConfig.outputFormat` value to one of the three
|
|
48878
|
-
* native scaler/encoder modes the session understands. The cap-level
|
|
48879
|
-
* format vocabulary is broader (it accepts `bgr`, `yuv420`) than what
|
|
48880
|
-
* libav's scaler is wired for here — anything else degrades to RGB
|
|
48881
|
-
* (the canonical raw mode) and the broker is expected to convert
|
|
48882
|
-
* downstream if a subscriber needs a different shape.
|
|
48883
|
-
*/
|
|
48884
|
-
static resolveOutputMode(format) {
|
|
48885
|
-
if (format === "jpeg" || format === void 0) return "jpeg";
|
|
48886
|
-
if (format === "gray") return "gray";
|
|
48887
|
-
return "rgb";
|
|
48888
|
-
}
|
|
48889
|
-
initialized = false;
|
|
48890
|
-
initializing = false;
|
|
48891
|
-
scalerInitializing = false;
|
|
48892
|
-
/**
|
|
48893
|
-
* Monotonic counter incremented by `updateConfig` whenever the
|
|
48894
|
-
* scaler + dstFrame get invalidated (e.g. output format toggle).
|
|
48895
|
-
* `initScaler` captures the current value at entry and aborts — or
|
|
48896
|
-
* disposes the locally-built scaler — if the epoch moved while
|
|
48897
|
-
* its async init was in flight. Without this, a toggle racing an
|
|
48898
|
-
* in-flight init could leave two scalers allocated natively while
|
|
48899
|
-
* `this.scaler` only holds a reference to one → libav leak.
|
|
48900
|
-
*/
|
|
48901
|
-
scalerEpoch = 0;
|
|
48902
|
-
/**
|
|
48903
|
-
* One-shot guard for the "first frame" diagnostic log + raw frame
|
|
48904
|
-
* dump. Setting this synchronously inside `emitDecodedFrame`
|
|
48905
|
-
* prevents re-entry — without it we were using `outputFrames === 0`
|
|
48906
|
-
* which stays true until the async sharp encode callback runs, so
|
|
48907
|
-
* several decoded frames could trigger the dump before the first
|
|
48908
|
-
* JPEG landed.
|
|
48909
|
-
*/
|
|
48910
|
-
firstFrameLogged = false;
|
|
48911
|
-
outWidth = 0;
|
|
48912
|
-
outHeight = 0;
|
|
48913
|
-
/**
|
|
48914
|
-
* The SOURCE geometry the current `scaler` was built for. `sws_scale` trusts
|
|
48915
|
-
* these dimensions blindly; if a later decoded frame arrives with different
|
|
48916
|
-
* width/height/format we MUST rebuild the scaler rather than feed it mismatched
|
|
48917
|
-
* planes (an out-of-bounds read/write → uncatchable SIGBUS). `-1` means "no
|
|
48918
|
-
* scaler built yet" (mirrors `this.scaler === null`).
|
|
48919
|
-
*/
|
|
48920
|
-
scalerSrcW = -1;
|
|
48921
|
-
scalerSrcH = -1;
|
|
48922
|
-
scalerSrcFmt = -1;
|
|
48923
|
-
lastEmitTime = 0;
|
|
48924
|
-
minIntervalMs;
|
|
48925
|
-
/** Gates {@link emitDebugFlash}; set from `config.debug === true`. Off = zero cost. */
|
|
48926
|
-
debugEnabled;
|
|
48927
|
-
/** Wall-clock of the last `decoder debug` flush — throttles the facility to ~1Hz. */
|
|
48928
|
-
lastDebugFlush = 0;
|
|
48929
|
-
/** `droppedFrames` snapshot at the last debug flush — window base for the drop delta. */
|
|
48930
|
-
debugWindowDropped = 0;
|
|
48931
|
-
/** Subscriber-requested emit ceiling (fps). `maxFps<=0` clamps to the fallback ceiling. */
|
|
48932
|
-
ceilingFps;
|
|
48933
|
-
/** Current lag-throttled emit rate (fps) — starts at the ceiling, floored at ADAPTIVE_MIN_FPS. */
|
|
48934
|
-
adaptiveFps;
|
|
48935
|
-
/** Frames actually emitted/sec over the last adaptive-tick window (feeds getStats + debug). */
|
|
48936
|
-
effectiveFps = 0;
|
|
48937
|
-
/** `outputFrames` snapshot at the last adaptive tick — window base for effective-fps. */
|
|
48938
|
-
adaptiveWindowFrames = 0;
|
|
48939
|
-
/** First sampled frame's pts (timebase units); `null` until the current dial seeds it. */
|
|
48940
|
-
basePts = null;
|
|
48941
|
-
/** Wall-clock at `basePts` — the real-time anchor for the drift computation. */
|
|
48942
|
-
baseWall = 0;
|
|
48943
|
-
/** EWMA of real-time drift (ms). Rising = decoder falling behind the live edge. */
|
|
48944
|
-
lagMsEwma = 0;
|
|
48945
|
-
/** Wall-clock the lag EWMA first dropped below ADAPTIVE_LAG_LOW_MS (0 = not low). */
|
|
48946
|
-
lowLagSince = 0;
|
|
48947
|
-
/** Wall-clock of the last adaptive-controller tick. */
|
|
48948
|
-
lastAdaptiveCheck = 0;
|
|
48949
|
-
/** Wall-clock of the last drop-to-live re-seat — rate-limits the re-dial. */
|
|
48950
|
-
lastDropToLive = 0;
|
|
48951
|
-
/** Set by drop-to-live so `runPullLoop` uses the SHORT re-dial backoff for one iteration. */
|
|
48952
|
-
pullFastRedial = false;
|
|
48953
|
-
inputPackets = 0;
|
|
48954
|
-
outputFrames = 0;
|
|
48955
|
-
droppedFrames = 0;
|
|
48956
|
-
totalDecodeTimeMs = 0;
|
|
48957
|
-
startTime = Date.now();
|
|
48958
|
-
hwaccelPref;
|
|
48959
|
-
hwaccelResolver;
|
|
48960
|
-
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
48961
|
-
activeHwAccel = "none";
|
|
48962
|
-
hwDevice = null;
|
|
48963
|
-
swTransferFrame = null;
|
|
48964
|
-
/** True once `openStream` has armed the pull loop; drives `isPullMode`. */
|
|
48965
|
-
pullActive = false;
|
|
48966
|
-
/** One-shot guard so a stray push into a pull session logs once, not per-packet. */
|
|
48967
|
-
pullStrayPushLogged = false;
|
|
48968
|
-
/** The URL the pull loop (re)dials — updatable while active. */
|
|
48969
|
-
pullUrl = null;
|
|
48970
|
-
/** Pending re-dial timer + its resolver, so `destroy` can wake it instantly. */
|
|
48971
|
-
pullRedialTimer = null;
|
|
48972
|
-
pullRedialResolve = null;
|
|
48973
|
-
/** High-level demuxer/decoder for the CURRENT dial — recreated per re-dial. */
|
|
48974
|
-
pullDemuxer = null;
|
|
48975
|
-
pullDecoder = null;
|
|
48976
|
-
/** HW context — resolved once, reused across re-dials, freed in `destroy`. */
|
|
48977
|
-
pullHwContext = null;
|
|
48978
|
-
constructor(config, logger = noopLogger, options) {
|
|
48979
|
-
this.config = { ...config };
|
|
48980
|
-
const sessionTags = {};
|
|
48981
|
-
if (typeof config.deviceId === "number") sessionTags["deviceId"] = config.deviceId;
|
|
48982
|
-
if (typeof config.tag === "string" && config.tag.length > 0) sessionTags["tag"] = config.tag;
|
|
48983
|
-
this.logger = Object.keys(sessionTags).length > 0 ? logger.withTags(sessionTags) : logger;
|
|
48984
|
-
this.minIntervalMs = config.maxFps > 0 ? 1e3 / config.maxFps : 0;
|
|
48985
|
-
this.debugEnabled = config.debug === true;
|
|
48986
|
-
this.ceilingFps = config.maxFps > 0 ? config.maxFps : ADAPTIVE_FALLBACK_CEILING_FPS;
|
|
48987
|
-
this.adaptiveFps = this.ceilingFps;
|
|
48988
|
-
this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
|
|
48989
|
-
this.hwaccelPref = options?.hwaccel ?? "auto";
|
|
48990
|
-
this.hwaccelResolver = options?.hwaccelResolver ?? null;
|
|
48991
|
-
}
|
|
48992
|
-
/**
|
|
48993
|
-
* Resolve the backend preference list and try each one against
|
|
48994
|
-
* node-av's HW context APIs. The first backend whose
|
|
48995
|
-
* `HardwareDeviceContext.create()` succeeds gets attached to
|
|
48996
|
-
* `codecCtx.hwDeviceCtx` + its hw pixel format registered via
|
|
48997
|
-
* `setHardwarePixelFormat`. On any failure, falls through to the
|
|
48998
|
-
* next backend; if all fail, returns with `activeHwAccel='none'`
|
|
48999
|
-
* and the decoder runs in software on the same context.
|
|
49000
|
-
*/
|
|
49001
|
-
async tryAttachHwAccel(nav, C) {
|
|
49002
|
-
if (!this.codecCtx) return;
|
|
49003
|
-
if (this.hwaccelPref === "none") {
|
|
49004
|
-
this.activeHwAccel = "none";
|
|
49005
|
-
return;
|
|
49006
|
-
}
|
|
49007
|
-
const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
|
|
49008
|
-
const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
|
|
49009
|
-
preferred: [explicit],
|
|
49010
|
-
rationale: "explicit (no resolver)"
|
|
49011
|
-
} : {
|
|
49012
|
-
preferred: [],
|
|
49013
|
-
rationale: "auto + no resolver → software"
|
|
49014
|
-
};
|
|
49015
|
-
if (resolution.preferred.length === 0) {
|
|
49016
|
-
this.activeHwAccel = "none";
|
|
49017
|
-
return;
|
|
49018
|
-
}
|
|
49019
|
-
for (const backend of resolution.preferred) {
|
|
49020
|
-
const deviceType = backendToHwDeviceConst(backend, C);
|
|
49021
|
-
const hwPixFmt = backendToHwPixFmt(backend, C);
|
|
49022
|
-
if (!deviceType || !hwPixFmt) continue;
|
|
49023
|
-
const device = new nav.HardwareDeviceContext();
|
|
49024
|
-
const rc = device.create(deviceType);
|
|
49025
|
-
if (rc < 0) {
|
|
49026
|
-
this.logger.warn("node-av: hwaccel device create failed — trying next", { meta: {
|
|
49027
|
-
backend,
|
|
49028
|
-
rc
|
|
49029
|
-
} });
|
|
49030
|
-
device.free();
|
|
49031
|
-
continue;
|
|
49032
|
-
}
|
|
49033
|
-
try {
|
|
49034
|
-
this.codecCtx.hwDeviceCtx = device;
|
|
49035
|
-
this.codecCtx.setHardwarePixelFormat(hwPixFmt);
|
|
49036
|
-
} catch (err) {
|
|
49037
|
-
this.logger.warn("node-av: hwaccel context attach failed — trying next", { meta: {
|
|
49038
|
-
backend,
|
|
49039
|
-
error: errMsg(err)
|
|
49040
|
-
} });
|
|
49041
|
-
device.free();
|
|
49042
|
-
continue;
|
|
49043
|
-
}
|
|
49044
|
-
this.hwDevice = device;
|
|
49045
|
-
this.activeHwAccel = backend;
|
|
49046
|
-
return;
|
|
49047
|
-
}
|
|
49048
|
-
this.logger.warn("node-av: no hwaccel backend initialised — using software", { meta: {
|
|
49049
|
-
attempted: resolution.preferred.join(","),
|
|
49050
|
-
rationale: resolution.rationale
|
|
49051
|
-
} });
|
|
49052
|
-
this.activeHwAccel = "none";
|
|
49053
|
-
}
|
|
49054
|
-
/**
|
|
49055
|
-
* Download a HW frame (format == hw pix fmt) into a SW frame so the
|
|
49056
|
-
* rest of the pipeline (scaler, JPEG encoder, grayscale passthrough)
|
|
49057
|
-
* handles it identically to the pure-software path. Uses the sync
|
|
49058
|
-
* variant so the synchronous receive loop below doesn't need to be
|
|
49059
|
-
* async-ified. Returns `null` on transfer failure, meaning the
|
|
49060
|
-
* caller should drop the frame.
|
|
49061
|
-
*/
|
|
49062
|
-
transferHwFrame(hwFrame) {
|
|
49063
|
-
if (this.activeHwAccel === "none" || !this.swTransferFrame) return hwFrame;
|
|
49064
|
-
const rc = hwFrame.hwframeTransferDataSync(this.swTransferFrame, 0);
|
|
49065
|
-
if (rc < 0) {
|
|
49066
|
-
this.logger.warn("node-av: hwframeTransferData failed", { meta: { rc } });
|
|
49067
|
-
return null;
|
|
49068
|
-
}
|
|
49069
|
-
return this.swTransferFrame;
|
|
49070
|
-
}
|
|
49071
|
-
/**
|
|
49072
|
-
* Initialize the decoder pipeline on the first keyframe.
|
|
49073
|
-
* After this returns, all hot-path methods are fully synchronous (except JPEG encode).
|
|
49074
|
-
*/
|
|
49075
|
-
async initDecoder() {
|
|
49076
|
-
if (this.initialized || this.initializing || this.destroyed) return;
|
|
49077
|
-
this.initializing = true;
|
|
49078
|
-
try {
|
|
49079
|
-
const nav = await getNodeAv();
|
|
49080
|
-
const C = await getConstants();
|
|
49081
|
-
this.EAGAIN = C.AVERROR_EAGAIN;
|
|
49082
|
-
this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
|
|
49083
|
-
this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
|
|
49084
|
-
this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
|
|
49085
|
-
if (this.outputMode === "jpeg") this.sharpFn = await getSharp();
|
|
49086
|
-
nav.Log.setLevel(C.AV_LOG_FATAL);
|
|
49087
|
-
const codecId = this.config.codec === "h265" || this.config.codec === "hevc" ? C.AV_CODEC_ID_HEVC : C.AV_CODEC_ID_H264;
|
|
49088
|
-
this.parser = new nav.CodecParser();
|
|
49089
|
-
this.parser.init(codecId);
|
|
49090
|
-
const codec = nav.Codec.findDecoder(codecId);
|
|
49091
|
-
if (!codec) {
|
|
49092
|
-
this.logger.error("node-av: no decoder found", { meta: { codec: this.config.codec } });
|
|
49093
|
-
return;
|
|
49094
|
-
}
|
|
49095
|
-
this.codecCtx = new nav.CodecContext();
|
|
49096
|
-
this.codecCtx.allocContext3(codec);
|
|
49097
|
-
if (this.config.width && this.config.height) {
|
|
49098
|
-
this.codecCtx.width = this.config.width;
|
|
49099
|
-
this.codecCtx.height = this.config.height;
|
|
49100
|
-
}
|
|
49101
|
-
this.codecCtx.threadCount = 1;
|
|
49102
|
-
await this.tryAttachHwAccel(nav, C);
|
|
49103
|
-
const ret = await this.codecCtx.open2(codec);
|
|
49104
|
-
if (ret < 0) if (this.activeHwAccel !== "none") {
|
|
49105
|
-
this.logger.warn("node-av: open2 failed with hwaccel — retrying in software", { meta: {
|
|
49106
|
-
ret,
|
|
49107
|
-
hwAccel: this.activeHwAccel
|
|
49108
|
-
} });
|
|
49109
|
-
this.hwDevice?.free();
|
|
49110
|
-
this.hwDevice = null;
|
|
49111
|
-
this.activeHwAccel = "none";
|
|
49112
|
-
this.codecCtx = new nav.CodecContext();
|
|
49113
|
-
this.codecCtx.allocContext3(codec);
|
|
49114
|
-
if (this.config.width && this.config.height) {
|
|
49115
|
-
this.codecCtx.width = this.config.width;
|
|
49116
|
-
this.codecCtx.height = this.config.height;
|
|
49117
|
-
}
|
|
49118
|
-
this.codecCtx.threadCount = 1;
|
|
49119
|
-
const retry = await this.codecCtx.open2(codec);
|
|
49120
|
-
if (retry < 0) {
|
|
49121
|
-
this.logger.error("node-av: failed to open decoder (sw fallback)", { meta: { ret: retry } });
|
|
49122
|
-
return;
|
|
49123
|
-
}
|
|
49124
|
-
} else {
|
|
49125
|
-
this.logger.error("node-av: failed to open decoder", { meta: { ret } });
|
|
49126
|
-
return;
|
|
49127
|
-
}
|
|
49128
|
-
if (this.activeHwAccel !== "none") {
|
|
49129
|
-
this.swTransferFrame = new nav.Frame();
|
|
49130
|
-
this.swTransferFrame.alloc();
|
|
49131
|
-
}
|
|
49132
|
-
this.avPacket = new nav.Packet();
|
|
49133
|
-
this.avPacket.alloc();
|
|
49134
|
-
this.avFrame = new nav.Frame();
|
|
49135
|
-
this.avFrame.alloc();
|
|
49136
|
-
this.initialized = true;
|
|
49137
|
-
this.logger.info("node-av push decoder initialized", { meta: {
|
|
49138
|
-
codec: this.config.codec,
|
|
49139
|
-
output: this.outputMode,
|
|
49140
|
-
hwAccel: this.activeHwAccel
|
|
49141
|
-
} });
|
|
49142
|
-
} catch (err) {
|
|
49143
|
-
this.logger.error("node-av init error", { meta: { error: errMsg(err) } });
|
|
49144
|
-
} finally {
|
|
49145
|
-
this.initializing = false;
|
|
49146
|
-
}
|
|
49147
|
-
}
|
|
49148
|
-
/**
|
|
49149
|
-
* Initialize the scaler after the first frame tells us the actual
|
|
49150
|
-
* dimensions. Output pixel format: RGB24 for JPEG encoding, GRAY8
|
|
49151
|
-
* for raw motion.
|
|
49152
|
-
*
|
|
49153
|
-
* Builds `scaler` + `dstFrame` on local variables and publishes
|
|
49154
|
-
* them onto `this` in a single atomic step at the end. Captures
|
|
49155
|
-
* `scalerEpoch` at entry; if `updateConfig` invalidated the scaler
|
|
49156
|
-
* while this init was in flight (epoch mismatch), the locally
|
|
49157
|
-
* built pair is disposed and discarded so the later init wins.
|
|
49158
|
-
* Without the local-first approach, partial state (scaler set,
|
|
49159
|
-
* dstFrame still null) could be observed by a concurrent
|
|
49160
|
-
* `emitDecodedFrame` call.
|
|
49161
|
-
*/
|
|
49162
|
-
async initScaler(srcW, srcH, srcFmt) {
|
|
49163
|
-
if (this.scalerInitializing) return;
|
|
49164
|
-
this.scalerInitializing = true;
|
|
49165
|
-
const myEpoch = this.scalerEpoch;
|
|
49166
|
-
let localScaler = null;
|
|
49167
|
-
let localDstFrame = null;
|
|
49168
|
-
try {
|
|
49169
|
-
const nav = await getNodeAv();
|
|
49170
|
-
if (this.destroyed || myEpoch !== this.scalerEpoch) return;
|
|
49171
|
-
const scale = this.config.scale > 1 ? this.config.scale : 1;
|
|
49172
|
-
const maxW = Math.floor(640 / scale);
|
|
49173
|
-
const outWidth = Math.min(srcW, maxW);
|
|
49174
|
-
const outHeight = Math.round(outWidth * srcH / srcW);
|
|
49175
|
-
const dstFmt = this.outputMode === "gray" ? this.PIX_FMT_GRAY8 : this.PIX_FMT_RGB24;
|
|
49176
|
-
const fmtName = this.outputMode === "gray" ? "gray8" : "rgb24";
|
|
49177
|
-
localScaler = new nav.SoftwareScaleContext();
|
|
49178
|
-
localScaler.getContext(srcW, srcH, srcFmt, outWidth, outHeight, dstFmt, this.SWS_FAST_BILINEAR);
|
|
49179
|
-
const ret = localScaler.initContext();
|
|
49180
|
-
if (ret < 0) {
|
|
49181
|
-
this.logger.error("node-av: sws_init_context failed", { meta: { ret } });
|
|
49182
|
-
return;
|
|
49183
|
-
}
|
|
49184
|
-
localDstFrame = new nav.Frame();
|
|
49185
|
-
localDstFrame.alloc();
|
|
49186
|
-
localDstFrame.width = outWidth;
|
|
49187
|
-
localDstFrame.height = outHeight;
|
|
49188
|
-
localDstFrame.format = dstFmt;
|
|
49189
|
-
const allocRet = localDstFrame.allocBuffer();
|
|
49190
|
-
if (allocRet < 0) {
|
|
49191
|
-
this.logger.error("node-av: dst frame allocBuffer failed", { meta: { ret: allocRet } });
|
|
49192
|
-
return;
|
|
49193
|
-
}
|
|
49194
|
-
if (this.destroyed || myEpoch !== this.scalerEpoch) return;
|
|
49195
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49196
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49197
|
-
this.scaler = localScaler;
|
|
49198
|
-
this.dstFrame = localDstFrame;
|
|
49199
|
-
this.outWidth = outWidth;
|
|
49200
|
-
this.outHeight = outHeight;
|
|
49201
|
-
this.scalerSrcW = srcW;
|
|
49202
|
-
this.scalerSrcH = srcH;
|
|
49203
|
-
this.scalerSrcFmt = srcFmt;
|
|
49204
|
-
localScaler = null;
|
|
49205
|
-
localDstFrame = null;
|
|
49206
|
-
this.logger.info("node-av scaler initialized", { meta: {
|
|
49207
|
-
srcWidth: srcW,
|
|
49208
|
-
srcHeight: srcH,
|
|
49209
|
-
outWidth,
|
|
49210
|
-
outHeight,
|
|
49211
|
-
format: fmtName
|
|
49212
|
-
} });
|
|
49213
|
-
} catch (err) {
|
|
49214
|
-
this.logger.error("Scaler init failed", { meta: { error: errMsg(err) } });
|
|
49215
|
-
} finally {
|
|
49216
|
-
localScaler?.[Symbol.dispose]?.();
|
|
49217
|
-
localDstFrame?.[Symbol.dispose]?.();
|
|
49218
|
-
this.scalerInitializing = false;
|
|
49219
|
-
}
|
|
49220
|
-
}
|
|
49221
|
-
/**
|
|
49222
|
-
* Start PULL mode: libav dials `url` and demuxes it in-process, decoding
|
|
49223
|
-
* every frame through the same emit path as push mode. Idempotent while
|
|
49224
|
-
* active (updates the re-dial target). Throws if the session already took a
|
|
49225
|
-
* push packet — a session is push-mode or pull-mode for its whole life.
|
|
49226
|
-
*/
|
|
49227
|
-
openStream(url) {
|
|
49228
|
-
if (this.destroyed) return Promise.resolve();
|
|
49229
|
-
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");
|
|
49230
|
-
if (this.pullActive) {
|
|
49231
|
-
this.pullUrl = url;
|
|
49232
|
-
return Promise.resolve();
|
|
49233
|
-
}
|
|
49234
|
-
this.pullActive = true;
|
|
49235
|
-
this.pullUrl = url;
|
|
49236
|
-
this.logger.info("node-av decoder: pull-mode openStream — dialing input URL", { meta: { url } });
|
|
49237
|
-
this.runPullLoop().catch((err) => {
|
|
49238
|
-
this.logger.error("node-av decoder: pull loop crashed", { meta: { error: errMsg(err) } });
|
|
49239
|
-
});
|
|
49240
|
-
return Promise.resolve();
|
|
49241
|
-
}
|
|
49242
|
-
/**
|
|
49243
|
-
* The pull-mode supervisor loop: resolve HW once, then repeatedly dial +
|
|
49244
|
-
* decode the URL, re-dialing after {@link PULL_REDIAL_MS} on any exit/error
|
|
49245
|
-
* until the session is destroyed. Each iteration rebuilds a fresh
|
|
49246
|
-
* demuxer/decoder (the previous one is disposed in `closePullInput`).
|
|
49247
|
-
*/
|
|
49248
|
-
async runPullLoop() {
|
|
49249
|
-
const nav = await getNodeAv();
|
|
49250
|
-
const C = await getConstants();
|
|
49251
|
-
this.EAGAIN = C.AVERROR_EAGAIN;
|
|
49252
|
-
this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
|
|
49253
|
-
this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
|
|
49254
|
-
this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
|
|
49255
|
-
nav.Log.setLevel(C.AV_LOG_FATAL);
|
|
49256
|
-
if (this.outputMode === "jpeg" && !this.sharpFn) this.sharpFn = await getSharp();
|
|
49257
|
-
await this.ensurePullHwContext(nav, C);
|
|
49258
|
-
while (!this.destroyed && this.pullActive) {
|
|
49259
|
-
const url = this.pullUrl;
|
|
49260
|
-
if (!url) break;
|
|
49261
|
-
try {
|
|
49262
|
-
await this.pullDialAndDecode(nav, C, url);
|
|
49263
|
-
} catch (err) {
|
|
49264
|
-
if (this.destroyed) break;
|
|
49265
|
-
this.logger.warn("node-av decoder: pull-mode dial ended — re-dial scheduled", { meta: {
|
|
49266
|
-
error: errMsg(err),
|
|
49267
|
-
redialInMs: PULL_REDIAL_MS
|
|
49268
|
-
} });
|
|
49269
|
-
} finally {
|
|
49270
|
-
this.closePullInput();
|
|
49271
|
-
}
|
|
49272
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49273
|
-
const backoffMs = this.pullFastRedial ? PULL_FAST_REDIAL_MS : PULL_REDIAL_MS;
|
|
49274
|
-
this.pullFastRedial = false;
|
|
49275
|
-
await this.pullSleep(backoffMs);
|
|
49276
|
-
}
|
|
49277
|
-
}
|
|
49278
|
-
/**
|
|
49279
|
-
* One dial: open the demuxer, pick the video stream, create a HW/SW decoder
|
|
49280
|
-
* that downloads + normalises frames to YUV420P (so the shared scaler in
|
|
49281
|
-
* `emitDecodedFrame` reads plain software planes regardless of hwaccel), and
|
|
49282
|
-
* pump `demuxer.packets(video) → decoder.frames → emitDecodedFrame` until the
|
|
49283
|
-
* stream ends or the session is torn down.
|
|
49284
|
-
*/
|
|
49285
|
-
async pullDialAndDecode(nav, C, url) {
|
|
49286
|
-
const demuxer = await nav.Demuxer.open(url, { options: buildPullDemuxerOptions() });
|
|
49287
|
-
if (this.destroyed || !this.pullActive) {
|
|
49288
|
-
demuxer[Symbol.dispose]?.();
|
|
49289
|
-
return;
|
|
49290
|
-
}
|
|
49291
|
-
this.pullDemuxer = demuxer;
|
|
49292
|
-
const videoStream = demuxer.video();
|
|
49293
|
-
if (!videoStream) throw new Error("node-av decoder: pull input has no video stream");
|
|
49294
|
-
const decoder = await nav.Decoder.create(videoStream, {
|
|
49295
|
-
...this.pullHwContext ? { hardware: this.pullHwContext } : {},
|
|
49296
|
-
rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
|
|
49297
|
-
exitOnError: false
|
|
49298
|
-
});
|
|
49299
|
-
if (this.destroyed || !this.pullActive) {
|
|
49300
|
-
decoder[Symbol.dispose]?.();
|
|
49301
|
-
return;
|
|
49302
|
-
}
|
|
49303
|
-
this.pullDecoder = decoder;
|
|
49304
|
-
this.logger.info("node-av pull decoder initialized", { meta: {
|
|
49305
|
-
codec: this.config.codec,
|
|
49306
|
-
output: this.outputMode,
|
|
49307
|
-
hwAccel: this.activeHwAccel,
|
|
49308
|
-
streamIndex: videoStream.index
|
|
49309
|
-
} });
|
|
49310
|
-
const probeTb = videoStream.timeBase;
|
|
49311
|
-
const tbNum = probeTb?.num ?? 0;
|
|
49312
|
-
const tbDen = probeTb?.den ?? 0;
|
|
49313
|
-
this.resetLagTracker(Date.now());
|
|
49314
|
-
await this.consumePullFrames(decoder.frames(demuxer.packets(videoStream.index)), tbNum, tbDen);
|
|
49315
|
-
}
|
|
49316
|
-
/**
|
|
49317
|
-
* Consume decoded pull frames until the stream ends or the session is torn
|
|
49318
|
-
* down. Each yielded frame is a node-av `Frame` — for the HW path a `clone`
|
|
49319
|
-
* that refs a VAAPI surface + the decoder's whole `hw_frames_ctx`, so a SINGLE
|
|
49320
|
-
* unfreed frame pins the dial's entire GPU surface pool (i915 GEM/shmem) until
|
|
49321
|
-
* the process dies. Therefore EVERY exit path (teardown break, drop-to-live
|
|
49322
|
-
* re-seat, normal emit, or a throw from the lag/emit calls) MUST `frame.free()`
|
|
49323
|
-
* — the `try/finally` below is the single release point that guarantees it.
|
|
49324
|
-
*
|
|
49325
|
-
* Extracted from {@link pullDialAndDecode} so this free-on-all-paths contract
|
|
49326
|
-
* is unit-testable with a stub async iterable (no live libav / GPU needed).
|
|
49327
|
-
*/
|
|
49328
|
-
async consumePullFrames(frames, tbNum, tbDen) {
|
|
49329
|
-
for await (const frame of frames) {
|
|
49330
|
-
if (!frame) {
|
|
49331
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49332
|
-
continue;
|
|
49333
|
-
}
|
|
49334
|
-
try {
|
|
49335
|
-
if (this.destroyed || !this.pullActive) break;
|
|
49336
|
-
this.inputPackets++;
|
|
49337
|
-
const wallNow = Date.now();
|
|
49338
|
-
this.updateLagTracker(frame.pts, tbNum, tbDen, wallNow);
|
|
49339
|
-
this.runAdaptiveController(wallNow);
|
|
49340
|
-
this.emitDebugFlash(wallNow);
|
|
49341
|
-
if (this.shouldDropToLive(wallNow)) {
|
|
49342
|
-
this.logger.warn("node-av decoder: lag beyond recovery — dropping to live edge", { meta: {
|
|
49343
|
-
lagMs: Math.round(this.lagMsEwma),
|
|
49344
|
-
redialInMs: PULL_FAST_REDIAL_MS
|
|
49345
|
-
} });
|
|
49346
|
-
this.pullFastRedial = true;
|
|
49347
|
-
break;
|
|
49348
|
-
}
|
|
49349
|
-
this.emitDecodedFrame(frame);
|
|
49350
|
-
} finally {
|
|
49351
|
-
frame.free();
|
|
49352
|
-
}
|
|
49353
|
-
}
|
|
49354
|
-
}
|
|
49355
|
-
/**
|
|
49356
|
-
* Resolve and build the HW context for pull mode ONCE (reused across every
|
|
49357
|
-
* re-dial, freed in `destroy`). Explicit backend → single try; `'auto'` → the
|
|
49358
|
-
* kernel resolver's ordered list; no resolver → software. The first
|
|
49359
|
-
* `HardwareContext.create` that succeeds wins; all failures fall through to
|
|
49360
|
-
* software decode (`hardware: null`).
|
|
49361
|
-
*/
|
|
49362
|
-
async ensurePullHwContext(nav, C) {
|
|
49363
|
-
if (this.hwaccelPref === "none") {
|
|
49364
|
-
this.activeHwAccel = "none";
|
|
49365
|
-
return;
|
|
49366
|
-
}
|
|
49367
|
-
const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
|
|
49368
|
-
const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
|
|
49369
|
-
preferred: [explicit],
|
|
49370
|
-
rationale: "explicit (no resolver)"
|
|
49371
|
-
} : {
|
|
49372
|
-
preferred: [],
|
|
49373
|
-
rationale: "auto + no resolver → software"
|
|
49374
|
-
};
|
|
49375
|
-
for (const backend of resolution.preferred) {
|
|
49376
|
-
const deviceType = backendToHwDeviceConst(backend, C);
|
|
49377
|
-
if (!deviceType) continue;
|
|
49378
|
-
const hw = nav.HardwareContext.create(deviceType);
|
|
49379
|
-
if (!hw) {
|
|
49380
|
-
this.logger.debug("node-av: pull hwaccel context create failed — trying next", { meta: { backend } });
|
|
49381
|
-
continue;
|
|
49382
|
-
}
|
|
49383
|
-
this.pullHwContext = hw;
|
|
49384
|
-
this.activeHwAccel = backend;
|
|
49385
|
-
return;
|
|
49386
|
-
}
|
|
49387
|
-
this.logger.warn("node-av: no pull hwaccel backend initialised — using software", { meta: {
|
|
49388
|
-
attempted: resolution.preferred.join(","),
|
|
49389
|
-
rationale: resolution.rationale
|
|
49390
|
-
} });
|
|
49391
|
-
this.activeHwAccel = "none";
|
|
49392
|
-
}
|
|
49393
|
-
/** Dispose the CURRENT dial's demuxer + decoder (HW context is reused). */
|
|
49394
|
-
closePullInput() {
|
|
49395
|
-
if (this.pullDecoder) {
|
|
49396
|
-
this.pullDecoder[Symbol.dispose]?.();
|
|
49397
|
-
this.pullDecoder = null;
|
|
49398
|
-
}
|
|
49399
|
-
if (this.pullDemuxer) {
|
|
49400
|
-
this.pullDemuxer[Symbol.dispose]?.();
|
|
49401
|
-
this.pullDemuxer = null;
|
|
49402
|
-
}
|
|
49403
|
-
}
|
|
49404
|
-
/** Interruptible sleep for the re-dial backoff — `destroy` resolves it early. */
|
|
49405
|
-
pullSleep(ms) {
|
|
49406
|
-
return new Promise((resolve) => {
|
|
49407
|
-
this.pullRedialResolve = resolve;
|
|
49408
|
-
this.pullRedialTimer = setTimeout(() => {
|
|
49409
|
-
this.pullRedialTimer = null;
|
|
49410
|
-
this.pullRedialResolve = null;
|
|
49411
|
-
resolve();
|
|
49412
|
-
}, ms);
|
|
49413
|
-
});
|
|
49414
|
-
}
|
|
49415
|
-
/**
|
|
49416
|
-
* Re-anchor the real-time lag tracker + per-dial controllers at the start of
|
|
49417
|
-
* each dial. A fresh dial (including a drop-to-live re-seat) begins at the
|
|
49418
|
-
* live edge, so the drift EWMA resets to 0 — otherwise the just-torn dial's
|
|
49419
|
-
* accumulated lag would re-trigger drop-to-live immediately. The learned
|
|
49420
|
-
* `adaptiveFps` is intentionally NOT reset: a re-seat should keep the shed
|
|
49421
|
-
* emit-rate and recover from there once lag stays low.
|
|
49422
|
-
*/
|
|
49423
|
-
resetLagTracker(wallNow) {
|
|
49424
|
-
this.basePts = null;
|
|
49425
|
-
this.baseWall = 0;
|
|
49426
|
-
this.lagMsEwma = 0;
|
|
49427
|
-
this.lowLagSince = 0;
|
|
49428
|
-
this.lastAdaptiveCheck = wallNow;
|
|
49429
|
-
this.adaptiveWindowFrames = this.outputFrames;
|
|
49430
|
-
this.lastDebugFlush = wallNow;
|
|
49431
|
-
this.debugWindowDropped = this.droppedFrames;
|
|
49432
|
-
}
|
|
49433
|
-
/**
|
|
49434
|
-
* Update the EWMA of the decoder's real-time drift from a decoded frame's
|
|
49435
|
-
* pts. The first frame of a dial anchors `basePts`/`baseWall`; every later
|
|
49436
|
-
* frame compares elapsed wall-clock against elapsed media time —
|
|
49437
|
-
* `lagMs = wallElapsed - mediaElapsed`. A rising EWMA means the decoder is
|
|
49438
|
-
* falling behind the live edge (wall time outrunning the media timeline).
|
|
49439
|
-
*/
|
|
49440
|
-
updateLagTracker(pts, tbNum, tbDen, wallNow) {
|
|
49441
|
-
if (tbDen === 0) return;
|
|
49442
|
-
if (this.basePts === null) {
|
|
49443
|
-
this.basePts = pts;
|
|
49444
|
-
this.baseWall = wallNow;
|
|
49445
|
-
return;
|
|
49446
|
-
}
|
|
49447
|
-
const sample = wallNow - this.baseWall - Number(pts - this.basePts) * 1e3 * tbNum / tbDen;
|
|
49448
|
-
this.lagMsEwma = .9 * this.lagMsEwma + .1 * sample;
|
|
49449
|
-
}
|
|
49450
|
-
/**
|
|
49451
|
-
* Lag-driven adaptive emit-fps controller (P1), self-throttled to
|
|
49452
|
-
* {@link ADAPTIVE_TICK_MS}. Under load (lag EWMA high) it halves the emit-fps
|
|
49453
|
-
* — fewer frames transferred + GPU-scaled + emitted, so the decoder stays
|
|
49454
|
-
* real-time instead of accumulating lag; once lag stays low for a sustained
|
|
49455
|
-
* window it grows the emit-fps back toward the subscriber's ceiling.
|
|
49456
|
-
* `minIntervalMs` (the throttle both emit paths honour) is recomputed from
|
|
49457
|
-
* `adaptiveFps`, NOT the raw ceiling.
|
|
49458
|
-
*/
|
|
49459
|
-
runAdaptiveController(wallNow) {
|
|
49460
|
-
const windowMs = wallNow - this.lastAdaptiveCheck;
|
|
49461
|
-
if (windowMs < ADAPTIVE_TICK_MS) return;
|
|
49462
|
-
this.lastAdaptiveCheck = wallNow;
|
|
49463
|
-
const framesDelta = this.outputFrames - this.adaptiveWindowFrames;
|
|
49464
|
-
this.adaptiveWindowFrames = this.outputFrames;
|
|
49465
|
-
this.effectiveFps = windowMs > 0 ? framesDelta * 1e3 / windowMs : 0;
|
|
49466
|
-
const lag = this.lagMsEwma;
|
|
49467
|
-
if (lag > ADAPTIVE_LAG_HIGH_MS) {
|
|
49468
|
-
this.adaptiveFps = Math.max(ADAPTIVE_MIN_FPS, this.adaptiveFps / 2);
|
|
49469
|
-
this.lowLagSince = 0;
|
|
49470
|
-
} else if (lag < ADAPTIVE_LAG_LOW_MS) {
|
|
49471
|
-
if (this.lowLagSince === 0) this.lowLagSince = wallNow;
|
|
49472
|
-
else if (wallNow - this.lowLagSince >= ADAPTIVE_RECOVER_SUSTAIN_MS) {
|
|
49473
|
-
this.adaptiveFps = Math.min(this.ceilingFps, this.adaptiveFps * 1.5);
|
|
49474
|
-
this.lowLagSince = wallNow;
|
|
49475
|
-
}
|
|
49476
|
-
} else this.lowLagSince = 0;
|
|
49477
|
-
this.minIntervalMs = this.adaptiveFps > 0 ? 1e3 / this.adaptiveFps : 0;
|
|
49478
|
-
}
|
|
49479
|
-
/**
|
|
49480
|
-
* Whether the decoder is hopelessly behind the live edge and should tear the
|
|
49481
|
-
* input down for a fast re-seat. Rate-limited to at most once per
|
|
49482
|
-
* {@link DROP_TO_LIVE_MIN_INTERVAL_MS} so a bad stretch can't hot-loop the
|
|
49483
|
-
* dial. Records the trigger time as a side effect when it returns `true`.
|
|
49484
|
-
*/
|
|
49485
|
-
shouldDropToLive(wallNow) {
|
|
49486
|
-
if (this.lagMsEwma <= DROP_TO_LIVE_LAG_MS) return false;
|
|
49487
|
-
if (wallNow - this.lastDropToLive < DROP_TO_LIVE_MIN_INTERVAL_MS) return false;
|
|
49488
|
-
this.lastDropToLive = wallNow;
|
|
49489
|
-
return true;
|
|
49490
|
-
}
|
|
49491
|
-
/**
|
|
49492
|
-
* Per-camera DEBUG facility (mirrors the stream-broker's `streamingDebug`
|
|
49493
|
-
* gate). When `config.debug` is set, flush a single structured `decoder
|
|
49494
|
-
* debug` line at ~1Hz — effective/adaptive fps, real-time lag, dropped-frame
|
|
49495
|
-
* delta, avg decode time, hwaccel. `deviceId`/`tag` ride on the logger tags.
|
|
49496
|
-
* Cheap: all values come from running counters, and the whole method is a
|
|
49497
|
-
* no-op (one comparison) when debug is disabled or the window hasn't elapsed.
|
|
49498
|
-
*/
|
|
49499
|
-
emitDebugFlash(wallNow) {
|
|
49500
|
-
if (!this.debugEnabled) return;
|
|
49501
|
-
if (wallNow - this.lastDebugFlush < DEBUG_FLUSH_MS) return;
|
|
49502
|
-
this.lastDebugFlush = wallNow;
|
|
49503
|
-
const droppedDelta = this.droppedFrames - this.debugWindowDropped;
|
|
49504
|
-
this.debugWindowDropped = this.droppedFrames;
|
|
49505
|
-
const decodeMsAvg = this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0;
|
|
49506
|
-
this.logger.info("decoder debug", { meta: {
|
|
49507
|
-
effectiveFps: Number(this.effectiveFps.toFixed(1)),
|
|
49508
|
-
adaptiveFps: Number(this.adaptiveFps.toFixed(1)),
|
|
49509
|
-
ceilingFps: this.ceilingFps,
|
|
49510
|
-
lagMs: Math.round(this.lagMsEwma),
|
|
49511
|
-
droppedFrames: droppedDelta,
|
|
49512
|
-
decodeMs: Number(decodeMsAvg.toFixed(2)),
|
|
49513
|
-
hwAccel: this.activeHwAccel
|
|
49514
|
-
} });
|
|
49515
|
-
}
|
|
49516
|
-
pushPacket(packet) {
|
|
49517
|
-
if (this.destroyed) return;
|
|
49518
|
-
if (this.pullActive) {
|
|
49519
|
-
if (!this.pullStrayPushLogged) {
|
|
49520
|
-
this.pullStrayPushLogged = true;
|
|
49521
|
-
this.logger.debug("node-av decoder: pushPacket on a pull-mode session — ignored");
|
|
49522
|
-
}
|
|
49523
|
-
return;
|
|
49524
|
-
}
|
|
49525
|
-
this.inputPackets++;
|
|
49526
|
-
if (!this.initialized && !this.initializing && packet.keyframe) {
|
|
49527
|
-
this.initDecoder().then(() => {
|
|
49528
|
-
if (this.initialized) this.decodeRawData(packet.data, packet.pts ?? Date.now());
|
|
49529
|
-
}).catch((err) => {
|
|
49530
|
-
this.logger.error("node-av init failed", { meta: { error: errMsg(err) } });
|
|
49531
|
-
});
|
|
49532
|
-
return;
|
|
49533
|
-
}
|
|
49534
|
-
if (!this.initialized) return;
|
|
49535
|
-
this.decodeRawData(packet.data, packet.pts ?? Date.now());
|
|
49536
|
-
}
|
|
49537
|
-
decodeRawData(data, pts) {
|
|
49538
|
-
if (!this.parser || !this.codecCtx || !this.avPacket || !this.avFrame) return;
|
|
49539
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
49540
|
-
const bigPts = BigInt(pts);
|
|
49541
|
-
let offset = 0;
|
|
49542
|
-
while (offset < buf.length) {
|
|
49543
|
-
const remaining = buf.subarray(offset);
|
|
49544
|
-
const consumed = this.parser.parse2(this.codecCtx, this.avPacket, remaining, bigPts, bigPts, offset);
|
|
49545
|
-
if (consumed < 0) {
|
|
49546
|
-
this.logger.warn("node-av parser error", { meta: { ret: consumed } });
|
|
49547
|
-
break;
|
|
49548
|
-
}
|
|
49549
|
-
offset += consumed;
|
|
49550
|
-
if (this.avPacket.size > 0) {
|
|
49551
|
-
this.decodePacket();
|
|
49552
|
-
this.avPacket.unref();
|
|
49553
|
-
}
|
|
49554
|
-
}
|
|
49555
|
-
}
|
|
49556
|
-
decodePacket() {
|
|
49557
|
-
if (!this.codecCtx || !this.avFrame) return;
|
|
49558
|
-
const sendRet = this.codecCtx.sendPacketSync(this.avPacket);
|
|
49559
|
-
if (sendRet < 0 && sendRet !== this.EAGAIN) {
|
|
49560
|
-
this.logger.warn("node-av sendPacket error", { meta: { ret: sendRet } });
|
|
49561
|
-
return;
|
|
49562
|
-
}
|
|
49563
|
-
while (true) {
|
|
49564
|
-
if (this.codecCtx.receiveFrameSync(this.avFrame) < 0) break;
|
|
49565
|
-
const frame = this.transferHwFrame(this.avFrame);
|
|
49566
|
-
if (!frame) continue;
|
|
49567
|
-
this.emitDecodedFrame(frame);
|
|
49568
|
-
}
|
|
49569
|
-
}
|
|
49570
|
-
emitDecodedFrame(frame) {
|
|
49571
|
-
const now = performance.now();
|
|
49572
|
-
if (frame.isHwFrame()) {
|
|
49573
|
-
this.droppedFrames++;
|
|
49574
|
-
return;
|
|
49575
|
-
}
|
|
49576
|
-
if (this.minIntervalMs > 0 && now - this.lastEmitTime < this.minIntervalMs) {
|
|
49577
|
-
this.droppedFrames++;
|
|
49578
|
-
return;
|
|
49579
|
-
}
|
|
49580
|
-
if (this.scalerInitializing) return;
|
|
49581
|
-
const incoming = {
|
|
49582
|
-
width: frame.width,
|
|
49583
|
-
height: frame.height,
|
|
49584
|
-
format: frame.format
|
|
49585
|
-
};
|
|
49586
|
-
const current = this.scaler !== null ? {
|
|
49587
|
-
width: this.scalerSrcW,
|
|
49588
|
-
height: this.scalerSrcH,
|
|
49589
|
-
format: this.scalerSrcFmt
|
|
49590
|
-
} : null;
|
|
49591
|
-
const action = resolveScalerAction(current, incoming);
|
|
49592
|
-
if (action === "init") {
|
|
49593
|
-
this.initScaler(incoming.width, incoming.height, incoming.format);
|
|
49594
|
-
return;
|
|
49595
|
-
}
|
|
49596
|
-
if (action === "rebuild") {
|
|
49597
|
-
this.logger.info("node-av: decoded source geometry changed — rebuilding scaler", { meta: {
|
|
49598
|
-
from: current,
|
|
49599
|
-
to: incoming
|
|
49600
|
-
} });
|
|
49601
|
-
this.scalerEpoch++;
|
|
49602
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49603
|
-
this.scaler = null;
|
|
49604
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49605
|
-
this.dstFrame = null;
|
|
49606
|
-
this.scalerSrcW = -1;
|
|
49607
|
-
this.scalerSrcH = -1;
|
|
49608
|
-
this.scalerSrcFmt = -1;
|
|
49609
|
-
this.initScaler(incoming.width, incoming.height, incoming.format);
|
|
49610
|
-
return;
|
|
49611
|
-
}
|
|
49612
|
-
if (!this.dstFrame || !this.scaler) return;
|
|
49613
|
-
const decodeStart = performance.now();
|
|
49614
|
-
try {
|
|
49615
|
-
this.dstFrame.makeWritable();
|
|
49616
|
-
this.scaler.scaleFrameSync(this.dstFrame, frame);
|
|
49617
|
-
} catch (err) {
|
|
49618
|
-
this.logger.warn("node-av scale error", { meta: { error: errMsg(err) } });
|
|
49619
|
-
return;
|
|
49620
|
-
}
|
|
49621
|
-
if (!this.firstFrameLogged) {
|
|
49622
|
-
this.firstFrameLogged = true;
|
|
49623
|
-
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
49624
|
-
const ls = this.dstFrame.linesize;
|
|
49625
|
-
const buf = this.dstFrame.toBuffer();
|
|
49626
|
-
this.logger.info("dstFrame after scale", { meta: {
|
|
49627
|
-
phase: "frame-debug",
|
|
49628
|
-
width: this.dstFrame.width,
|
|
49629
|
-
height: this.dstFrame.height,
|
|
49630
|
-
linesize: [
|
|
49631
|
-
ls[0],
|
|
49632
|
-
ls[1],
|
|
49633
|
-
ls[2]
|
|
49634
|
-
],
|
|
49635
|
-
expectedStride: this.dstFrame.width * channels,
|
|
49636
|
-
bufLen: buf.length,
|
|
49637
|
-
expectedPacked: this.dstFrame.width * channels * this.dstFrame.height,
|
|
49638
|
-
srcFormat: frame.format ?? "?"
|
|
49639
|
-
} });
|
|
49640
|
-
if (this.outputMode !== "gray") import("node:fs").then((fsModule) => {
|
|
49641
|
-
import("node:path").then((pathModule) => {
|
|
49642
|
-
const dumpPath = pathModule.join(process.cwd(), "camstack-data", "debug-frame-rgb24.raw");
|
|
49643
|
-
fsModule.writeFileSync(dumpPath, buf);
|
|
49644
|
-
this.logger.info("Dumped first RGB24 frame", { meta: {
|
|
49645
|
-
phase: "frame-debug",
|
|
49646
|
-
path: dumpPath,
|
|
49647
|
-
bytes: buf.length
|
|
49648
|
-
} });
|
|
49649
|
-
}).catch(() => {});
|
|
49650
|
-
}).catch(() => {});
|
|
49651
|
-
}
|
|
49652
|
-
const rawBuf = this.extractPackedBuffer(this.dstFrame);
|
|
49653
|
-
if (this.outputMode === "jpeg") this.encodeAndEmitJpeg(rawBuf, decodeStart);
|
|
49654
|
-
else if (this.outputMode === "rgb") this.emitRawFrame(rawBuf, "rgb", decodeStart);
|
|
49655
|
-
else this.emitRawFrame(rawBuf, "gray", decodeStart);
|
|
49656
|
-
}
|
|
49657
|
-
/**
|
|
49658
|
-
* Extract packed pixel buffer from a decoded frame.
|
|
49659
|
-
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
49660
|
-
* Sharp and WASM consumers expect tightly-packed rows (stride = width * channels).
|
|
49661
|
-
* If linesize matches expected stride, return the buffer directly (zero-copy).
|
|
49662
|
-
*/
|
|
49663
|
-
extractPackedBuffer(frame) {
|
|
49664
|
-
const channels = this.outputMode === "gray" ? 1 : 3;
|
|
49665
|
-
const expectedStride = frame.width * channels;
|
|
49666
|
-
const actualStride = frame.linesize[0] ?? expectedStride;
|
|
49667
|
-
const src = frame.data?.[0];
|
|
49668
|
-
if (!src) return frame.toBuffer();
|
|
49669
|
-
if (actualStride === expectedStride) return Buffer.from(src.buffer, src.byteOffset, expectedStride * frame.height);
|
|
49670
|
-
const dst = Buffer.allocUnsafe(expectedStride * frame.height);
|
|
49671
|
-
for (let y = 0; y < frame.height; y++) src.copy(dst, y * expectedStride, y * actualStride, y * actualStride + expectedStride);
|
|
49672
|
-
return dst;
|
|
49673
|
-
}
|
|
49674
|
-
/**
|
|
49675
|
-
* Encode RGB24 raw buffer as JPEG and emit.
|
|
49676
|
-
*
|
|
49677
|
-
* Drops the frame (and counts it) when `MAX_JPEG_INFLIGHT` encodes
|
|
49678
|
-
* are already pending — prevents unbounded growth of the
|
|
49679
|
-
* fire-and-forget promise chain when sharp cannot keep up with the
|
|
49680
|
-
* decode rate.
|
|
49681
|
-
*/
|
|
49682
|
-
encodeAndEmitJpeg(rgb, decodeStart) {
|
|
49683
|
-
if (!this.sharpFn) return;
|
|
49684
|
-
if (this.jpegEncodeInFlight >= NodeAvDecoderSession.MAX_JPEG_INFLIGHT) {
|
|
49685
|
-
this.droppedFrames++;
|
|
49686
|
-
return;
|
|
49687
|
-
}
|
|
49688
|
-
this.jpegEncodeInFlight++;
|
|
49689
|
-
this.sharpFn(rgb, { raw: {
|
|
49690
|
-
width: this.outWidth,
|
|
49691
|
-
height: this.outHeight,
|
|
49692
|
-
channels: 3
|
|
49693
|
-
} }).jpeg({
|
|
49694
|
-
quality: 80,
|
|
49695
|
-
mozjpeg: false
|
|
49696
|
-
}).toBuffer().then((jpegBuf) => {
|
|
49697
|
-
if (this.destroyed) return;
|
|
49698
|
-
this.emitRawFrame(jpegBuf, "jpeg", decodeStart);
|
|
49699
|
-
}).catch((err) => {
|
|
49700
|
-
this.logger.warn("sharp jpeg encode error", { meta: { error: errMsg(err) } });
|
|
49701
|
-
}).finally(() => {
|
|
49702
|
-
this.jpegEncodeInFlight--;
|
|
49703
|
-
});
|
|
49704
|
-
}
|
|
49705
|
-
emitRawFrame(data, format, decodeStart) {
|
|
49706
|
-
const decodeMs = performance.now() - decodeStart;
|
|
49707
|
-
this.totalDecodeTimeMs += decodeMs;
|
|
49708
|
-
this.outputFrames++;
|
|
49709
|
-
this.lastEmitTime = performance.now();
|
|
49710
|
-
if (this.outputFrames === 1 || this.outputFrames % 500 === 0) this.logger.info("node-av frame emitted", { meta: {
|
|
49711
|
-
frameNumber: this.outputFrames,
|
|
49712
|
-
width: this.outWidth,
|
|
49713
|
-
height: this.outHeight,
|
|
49714
|
-
format,
|
|
49715
|
-
decodeMs,
|
|
49716
|
-
subs: this.frameCallbacks.size
|
|
49717
|
-
} });
|
|
49718
|
-
const decodedFrame = {
|
|
49719
|
-
data,
|
|
49720
|
-
width: this.outWidth,
|
|
49721
|
-
height: this.outHeight,
|
|
49722
|
-
format,
|
|
49723
|
-
timestamp: Date.now()
|
|
49724
|
-
};
|
|
49725
|
-
for (const cb of this.frameCallbacks) cb(decodedFrame);
|
|
49726
|
-
}
|
|
49727
|
-
onFrame(callback) {
|
|
49728
|
-
this.frameCallbacks.add(callback);
|
|
49729
|
-
return () => {
|
|
49730
|
-
this.frameCallbacks.delete(callback);
|
|
49731
|
-
};
|
|
49732
|
-
}
|
|
49733
|
-
updateConfig(update) {
|
|
49734
|
-
const prevFormat = this.config.outputFormat;
|
|
49735
|
-
this.config = {
|
|
49736
|
-
...this.config,
|
|
49737
|
-
...update
|
|
49738
|
-
};
|
|
49739
|
-
if (update.maxFps !== void 0) this.minIntervalMs = update.maxFps > 0 ? 1e3 / update.maxFps : 0;
|
|
49740
|
-
if (update.outputFormat !== void 0 && update.outputFormat !== prevFormat) {
|
|
49741
|
-
const prevMode = this.outputMode;
|
|
49742
|
-
this.outputMode = NodeAvDecoderSession.resolveOutputMode(update.outputFormat);
|
|
49743
|
-
if (this.outputMode === prevMode) return;
|
|
49744
|
-
this.scalerEpoch++;
|
|
49745
|
-
if (this.scaler) {
|
|
49746
|
-
this.scaler[Symbol.dispose]?.();
|
|
49747
|
-
this.scaler = null;
|
|
49748
|
-
}
|
|
49749
|
-
if (this.dstFrame) {
|
|
49750
|
-
this.dstFrame[Symbol.dispose]?.();
|
|
49751
|
-
this.dstFrame = null;
|
|
49752
|
-
}
|
|
49753
|
-
this.scalerSrcW = -1;
|
|
49754
|
-
this.scalerSrcH = -1;
|
|
49755
|
-
this.scalerSrcFmt = -1;
|
|
49756
|
-
if (this.outputMode === "jpeg" && !this.sharpFn) getSharp().then((fn) => {
|
|
49757
|
-
this.sharpFn = fn;
|
|
49758
|
-
}).catch(() => {});
|
|
49759
|
-
this.logger.info("node-av: output format changed — scaler will reinit", { meta: {
|
|
49760
|
-
from: prevFormat,
|
|
49761
|
-
to: update.outputFormat,
|
|
49762
|
-
mode: this.outputMode
|
|
49763
|
-
} });
|
|
49764
|
-
}
|
|
49765
|
-
}
|
|
49766
|
-
async destroy() {
|
|
49767
|
-
if (this.destroyed) return;
|
|
49768
|
-
this.destroyed = true;
|
|
49769
|
-
this.frameCallbacks.clear();
|
|
49770
|
-
this.pullActive = false;
|
|
49771
|
-
if (this.pullRedialTimer) {
|
|
49772
|
-
clearTimeout(this.pullRedialTimer);
|
|
49773
|
-
this.pullRedialTimer = null;
|
|
49774
|
-
}
|
|
49775
|
-
if (this.pullRedialResolve) {
|
|
49776
|
-
const resolve = this.pullRedialResolve;
|
|
49777
|
-
this.pullRedialResolve = null;
|
|
49778
|
-
resolve();
|
|
49779
|
-
}
|
|
49780
|
-
this.closePullInput();
|
|
49781
|
-
if (this.pullHwContext) {
|
|
49782
|
-
if (!this.pullHwContext.isDisposed) this.pullHwContext.dispose();
|
|
49783
|
-
this.pullHwContext = null;
|
|
49784
|
-
}
|
|
49785
|
-
this.dstFrame?.[Symbol.dispose]?.();
|
|
49786
|
-
this.avFrame?.[Symbol.dispose]?.();
|
|
49787
|
-
this.avPacket?.[Symbol.dispose]?.();
|
|
49788
|
-
this.scaler?.[Symbol.dispose]?.();
|
|
49789
|
-
this.parser?.[Symbol.dispose]?.();
|
|
49790
|
-
this.swTransferFrame?.[Symbol.dispose]?.();
|
|
49791
|
-
this.codecCtx?.[Symbol.dispose]?.();
|
|
49792
|
-
this.hwDevice?.free();
|
|
49793
|
-
this.dstFrame = null;
|
|
49794
|
-
this.avFrame = null;
|
|
49795
|
-
this.avPacket = null;
|
|
49796
|
-
this.scaler = null;
|
|
49797
|
-
this.parser = null;
|
|
49798
|
-
this.codecCtx = null;
|
|
49799
|
-
this.swTransferFrame = null;
|
|
49800
|
-
this.hwDevice = null;
|
|
49801
|
-
}
|
|
49802
|
-
getStats() {
|
|
49803
|
-
const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
|
|
49804
|
-
const base = {
|
|
49805
|
-
inputFps: this.inputPackets / uptimeSec,
|
|
49806
|
-
outputFps: this.outputFrames / uptimeSec,
|
|
49807
|
-
avgDecodeTimeMs: this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0,
|
|
49808
|
-
droppedFrames: this.droppedFrames
|
|
49809
|
-
};
|
|
49810
|
-
if (!this.pullActive) return base;
|
|
49811
|
-
return {
|
|
49812
|
-
...base,
|
|
49813
|
-
lagMs: this.lagMsEwma,
|
|
49814
|
-
effectiveFps: this.effectiveFps,
|
|
49815
|
-
adaptiveFps: this.adaptiveFps
|
|
49816
|
-
};
|
|
49817
|
-
}
|
|
49818
|
-
get isPullMode() {
|
|
49819
|
-
return this.pullActive;
|
|
49820
|
-
}
|
|
49821
|
-
};
|
|
49822
|
-
//#endregion
|
|
49823
49085
|
//#region src/audio-codec/codec-catalog.ts
|
|
49824
49086
|
/**
|
|
49825
49087
|
* Normalise an SDP-reported codec name to the libav/ffmpeg name. Mirrors
|
|
@@ -50635,21 +49897,22 @@ function peekNodeAvRuntime() {
|
|
|
50635
49897
|
}
|
|
50636
49898
|
//#endregion
|
|
50637
49899
|
//#region src/addon/index.ts
|
|
50638
|
-
|
|
49900
|
+
/**
|
|
49901
|
+
* The node's `decoder` + `audio-codec` provider.
|
|
49902
|
+
*
|
|
49903
|
+
* The `decoder` cap is per-node hwaccel only (D763): `getInfo` reports this
|
|
49904
|
+
* node's operator choice and probed best backend, `reprobeHwaccel` re-runs the
|
|
49905
|
+
* probe. Both are per-node settings (`hwaccel@<node>`,
|
|
49906
|
+
* `probedBestHwaccel@<node>`); the egress transcode, HomeKit and the load view
|
|
49907
|
+
* read them through `getInfo`, pinned to the node. Video decoding itself runs in the pipeline runner's session-decode
|
|
49908
|
+
* workers, which use node-av directly. The `audio-codec` cap is the node-av
|
|
49909
|
+
* codec (encode/decode + resample) every audio session on the node runs on.
|
|
49910
|
+
*/
|
|
50639
49911
|
var DecoderNodeAvAddon = class extends BaseAddon {
|
|
50640
49912
|
/**
|
|
50641
|
-
*
|
|
50642
|
-
* this addon
|
|
50643
|
-
|
|
50644
|
-
sessions = /* @__PURE__ */ new Map();
|
|
50645
|
-
/** Pixel-frame buffers drained by `pullFrames`. */
|
|
50646
|
-
frameBuffers = /* @__PURE__ */ new Map();
|
|
50647
|
-
unsubscribers = /* @__PURE__ */ new Map();
|
|
50648
|
-
sessionMeta = /* @__PURE__ */ new Map();
|
|
50649
|
-
/**
|
|
50650
|
-
* node-av audio-codec provider — registered independently of the video
|
|
50651
|
-
* decoder backend gate whenever node-av loads on this node, so this single
|
|
50652
|
-
* addon provides both the `decoder` and `audio-codec` caps.
|
|
49913
|
+
* node-av audio-codec provider — registered whenever node-av loads on this
|
|
49914
|
+
* node, so this single addon provides both the `decoder` and `audio-codec`
|
|
49915
|
+
* caps.
|
|
50653
49916
|
*/
|
|
50654
49917
|
audioProvider = null;
|
|
50655
49918
|
constructor() {
|
|
@@ -50660,7 +49923,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50660
49923
|
id: "hwaccel",
|
|
50661
49924
|
title: "Hardware acceleration",
|
|
50662
49925
|
tab: "decoder",
|
|
50663
|
-
description: "
|
|
49926
|
+
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.",
|
|
50664
49927
|
fields: [this.field({
|
|
50665
49928
|
type: "select",
|
|
50666
49929
|
key: "hwaccel",
|
|
@@ -50705,12 +49968,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50705
49968
|
provider: this.audioProvider
|
|
50706
49969
|
});
|
|
50707
49970
|
}
|
|
50708
|
-
|
|
50709
|
-
if (backend !== "nodeav") {
|
|
50710
|
-
this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
|
|
50711
|
-
return registrations;
|
|
50712
|
-
}
|
|
50713
|
-
this.ctx.logger.info("node-av decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
49971
|
+
this.ctx.logger.info("node-av decoder addon initialized");
|
|
50714
49972
|
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
50715
49973
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50716
49974
|
});
|
|
@@ -50721,16 +49979,6 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50721
49979
|
return registrations;
|
|
50722
49980
|
}
|
|
50723
49981
|
/**
|
|
50724
|
-
* Resolve the effective hwaccel backend for a new session — from THIS addon's
|
|
50725
|
-
* OWN per-node `hwaccel` config (edited via the Pipeline UI when node-av is
|
|
50726
|
-
* the active decoder). No cross-read of another addon's store. `'auto'`
|
|
50727
|
-
* defers to the session's local resolver (`ctx.kernel.hwaccel`); fails safe
|
|
50728
|
-
* to `'auto'`.
|
|
50729
|
-
*/
|
|
50730
|
-
resolveHwAccelPref() {
|
|
50731
|
-
return Promise.resolve(this.config.hwaccel ?? "auto");
|
|
50732
|
-
}
|
|
50733
|
-
/**
|
|
50734
49982
|
* Re-run the platform probe on this host and persist the detected
|
|
50735
49983
|
* backend as `probedBestHwaccel`. The operator's `hwaccel` setting
|
|
50736
49984
|
* is intentionally left alone — the probe only updates the hint.
|
|
@@ -50758,191 +50006,30 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
50758
50006
|
return { backend: "none" };
|
|
50759
50007
|
}
|
|
50760
50008
|
}
|
|
50761
|
-
async supportsCodec(input) {
|
|
50762
|
-
return [
|
|
50763
|
-
"h264",
|
|
50764
|
-
"h265",
|
|
50765
|
-
"hevc"
|
|
50766
|
-
].includes(input.codec.toLowerCase());
|
|
50767
|
-
}
|
|
50768
50009
|
async getInfo() {
|
|
50769
50010
|
return {
|
|
50770
50011
|
id: "decoder-nodeav",
|
|
50771
50012
|
name: "Decoder (node-av)",
|
|
50772
|
-
isPullMode: true,
|
|
50773
|
-
priority: 10,
|
|
50774
50013
|
hwaccel: this.config.hwaccel,
|
|
50775
50014
|
probedBestHwaccel: this.config.probedBestHwaccel || void 0
|
|
50776
50015
|
};
|
|
50777
50016
|
}
|
|
50778
50017
|
/**
|
|
50779
|
-
* The cluster node id of this
|
|
50780
|
-
* `dev-agent-0`, …), not the addon id.
|
|
50781
|
-
*
|
|
50782
|
-
*
|
|
50783
|
-
*
|
|
50784
|
-
* resolution `PipelineRunnerAddon.onInitialize` performs.
|
|
50018
|
+
* The cluster node id of this addon — the Moleculer nodeID (`hub`,
|
|
50019
|
+
* `dev-agent-0`, …), not the addon id. A hierarchical broker nodeID
|
|
50020
|
+
* (`hub/...`) is collapsed to the cluster-visible parent; in-process boot is
|
|
50021
|
+
* left as-is. Mirrors the resolution `PipelineRunnerAddon.onInitialize`
|
|
50022
|
+
* performs.
|
|
50785
50023
|
*/
|
|
50786
50024
|
resolveLocalNodeId() {
|
|
50787
50025
|
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
50788
50026
|
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
50789
50027
|
}
|
|
50790
|
-
async createSession(config) {
|
|
50791
|
-
const sessionId = randomUUID();
|
|
50792
|
-
const hwaccel = await this.resolveHwAccelPref();
|
|
50793
|
-
const nodeId = this.resolveLocalNodeId();
|
|
50794
|
-
const session = new NodeAvDecoderSession(config, this.ctx.logger, {
|
|
50795
|
-
hwaccel,
|
|
50796
|
-
hwaccelResolver: this.ctx.kernel.hwaccel
|
|
50797
|
-
});
|
|
50798
|
-
const unsub = this.wireCallbackSink(sessionId, session);
|
|
50799
|
-
this.sessions.set(sessionId, session);
|
|
50800
|
-
this.unsubscribers.set(sessionId, unsub);
|
|
50801
|
-
this.sessionMeta.set(sessionId, {
|
|
50802
|
-
codec: config.codec,
|
|
50803
|
-
outputFormat: config.outputFormat,
|
|
50804
|
-
createdAtMs: Date.now(),
|
|
50805
|
-
deviceId: config.deviceId
|
|
50806
|
-
});
|
|
50807
|
-
this.ctx.logger.info("node-av: created session", {
|
|
50808
|
-
tags: typeof config.deviceId === "number" ? { deviceId: config.deviceId } : void 0,
|
|
50809
|
-
meta: {
|
|
50810
|
-
sessionId,
|
|
50811
|
-
codec: config.codec,
|
|
50812
|
-
hwaccelPref: hwaccel,
|
|
50813
|
-
nodeId
|
|
50814
|
-
}
|
|
50815
|
-
});
|
|
50816
|
-
return {
|
|
50817
|
-
sessionId,
|
|
50818
|
-
nodeId
|
|
50819
|
-
};
|
|
50820
|
-
}
|
|
50821
|
-
/**
|
|
50822
|
-
* Subscribe a `'callback'` session's pixel frames into a per-session
|
|
50823
|
-
* ring buffer drained by `pullFrames`.
|
|
50824
|
-
*/
|
|
50825
|
-
wireCallbackSink(sessionId, session) {
|
|
50826
|
-
const ringBuffer = new NotifyingRingBuffer(FRAME_BUFFER_CAPACITY);
|
|
50827
|
-
this.frameBuffers.set(sessionId, ringBuffer);
|
|
50828
|
-
return session.onFrame((frame) => {
|
|
50829
|
-
const { format } = frame;
|
|
50830
|
-
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
50831
|
-
const arrayBuf = new ArrayBuffer(frame.data.byteLength);
|
|
50832
|
-
new Uint8Array(arrayBuf).set(frame.data);
|
|
50833
|
-
const capFrame = {
|
|
50834
|
-
data: new Uint8Array(arrayBuf),
|
|
50835
|
-
width: frame.width,
|
|
50836
|
-
height: frame.height,
|
|
50837
|
-
format,
|
|
50838
|
-
timestamp: frame.timestamp
|
|
50839
|
-
};
|
|
50840
|
-
ringBuffer.push(capFrame);
|
|
50841
|
-
});
|
|
50842
|
-
}
|
|
50843
|
-
async destroySession(input) {
|
|
50844
|
-
const { sessionId } = input;
|
|
50845
|
-
const session = this.sessions.get(sessionId);
|
|
50846
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${sessionId}`);
|
|
50847
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
50848
|
-
if (unsub) unsub();
|
|
50849
|
-
this.frameBuffers.get(sessionId)?.cancel();
|
|
50850
|
-
const deviceId = this.sessionMeta.get(sessionId)?.deviceId;
|
|
50851
|
-
await session.destroy();
|
|
50852
|
-
this.sessions.delete(sessionId);
|
|
50853
|
-
this.frameBuffers.delete(sessionId);
|
|
50854
|
-
this.unsubscribers.delete(sessionId);
|
|
50855
|
-
this.sessionMeta.delete(sessionId);
|
|
50856
|
-
this.ctx.logger.info("node-av: destroyed session", {
|
|
50857
|
-
tags: typeof deviceId === "number" ? { deviceId } : void 0,
|
|
50858
|
-
meta: { sessionId }
|
|
50859
|
-
});
|
|
50860
|
-
}
|
|
50861
|
-
async listActiveSessions() {
|
|
50862
|
-
const out = [];
|
|
50863
|
-
for (const [sessionId, meta] of this.sessionMeta) out.push({
|
|
50864
|
-
sessionId,
|
|
50865
|
-
codec: meta.codec,
|
|
50866
|
-
outputFormat: meta.outputFormat,
|
|
50867
|
-
createdAtMs: meta.createdAtMs
|
|
50868
|
-
});
|
|
50869
|
-
return out;
|
|
50870
|
-
}
|
|
50871
|
-
async pushPacket(input) {
|
|
50872
|
-
const session = this.sessions.get(input.sessionId);
|
|
50873
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50874
|
-
const rawData = input.packet.data;
|
|
50875
|
-
const data = Buffer.isBuffer(rawData) ? rawData : rawData instanceof Uint8Array ? Buffer.from(rawData.buffer, rawData.byteOffset, rawData.byteLength) : Buffer.from(rawData);
|
|
50876
|
-
session.pushPacket({
|
|
50877
|
-
...input.packet,
|
|
50878
|
-
data
|
|
50879
|
-
});
|
|
50880
|
-
}
|
|
50881
|
-
async openStream(input) {
|
|
50882
|
-
const session = this.sessions.get(input.sessionId);
|
|
50883
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50884
|
-
await session.openStream(input.url);
|
|
50885
|
-
}
|
|
50886
|
-
async pullFrames(input) {
|
|
50887
|
-
if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50888
|
-
const ringBuffer = this.frameBuffers.get(input.sessionId);
|
|
50889
|
-
if (!ringBuffer) return [];
|
|
50890
|
-
if (ringBuffer.size === 0 && input.waitMs) await ringBuffer.waitForItem(input.waitMs);
|
|
50891
|
-
return ringBuffer.drain(input.maxCount);
|
|
50892
|
-
}
|
|
50893
|
-
/**
|
|
50894
|
-
* DORMANT — the shm `FrameHandle` frame plane is retired. Every session now
|
|
50895
|
-
* delivers pixels over the callback path drained by `pullFrames`, so no
|
|
50896
|
-
* handles are ever produced. Kept to satisfy the (still-defined) cap method;
|
|
50897
|
-
* always returns an empty list. Removal rides a later framework train.
|
|
50898
|
-
*/
|
|
50899
|
-
async pullHandles() {
|
|
50900
|
-
return [];
|
|
50901
|
-
}
|
|
50902
|
-
async updateConfig(input) {
|
|
50903
|
-
const session = this.sessions.get(input.sessionId);
|
|
50904
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50905
|
-
session.updateConfig(input.config);
|
|
50906
|
-
}
|
|
50907
|
-
async getStats(input) {
|
|
50908
|
-
const session = this.sessions.get(input.sessionId);
|
|
50909
|
-
if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
|
|
50910
|
-
return session.getStats();
|
|
50911
|
-
}
|
|
50912
|
-
/**
|
|
50913
|
-
* DORMANT — shm handle read-back is retired (the decoder no longer owns a
|
|
50914
|
-
* `/dev/shm` ring). Kept to satisfy the (still-defined) cap method; always
|
|
50915
|
-
* returns `null`. Full-frame retrieval by handle now flows through the
|
|
50916
|
-
* runner's RAM-backed `pipelineRunner.getNativeCrop`. Removal rides a later
|
|
50917
|
-
* framework train.
|
|
50918
|
-
*/
|
|
50919
|
-
async getFrame() {
|
|
50920
|
-
return null;
|
|
50921
|
-
}
|
|
50922
|
-
/**
|
|
50923
|
-
* DORMANT — no shm ring exists to report stats for. Kept to satisfy the
|
|
50924
|
-
* (still-defined) cap method; always returns `null`. Removal rides a later
|
|
50925
|
-
* framework train.
|
|
50926
|
-
*/
|
|
50927
|
-
async getShmStats() {
|
|
50928
|
-
return null;
|
|
50929
|
-
}
|
|
50930
50028
|
async onShutdown() {
|
|
50931
50029
|
this.audioProvider?.stop();
|
|
50932
50030
|
this.audioProvider = null;
|
|
50933
|
-
this.ctx.logger.info("node-av decoder addon shutdown
|
|
50934
|
-
const destroyPromises = [];
|
|
50935
|
-
for (const [sessionId, session] of this.sessions) {
|
|
50936
|
-
const unsub = this.unsubscribers.get(sessionId);
|
|
50937
|
-
if (unsub) unsub();
|
|
50938
|
-
destroyPromises.push(session.destroy());
|
|
50939
|
-
}
|
|
50940
|
-
await Promise.all(destroyPromises);
|
|
50941
|
-
this.sessions.clear();
|
|
50942
|
-
this.frameBuffers.clear();
|
|
50943
|
-
this.unsubscribers.clear();
|
|
50944
|
-
this.sessionMeta.clear();
|
|
50031
|
+
this.ctx.logger.info("node-av decoder addon shutdown");
|
|
50945
50032
|
}
|
|
50946
50033
|
};
|
|
50947
50034
|
//#endregion
|
|
50948
|
-
export { DecoderNodeAvAddon, DecoderNodeAvAddon as default, NodeAvAudioCodecProvider, NodeAvAudioDecodeSession, NodeAvAudioEncodeSession,
|
|
50035
|
+
export { DecoderNodeAvAddon, DecoderNodeAvAddon as default, NodeAvAudioCodecProvider, NodeAvAudioDecodeSession, NodeAvAudioEncodeSession, loadNodeAvRuntime, peekNodeAvRuntime };
|