@camstack/addon-decoder-nodeav 1.2.118 → 1.2.120

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