@camstack/addon-provider-amcrest 0.1.4 → 0.1.6

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/addon.js +304 -233
  2. package/dist/addon.mjs +304 -233
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4633,7 +4633,7 @@ function _instanceof(cls, params = {}) {
4633
4633
  return inst;
4634
4634
  }
4635
4635
  //#endregion
4636
- //#region ../types/dist/sleep-CZDdRBua.mjs
4636
+ //#region ../types/dist/sleep-Cc14_yxc.mjs
4637
4637
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4638
4638
  EventCategory["SystemBoot"] = "system.boot";
4639
4639
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -4819,6 +4819,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
4819
4819
  */
4820
4820
  EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
4821
4821
  /**
4822
+ * The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
4823
+ * Emitted by addon-pipeline-orchestrator whenever it (re)derives node
4824
+ * capabilities — at boot, on agent online/offline, and on an ingest-node
4825
+ * flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
4826
+ * keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
4827
+ * cross-process `getIngestOwner` query (push the authority's decision instead
4828
+ * of polling it on the hot path). Idempotent state — re-emitted on every
4829
+ * topology change, so a dropped event self-heals on the next one (plus the
4830
+ * broker's long backstop reconcile query).
4831
+ */
4832
+ EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
4833
+ /**
4822
4834
  * Periodic snapshot of per-node pipeline-runner load
4823
4835
  * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
4824
4836
  * subscribe instead of polling `pipelineRunner.getLocalLoad`.
@@ -6902,6 +6914,36 @@ var ModelFormatsSchema = object({
6902
6914
  tflite: ModelFormatEntrySchema.optional(),
6903
6915
  pt: ModelFormatEntrySchema.optional()
6904
6916
  });
6917
+ /**
6918
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
6919
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
6920
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
6921
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
6922
+ * resolution/download/persistence; this is a presentation overlay resolved back
6923
+ * to an `id`.
6924
+ */
6925
+ var ModelVariantGroupSchema = object({
6926
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
6927
+ family: string(),
6928
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
6929
+ tier: string(),
6930
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
6931
+ precision: _enum(["fp32", "int8"]).optional(),
6932
+ /**
6933
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
6934
+ * latency-optimized export (e.g. ReLU-activation variant) — the slot the
6935
+ * future performance variants plug into.
6936
+ */
6937
+ optimization: _enum(["standard", "fast"]).optional(),
6938
+ /**
6939
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
6940
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
6941
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
6942
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
6943
+ * the group so the selector can offer it as a variant axis.
6944
+ */
6945
+ resolution: number().int().positive().optional()
6946
+ });
6905
6947
  var ModelCatalogEntrySchema = object({
6906
6948
  id: string(),
6907
6949
  name: string(),
@@ -6931,7 +6973,43 @@ var ModelCatalogEntrySchema = object({
6931
6973
  * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
6932
6974
  * Downloaded into the same modelsDir alongside the model file.
6933
6975
  */
6934
- extraFiles: array(ModelExtraFileSchema).readonly().optional()
6976
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
6977
+ /**
6978
+ * LEGACY entry — retained in the catalog so a persisted operator selection
6979
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
6980
+ * model list and excluded from the auto format-default pick. Set on the
6981
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
6982
+ * the active lineup stays the coherent curated ladder without deleting a
6983
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
6984
+ * an explicit legacy id that has a build for the node's format.
6985
+ */
6986
+ legacy: boolean().optional(),
6987
+ /**
6988
+ * Measured quality/latency metadata — populated from the benchmark addon on
6989
+ * the real node classes. Absent = not yet measured (most entries today; the
6990
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
6991
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
6992
+ */
6993
+ metrics: object({
6994
+ map50: number().optional(),
6995
+ p95LatencyMs: record(string(), number()).optional()
6996
+ }).optional(),
6997
+ /**
6998
+ * SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
6999
+ * YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
7000
+ * the retraining addon and any future commercial distribution.
7001
+ */
7002
+ license: string().optional(),
7003
+ /**
7004
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
7005
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
7006
+ * of a family's sizes and quantizations collapse into one grouped picker
7007
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
7008
+ * (legacy / custom models) — never shown in the grouped selector. The flat
7009
+ * `id` stays the source of truth for resolution/download/persistence; grouping
7010
+ * is a presentation overlay resolved back to an `id`.
7011
+ */
7012
+ group: ModelVariantGroupSchema.optional()
6935
7013
  });
6936
7014
  var ConvertTargetSchema = discriminatedUnion("format", [object({
6937
7015
  format: literal("openvino"),
@@ -6992,8 +7070,8 @@ var RecordingModeSchema = _enum([
6992
7070
  "onAudioThreshold"
6993
7071
  ]);
6994
7072
  /**
6995
- * First-class, authoritative per-camera storage mode — the netta choice the UI
6996
- * reads directly (never inferred from `rules`):
7073
+ * First-class, authoritative per-camera storage mode — the explicit choice the
7074
+ * UI reads directly (never inferred from `rules`):
6997
7075
  * - `off` — not recording.
6998
7076
  * - `events` — record only around triggers (motion / audio threshold),
6999
7077
  * with pre/post-buffer.
@@ -8517,6 +8595,72 @@ function createRuntimeStateBridge(params) {
8517
8595
  getStatus
8518
8596
  };
8519
8597
  }
8598
+ /** Reject after `ms`; always clears its own timer. */
8599
+ async function withTimeout(promise, ms, label) {
8600
+ let timer;
8601
+ const timeout = new Promise((_resolve, reject) => {
8602
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
8603
+ });
8604
+ try {
8605
+ return await Promise.race([promise, timeout]);
8606
+ } finally {
8607
+ if (timer !== void 0) clearTimeout(timer);
8608
+ }
8609
+ }
8610
+ /**
8611
+ * Start the reachability poll loop. Returns a handle whose `stop()` clears the
8612
+ * timer and prevents any further ticks. Start on device activation, stop on
8613
+ * device teardown (`removeDevice`) so no timer leaks.
8614
+ */
8615
+ function startReachabilityPoll(options) {
8616
+ const intervalMs = options.intervalMs ?? 3e4;
8617
+ const failuresToOffline = options.failuresToOffline ?? 3;
8618
+ const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
8619
+ const runImmediately = options.runImmediately ?? true;
8620
+ let stopped = false;
8621
+ let running = false;
8622
+ let consecutiveFailures = 0;
8623
+ let timer;
8624
+ const tick = async () => {
8625
+ if (stopped) return;
8626
+ if (running) return;
8627
+ if (options.isEnabled && !options.isEnabled()) return;
8628
+ running = true;
8629
+ try {
8630
+ const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
8631
+ if (stopped) return;
8632
+ if (reachable) {
8633
+ consecutiveFailures = 0;
8634
+ options.setOnline(true);
8635
+ } else registerFailure("probe resolved unreachable");
8636
+ } catch (error) {
8637
+ if (stopped) return;
8638
+ registerFailure(error instanceof Error ? error.message : "probe threw");
8639
+ } finally {
8640
+ running = false;
8641
+ }
8642
+ };
8643
+ const registerFailure = (reason) => {
8644
+ consecutiveFailures += 1;
8645
+ options.logger?.debug("reachability probe failed", {
8646
+ reason,
8647
+ consecutiveFailures,
8648
+ failuresToOffline
8649
+ });
8650
+ if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
8651
+ };
8652
+ timer = setInterval(() => {
8653
+ tick();
8654
+ }, intervalMs);
8655
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
8656
+ if (runImmediately) tick();
8657
+ return { stop: () => {
8658
+ if (stopped) return;
8659
+ stopped = true;
8660
+ if (timer !== void 0) clearInterval(timer);
8661
+ timer = void 0;
8662
+ } };
8663
+ }
8520
8664
  /**
8521
8665
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
8522
8666
  * for every device, regardless of provider — the kernel needs a uniform
@@ -9180,26 +9324,13 @@ onBrightnessChanged: { data: object({
9180
9324
  */
9181
9325
  runtimeState: BrightnessStatusSchema
9182
9326
  };
9327
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9183
9328
  var StreamFormatSchema = _enum([
9184
9329
  "webrtc",
9185
9330
  "hls",
9186
9331
  "mjpeg",
9187
9332
  "rtsp"
9188
9333
  ]);
9189
- var StreamInfoSchema = object({
9190
- streamId: string(),
9191
- format: StreamFormatSchema,
9192
- url: string().nullable(),
9193
- active: boolean()
9194
- });
9195
- method(object({
9196
- streamId: string(),
9197
- sourceUrl: string(),
9198
- codec: string().optional()
9199
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
9200
- streamId: string(),
9201
- format: StreamFormatSchema
9202
- }), string().nullable()), method(_void(), array(StreamInfoSchema));
9203
9334
  var RtspRestreamEntrySchema = object({
9204
9335
  brokerId: string(),
9205
9336
  url: string(),
@@ -10067,37 +10198,7 @@ var consumablesCapability = {
10067
10198
  scope: "device",
10068
10199
  deviceNative: true,
10069
10200
  mode: "singleton",
10070
- deviceTypes: [
10071
- DeviceType.Camera,
10072
- DeviceType.Hub,
10073
- DeviceType.Light,
10074
- DeviceType.Siren,
10075
- DeviceType.Switch,
10076
- DeviceType.Sensor,
10077
- DeviceType.Thermostat,
10078
- DeviceType.Button,
10079
- DeviceType.EventEmitter,
10080
- DeviceType.Update,
10081
- DeviceType.Generic,
10082
- DeviceType.Notifier,
10083
- DeviceType.Script,
10084
- DeviceType.Automation,
10085
- DeviceType.Lock,
10086
- DeviceType.Cover,
10087
- DeviceType.Valve,
10088
- DeviceType.Humidifier,
10089
- DeviceType.WaterHeater,
10090
- DeviceType.Fan,
10091
- DeviceType.MediaPlayer,
10092
- DeviceType.AlarmPanel,
10093
- DeviceType.Control,
10094
- DeviceType.Presence,
10095
- DeviceType.Weather,
10096
- DeviceType.Vacuum,
10097
- DeviceType.LawnMower,
10098
- DeviceType.Container,
10099
- DeviceType.Image
10100
- ],
10201
+ deviceTypes: Object.values(DeviceType),
10101
10202
  deviceConfig: { ui: {
10102
10203
  kind: "widget",
10103
10204
  widgetId: "host/consumables-panel",
@@ -11690,7 +11791,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
11690
11791
  enabled: boolean(),
11691
11792
  modelId: string(),
11692
11793
  children: array(PipelineDefaultStepSchema).readonly(),
11693
- engine: PipelineEngineChoiceSchema.optional(),
11694
11794
  group: string().optional(),
11695
11795
  settings: record(string(), unknown()).optional()
11696
11796
  }));
@@ -11715,7 +11815,9 @@ var PipelineModelOptionSchema = object({
11715
11815
  formats: record(string(), object({
11716
11816
  downloaded: boolean(),
11717
11817
  sizeMB: number()
11718
- }))
11818
+ })),
11819
+ group: ModelVariantGroupSchema.optional(),
11820
+ legacy: boolean().optional()
11719
11821
  });
11720
11822
  var ConfigFieldBridge = custom();
11721
11823
  var PipelineAddonSchemaSchema = object({
@@ -11766,15 +11868,42 @@ var EngineProvisioningSchema = object({
11766
11868
  ]),
11767
11869
  progress: number().optional(),
11768
11870
  error: string().optional(),
11769
- nextRetryAt: number().optional()
11871
+ nextRetryAt: number().optional(),
11872
+ /**
11873
+ * Gate A (config-correctness gate at engine change): human-readable
11874
+ * config issues surfaced EAGERLY when the node's engine changes — model
11875
+ * substitutions ("chose X, running Y") and zero-build steps ("no model
11876
+ * has a <format> build"). Additive/optional: informational only, never
11877
+ * enforced here — `assertEngineReady` (readiness) still gates inference.
11878
+ * Absent/empty when the node-default tree resolves cleanly.
11879
+ */
11880
+ configIssues: array(string()).optional()
11770
11881
  });
11771
11882
  var PipelineStepInputSchema = lazy(() => object({
11772
11883
  addonId: string(),
11773
- modelId: string(),
11884
+ modelId: string().optional(),
11774
11885
  enabled: boolean().default(true),
11775
11886
  children: array(PipelineStepInputSchema).optional(),
11776
11887
  settings: record(string(), unknown()).optional()
11777
11888
  }));
11889
+ var ModelSubstitutionSchema = object({
11890
+ addonId: string(),
11891
+ chosen: string(),
11892
+ running: string(),
11893
+ format: string()
11894
+ });
11895
+ var PipelineValidationIssueSchema = object({
11896
+ addonId: string(),
11897
+ kind: _enum(["unknown-addon", "no-format-build"]),
11898
+ detail: string()
11899
+ });
11900
+ var PipelineValidationResultSchema = object({
11901
+ ok: boolean(),
11902
+ issues: array(PipelineValidationIssueSchema).readonly(),
11903
+ substitutions: array(ModelSubstitutionSchema).readonly(),
11904
+ /** The node's `currentEngine.format` this validation ran against. */
11905
+ format: string()
11906
+ });
11778
11907
  var ReferenceImageEntrySchema = object({
11779
11908
  filename: string(),
11780
11909
  stepIds: array(string()).readonly().optional()
@@ -11845,7 +11974,13 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
11845
11974
  })) }), object({ success: literal(true) }), {
11846
11975
  kind: "mutation",
11847
11976
  auth: "admin"
11848
- }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11977
+ }), method(object({ nodeId: string() }), object({
11978
+ success: literal(true),
11979
+ regeneratedModelId: string().nullable()
11980
+ }), {
11981
+ kind: "mutation",
11982
+ auth: "admin"
11983
+ }), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
11849
11984
  name: string(),
11850
11985
  steps: array(PipelineTemplateStepSchema).readonly(),
11851
11986
  engine: PipelineEngineChoiceSchema
@@ -12138,6 +12273,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12138
12273
  kind: literal("remote-restream"),
12139
12274
  /** The camera's source-owner node (slice 1: always the hub). */
12140
12275
  ownerNodeId: string(),
12276
+ /**
12277
+ * The owner's LAN-reachable host, resolved by the orchestrator from the
12278
+ * per-node `reachableHost` override (Cluster UI). When present the runner
12279
+ * dials THIS host for the owner's restream, in preference to the
12280
+ * `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
12281
+ */
12282
+ ownerReachableHost: string().optional(),
12141
12283
  /** Operator override for the owner host the runner dials. */
12142
12284
  hubHostnameOverride: string().optional()
12143
12285
  })]).describe("Per-camera frame-source mode for the runner (P2c)");
@@ -12146,13 +12288,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12146
12288
  * specific runner instance via `attachCamera`. Carries everything the
12147
12289
  * runner needs to subscribe to the local broker and execute inference.
12148
12290
  *
12149
- * Stateless-pipeline model: the full pipeline content (`engine`, `steps`,
12150
- * optional `audio`) travels with the attach payload. The runner keeps it
12151
- * in RAM for the lifetime of the attach — on rebalance, edit, or
12152
- * restart the orchestrator re-sends the latest snapshot.
12153
- *
12154
- * `engine`/`steps`/`audio` are optional during the additive migration
12155
- * window; once orchestrator + UI are migrated they become required.
12291
+ * Stateless-pipeline model: the pipeline content (`steps`, optional
12292
+ * `audio`) travels with the attach payload. The runner keeps it in RAM
12293
+ * for the lifetime of the attach — on rebalance, edit, or restart the
12294
+ * orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
12295
+ * node-local, resolved by the executing runner at dispatch time.
12156
12296
  */
12157
12297
  var RunnerCameraConfigSchema = object({
12158
12298
  deviceId: number(),
@@ -12203,14 +12343,11 @@ var RunnerCameraConfigSchema = object({
12203
12343
  */
12204
12344
  motionSources: MotionSourcesSchema.default(["analyzer"]),
12205
12345
  pipelineEnabled: boolean().default(true),
12206
- /** Engine choice for video steps (runtime+backend+format). */
12207
- engine: PipelineEngineChoiceSchema.optional(),
12208
12346
  /** Ordered tree of video steps. Absent → runner skips video detection. */
12209
12347
  steps: array(PipelineStepInputSchema).readonly().optional(),
12210
12348
  /** Audio classification branch. `enabled:false` disables, null skips. */
12211
12349
  audio: object({
12212
- engine: PipelineEngineChoiceSchema,
12213
- modelId: string(),
12350
+ modelId: string().optional(),
12214
12351
  enabled: boolean()
12215
12352
  }).nullable().optional(),
12216
12353
  /**
@@ -18073,11 +18210,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18073
18210
  timestamp: number()
18074
18211
  });
18075
18212
  var CameraPipelineConfigSchema = object({
18076
- engine: PipelineEngineChoiceSchema,
18213
+ engine: PipelineEngineChoiceSchema.optional(),
18077
18214
  steps: array(PipelineStepInputSchema).readonly(),
18078
18215
  audio: object({
18079
- engine: PipelineEngineChoiceSchema,
18080
- modelId: string(),
18216
+ engine: PipelineEngineChoiceSchema.optional(),
18217
+ modelId: string().optional(),
18081
18218
  enabled: boolean(),
18082
18219
  settings: record(string(), unknown()).readonly().optional()
18083
18220
  }).nullable().optional()
@@ -18092,7 +18229,7 @@ var PipelineTemplateSchema = object({
18092
18229
  });
18093
18230
  var AgentAddonConfigSchema = object({
18094
18231
  enabled: boolean(),
18095
- modelId: string(),
18232
+ modelId: string().optional(),
18096
18233
  settings: record(string(), unknown()).readonly()
18097
18234
  });
18098
18235
  var AgentPipelineSettingsSchema = object({
@@ -18107,7 +18244,15 @@ var AgentPipelineSettingsSchema = object({
18107
18244
  /** Node is eligible to run audio-analyzer sessions. */
18108
18245
  audio: boolean().optional(),
18109
18246
  /** Node is eligible to be the ingest / source-owner (serve the restream). */
18110
- ingest: boolean().optional()
18247
+ ingest: boolean().optional(),
18248
+ /**
18249
+ * Operator override for the LAN host a cross-node decoder dials to reach
18250
+ * THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
18251
+ * falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
18252
+ * it already uses to reach the hub). Set this only when the auto-detected
18253
+ * address is wrong (multi-homed host, NAT, custom interface).
18254
+ */
18255
+ reachableHost: string().optional()
18111
18256
  });
18112
18257
  var CameraPipelineForAgentSchema = object({
18113
18258
  steps: array(PipelineStepInputSchema).readonly(),
@@ -18213,6 +18358,15 @@ var GlobalMetricsSchema = object({
18213
18358
  * capability providers.
18214
18359
  */
18215
18360
  var CapabilityBindingsSchema = record(string(), string());
18361
+ /**
18362
+ * The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
18363
+ * its LAN-reachable host, if one is registered. See `getIngestOwner`.
18364
+ */
18365
+ var IngestOwnerSchema = object({
18366
+ ownerNodeId: string(),
18367
+ reachableHost: string().optional(),
18368
+ configIssue: string().optional()
18369
+ });
18216
18370
  /** Source block — always present; derives from the stream catalog. */
18217
18371
  var CameraSourceStatusSchema = object({ streams: array(object({
18218
18372
  camStreamId: string(),
@@ -18227,6 +18381,14 @@ var CameraAssignmentStatusSchema = object({
18227
18381
  detectionNodeId: string().nullable(),
18228
18382
  decoderNodeId: string().nullable(),
18229
18383
  audioNodeId: string().nullable(),
18384
+ /**
18385
+ * The node that OWNS this camera's physical source pull (dials the RTSP and
18386
+ * hosts the broker/restream) — the cluster ingest owner today
18387
+ * (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
18388
+ * the UI show WHERE a camera is sourced without SSH/logs, and is the node the
18389
+ * broker block below was read from (pinned). Nullable only pre-wiring.
18390
+ */
18391
+ sourceNodeId: string().nullable(),
18230
18392
  pinned: object({
18231
18393
  detection: boolean(),
18232
18394
  decoder: boolean(),
@@ -18359,7 +18521,7 @@ method(object({
18359
18521
  }), object({ success: literal(true) }), {
18360
18522
  kind: "mutation",
18361
18523
  auth: "admin"
18362
- }), method(object({
18524
+ }), method(_void(), IngestOwnerSchema), method(object({
18363
18525
  deviceId: number(),
18364
18526
  nodeId: string()
18365
18527
  }), _void(), {
@@ -18428,6 +18590,12 @@ method(object({
18428
18590
  }), object({ success: literal(true) }), {
18429
18591
  kind: "mutation",
18430
18592
  auth: "admin"
18593
+ }), method(object({
18594
+ agentNodeId: string(),
18595
+ reachableHost: string().nullable()
18596
+ }), object({ success: literal(true) }), {
18597
+ kind: "mutation",
18598
+ auth: "admin"
18431
18599
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
18432
18600
  deviceId: number(),
18433
18601
  addonId: string(),
@@ -18472,22 +18640,6 @@ method(object({
18472
18640
  kind: "mutation",
18473
18641
  auth: "admin"
18474
18642
  });
18475
- var RegisteredStreamSchema = object({
18476
- streamId: string(),
18477
- label: string().optional(),
18478
- codec: string(),
18479
- type: _enum(["video", "audio"]),
18480
- sourceUrl: string()
18481
- });
18482
- var ExposedResourceSchema = object({
18483
- streamId: string(),
18484
- format: string(),
18485
- value: string()
18486
- });
18487
- method(object({
18488
- deviceId: number(),
18489
- streams: array(RegisteredStreamSchema).readonly()
18490
- }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
18491
18643
  /**
18492
18644
  * Query filter for settings-store collections.
18493
18645
  */
@@ -18640,9 +18792,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
18640
18792
  /**
18641
18793
  * A single device snapshot returned as base64 JPEG/PNG.
18642
18794
  *
18643
- * Shared with the `snapshot-provider` collection cap the orchestrator
18644
- * receives the same shape from each native provider and from the
18645
- * broker-based fallback.
18795
+ * The `SnapshotAddon` wrapper returns this shape whether the frame came from
18796
+ * the device-native provider (onboard capture) or from the stream-broker
18797
+ * prebuffer fallback.
18646
18798
  */
18647
18799
  var SnapshotImageSchema = object({
18648
18800
  base64: string(),
@@ -18717,10 +18869,6 @@ var snapshotCapability = {
18717
18869
  kind: "poll"
18718
18870
  }
18719
18871
  };
18720
- method(object({ deviceId: number() }), boolean()), method(object({
18721
- deviceId: number(),
18722
- streamId: string().optional()
18723
- }), SnapshotImageSchema.nullable());
18724
18872
  /**
18725
18873
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
18726
18874
  * providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
@@ -19081,9 +19229,10 @@ method(object({
19081
19229
  auth: "admin"
19082
19230
  });
19083
19231
  /**
19084
- * Optional client-side hints sent at session creation to help the
19085
- * provider pick the best native source. All fields are optional —
19086
- * a viewer that knows nothing still gets a sane default.
19232
+ * Optional client-side hints sent at session creation to help the provider
19233
+ * pick the best native source. All fields optional — a viewer that knows
19234
+ * nothing still gets a sane default. (Relocated from the retired `webrtc`
19235
+ * collection cap; this `webrtc-session` cap is the live signaling surface.)
19087
19236
  */
19088
19237
  var webrtcClientHintsSchema = object({
19089
19238
  viewportWidth: number().int().positive().optional(),
@@ -19094,22 +19243,6 @@ var webrtcClientHintsSchema = object({
19094
19243
  /** Hard tier override; takes precedence over scoring when registered. */
19095
19244
  prefersTier: string().optional()
19096
19245
  }).partial();
19097
- method(object({
19098
- streamId: string(),
19099
- sdpOffer: string()
19100
- }), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
19101
- streamId: string(),
19102
- codec: string()
19103
- }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
19104
- streamId: string(),
19105
- hints: webrtcClientHintsSchema.optional()
19106
- }), object({
19107
- sessionId: string(),
19108
- sdpOffer: string()
19109
- }), { kind: "mutation" }), method(object({
19110
- sessionId: string(),
19111
- sdpAnswer: string()
19112
- }), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
19113
19246
  /**
19114
19247
  * Discriminated target for a WebRTC session. The client sends this
19115
19248
  * structured object instead of building / parsing brokerId strings;
@@ -24451,6 +24584,12 @@ Object.freeze({
24451
24584
  addonId: null,
24452
24585
  access: "create"
24453
24586
  },
24587
+ "pipelineExecutor.resetToDefault": {
24588
+ capName: "pipeline-executor",
24589
+ capScope: "system",
24590
+ addonId: null,
24591
+ access: "delete"
24592
+ },
24454
24593
  "pipelineExecutor.runAudioTest": {
24455
24594
  capName: "pipeline-executor",
24456
24595
  capScope: "system",
@@ -24499,6 +24638,12 @@ Object.freeze({
24499
24638
  addonId: null,
24500
24639
  access: "create"
24501
24640
  },
24641
+ "pipelineExecutor.validatePipeline": {
24642
+ capName: "pipeline-executor",
24643
+ capScope: "system",
24644
+ addonId: null,
24645
+ access: "view"
24646
+ },
24502
24647
  "pipelineOrchestrator.assignAudio": {
24503
24648
  capName: "pipeline-orchestrator",
24504
24649
  capScope: "system",
@@ -24607,6 +24752,12 @@ Object.freeze({
24607
24752
  addonId: null,
24608
24753
  access: "view"
24609
24754
  },
24755
+ "pipelineOrchestrator.getIngestOwner": {
24756
+ capName: "pipeline-orchestrator",
24757
+ capScope: "system",
24758
+ addonId: null,
24759
+ access: "view"
24760
+ },
24610
24761
  "pipelineOrchestrator.getPipelineAssignment": {
24611
24762
  capName: "pipeline-orchestrator",
24612
24763
  capScope: "system",
@@ -24679,6 +24830,12 @@ Object.freeze({
24679
24830
  addonId: null,
24680
24831
  access: "create"
24681
24832
  },
24833
+ "pipelineOrchestrator.setAgentReachableHost": {
24834
+ capName: "pipeline-orchestrator",
24835
+ capScope: "system",
24836
+ addonId: null,
24837
+ access: "create"
24838
+ },
24682
24839
  "pipelineOrchestrator.setCameraPipelineForAgent": {
24683
24840
  capName: "pipeline-orchestrator",
24684
24841
  capScope: "system",
@@ -25009,24 +25166,6 @@ Object.freeze({
25009
25166
  addonId: null,
25010
25167
  access: "create"
25011
25168
  },
25012
- "restreamer.getExposedResources": {
25013
- capName: "restreamer",
25014
- capScope: "system",
25015
- addonId: null,
25016
- access: "view"
25017
- },
25018
- "restreamer.registerDevice": {
25019
- capName: "restreamer",
25020
- capScope: "system",
25021
- addonId: null,
25022
- access: "create"
25023
- },
25024
- "restreamer.unregisterDevice": {
25025
- capName: "restreamer",
25026
- capScope: "system",
25027
- addonId: null,
25028
- access: "delete"
25029
- },
25030
25169
  "scriptRunner.run": {
25031
25170
  capName: "script-runner",
25032
25171
  capScope: "device",
@@ -25129,18 +25268,6 @@ Object.freeze({
25129
25268
  addonId: null,
25130
25269
  access: "create"
25131
25270
  },
25132
- "snapshotProvider.getSnapshot": {
25133
- capName: "snapshot-provider",
25134
- capScope: "system",
25135
- addonId: null,
25136
- access: "view"
25137
- },
25138
- "snapshotProvider.supportsDevice": {
25139
- capName: "snapshot-provider",
25140
- capScope: "system",
25141
- addonId: null,
25142
- access: "view"
25143
- },
25144
25271
  "ssoBridge.signBridgeToken": {
25145
25272
  capName: "sso-bridge",
25146
25273
  capScope: "system",
@@ -25567,30 +25694,6 @@ Object.freeze({
25567
25694
  addonId: null,
25568
25695
  access: "view"
25569
25696
  },
25570
- "streamingEngine.getStreamUrl": {
25571
- capName: "streaming-engine",
25572
- capScope: "system",
25573
- addonId: null,
25574
- access: "view"
25575
- },
25576
- "streamingEngine.listStreams": {
25577
- capName: "streaming-engine",
25578
- capScope: "system",
25579
- addonId: null,
25580
- access: "view"
25581
- },
25582
- "streamingEngine.registerStream": {
25583
- capName: "streaming-engine",
25584
- capScope: "system",
25585
- addonId: null,
25586
- access: "create"
25587
- },
25588
- "streamingEngine.unregisterStream": {
25589
- capName: "streaming-engine",
25590
- capScope: "system",
25591
- addonId: null,
25592
- access: "delete"
25593
- },
25594
25697
  "streamParams.getConfigSchema": {
25595
25698
  capName: "stream-params",
25596
25699
  capScope: "device",
@@ -25957,54 +26060,6 @@ Object.freeze({
25957
26060
  addonId: null,
25958
26061
  access: "create"
25959
26062
  },
25960
- "webrtc.closeSession": {
25961
- capName: "webrtc",
25962
- capScope: "system",
25963
- addonId: null,
25964
- access: "create"
25965
- },
25966
- "webrtc.createSession": {
25967
- capName: "webrtc",
25968
- capScope: "system",
25969
- addonId: null,
25970
- access: "create"
25971
- },
25972
- "webrtc.handleAnswer": {
25973
- capName: "webrtc",
25974
- capScope: "system",
25975
- addonId: null,
25976
- access: "create"
25977
- },
25978
- "webrtc.handleOffer": {
25979
- capName: "webrtc",
25980
- capScope: "system",
25981
- addonId: null,
25982
- access: "create"
25983
- },
25984
- "webrtc.hasAdaptiveBitrate": {
25985
- capName: "webrtc",
25986
- capScope: "system",
25987
- addonId: null,
25988
- access: "view"
25989
- },
25990
- "webrtc.registerStream": {
25991
- capName: "webrtc",
25992
- capScope: "system",
25993
- addonId: null,
25994
- access: "create"
25995
- },
25996
- "webrtc.supportsStream": {
25997
- capName: "webrtc",
25998
- capScope: "system",
25999
- addonId: null,
26000
- access: "view"
26001
- },
26002
- "webrtc.unregisterStream": {
26003
- capName: "webrtc",
26004
- capScope: "system",
26005
- addonId: null,
26006
- access: "delete"
26007
- },
26008
26063
  "webrtcSession.addIceCandidate": {
26009
26064
  capName: "webrtc-session",
26010
26065
  capScope: "device",
@@ -26756,6 +26811,10 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26756
26811
  motionActive = false;
26757
26812
  /** Keepalive re-emit timer for a sustained motion window. */
26758
26813
  motionKeepaliveTimer = null;
26814
+ /** Control-plane reachability poll — drives `device.online` from Dahua CGI
26815
+ * `getDeviceInfo` liveness, decoupled from stream-broker video health.
26816
+ * Started in `onActivate`, stopped in `removeDevice`. */
26817
+ reachabilityPoll = null;
26759
26818
  /** Single-flight guard for the `stream-params` camera refresh. */
26760
26819
  streamParamsRefreshInFlight = null;
26761
26820
  /** Single-flight guard for the `motion-zones` camera refresh. */
@@ -26928,12 +26987,33 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
26928
26987
  /** Phase 5 — device is live: open the onboard-motion event stream. */
26929
26988
  async onActivate() {
26930
26989
  this.ensureEventSubscription();
26990
+ this.startReachabilityPolling();
26991
+ }
26992
+ /** Start the control-plane reachability poll: a Dahua CGI `getDeviceInfo`
26993
+ * round-trip every 30s drives `device.online`, with hysteresis. Replaces
26994
+ * the old stream-health→online mirror so an on-demand (idle) but reachable
26995
+ * camera still reports ONLINE. Idempotent. */
26996
+ startReachabilityPolling() {
26997
+ if (this.reachabilityPoll) return;
26998
+ this.reachabilityPoll = startReachabilityPoll({
26999
+ probe: async () => {
27000
+ await this.ensureClient().getDeviceInfo();
27001
+ return true;
27002
+ },
27003
+ setOnline: (online) => {
27004
+ this.markOnline(online);
27005
+ },
27006
+ isEnabled: () => !this.disabled,
27007
+ logger: this.ctx.logger
27008
+ });
26931
27009
  }
26932
27010
  /** Teardown — stop the stream + motion timers, drop the client. */
26933
27011
  async removeDevice() {
26934
27012
  this.ctx.logger.info("Removing Amcrest camera", { tags: { deviceId: this.id } });
26935
27013
  this.teardownEventSubscription();
26936
27014
  this.clearPtzAutoStop();
27015
+ this.reachabilityPoll?.stop();
27016
+ this.reachabilityPoll = null;
26937
27017
  this.client = null;
26938
27018
  }
26939
27019
  registerStreamCatalogProvider() {
@@ -27184,9 +27264,14 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
27184
27264
  if (deviceId !== this.id) return;
27185
27265
  const client = this.ensureClient();
27186
27266
  const requested = Number.parseInt(presetId, 10);
27187
- const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27188
- let slot = Number.isFinite(requested) && requested >= 1 && !used.has(requested) ? requested : 1;
27189
- while (used.has(slot)) slot += 1;
27267
+ const DAHUA_MAX_PRESETS = 25;
27268
+ let slot;
27269
+ if (Number.isInteger(requested) && requested >= 1 && requested <= DAHUA_MAX_PRESETS) slot = requested;
27270
+ else {
27271
+ const used = new Set((await client.getPtzPresets(this.channel)).map((p) => p.id));
27272
+ slot = 1;
27273
+ while (used.has(slot)) slot += 1;
27274
+ }
27190
27275
  await client.ptzSetPreset(this.channel, slot);
27191
27276
  },
27192
27277
  deletePreset: async ({ deviceId, presetId }) => {
@@ -36949,21 +37034,7 @@ var AmcrestProviderAddon = class extends BaseDeviceProvider {
36949
37034
  throw new Error(`Amcrest: probe on ${host || "(unknown host)"} resolved neither mac nor host address — cannot persist a stable row key. Verify network reachability + credentials, then retry.`);
36950
37035
  }
36951
37036
  async onInitialize() {
36952
- const regs = await super.onInitialize();
36953
- this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
36954
- const data = event.data;
36955
- if (data.capName !== "camera-streams") return;
36956
- const deviceId = data.deviceId;
36957
- if (typeof deviceId !== "number") return;
36958
- const registry = this.ctx.kernel.deviceRegistry;
36959
- if (!registry) return;
36960
- if (registry.getAddonId(deviceId) !== this.addonId) return;
36961
- const device = registry.getById(deviceId);
36962
- if (!device) return;
36963
- const online = data.slice?.online === true;
36964
- if (device.online !== online) device.online = online;
36965
- });
36966
- return regs;
37037
+ return await super.onInitialize();
36967
37038
  }
36968
37039
  async supportsDiscovery() {
36969
37040
  return true;