@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.
Files changed (3) hide show
  1. package/dist/index.js +1117 -2031
  2. package/dist/index.mjs +1118 -2031
  3. 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-BGmz_oxW.mjs
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$1(schema, ctx, _params = {
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$1(parent, ctx, params);
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$1(schema, ctx);
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$1(schema, ctx);
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$1(def.element, ctx, {
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$1(shape[key], ctx, {
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$1(def.catchall, ctx, {
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$1(x, ctx, {
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$1(def.left, ctx, {
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$1(def.right, ctx, {
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$1(x, ctx, {
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$1(def.rest, ctx, {
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$1(def.valueType, ctx, {
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$1(def.keyType, ctx, {
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$1(def.valueType, ctx, {
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$1(def.innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(def.innerType, ctx, params);
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$1(innerType, ctx, params);
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-DXKLNFvA.mjs
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 decoder cap
6773
- * method (`pullHandles`) can carry handles over tRPC / Moleculer.
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`) to match `DecodedFrameSchema`
6851
- * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
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). The
6870
- * handle-free, ship-bytes-inline analogue of `subscribeFrames`: a consumer
6871
- * asks the broker for the decoded audio-chunk stream of one broker; the
6872
- * broker keeps a per-subscription bounded FIFO queue the consumer drains via
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
- * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
10534
- * latency-optimized export (e.g. ReLU-activation variant) — the slot the
10535
- * future performance variants plug into.
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", "fast"]).optional(),
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 admin "Models and licenses" page,
10561
- * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
10562
- * these records by `scripts/gen-model-licenses.ts` (D661).
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(SubscribeFramesInputSchema, SubscribeFramesResultSchema, { kind: "mutation" }), method(object({
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.x.x/` is a URL a browser accepts. The
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
- nodeId: string(),
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 report a
19672
- * still-true state. `backups-exports` and `alarm` are not sections — a completed
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
- id: "auraface-r100",
21137
- label: "AuraFace R100"
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: "mobileclip-s1",
21104
+ defaultModelId: "siglip2-b16-224",
21152
21105
  indexName: "the object semantic-search index",
21153
- options: [
21154
- {
21155
- id: "mobileclip-s1",
21156
- label: "MobileCLIP S1"
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 for recognition (detection px)",
21172
- description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
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$1 = _enum([
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$1,
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(), object({
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(), array(PipelineTemplateSchema$1).readonly()), method(object({
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$1
23381
+ format: ModelFormatSchema$2
23420
23382
  }), DownloadModelResultSchema, { kind: "mutation" }), method(object({
23421
23383
  addonId: string(),
23422
23384
  modelId: string(),
23423
- format: ModelFormatSchema$1
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. `queueDepthTotal`
24425
- * per device is 0 until the pool backlog gets a public accessor (follow-up).
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
- inFps: number(),
24836
- outFps: number()
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
- /** Shared-memory ring statistics within the decoder block. */
24842
- var CameraDecoderShmSchema = object({
24843
- framesWritten: number(),
24844
- getFrameHits: number(),
24845
- getFrameMisses: number(),
24846
- budgetMb: number()
24847
- });
24848
- /** Decoder block — null when the decoder stage is unreachable or inactive. */
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" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
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({ packageName: string() }), unknown(), {
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.x.x/24`). Empty when OS doesn't expose. */
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 chosen outbound endpoint for notification artifacts. `baseUrl: null` =
30612
- * AUTO (resolved from the candidate ranking at send time); `resolved` reports
30613
- * what AUTO currently picks, so the UI can show the effective value either way.
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
- /** What the ranking currently resolves to (null when nothing is reachable). */
30619
- resolved: string().nullable()
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
- }), 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(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
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.x.x/24"]`). */
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` answers from the hub's slice.
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.deleteModel": {
41191
+ "pipelineExecutor.clearModelBoundDeviceOverrides": {
40685
41192
  capName: "pipeline-executor",
40686
41193
  capScope: "system",
40687
41194
  addonId: null,
40688
41195
  access: "delete"
40689
41196
  },
40690
- "pipelineExecutor.deleteTemplate": {
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.applyDeviceSettingsPatch": {
41389
+ "pipelineOrchestrator.applyAutoTune": {
40901
41390
  capName: "pipeline-orchestrator",
40902
41391
  capScope: "system",
40903
41392
  addonId: null,
40904
41393
  access: "create"
40905
41394
  },
40906
- "pipelineOrchestrator.assignAudio": {
41395
+ "pipelineOrchestrator.applyDeviceSettingsPatch": {
40907
41396
  capName: "pipeline-orchestrator",
40908
41397
  capScope: "system",
40909
41398
  addonId: null,
40910
41399
  access: "create"
40911
41400
  },
40912
- "pipelineOrchestrator.assignPipeline": {
41401
+ "pipelineOrchestrator.assignAudio": {
40913
41402
  capName: "pipeline-orchestrator",
40914
41403
  capScope: "system",
40915
41404
  addonId: null,
40916
41405
  access: "create"
40917
41406
  },
40918
- "pipelineOrchestrator.deleteTemplate": {
41407
+ "pipelineOrchestrator.assignPipeline": {
40919
41408
  capName: "pipeline-orchestrator",
40920
41409
  capScope: "system",
40921
41410
  addonId: null,
40922
- access: "delete"
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.getReconcileFromDiskStatus": {
41545
+ "pipelineOrchestrator.getPlacementStats": {
41051
41546
  capName: "pipeline-orchestrator",
41052
41547
  capScope: "system",
41053
41548
  addonId: null,
41054
41549
  access: "view"
41055
41550
  },
41056
- "pipelineOrchestrator.listAgentSettings": {
41551
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
41057
41552
  capName: "pipeline-orchestrator",
41058
41553
  capScope: "system",
41059
41554
  addonId: null,
41060
41555
  access: "view"
41061
41556
  },
41062
- "pipelineOrchestrator.listTemplates": {
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.unassignAudio": {
41701
+ "pipelineOrchestrator.setShareSource": {
41201
41702
  capName: "pipeline-orchestrator",
41202
41703
  capScope: "system",
41203
41704
  addonId: null,
41204
41705
  access: "create"
41205
41706
  },
41206
- "pipelineOrchestrator.unassignPipeline": {
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.updateTemplate": {
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
- "addon-crash-loop",
47681
- "addon-update-available",
47682
- "addon-updated"
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: "@camstack/addon-recorder 1.2.3 → 1.2.4",
47845
- kinds: [
47846
- "addon-crash-loop",
47847
- "addon-update-available",
47848
- "addon-updated"
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 (D698); on a summary sent as one message, every section it reports, comma-separated.",
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/ffmpeg-args.ts
49011
+ //#region src/decode-hwaccel-rank.ts
48529
49012
  /**
48530
- * Canonical decode-hwaccel preference order for the ffmpeg-subprocess decoder.
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
- var FRAME_BUFFER_CAPACITY = 32;
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
- * Sessions are stored as the concrete `NodeAvDecoderSession` (the only type
50642
- * this addon ever creates).
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: "Backend used by node-av decoder sessions. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.",
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
- const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
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 decoder — the Moleculer nodeID (`hub`,
50780
- * `dev-agent-0`, …), not the addon id. Returned from `createSession` so the
50781
- * caller can route session-scoped calls to the node that owns the session.
50782
- * A hierarchical broker nodeID (`hub/...`) is collapsed to the
50783
- * cluster-visible parent; in-process boot is left as-is. Mirrors the
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 — destroying all sessions");
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, NodeAvDecoderSession, loadNodeAvRuntime, peekNodeAvRuntime };
50035
+ export { DecoderNodeAvAddon, DecoderNodeAvAddon as default, NodeAvAudioCodecProvider, NodeAvAudioDecodeSession, NodeAvAudioEncodeSession, loadNodeAvRuntime, peekNodeAvRuntime };