@camstack/addon-pipeline-orchestrator 1.1.30 → 1.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-B5x-vmYL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-BvomAHii.mjs} +2 -2
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DnGsQN5q.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B8GJlTM4.mjs +26 -0
- package/dist/{hostInit-BfWJfsr0.mjs → hostInit-CZlukFwM.mjs} +2 -2
- package/dist/index.js +600 -387
- package/dist/index.mjs +600 -387
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DF4cZqbE.mjs +0 -26
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-ScLTfhI7.mjs +0 -26
package/dist/index.js
CHANGED
|
@@ -4631,7 +4631,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4631
4631
|
return inst;
|
|
4632
4632
|
}
|
|
4633
4633
|
//#endregion
|
|
4634
|
-
//#region ../types/dist/sleep-
|
|
4634
|
+
//#region ../types/dist/sleep-Cc14_yxc.mjs
|
|
4635
4635
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4636
4636
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4637
4637
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -4817,6 +4817,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4817
4817
|
*/
|
|
4818
4818
|
EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
|
|
4819
4819
|
/**
|
|
4820
|
+
* The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
|
|
4821
|
+
* Emitted by addon-pipeline-orchestrator whenever it (re)derives node
|
|
4822
|
+
* capabilities — at boot, on agent online/offline, and on an ingest-node
|
|
4823
|
+
* flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
|
|
4824
|
+
* keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
|
|
4825
|
+
* cross-process `getIngestOwner` query (push the authority's decision instead
|
|
4826
|
+
* of polling it on the hot path). Idempotent state — re-emitted on every
|
|
4827
|
+
* topology change, so a dropped event self-heals on the next one (plus the
|
|
4828
|
+
* broker's long backstop reconcile query).
|
|
4829
|
+
*/
|
|
4830
|
+
EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
|
|
4831
|
+
/**
|
|
4820
4832
|
* Periodic snapshot of per-node pipeline-runner load
|
|
4821
4833
|
* (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
|
|
4822
4834
|
* subscribe instead of polling `pipelineRunner.getLocalLoad`.
|
|
@@ -7308,6 +7320,28 @@ var ModelFormatsSchema = object({
|
|
|
7308
7320
|
tflite: ModelFormatEntrySchema.optional(),
|
|
7309
7321
|
pt: ModelFormatEntrySchema.optional()
|
|
7310
7322
|
});
|
|
7323
|
+
/**
|
|
7324
|
+
* Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
|
|
7325
|
+
* the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
|
|
7326
|
+
* grouped Family→Tier→Variant picker renders identically in the config UI and
|
|
7327
|
+
* in the pipeline/device steppers. The flat `id` stays the source of truth for
|
|
7328
|
+
* resolution/download/persistence; this is a presentation overlay resolved back
|
|
7329
|
+
* to an `id`.
|
|
7330
|
+
*/
|
|
7331
|
+
var ModelVariantGroupSchema = object({
|
|
7332
|
+
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
7333
|
+
family: string(),
|
|
7334
|
+
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
7335
|
+
tier: string(),
|
|
7336
|
+
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
7337
|
+
precision: _enum(["fp32", "int8"]).optional(),
|
|
7338
|
+
/**
|
|
7339
|
+
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
7340
|
+
* latency-optimized export (e.g. ReLU-activation / reduced-input variant)
|
|
7341
|
+
* — the slot the future performance variants plug into.
|
|
7342
|
+
*/
|
|
7343
|
+
optimization: _enum(["standard", "fast"]).optional()
|
|
7344
|
+
});
|
|
7311
7345
|
var ModelCatalogEntrySchema = object({
|
|
7312
7346
|
id: string(),
|
|
7313
7347
|
name: string(),
|
|
@@ -7337,7 +7371,43 @@ var ModelCatalogEntrySchema = object({
|
|
|
7337
7371
|
* Auxiliary files required at runtime (labels JSON, charset dict, etc.).
|
|
7338
7372
|
* Downloaded into the same modelsDir alongside the model file.
|
|
7339
7373
|
*/
|
|
7340
|
-
extraFiles: array(ModelExtraFileSchema).readonly().optional()
|
|
7374
|
+
extraFiles: array(ModelExtraFileSchema).readonly().optional(),
|
|
7375
|
+
/**
|
|
7376
|
+
* LEGACY entry — retained in the catalog so a persisted operator selection
|
|
7377
|
+
* still RESOLVES (and can be re-activated), but hidden from the selectable
|
|
7378
|
+
* model list and excluded from the auto format-default pick. Set on the
|
|
7379
|
+
* superseded / consolidated models (older lineages, redundant fp16 IRs) so
|
|
7380
|
+
* the active lineup stays the coherent curated ladder without deleting a
|
|
7381
|
+
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
7382
|
+
* an explicit legacy id that has a build for the node's format.
|
|
7383
|
+
*/
|
|
7384
|
+
legacy: boolean().optional(),
|
|
7385
|
+
/**
|
|
7386
|
+
* Measured quality/latency metadata — populated from the benchmark addon on
|
|
7387
|
+
* the real node classes. Absent = not yet measured (most entries today; the
|
|
7388
|
+
* catalog historically carried only `sizeMB`, a poor cross-architecture
|
|
7389
|
+
* speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
|
|
7390
|
+
*/
|
|
7391
|
+
metrics: object({
|
|
7392
|
+
map50: number().optional(),
|
|
7393
|
+
p95LatencyMs: record(string(), number()).optional()
|
|
7394
|
+
}).optional(),
|
|
7395
|
+
/**
|
|
7396
|
+
* SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
|
|
7397
|
+
* YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
|
|
7398
|
+
* the retraining addon and any future commercial distribution.
|
|
7399
|
+
*/
|
|
7400
|
+
license: string().optional(),
|
|
7401
|
+
/**
|
|
7402
|
+
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
7403
|
+
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
7404
|
+
* of a family's sizes and quantizations collapse into one grouped picker
|
|
7405
|
+
* instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
|
|
7406
|
+
* (legacy / custom models) — never shown in the grouped selector. The flat
|
|
7407
|
+
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
7408
|
+
* is a presentation overlay resolved back to an `id`.
|
|
7409
|
+
*/
|
|
7410
|
+
group: ModelVariantGroupSchema.optional()
|
|
7341
7411
|
});
|
|
7342
7412
|
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
7343
7413
|
format: literal("openvino"),
|
|
@@ -7398,8 +7468,8 @@ var RecordingModeSchema = _enum([
|
|
|
7398
7468
|
"onAudioThreshold"
|
|
7399
7469
|
]);
|
|
7400
7470
|
/**
|
|
7401
|
-
* First-class, authoritative per-camera storage mode — the
|
|
7402
|
-
* reads directly (never inferred from `rules`):
|
|
7471
|
+
* First-class, authoritative per-camera storage mode — the explicit choice the
|
|
7472
|
+
* UI reads directly (never inferred from `rules`):
|
|
7403
7473
|
* - `off` — not recording.
|
|
7404
7474
|
* - `events` — record only around triggers (motion / audio threshold),
|
|
7405
7475
|
* with pre/post-buffer.
|
|
@@ -9108,26 +9178,13 @@ DeviceType.Light, method(object({
|
|
|
9108
9178
|
percentage: number().min(0).max(100),
|
|
9109
9179
|
lastChangedAt: number()
|
|
9110
9180
|
});
|
|
9181
|
+
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
9111
9182
|
var StreamFormatSchema = _enum([
|
|
9112
9183
|
"webrtc",
|
|
9113
9184
|
"hls",
|
|
9114
9185
|
"mjpeg",
|
|
9115
9186
|
"rtsp"
|
|
9116
9187
|
]);
|
|
9117
|
-
var StreamInfoSchema = object({
|
|
9118
|
-
streamId: string(),
|
|
9119
|
-
format: StreamFormatSchema,
|
|
9120
|
-
url: string().nullable(),
|
|
9121
|
-
active: boolean()
|
|
9122
|
-
});
|
|
9123
|
-
method(object({
|
|
9124
|
-
streamId: string(),
|
|
9125
|
-
sourceUrl: string(),
|
|
9126
|
-
codec: string().optional()
|
|
9127
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
9128
|
-
streamId: string(),
|
|
9129
|
-
format: StreamFormatSchema
|
|
9130
|
-
}), string().nullable()), method(_void(), array(StreamInfoSchema));
|
|
9131
9188
|
var RtspRestreamEntrySchema = object({
|
|
9132
9189
|
brokerId: string(),
|
|
9133
9190
|
url: string(),
|
|
@@ -9792,7 +9849,7 @@ var ConsumablesStatusSchema = object({
|
|
|
9792
9849
|
})),
|
|
9793
9850
|
lastChangedAt: number()
|
|
9794
9851
|
});
|
|
9795
|
-
|
|
9852
|
+
Object.values(DeviceType), method(object({
|
|
9796
9853
|
deviceId: number().int().nonnegative(),
|
|
9797
9854
|
key: string().min(1)
|
|
9798
9855
|
}), _void(), {
|
|
@@ -10842,7 +10899,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
10842
10899
|
enabled: boolean(),
|
|
10843
10900
|
modelId: string(),
|
|
10844
10901
|
children: array(PipelineDefaultStepSchema).readonly(),
|
|
10845
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
10846
10902
|
group: string().optional(),
|
|
10847
10903
|
settings: record(string(), unknown()).optional()
|
|
10848
10904
|
}));
|
|
@@ -10867,7 +10923,9 @@ var PipelineModelOptionSchema = object({
|
|
|
10867
10923
|
formats: record(string(), object({
|
|
10868
10924
|
downloaded: boolean(),
|
|
10869
10925
|
sizeMB: number()
|
|
10870
|
-
}))
|
|
10926
|
+
})),
|
|
10927
|
+
group: ModelVariantGroupSchema.optional(),
|
|
10928
|
+
legacy: boolean().optional()
|
|
10871
10929
|
});
|
|
10872
10930
|
var ConfigFieldBridge = custom();
|
|
10873
10931
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -10918,15 +10976,42 @@ var EngineProvisioningSchema = object({
|
|
|
10918
10976
|
]),
|
|
10919
10977
|
progress: number().optional(),
|
|
10920
10978
|
error: string().optional(),
|
|
10921
|
-
nextRetryAt: number().optional()
|
|
10979
|
+
nextRetryAt: number().optional(),
|
|
10980
|
+
/**
|
|
10981
|
+
* Gate A (config-correctness gate at engine change): human-readable
|
|
10982
|
+
* config issues surfaced EAGERLY when the node's engine changes — model
|
|
10983
|
+
* substitutions ("chose X, running Y") and zero-build steps ("no model
|
|
10984
|
+
* has a <format> build"). Additive/optional: informational only, never
|
|
10985
|
+
* enforced here — `assertEngineReady` (readiness) still gates inference.
|
|
10986
|
+
* Absent/empty when the node-default tree resolves cleanly.
|
|
10987
|
+
*/
|
|
10988
|
+
configIssues: array(string()).optional()
|
|
10922
10989
|
});
|
|
10923
10990
|
var PipelineStepInputSchema = lazy(() => object({
|
|
10924
10991
|
addonId: string(),
|
|
10925
|
-
modelId: string(),
|
|
10992
|
+
modelId: string().optional(),
|
|
10926
10993
|
enabled: boolean().default(true),
|
|
10927
10994
|
children: array(PipelineStepInputSchema).optional(),
|
|
10928
10995
|
settings: record(string(), unknown()).optional()
|
|
10929
10996
|
}));
|
|
10997
|
+
var ModelSubstitutionSchema = object({
|
|
10998
|
+
addonId: string(),
|
|
10999
|
+
chosen: string(),
|
|
11000
|
+
running: string(),
|
|
11001
|
+
format: string()
|
|
11002
|
+
});
|
|
11003
|
+
var PipelineValidationIssueSchema = object({
|
|
11004
|
+
addonId: string(),
|
|
11005
|
+
kind: _enum(["unknown-addon", "no-format-build"]),
|
|
11006
|
+
detail: string()
|
|
11007
|
+
});
|
|
11008
|
+
var PipelineValidationResultSchema = object({
|
|
11009
|
+
ok: boolean(),
|
|
11010
|
+
issues: array(PipelineValidationIssueSchema).readonly(),
|
|
11011
|
+
substitutions: array(ModelSubstitutionSchema).readonly(),
|
|
11012
|
+
/** The node's `currentEngine.format` this validation ran against. */
|
|
11013
|
+
format: string()
|
|
11014
|
+
});
|
|
10930
11015
|
var ReferenceImageEntrySchema = object({
|
|
10931
11016
|
filename: string(),
|
|
10932
11017
|
stepIds: array(string()).readonly().optional()
|
|
@@ -11043,6 +11128,19 @@ var pipelineExecutorCapability = {
|
|
|
11043
11128
|
getGlobalSteps: method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()),
|
|
11044
11129
|
getGlobalPipelineConfig: method(_void(), PipelineConfigBridge),
|
|
11045
11130
|
getOrchestratorConfigSchema: method(_void(), ConfigUISchemaBridge),
|
|
11131
|
+
/**
|
|
11132
|
+
* Gate B (pre-init config-correctness gate). PURE COMPUTE against this
|
|
11133
|
+
* node's `currentEngine.format` — resolves `steps` the same way the
|
|
11134
|
+
* runtime dispatch path would, and reports what WOULD happen without
|
|
11135
|
+
* touching any node-global state. Called by the orchestrator at attach
|
|
11136
|
+
* time (`attachOn`), node-pinned to the TARGET node, so config problems
|
|
11137
|
+
* surface BEFORE `pipelineRunner.attachCamera` rather than at the first
|
|
11138
|
+
* per-frame resolve. `ok` is false iff `issues` is non-empty (both
|
|
11139
|
+
* `unknown-addon` and `no-format-build` are HARD issues); `substitutions`
|
|
11140
|
+
* is informational (a degraded-but-loadable model swap) and never
|
|
11141
|
+
* affects `ok`. Never throws.
|
|
11142
|
+
*/
|
|
11143
|
+
validatePipeline: method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema),
|
|
11046
11144
|
listTemplates: method(_void(), array(PipelineTemplateSchema$1).readonly()),
|
|
11047
11145
|
saveTemplate: method(object({
|
|
11048
11146
|
name: string(),
|
|
@@ -11425,6 +11523,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
11425
11523
|
kind: literal("remote-restream"),
|
|
11426
11524
|
/** The camera's source-owner node (slice 1: always the hub). */
|
|
11427
11525
|
ownerNodeId: string(),
|
|
11526
|
+
/**
|
|
11527
|
+
* The owner's LAN-reachable host, resolved by the orchestrator from the
|
|
11528
|
+
* per-node `reachableHost` override (Cluster UI). When present the runner
|
|
11529
|
+
* dials THIS host for the owner's restream, in preference to the
|
|
11530
|
+
* `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
|
|
11531
|
+
*/
|
|
11532
|
+
ownerReachableHost: string().optional(),
|
|
11428
11533
|
/** Operator override for the owner host the runner dials. */
|
|
11429
11534
|
hubHostnameOverride: string().optional()
|
|
11430
11535
|
})]).describe("Per-camera frame-source mode for the runner (P2c)");
|
|
@@ -11433,13 +11538,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
11433
11538
|
* specific runner instance via `attachCamera`. Carries everything the
|
|
11434
11539
|
* runner needs to subscribe to the local broker and execute inference.
|
|
11435
11540
|
*
|
|
11436
|
-
* Stateless-pipeline model: the
|
|
11437
|
-
*
|
|
11438
|
-
*
|
|
11439
|
-
*
|
|
11440
|
-
*
|
|
11441
|
-
* `engine`/`steps`/`audio` are optional during the additive migration
|
|
11442
|
-
* window; once orchestrator + UI are migrated they become required.
|
|
11541
|
+
* Stateless-pipeline model: the pipeline content (`steps`, optional
|
|
11542
|
+
* `audio`) travels with the attach payload. The runner keeps it in RAM
|
|
11543
|
+
* for the lifetime of the attach — on rebalance, edit, or restart the
|
|
11544
|
+
* orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
|
|
11545
|
+
* node-local, resolved by the executing runner at dispatch time.
|
|
11443
11546
|
*/
|
|
11444
11547
|
var RunnerCameraConfigSchema = object({
|
|
11445
11548
|
deviceId: number(),
|
|
@@ -11490,14 +11593,11 @@ var RunnerCameraConfigSchema = object({
|
|
|
11490
11593
|
*/
|
|
11491
11594
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
11492
11595
|
pipelineEnabled: boolean().default(true),
|
|
11493
|
-
/** Engine choice for video steps (runtime+backend+format). */
|
|
11494
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
11495
11596
|
/** Ordered tree of video steps. Absent → runner skips video detection. */
|
|
11496
11597
|
steps: array(PipelineStepInputSchema).readonly().optional(),
|
|
11497
11598
|
/** Audio classification branch. `enabled:false` disables, null skips. */
|
|
11498
11599
|
audio: object({
|
|
11499
|
-
|
|
11500
|
-
modelId: string(),
|
|
11600
|
+
modelId: string().optional(),
|
|
11501
11601
|
enabled: boolean()
|
|
11502
11602
|
}).nullable().optional(),
|
|
11503
11603
|
/**
|
|
@@ -15953,11 +16053,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
15953
16053
|
timestamp: number()
|
|
15954
16054
|
});
|
|
15955
16055
|
var CameraPipelineConfigSchema = object({
|
|
15956
|
-
engine: PipelineEngineChoiceSchema,
|
|
16056
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
15957
16057
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
15958
16058
|
audio: object({
|
|
15959
|
-
engine: PipelineEngineChoiceSchema,
|
|
15960
|
-
modelId: string(),
|
|
16059
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
16060
|
+
modelId: string().optional(),
|
|
15961
16061
|
enabled: boolean(),
|
|
15962
16062
|
settings: record(string(), unknown()).readonly().optional()
|
|
15963
16063
|
}).nullable().optional()
|
|
@@ -15972,7 +16072,7 @@ var PipelineTemplateSchema = object({
|
|
|
15972
16072
|
});
|
|
15973
16073
|
var AgentAddonConfigSchema = object({
|
|
15974
16074
|
enabled: boolean(),
|
|
15975
|
-
modelId: string(),
|
|
16075
|
+
modelId: string().optional(),
|
|
15976
16076
|
settings: record(string(), unknown()).readonly()
|
|
15977
16077
|
});
|
|
15978
16078
|
var AgentPipelineSettingsSchema = object({
|
|
@@ -15987,7 +16087,15 @@ var AgentPipelineSettingsSchema = object({
|
|
|
15987
16087
|
/** Node is eligible to run audio-analyzer sessions. */
|
|
15988
16088
|
audio: boolean().optional(),
|
|
15989
16089
|
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
15990
|
-
ingest: boolean().optional()
|
|
16090
|
+
ingest: boolean().optional(),
|
|
16091
|
+
/**
|
|
16092
|
+
* Operator override for the LAN host a cross-node decoder dials to reach
|
|
16093
|
+
* THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
|
|
16094
|
+
* falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
|
|
16095
|
+
* it already uses to reach the hub). Set this only when the auto-detected
|
|
16096
|
+
* address is wrong (multi-homed host, NAT, custom interface).
|
|
16097
|
+
*/
|
|
16098
|
+
reachableHost: string().optional()
|
|
15991
16099
|
});
|
|
15992
16100
|
var CameraPipelineForAgentSchema = object({
|
|
15993
16101
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -16093,6 +16201,15 @@ var GlobalMetricsSchema = object({
|
|
|
16093
16201
|
* capability providers.
|
|
16094
16202
|
*/
|
|
16095
16203
|
var CapabilityBindingsSchema = record(string(), string());
|
|
16204
|
+
/**
|
|
16205
|
+
* The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
|
|
16206
|
+
* its LAN-reachable host, if one is registered. See `getIngestOwner`.
|
|
16207
|
+
*/
|
|
16208
|
+
var IngestOwnerSchema = object({
|
|
16209
|
+
ownerNodeId: string(),
|
|
16210
|
+
reachableHost: string().optional(),
|
|
16211
|
+
configIssue: string().optional()
|
|
16212
|
+
});
|
|
16096
16213
|
/** Source block — always present; derives from the stream catalog. */
|
|
16097
16214
|
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
16098
16215
|
camStreamId: string(),
|
|
@@ -16107,6 +16224,14 @@ var CameraAssignmentStatusSchema = object({
|
|
|
16107
16224
|
detectionNodeId: string().nullable(),
|
|
16108
16225
|
decoderNodeId: string().nullable(),
|
|
16109
16226
|
audioNodeId: string().nullable(),
|
|
16227
|
+
/**
|
|
16228
|
+
* The node that OWNS this camera's physical source pull (dials the RTSP and
|
|
16229
|
+
* hosts the broker/restream) — the cluster ingest owner today
|
|
16230
|
+
* (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
|
|
16231
|
+
* the UI show WHERE a camera is sourced without SSH/logs, and is the node the
|
|
16232
|
+
* broker block below was read from (pinned). Nullable only pre-wiring.
|
|
16233
|
+
*/
|
|
16234
|
+
sourceNodeId: string().nullable(),
|
|
16110
16235
|
pinned: object({
|
|
16111
16236
|
detection: boolean(),
|
|
16112
16237
|
decoder: boolean(),
|
|
@@ -16307,6 +16432,14 @@ var pipelineOrchestratorCapability = {
|
|
|
16307
16432
|
kind: "mutation",
|
|
16308
16433
|
auth: "admin"
|
|
16309
16434
|
}),
|
|
16435
|
+
/**
|
|
16436
|
+
* The cluster's single camera-source owner — resolved UNCONDITIONALLY
|
|
16437
|
+
* from `clusterRoles.ingestNode` (NOT the detection-decode
|
|
16438
|
+
* `remoteSourcingNodes` knob). Consumers (recorder/snapshot/audio) pin
|
|
16439
|
+
* their broker calls to `ownerNodeId` and dial `reachableHost` when
|
|
16440
|
+
* present. Defaults to `{ ownerNodeId: 'hub' }`.
|
|
16441
|
+
*/
|
|
16442
|
+
getIngestOwner: method(_void(), IngestOwnerSchema),
|
|
16310
16443
|
/** Pin a device's decoder to a specific node. */
|
|
16311
16444
|
assignDecoder: method(object({
|
|
16312
16445
|
deviceId: number(),
|
|
@@ -16453,6 +16586,22 @@ var pipelineOrchestratorCapability = {
|
|
|
16453
16586
|
kind: "mutation",
|
|
16454
16587
|
auth: "admin"
|
|
16455
16588
|
}),
|
|
16589
|
+
/**
|
|
16590
|
+
* Set (or clear) the operator override for the LAN host a cross-node
|
|
16591
|
+
* decoder dials to reach this node's restream. Pass a non-empty string to
|
|
16592
|
+
* override the auto-detected address (the runner's `CAMSTACK_HUB_URL`
|
|
16593
|
+
* default); pass `null` or an empty string to clear it and revert to
|
|
16594
|
+
* auto-detect. Applied on the next dispatch cycle — takes effect for new
|
|
16595
|
+
* cross-node attaches; existing attaches keep their current host until
|
|
16596
|
+
* re-dispatched.
|
|
16597
|
+
*/
|
|
16598
|
+
setAgentReachableHost: method(object({
|
|
16599
|
+
agentNodeId: string(),
|
|
16600
|
+
reachableHost: string().nullable()
|
|
16601
|
+
}), object({ success: literal(true) }), {
|
|
16602
|
+
kind: "mutation",
|
|
16603
|
+
auth: "admin"
|
|
16604
|
+
}),
|
|
16456
16605
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
16457
16606
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
16458
16607
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -16544,22 +16693,6 @@ var pipelineOrchestratorCapability = {
|
|
|
16544
16693
|
})
|
|
16545
16694
|
}
|
|
16546
16695
|
};
|
|
16547
|
-
var RegisteredStreamSchema = object({
|
|
16548
|
-
streamId: string(),
|
|
16549
|
-
label: string().optional(),
|
|
16550
|
-
codec: string(),
|
|
16551
|
-
type: _enum(["video", "audio"]),
|
|
16552
|
-
sourceUrl: string()
|
|
16553
|
-
});
|
|
16554
|
-
var ExposedResourceSchema = object({
|
|
16555
|
-
streamId: string(),
|
|
16556
|
-
format: string(),
|
|
16557
|
-
value: string()
|
|
16558
|
-
});
|
|
16559
|
-
method(object({
|
|
16560
|
-
deviceId: number(),
|
|
16561
|
-
streams: array(RegisteredStreamSchema).readonly()
|
|
16562
|
-
}), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
|
|
16563
16696
|
/**
|
|
16564
16697
|
* Query filter for settings-store collections.
|
|
16565
16698
|
*/
|
|
@@ -16712,9 +16845,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
16712
16845
|
/**
|
|
16713
16846
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
16714
16847
|
*
|
|
16715
|
-
*
|
|
16716
|
-
*
|
|
16717
|
-
*
|
|
16848
|
+
* The `SnapshotAddon` wrapper returns this shape whether the frame came from
|
|
16849
|
+
* the device-native provider (onboard capture) or from the stream-broker
|
|
16850
|
+
* prebuffer fallback.
|
|
16718
16851
|
*/
|
|
16719
16852
|
var SnapshotImageSchema = object({
|
|
16720
16853
|
base64: string(),
|
|
@@ -16746,10 +16879,6 @@ DeviceType.Camera, method(object({
|
|
|
16746
16879
|
kind: "mutation",
|
|
16747
16880
|
auth: "admin"
|
|
16748
16881
|
});
|
|
16749
|
-
method(object({ deviceId: number() }), boolean()), method(object({
|
|
16750
|
-
deviceId: number(),
|
|
16751
|
-
streamId: string().optional()
|
|
16752
|
-
}), SnapshotImageSchema.nullable());
|
|
16753
16882
|
/**
|
|
16754
16883
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
16755
16884
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -17110,9 +17239,10 @@ method(object({
|
|
|
17110
17239
|
auth: "admin"
|
|
17111
17240
|
});
|
|
17112
17241
|
/**
|
|
17113
|
-
* Optional client-side hints sent at session creation to help the
|
|
17114
|
-
*
|
|
17115
|
-
*
|
|
17242
|
+
* Optional client-side hints sent at session creation to help the provider
|
|
17243
|
+
* pick the best native source. All fields optional — a viewer that knows
|
|
17244
|
+
* nothing still gets a sane default. (Relocated from the retired `webrtc`
|
|
17245
|
+
* collection cap; this `webrtc-session` cap is the live signaling surface.)
|
|
17116
17246
|
*/
|
|
17117
17247
|
var webrtcClientHintsSchema = object({
|
|
17118
17248
|
viewportWidth: number().int().positive().optional(),
|
|
@@ -17123,22 +17253,6 @@ var webrtcClientHintsSchema = object({
|
|
|
17123
17253
|
/** Hard tier override; takes precedence over scoring when registered. */
|
|
17124
17254
|
prefersTier: string().optional()
|
|
17125
17255
|
}).partial();
|
|
17126
|
-
method(object({
|
|
17127
|
-
streamId: string(),
|
|
17128
|
-
sdpOffer: string()
|
|
17129
|
-
}), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
|
|
17130
|
-
streamId: string(),
|
|
17131
|
-
codec: string()
|
|
17132
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
17133
|
-
streamId: string(),
|
|
17134
|
-
hints: webrtcClientHintsSchema.optional()
|
|
17135
|
-
}), object({
|
|
17136
|
-
sessionId: string(),
|
|
17137
|
-
sdpOffer: string()
|
|
17138
|
-
}), { kind: "mutation" }), method(object({
|
|
17139
|
-
sessionId: string(),
|
|
17140
|
-
sdpAnswer: string()
|
|
17141
|
-
}), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
|
|
17142
17256
|
/**
|
|
17143
17257
|
* Discriminated target for a WebRTC session. The client sends this
|
|
17144
17258
|
* structured object instead of building / parsing brokerId strings;
|
|
@@ -22229,6 +22343,12 @@ Object.freeze({
|
|
|
22229
22343
|
addonId: null,
|
|
22230
22344
|
access: "create"
|
|
22231
22345
|
},
|
|
22346
|
+
"pipelineExecutor.validatePipeline": {
|
|
22347
|
+
capName: "pipeline-executor",
|
|
22348
|
+
capScope: "system",
|
|
22349
|
+
addonId: null,
|
|
22350
|
+
access: "view"
|
|
22351
|
+
},
|
|
22232
22352
|
"pipelineOrchestrator.assignAudio": {
|
|
22233
22353
|
capName: "pipeline-orchestrator",
|
|
22234
22354
|
capScope: "system",
|
|
@@ -22337,6 +22457,12 @@ Object.freeze({
|
|
|
22337
22457
|
addonId: null,
|
|
22338
22458
|
access: "view"
|
|
22339
22459
|
},
|
|
22460
|
+
"pipelineOrchestrator.getIngestOwner": {
|
|
22461
|
+
capName: "pipeline-orchestrator",
|
|
22462
|
+
capScope: "system",
|
|
22463
|
+
addonId: null,
|
|
22464
|
+
access: "view"
|
|
22465
|
+
},
|
|
22340
22466
|
"pipelineOrchestrator.getPipelineAssignment": {
|
|
22341
22467
|
capName: "pipeline-orchestrator",
|
|
22342
22468
|
capScope: "system",
|
|
@@ -22409,6 +22535,12 @@ Object.freeze({
|
|
|
22409
22535
|
addonId: null,
|
|
22410
22536
|
access: "create"
|
|
22411
22537
|
},
|
|
22538
|
+
"pipelineOrchestrator.setAgentReachableHost": {
|
|
22539
|
+
capName: "pipeline-orchestrator",
|
|
22540
|
+
capScope: "system",
|
|
22541
|
+
addonId: null,
|
|
22542
|
+
access: "create"
|
|
22543
|
+
},
|
|
22412
22544
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
22413
22545
|
capName: "pipeline-orchestrator",
|
|
22414
22546
|
capScope: "system",
|
|
@@ -22739,24 +22871,6 @@ Object.freeze({
|
|
|
22739
22871
|
addonId: null,
|
|
22740
22872
|
access: "create"
|
|
22741
22873
|
},
|
|
22742
|
-
"restreamer.getExposedResources": {
|
|
22743
|
-
capName: "restreamer",
|
|
22744
|
-
capScope: "system",
|
|
22745
|
-
addonId: null,
|
|
22746
|
-
access: "view"
|
|
22747
|
-
},
|
|
22748
|
-
"restreamer.registerDevice": {
|
|
22749
|
-
capName: "restreamer",
|
|
22750
|
-
capScope: "system",
|
|
22751
|
-
addonId: null,
|
|
22752
|
-
access: "create"
|
|
22753
|
-
},
|
|
22754
|
-
"restreamer.unregisterDevice": {
|
|
22755
|
-
capName: "restreamer",
|
|
22756
|
-
capScope: "system",
|
|
22757
|
-
addonId: null,
|
|
22758
|
-
access: "delete"
|
|
22759
|
-
},
|
|
22760
22874
|
"scriptRunner.run": {
|
|
22761
22875
|
capName: "script-runner",
|
|
22762
22876
|
capScope: "device",
|
|
@@ -22859,18 +22973,6 @@ Object.freeze({
|
|
|
22859
22973
|
addonId: null,
|
|
22860
22974
|
access: "create"
|
|
22861
22975
|
},
|
|
22862
|
-
"snapshotProvider.getSnapshot": {
|
|
22863
|
-
capName: "snapshot-provider",
|
|
22864
|
-
capScope: "system",
|
|
22865
|
-
addonId: null,
|
|
22866
|
-
access: "view"
|
|
22867
|
-
},
|
|
22868
|
-
"snapshotProvider.supportsDevice": {
|
|
22869
|
-
capName: "snapshot-provider",
|
|
22870
|
-
capScope: "system",
|
|
22871
|
-
addonId: null,
|
|
22872
|
-
access: "view"
|
|
22873
|
-
},
|
|
22874
22976
|
"ssoBridge.signBridgeToken": {
|
|
22875
22977
|
capName: "sso-bridge",
|
|
22876
22978
|
capScope: "system",
|
|
@@ -23297,30 +23399,6 @@ Object.freeze({
|
|
|
23297
23399
|
addonId: null,
|
|
23298
23400
|
access: "view"
|
|
23299
23401
|
},
|
|
23300
|
-
"streamingEngine.getStreamUrl": {
|
|
23301
|
-
capName: "streaming-engine",
|
|
23302
|
-
capScope: "system",
|
|
23303
|
-
addonId: null,
|
|
23304
|
-
access: "view"
|
|
23305
|
-
},
|
|
23306
|
-
"streamingEngine.listStreams": {
|
|
23307
|
-
capName: "streaming-engine",
|
|
23308
|
-
capScope: "system",
|
|
23309
|
-
addonId: null,
|
|
23310
|
-
access: "view"
|
|
23311
|
-
},
|
|
23312
|
-
"streamingEngine.registerStream": {
|
|
23313
|
-
capName: "streaming-engine",
|
|
23314
|
-
capScope: "system",
|
|
23315
|
-
addonId: null,
|
|
23316
|
-
access: "create"
|
|
23317
|
-
},
|
|
23318
|
-
"streamingEngine.unregisterStream": {
|
|
23319
|
-
capName: "streaming-engine",
|
|
23320
|
-
capScope: "system",
|
|
23321
|
-
addonId: null,
|
|
23322
|
-
access: "delete"
|
|
23323
|
-
},
|
|
23324
23402
|
"streamParams.getConfigSchema": {
|
|
23325
23403
|
capName: "stream-params",
|
|
23326
23404
|
capScope: "device",
|
|
@@ -23687,54 +23765,6 @@ Object.freeze({
|
|
|
23687
23765
|
addonId: null,
|
|
23688
23766
|
access: "create"
|
|
23689
23767
|
},
|
|
23690
|
-
"webrtc.closeSession": {
|
|
23691
|
-
capName: "webrtc",
|
|
23692
|
-
capScope: "system",
|
|
23693
|
-
addonId: null,
|
|
23694
|
-
access: "create"
|
|
23695
|
-
},
|
|
23696
|
-
"webrtc.createSession": {
|
|
23697
|
-
capName: "webrtc",
|
|
23698
|
-
capScope: "system",
|
|
23699
|
-
addonId: null,
|
|
23700
|
-
access: "create"
|
|
23701
|
-
},
|
|
23702
|
-
"webrtc.handleAnswer": {
|
|
23703
|
-
capName: "webrtc",
|
|
23704
|
-
capScope: "system",
|
|
23705
|
-
addonId: null,
|
|
23706
|
-
access: "create"
|
|
23707
|
-
},
|
|
23708
|
-
"webrtc.handleOffer": {
|
|
23709
|
-
capName: "webrtc",
|
|
23710
|
-
capScope: "system",
|
|
23711
|
-
addonId: null,
|
|
23712
|
-
access: "create"
|
|
23713
|
-
},
|
|
23714
|
-
"webrtc.hasAdaptiveBitrate": {
|
|
23715
|
-
capName: "webrtc",
|
|
23716
|
-
capScope: "system",
|
|
23717
|
-
addonId: null,
|
|
23718
|
-
access: "view"
|
|
23719
|
-
},
|
|
23720
|
-
"webrtc.registerStream": {
|
|
23721
|
-
capName: "webrtc",
|
|
23722
|
-
capScope: "system",
|
|
23723
|
-
addonId: null,
|
|
23724
|
-
access: "create"
|
|
23725
|
-
},
|
|
23726
|
-
"webrtc.supportsStream": {
|
|
23727
|
-
capName: "webrtc",
|
|
23728
|
-
capScope: "system",
|
|
23729
|
-
addonId: null,
|
|
23730
|
-
access: "view"
|
|
23731
|
-
},
|
|
23732
|
-
"webrtc.unregisterStream": {
|
|
23733
|
-
capName: "webrtc",
|
|
23734
|
-
capScope: "system",
|
|
23735
|
-
addonId: null,
|
|
23736
|
-
access: "delete"
|
|
23737
|
-
},
|
|
23738
23768
|
"webrtcSession.addIceCandidate": {
|
|
23739
23769
|
capName: "webrtc-session",
|
|
23740
23770
|
capScope: "device",
|
|
@@ -23897,18 +23927,21 @@ function applyStepPatch(base, patch) {
|
|
|
23897
23927
|
/**
|
|
23898
23928
|
* Seed an agent's addonDefaults map from the catalog. For each addonId
|
|
23899
23929
|
* advertised by any slot, populate a sensible entry:
|
|
23900
|
-
* - `modelId` = addon's `defaultModelIdByFormat[engine.format]` if
|
|
23901
|
-
* present, else `defaultModelId` (from StepDefinition).
|
|
23902
23930
|
* - `enabled` = addon's `enabledByDefault` (absent = true).
|
|
23903
23931
|
* - `settings` = catalog's default settings (empty for now — the
|
|
23904
23932
|
* runtime applies step-level defaults when settings[key] is absent).
|
|
23905
23933
|
*
|
|
23934
|
+
* `modelId` is intentionally ABSENT on a fresh entry — model selection is
|
|
23935
|
+
* data only now. When the operator hasn't explicitly chosen a model, the
|
|
23936
|
+
* step carries no `modelId` and the executing NODE resolves its own
|
|
23937
|
+
* format default at runtime (the resolver has no notion of engines).
|
|
23938
|
+
*
|
|
23906
23939
|
* Entries already present in `current` are LEFT ALONE — operators who
|
|
23907
23940
|
* customised a model keep their choice after catalog growth adds new
|
|
23908
23941
|
* addons. Entries for addonIds that no longer exist in the catalog are
|
|
23909
23942
|
* dropped (the addon was removed; its config is dead weight).
|
|
23910
23943
|
*/
|
|
23911
|
-
function seedAgentAddonDefaults(current,
|
|
23944
|
+
function seedAgentAddonDefaults(current, catalog) {
|
|
23912
23945
|
const next = {};
|
|
23913
23946
|
const liveAddonIds = /* @__PURE__ */ new Set();
|
|
23914
23947
|
for (const slot of catalog.slots) for (const addon of slot.addons) {
|
|
@@ -23919,7 +23952,6 @@ function seedAgentAddonDefaults(current, engine, catalog) {
|
|
|
23919
23952
|
}
|
|
23920
23953
|
next[addon.id] = {
|
|
23921
23954
|
enabled: addon.enabledByDefault ?? true,
|
|
23922
|
-
modelId: pickDefaultModelId(addon, engine.format),
|
|
23923
23955
|
settings: {}
|
|
23924
23956
|
};
|
|
23925
23957
|
}
|
|
@@ -23927,27 +23959,11 @@ function seedAgentAddonDefaults(current, engine, catalog) {
|
|
|
23927
23959
|
return next;
|
|
23928
23960
|
}
|
|
23929
23961
|
/**
|
|
23930
|
-
* Pick the best model ID for `(addon, agent.format)`:
|
|
23931
|
-
* 1. `defaultModelIdByFormat[agent.format]` — explicit per-format choice
|
|
23932
|
-
* 2. `defaultModelId` from the StepDefinition — format-agnostic default
|
|
23933
|
-
* 3. First model in `addon.models[]` that has `formats[agent.format]` — desperate fallback
|
|
23934
|
-
*
|
|
23935
|
-
* Returns the first candidate whose model exists in the catalog entry's
|
|
23936
|
-
* `formats` map for the target engine format. If no candidate matches,
|
|
23937
|
-
* falls through to `addon.defaultModelId` (even if incompatible — let
|
|
23938
|
-
* the runtime error explicitly rather than silently substitute).
|
|
23939
|
-
*/
|
|
23940
|
-
function pickDefaultModelId(addon, format) {
|
|
23941
|
-
const byFormat = addon.defaultModelIdByFormat?.[format];
|
|
23942
|
-
if (byFormat && addon.models.some((m) => m.id === byFormat)) return byFormat;
|
|
23943
|
-
if (addon.models.some((m) => m.id === addon.defaultModelId && m.formats?.[format])) return addon.defaultModelId;
|
|
23944
|
-
const compat = addon.models.find((m) => m.formats?.[format]);
|
|
23945
|
-
if (compat) return compat.id;
|
|
23946
|
-
return addon.defaultModelId;
|
|
23947
|
-
}
|
|
23948
|
-
/**
|
|
23949
23962
|
* Main resolver — produces the `CameraPipelineConfig` the runner should
|
|
23950
23963
|
* receive for `(deviceId, agent)`. Pure: zero I/O, zero side effects.
|
|
23964
|
+
* DATA ONLY: this resolver has no notion of engines. A step carries a
|
|
23965
|
+
* `modelId` only when the operator explicitly chose one; when absent,
|
|
23966
|
+
* the executing node resolves its own format default at runtime.
|
|
23951
23967
|
*
|
|
23952
23968
|
* Step chain:
|
|
23953
23969
|
* 1. `cameraSettings.pipelineByAgent[agentNodeId]` → wholesale override
|
|
@@ -23955,16 +23971,14 @@ function pickDefaultModelId(addon, format) {
|
|
|
23955
23971
|
*
|
|
23956
23972
|
* "Inference disabled for this camera" is NOT handled here — the caller
|
|
23957
23973
|
* (resolvePipelineForDevice) consults the detection-pipeline binding and
|
|
23958
|
-
* short-circuits to
|
|
23974
|
+
* short-circuits to an empty pipeline before calling this resolver.
|
|
23959
23975
|
*/
|
|
23960
|
-
function resolvePipeline(agentSettings, cameraSettings, agentNodeId,
|
|
23976
|
+
function resolvePipeline(agentSettings, cameraSettings, agentNodeId, catalog) {
|
|
23961
23977
|
const cam = cameraSettings ?? {};
|
|
23962
23978
|
const wholesale = cam.pipelineByAgent?.[agentNodeId];
|
|
23963
23979
|
if (wholesale) return {
|
|
23964
|
-
engine,
|
|
23965
23980
|
steps: wholesale.steps,
|
|
23966
23981
|
audio: wholesale.audio ? {
|
|
23967
|
-
engine,
|
|
23968
23982
|
modelId: wholesale.audio.modelId,
|
|
23969
23983
|
enabled: wholesale.audio.enabled
|
|
23970
23984
|
} : null
|
|
@@ -23986,11 +24000,9 @@ function resolvePipeline(agentSettings, cameraSettings, agentNodeId, engine, cat
|
|
|
23986
24000
|
for (const [addonId, override] of Object.entries(toggles)) {
|
|
23987
24001
|
if (override !== true) continue;
|
|
23988
24002
|
if (addonDefaults[addonId]) continue;
|
|
23989
|
-
|
|
23990
|
-
if (!catalogAddon) continue;
|
|
24003
|
+
if (!findInCatalog(catalog, addonId)) continue;
|
|
23991
24004
|
const patched = applyStepPatch({
|
|
23992
24005
|
enabled: true,
|
|
23993
|
-
modelId: pickDefaultModelId(catalogAddon, engine.format),
|
|
23994
24006
|
settings: {}
|
|
23995
24007
|
}, overridesForAgent[addonId]);
|
|
23996
24008
|
if (!patched.enabled) continue;
|
|
@@ -24008,10 +24020,8 @@ function resolvePipeline(agentSettings, cameraSettings, agentNodeId, engine, cat
|
|
|
24008
24020
|
else videoAddons.push(entry);
|
|
24009
24021
|
}
|
|
24010
24022
|
return {
|
|
24011
|
-
engine,
|
|
24012
24023
|
steps: buildTreeFromAddons(videoAddons, catalog),
|
|
24013
24024
|
audio: audioAddons.length > 0 ? {
|
|
24014
|
-
engine,
|
|
24015
24025
|
modelId: audioAddons[0].cfg.modelId,
|
|
24016
24026
|
enabled: true,
|
|
24017
24027
|
...Object.keys(audioAddons[0].cfg.settings).length > 0 ? { settings: audioAddons[0].cfg.settings } : {}
|
|
@@ -24049,9 +24059,9 @@ function buildTreeFromAddons(enabled, catalog) {
|
|
|
24049
24059
|
metaById.set(addonId, meta);
|
|
24050
24060
|
nodes.set(addonId, {
|
|
24051
24061
|
addonId,
|
|
24052
|
-
modelId: cfg.modelId,
|
|
24053
24062
|
enabled: true,
|
|
24054
24063
|
children: [],
|
|
24064
|
+
...cfg.modelId ? { modelId: cfg.modelId } : {},
|
|
24055
24065
|
...Object.keys(cfg.settings).length > 0 ? { settings: cfg.settings } : {}
|
|
24056
24066
|
});
|
|
24057
24067
|
}
|
|
@@ -24159,7 +24169,7 @@ function startAudioChunkPoller(options) {
|
|
|
24159
24169
|
const subId = lifecycle.activeSubscriptionId;
|
|
24160
24170
|
if (subId) {
|
|
24161
24171
|
lifecycle.activeSubscriptionId = null;
|
|
24162
|
-
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }).catch((err) => {
|
|
24172
|
+
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }, nodePin(options.ownerNodeId)).catch((err) => {
|
|
24163
24173
|
options.logger.warn("audio-chunk poller: unsubscribeAudioChunks failed", { meta: {
|
|
24164
24174
|
brokerId: options.brokerId,
|
|
24165
24175
|
subscriptionId: subId,
|
|
@@ -24177,7 +24187,8 @@ function startAudioChunkPoller(options) {
|
|
|
24177
24187
|
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
24178
24188
|
*/
|
|
24179
24189
|
async function subscribeWithRetry(options, lifecycle) {
|
|
24180
|
-
const { api, brokerId, tag, logger } = options;
|
|
24190
|
+
const { api, brokerId, tag, ownerNodeId, logger } = options;
|
|
24191
|
+
const pin = nodePin(ownerNodeId);
|
|
24181
24192
|
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
24182
24193
|
let attempt = 0;
|
|
24183
24194
|
while (!lifecycle.stopped) {
|
|
@@ -24186,9 +24197,9 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
24186
24197
|
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
24187
24198
|
brokerId,
|
|
24188
24199
|
tag
|
|
24189
|
-
});
|
|
24200
|
+
}, pin);
|
|
24190
24201
|
if (lifecycle.stopped) {
|
|
24191
|
-
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
24202
|
+
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }, pin).catch((err) => {
|
|
24192
24203
|
logger.warn("audio-chunk poller: late unsubscribe failed", { meta: {
|
|
24193
24204
|
brokerId,
|
|
24194
24205
|
subscriptionId: result.subscriptionId,
|
|
@@ -24232,14 +24243,15 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
24232
24243
|
* disowned.
|
|
24233
24244
|
*/
|
|
24234
24245
|
function startPolling(options, lifecycle) {
|
|
24235
|
-
const { api, brokerId, tag, onChunk, logger } = options;
|
|
24246
|
+
const { api, brokerId, tag, ownerNodeId, onChunk, logger } = options;
|
|
24247
|
+
const pin = nodePin(ownerNodeId);
|
|
24236
24248
|
let consecutiveFailures = 0;
|
|
24237
24249
|
const resubscribe = async () => {
|
|
24238
24250
|
try {
|
|
24239
24251
|
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
24240
24252
|
brokerId,
|
|
24241
24253
|
tag
|
|
24242
|
-
});
|
|
24254
|
+
}, pin);
|
|
24243
24255
|
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
24244
24256
|
logger.info("audio-chunk poller: re-subscribed after broker outage", { meta: {
|
|
24245
24257
|
brokerId,
|
|
@@ -24260,7 +24272,7 @@ function startPolling(options, lifecycle) {
|
|
|
24260
24272
|
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
24261
24273
|
subscriptionId: subId,
|
|
24262
24274
|
maxCount: PULL_MAX_COUNT
|
|
24263
|
-
});
|
|
24275
|
+
}, pin);
|
|
24264
24276
|
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
24265
24277
|
brokerId,
|
|
24266
24278
|
subscriptionId: subId
|
|
@@ -24325,6 +24337,7 @@ function mapAssignment(input) {
|
|
|
24325
24337
|
detectionNodeId: input.detectionNodeId,
|
|
24326
24338
|
decoderNodeId: input.decoderNodeId,
|
|
24327
24339
|
audioNodeId: input.audioNodeId,
|
|
24340
|
+
sourceNodeId: input.sourceNodeId,
|
|
24328
24341
|
pinned: {
|
|
24329
24342
|
detection: input.pinned.detection,
|
|
24330
24343
|
decoder: input.pinned.decoder,
|
|
@@ -24498,14 +24511,58 @@ function computeFrameSourceNodes(input) {
|
|
|
24498
24511
|
return enabledDecoderNodes.filter((nodeId) => nodeId === sourceOwnerNodeId || remote.includes(nodeId));
|
|
24499
24512
|
}
|
|
24500
24513
|
//#endregion
|
|
24514
|
+
//#region src/ingest-owner.ts
|
|
24515
|
+
/**
|
|
24516
|
+
* The cluster's single camera-source owner + its LAN-reachable host, resolved
|
|
24517
|
+
* UNCONDITIONALLY from `clusterRoles.ingestNode` (NOT `resolveSourceOwner`,
|
|
24518
|
+
* which is gated on the detection-decode `remoteSourcingNodes` knob). Consumers
|
|
24519
|
+
* (recorder/snapshot/audio) pin their broker calls to `ownerNodeId` and dial
|
|
24520
|
+
* `reachableHost`. Defaults to `{ ownerNodeId: 'hub' }` — byte-identical to
|
|
24521
|
+
* today's hub-only behavior.
|
|
24522
|
+
*
|
|
24523
|
+
* `configIssue` is a NON-FATAL guard: when the configured ingest owner is an
|
|
24524
|
+
* agent that's unreachable or not ingest-capable, it surfaces a human-readable
|
|
24525
|
+
* warning string. The hub is always assumed reachable and ingest-capable, so
|
|
24526
|
+
* `ownerNodeId === 'hub'` never produces a `configIssue`.
|
|
24527
|
+
*/
|
|
24528
|
+
function resolveIngestOwner(clusterRoles, reachableHostByNode, ingestCapableNodes) {
|
|
24529
|
+
const ownerNodeId = clusterRoles.ingestNode;
|
|
24530
|
+
const reachableHost = reachableHostByNode.get(ownerNodeId);
|
|
24531
|
+
const configIssue = computeIngestConfigIssue(ownerNodeId, reachableHost, ingestCapableNodes);
|
|
24532
|
+
return {
|
|
24533
|
+
ownerNodeId,
|
|
24534
|
+
...reachableHost !== void 0 ? { reachableHost } : {},
|
|
24535
|
+
...configIssue !== void 0 ? { configIssue } : {}
|
|
24536
|
+
};
|
|
24537
|
+
}
|
|
24538
|
+
function computeIngestConfigIssue(ownerNodeId, reachableHost, ingestCapableNodes) {
|
|
24539
|
+
if (ownerNodeId === "hub") return void 0;
|
|
24540
|
+
if (reachableHost === void 0) return `ingest node '${ownerNodeId}' has no reachable host — set its reachableHost or cameras cannot be sourced from it`;
|
|
24541
|
+
if (!ingestCapableNodes.includes(ownerNodeId)) return `ingest node '${ownerNodeId}' is not ingest-capable — enable its ingest capability`;
|
|
24542
|
+
}
|
|
24543
|
+
//#endregion
|
|
24501
24544
|
//#region src/source-owner.ts
|
|
24502
24545
|
/**
|
|
24546
|
+
* The set of nodes allowed to run the REMOTE-SOURCE leg — every decode-enabled
|
|
24547
|
+
* node EXCEPT the camera-source OWNER (the node that dials the camera). Derived
|
|
24548
|
+
* from `enabledDecoderNodes`. When the owner is the hub this equals the old
|
|
24549
|
+
* hub-exclusion (byte-identical to Phase 1); when the owner is an agent the hub
|
|
24550
|
+
* itself becomes a valid remote-source target.
|
|
24551
|
+
*
|
|
24552
|
+
* Rollout-safe: when the owner is the only decode node the result is `[]`, which
|
|
24553
|
+
* keeps `resolveSourceOwner` unmodeled and every frame source `local-broker`.
|
|
24554
|
+
* Pure: preserves input order, never mutates.
|
|
24555
|
+
*/
|
|
24556
|
+
function deriveRemoteSourcingNodes(enabledDecoderNodes, ownerNodeId) {
|
|
24557
|
+
return enabledDecoderNodes.filter((nodeId) => nodeId !== ownerNodeId);
|
|
24558
|
+
}
|
|
24559
|
+
/**
|
|
24503
24560
|
* The node that owns a camera's source pull (dials the real RTSP, hosts its
|
|
24504
24561
|
* broker), or `undefined` while ownership is unmodeled (rollout OFF).
|
|
24505
24562
|
*/
|
|
24506
24563
|
function resolveSourceOwner(input) {
|
|
24507
24564
|
if (input.remoteSourcingNodes.length === 0) return void 0;
|
|
24508
|
-
return input.assignedOwner ?? input.
|
|
24565
|
+
return input.assignedOwner ?? input.ownerNodeId;
|
|
24509
24566
|
}
|
|
24510
24567
|
/**
|
|
24511
24568
|
* The `frameSource` the attach payload carries for a target node:
|
|
@@ -24520,13 +24577,14 @@ function resolveSourceOwner(input) {
|
|
|
24520
24577
|
* degrade to the safe local path, never to an unauthorized cross-node pull.
|
|
24521
24578
|
*/
|
|
24522
24579
|
function selectRunnerFrameSource(input) {
|
|
24523
|
-
const { targetNodeId, sourceOwnerNodeId, remoteSourcingNodes, hubHostnameOverride } = input;
|
|
24580
|
+
const { targetNodeId, sourceOwnerNodeId, remoteSourcingNodes, ownerReachableHost, hubHostnameOverride } = input;
|
|
24524
24581
|
if (sourceOwnerNodeId === void 0) return { kind: "local-broker" };
|
|
24525
24582
|
if (targetNodeId === sourceOwnerNodeId) return { kind: "local-broker" };
|
|
24526
24583
|
if (!remoteSourcingNodes.includes(targetNodeId)) return { kind: "local-broker" };
|
|
24527
24584
|
return {
|
|
24528
24585
|
kind: "remote-restream",
|
|
24529
24586
|
ownerNodeId: sourceOwnerNodeId,
|
|
24587
|
+
...ownerReachableHost !== void 0 ? { ownerReachableHost } : {},
|
|
24530
24588
|
...hubHostnameOverride !== void 0 ? { hubHostnameOverride } : {}
|
|
24531
24589
|
};
|
|
24532
24590
|
}
|
|
@@ -24534,16 +24592,29 @@ function selectRunnerFrameSource(input) {
|
|
|
24534
24592
|
//#region src/load-balancer.ts
|
|
24535
24593
|
/**
|
|
24536
24594
|
* Compute the L2 capacity score for a runner node. Lower is better.
|
|
24537
|
-
* The score is a weighted sum of the runner's active workload so the balancer
|
|
24538
|
-
* prefers agents that are serving fewer cameras OR draining queues quickly.
|
|
24539
24595
|
*
|
|
24540
|
-
*
|
|
24541
|
-
*
|
|
24542
|
-
*
|
|
24543
|
-
*
|
|
24596
|
+
* The score is **committed standing load + real-time backpressure**:
|
|
24597
|
+
*
|
|
24598
|
+
* - `attachedCameras` — the primary signal. Every attached camera is a
|
|
24599
|
+
* standing commitment: an `on-motion` camera costs ≈ zero while idle but
|
|
24600
|
+
* WILL fork a decode+inference session on its node the moment motion fires.
|
|
24601
|
+
* Counting *attachments* (not current inference activity) is what makes the
|
|
24602
|
+
* balancer honest about steady-state load and gives the per-node `weights`
|
|
24603
|
+
* sliders camera-count-share semantics.
|
|
24604
|
+
* - `queueDepthTotal` — real-time pressure: a node whose frame queues are
|
|
24605
|
+
* backing up is falling behind NOW and must be deprioritised beyond its
|
|
24606
|
+
* camera count.
|
|
24607
|
+
*
|
|
24608
|
+
* The previous formula multiplied `attachedCameras * avgInferenceFps`, which
|
|
24609
|
+
* scored a node carrying many IDLE on-motion cameras (fps 0) identically to an
|
|
24610
|
+
* empty node — the balancer then piled unpinned cameras onto an already-loaded
|
|
24611
|
+
* node until `maxCameras` bit. `avgInferenceFps` (in practice a mislabeled SUM
|
|
24612
|
+
* of per-camera fps) is deliberately dropped: transient inference activity is
|
|
24613
|
+
* not steady-state load, and a genuinely overloaded node surfaces through
|
|
24614
|
+
* `queueDepthTotal` instead.
|
|
24544
24615
|
*/
|
|
24545
24616
|
function computeCapacityScore(load) {
|
|
24546
|
-
return
|
|
24617
|
+
return Math.max(load.attachedCameras, 0) + Math.max(load.queueDepthTotal, 0);
|
|
24547
24618
|
}
|
|
24548
24619
|
/**
|
|
24549
24620
|
* Node detection weight, defaulting to 1 when absent/invalid. A higher weight
|
|
@@ -24563,6 +24634,18 @@ function weightedScore(load, weights) {
|
|
|
24563
24634
|
return computeCapacityScore(load) / nodeWeight(load.nodeId, weights);
|
|
24564
24635
|
}
|
|
24565
24636
|
/**
|
|
24637
|
+
* Weighted-score improvement of moving one camera off `current` onto `target`
|
|
24638
|
+
* (both weight-adjusted). Positive = `target` is less loaded than `current`.
|
|
24639
|
+
*
|
|
24640
|
+
* Used by the auto-rebalance hysteresis: a migration is applied only when this
|
|
24641
|
+
* clears a margin (> 1), so equalizing a mere 1-camera gap — which would just
|
|
24642
|
+
* reverse the imbalance — is skipped and a periodic pass never churns an
|
|
24643
|
+
* already-balanced cluster.
|
|
24644
|
+
*/
|
|
24645
|
+
function capacityImprovement(current, target, weights) {
|
|
24646
|
+
return weightedScore(current, weights) - weightedScore(target, weights);
|
|
24647
|
+
}
|
|
24648
|
+
/**
|
|
24566
24649
|
* Returns true when the node has remaining capacity.
|
|
24567
24650
|
* A node is eligible iff `cap` is unlimited (null/absent/<=0) OR
|
|
24568
24651
|
* its `attachedCameras` count is strictly less than the cap.
|
|
@@ -24707,11 +24790,11 @@ var EngineChoiceSchema = object({
|
|
|
24707
24790
|
});
|
|
24708
24791
|
var NodeBindingsSchema = record(string(), record(string(), string()));
|
|
24709
24792
|
var StoredPipelineConfigSchema = object({
|
|
24710
|
-
engine: EngineChoiceSchema,
|
|
24793
|
+
engine: EngineChoiceSchema.optional(),
|
|
24711
24794
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
24712
24795
|
audio: object({
|
|
24713
|
-
engine: EngineChoiceSchema,
|
|
24714
|
-
modelId: string(),
|
|
24796
|
+
engine: EngineChoiceSchema.optional(),
|
|
24797
|
+
modelId: string().optional(),
|
|
24715
24798
|
enabled: boolean(),
|
|
24716
24799
|
settings: record(string(), unknown()).readonly().optional()
|
|
24717
24800
|
}).nullable().optional()
|
|
@@ -24727,7 +24810,7 @@ var StoredPipelineTemplateSchema = object({
|
|
|
24727
24810
|
var TemplatesSchema = record(string(), StoredPipelineTemplateSchema);
|
|
24728
24811
|
var StoredAgentAddonConfigSchema = object({
|
|
24729
24812
|
enabled: boolean(),
|
|
24730
|
-
modelId: string(),
|
|
24813
|
+
modelId: string().optional(),
|
|
24731
24814
|
settings: record(string(), unknown()).readonly()
|
|
24732
24815
|
});
|
|
24733
24816
|
var StoredAgentPipelineSettingsSchema = object({
|
|
@@ -24737,7 +24820,8 @@ var StoredAgentPipelineSettingsSchema = object({
|
|
|
24737
24820
|
detect: boolean().optional(),
|
|
24738
24821
|
decode: boolean().optional(),
|
|
24739
24822
|
audio: boolean().optional(),
|
|
24740
|
-
ingest: boolean().optional()
|
|
24823
|
+
ingest: boolean().optional(),
|
|
24824
|
+
reachableHost: string().optional()
|
|
24741
24825
|
});
|
|
24742
24826
|
var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
|
|
24743
24827
|
var StoredCameraStepOverridePatchSchema = object({
|
|
@@ -24748,7 +24832,7 @@ var StoredCameraStepOverridePatchSchema = object({
|
|
|
24748
24832
|
var StoredCameraPipelineForAgentSchema = object({
|
|
24749
24833
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
24750
24834
|
audio: object({
|
|
24751
|
-
modelId: string(),
|
|
24835
|
+
modelId: string().optional(),
|
|
24752
24836
|
enabled: boolean()
|
|
24753
24837
|
}).nullable()
|
|
24754
24838
|
});
|
|
@@ -24830,8 +24914,8 @@ var PipelineWatchdog = class {
|
|
|
24830
24914
|
const now = this.deps.now();
|
|
24831
24915
|
for (const cam of this.cameras.values()) {
|
|
24832
24916
|
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
24833
|
-
if (stalled) this.deps.logger.warn(line);
|
|
24834
|
-
else this.deps.logger.info(line);
|
|
24917
|
+
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
24918
|
+
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
24835
24919
|
for (const r of recoveries) {
|
|
24836
24920
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
24837
24921
|
if (rt) {
|
|
@@ -25270,6 +25354,21 @@ var PENDING_RETRY_INTERVAL_MS = 6e4;
|
|
|
25270
25354
|
/** Debounce window for `schedulePendingRetry` — coalesces capacity/eligibility/readiness signals. */
|
|
25271
25355
|
var PENDING_RETRY_DEBOUNCE_MS = 2e3;
|
|
25272
25356
|
/**
|
|
25357
|
+
* Periodic auto-rebalance sweep. New attaches are already load-balanced at
|
|
25358
|
+
* dispatch time; this corrects DRIFT that accumulates over time (uneven
|
|
25359
|
+
* detach, a node returning online, a weight change) so the steady-state
|
|
25360
|
+
* distribution tracks the per-node weights. Only runs with ≥2 enabled detect
|
|
25361
|
+
* nodes, and migrates under hysteresis so a balanced cluster is a no-op.
|
|
25362
|
+
*/
|
|
25363
|
+
var AUTO_REBALANCE_INTERVAL_MS = 6e4;
|
|
25364
|
+
/**
|
|
25365
|
+
* Hysteresis margin (weight-adjusted cameras) for the periodic auto-rebalance:
|
|
25366
|
+
* migrate a camera only when its target node is at least this much less loaded
|
|
25367
|
+
* than its current node. > 1 so equalizing a single-camera gap (which would
|
|
25368
|
+
* only reverse the imbalance) is skipped — prevents periodic churn.
|
|
25369
|
+
*/
|
|
25370
|
+
var AUTO_REBALANCE_MIN_IMPROVEMENT = 1.5;
|
|
25371
|
+
/**
|
|
25273
25372
|
* Remote-assignment health loop (T7) bounded backoff. A remote camera flagged
|
|
25274
25373
|
* as unhealthy (0-fps / metrics-stale) is re-placed at most
|
|
25275
25374
|
* `REMOTE_HEALTH_MAX_ATTEMPTS` times inside a rolling `REMOTE_HEALTH_WINDOW_MS`
|
|
@@ -25307,6 +25406,7 @@ var OrchestratorDiagnosticsSchema = object({
|
|
|
25307
25406
|
enabledNodes: array(string()),
|
|
25308
25407
|
enabledDecoderNodes: array(string()),
|
|
25309
25408
|
enabledAudioNodes: array(string()),
|
|
25409
|
+
enabledIngestNodes: array(string()),
|
|
25310
25410
|
clusterRoles: object({
|
|
25311
25411
|
ingestNode: string(),
|
|
25312
25412
|
audioNode: string()
|
|
@@ -25452,21 +25552,38 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25452
25552
|
enabledDecoderNodes = ["hub"];
|
|
25453
25553
|
/**
|
|
25454
25554
|
* Nodes allowed to run the REMOTE-SOURCE leg — dial a camera's source-owner
|
|
25455
|
-
* restream over the LAN and decode it locally.
|
|
25456
|
-
*
|
|
25457
|
-
*
|
|
25458
|
-
*
|
|
25459
|
-
* `
|
|
25460
|
-
* pre-
|
|
25555
|
+
* restream over the LAN and decode it locally. DERIVED (Phase-1 activation,
|
|
25556
|
+
* `deriveRemoteSourcingNodes`) from `enabledDecoderNodes` minus the hub, in
|
|
25557
|
+
* `refreshNodeCapabilities`: every decode-enabled AGENT is a remote-sourcing
|
|
25558
|
+
* node. When only the hub decodes this stays `[]`, so `resolveSourceOwner`
|
|
25559
|
+
* returns `undefined` and every `selectRunnerFrameSource` emits
|
|
25560
|
+
* `local-broker` — bit-identical to pre-activation (rollout-safe).
|
|
25461
25561
|
*/
|
|
25462
25562
|
remoteSourcingNodes = [];
|
|
25463
25563
|
/**
|
|
25564
|
+
* Per-node `reachableHost` override (the LAN host a cross-node decoder dials
|
|
25565
|
+
* to reach that node's restream), from `agentSettings[node].reachableHost`.
|
|
25566
|
+
* DERIVED alongside the enabled-node sets in {@link refreshNodeCapabilities}
|
|
25567
|
+
* so `attachOn` resolves the owner host synchronously (no per-attach durable
|
|
25568
|
+
* read). Absent entry → the runner auto-detects via `CAMSTACK_HUB_URL`.
|
|
25569
|
+
*/
|
|
25570
|
+
reachableHostByNode = /* @__PURE__ */ new Map();
|
|
25571
|
+
/**
|
|
25464
25572
|
* Hub-wide allow-list of node ids eligible to run audio-analyzer sessions.
|
|
25465
25573
|
* DERIVED from the per-node capability store (`agentSettings[node].audio`).
|
|
25466
25574
|
* Defaults to `['hub']`.
|
|
25467
25575
|
*/
|
|
25468
25576
|
enabledAudioNodes = ["hub"];
|
|
25469
25577
|
/**
|
|
25578
|
+
* Hub-wide allow-list of node ids eligible to run the INGEST leg (dial the
|
|
25579
|
+
* camera's real source, host its broker). DERIVED from the per-node
|
|
25580
|
+
* capability store (`agentSettings[node].ingest`), mirroring
|
|
25581
|
+
* `enabledAudioNodes`. Defaults to `['hub']`. Not yet consumed by any
|
|
25582
|
+
* placement logic — this field only makes the persisted `ingest` flag
|
|
25583
|
+
* readable for a later config guard.
|
|
25584
|
+
*/
|
|
25585
|
+
enabledIngestNodes = ["hub"];
|
|
25586
|
+
/**
|
|
25470
25587
|
* Cluster-wide singleton role assignments (placement-model §6). Each role
|
|
25471
25588
|
* is the ONE node that serves it for every camera. Driven by the
|
|
25472
25589
|
* `ingestNode` / `audioNode` node-selects in the addon global schema;
|
|
@@ -25574,6 +25691,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25574
25691
|
pendingRetryInFlight = false;
|
|
25575
25692
|
/** Set when a pending-retry is requested while one is already in-flight; triggers a follow-up pass. */
|
|
25576
25693
|
pendingRetryRerunRequested = false;
|
|
25694
|
+
/** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
|
|
25695
|
+
autoRebalanceTimer = null;
|
|
25696
|
+
/** True while an auto-rebalance pass is running (skip overlap). */
|
|
25697
|
+
autoRebalanceInFlight = false;
|
|
25577
25698
|
initTimestamp = 0;
|
|
25578
25699
|
constructor() {
|
|
25579
25700
|
super({});
|
|
@@ -25676,7 +25797,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25676
25797
|
if (!isEvent(event, EventCategory.PipelineInferenceResult)) return;
|
|
25677
25798
|
this.handleInferenceResult(event.data).catch((err) => {
|
|
25678
25799
|
const msg = errMsg(err);
|
|
25679
|
-
this.ctx.logger.error("PipelineInferenceResult post-processing failed", {
|
|
25800
|
+
this.ctx.logger.error("PipelineInferenceResult post-processing failed", {
|
|
25801
|
+
tags: { deviceId: event.data.deviceId },
|
|
25802
|
+
meta: { error: msg }
|
|
25803
|
+
});
|
|
25680
25804
|
});
|
|
25681
25805
|
});
|
|
25682
25806
|
const PROFILE_SLOTS_DEBOUNCE_MS = 200;
|
|
@@ -25792,6 +25916,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25792
25916
|
});
|
|
25793
25917
|
this.pipelineWatchdog.start(PipelineOrchestratorAddon.WATCHDOG_INTERVAL_MS);
|
|
25794
25918
|
this.pendingRetryTimer = setInterval(() => void this.retryPendingDispatches(), PENDING_RETRY_INTERVAL_MS);
|
|
25919
|
+
this.autoRebalanceTimer = setInterval(() => void this.runAutoRebalance(), AUTO_REBALANCE_INTERVAL_MS);
|
|
25795
25920
|
this.ctx.logger.info("Pipeline orchestrator detection-wiring subscriptions installed");
|
|
25796
25921
|
this.migrateLegacyFlagsToBindings().catch((err) => {
|
|
25797
25922
|
this.ctxIfReady?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg(err) } });
|
|
@@ -26002,6 +26127,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26002
26127
|
enabledNodes: [...this.enabledNodes],
|
|
26003
26128
|
enabledDecoderNodes: [...this.enabledDecoderNodes],
|
|
26004
26129
|
enabledAudioNodes: [...this.enabledAudioNodes],
|
|
26130
|
+
enabledIngestNodes: [...this.enabledIngestNodes],
|
|
26005
26131
|
clusterRoles: { ...this.clusterRoles },
|
|
26006
26132
|
assignedDeviceCount: this.assignments.size,
|
|
26007
26133
|
cameraConfigCount: this.cameraConfigs.size,
|
|
@@ -26027,6 +26153,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26027
26153
|
clearTimeout(this.pendingRetryDebounceTimer);
|
|
26028
26154
|
this.pendingRetryDebounceTimer = null;
|
|
26029
26155
|
}
|
|
26156
|
+
if (this.autoRebalanceTimer !== null) {
|
|
26157
|
+
clearInterval(this.autoRebalanceTimer);
|
|
26158
|
+
this.autoRebalanceTimer = null;
|
|
26159
|
+
}
|
|
26030
26160
|
for (const t of this.slotReadRetryTimers.values()) clearTimeout(t);
|
|
26031
26161
|
this.slotReadRetryTimers.clear();
|
|
26032
26162
|
this.slotReadRetryAttempts.clear();
|
|
@@ -26292,8 +26422,30 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26292
26422
|
this.assignments.delete(input.deviceId);
|
|
26293
26423
|
return { success: true };
|
|
26294
26424
|
}
|
|
26295
|
-
|
|
26425
|
+
/**
|
|
26426
|
+
* Periodic auto-rebalance tick. Corrects placement drift toward the per-node
|
|
26427
|
+
* weights without operator action. Guards: never overlaps itself; skips when
|
|
26428
|
+
* fewer than two nodes can run detection (a hub-only cluster has nothing to
|
|
26429
|
+
* balance); migrates under {@link AUTO_REBALANCE_MIN_IMPROVEMENT} hysteresis
|
|
26430
|
+
* so a balanced cluster is a no-op. Errors are swallowed (best-effort net).
|
|
26431
|
+
*/
|
|
26432
|
+
async runAutoRebalance() {
|
|
26433
|
+
if (!this.ctxIfReady) return;
|
|
26434
|
+
if (this.autoRebalanceInFlight) return;
|
|
26435
|
+
if (this.enabledNodes.length < 2) return;
|
|
26436
|
+
this.autoRebalanceInFlight = true;
|
|
26437
|
+
try {
|
|
26438
|
+
const { migrated } = await this.rebalance({ minImprovement: AUTO_REBALANCE_MIN_IMPROVEMENT });
|
|
26439
|
+
if (migrated > 0) this.ctx.logger.info("auto-rebalance: corrected placement drift", { meta: { migrated } });
|
|
26440
|
+
} catch (err) {
|
|
26441
|
+
this.ctx.logger.debug("auto-rebalance pass failed", { meta: { error: errMsg(err) } });
|
|
26442
|
+
} finally {
|
|
26443
|
+
this.autoRebalanceInFlight = false;
|
|
26444
|
+
}
|
|
26445
|
+
}
|
|
26446
|
+
async rebalance(opts) {
|
|
26296
26447
|
if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
|
|
26448
|
+
const minImprovement = Math.max(opts?.minImprovement ?? 0, 0);
|
|
26297
26449
|
const loads = await this.collectAgentLoad({ onlyEnabled: true });
|
|
26298
26450
|
const nodeCaps = await this.buildNodeCaps();
|
|
26299
26451
|
const nodeWeights = await this.buildNodeWeights();
|
|
@@ -26331,6 +26483,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26331
26483
|
continue;
|
|
26332
26484
|
}
|
|
26333
26485
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
26486
|
+
if (current && minImprovement > 0) {
|
|
26487
|
+
const tallied = talliedLoads();
|
|
26488
|
+
const currentLoad = tallied.find((l) => l.nodeId === current.agentNodeId);
|
|
26489
|
+
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
26490
|
+
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
26491
|
+
}
|
|
26334
26492
|
if (current) {
|
|
26335
26493
|
await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
|
|
26336
26494
|
const msg = errMsg(err);
|
|
@@ -26341,7 +26499,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26341
26499
|
});
|
|
26342
26500
|
bumpAttached(current.agentNodeId, -1);
|
|
26343
26501
|
}
|
|
26344
|
-
await this.
|
|
26502
|
+
const reResolved = await this.resolvePipelineForDevice(deviceId, decision.agentNodeId);
|
|
26503
|
+
const targetConfig = reResolved.steps.length > 0 ? {
|
|
26504
|
+
...config,
|
|
26505
|
+
steps: reResolved.steps,
|
|
26506
|
+
audio: reResolved.audio ?? null
|
|
26507
|
+
} : config;
|
|
26508
|
+
this.cameraConfigs.set(deviceId, targetConfig);
|
|
26509
|
+
await this.attachOn(decision.agentNodeId, targetConfig);
|
|
26345
26510
|
bumpAttached(decision.agentNodeId, 1);
|
|
26346
26511
|
this.recordAssignment(deviceId, decision.agentNodeId, "rebalance", false);
|
|
26347
26512
|
migrated++;
|
|
@@ -26440,11 +26605,37 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26440
26605
|
async attachOn(nodeId, config) {
|
|
26441
26606
|
const api = this.ctx.api;
|
|
26442
26607
|
if (!api) throw new Error(`attachOn(${nodeId}, ${config.deviceId}): this.ctx.api not available`);
|
|
26608
|
+
const sourceOwnerNodeId = this.sourceOwner(config.deviceId);
|
|
26609
|
+
const ownerReachableHost = sourceOwnerNodeId !== void 0 ? this.reachableHostByNode.get(sourceOwnerNodeId) : void 0;
|
|
26443
26610
|
const frameSource = selectRunnerFrameSource({
|
|
26444
26611
|
targetNodeId: nodeId,
|
|
26445
|
-
sourceOwnerNodeId
|
|
26446
|
-
remoteSourcingNodes: this.remoteSourcingNodes
|
|
26612
|
+
sourceOwnerNodeId,
|
|
26613
|
+
remoteSourcingNodes: this.remoteSourcingNodes,
|
|
26614
|
+
...ownerReachableHost !== void 0 ? { ownerReachableHost } : {}
|
|
26447
26615
|
});
|
|
26616
|
+
api.pipelineExecutor.validatePipeline.query({ steps: [...config.steps ?? []] }, nodePin(nodeId)).then((validation) => {
|
|
26617
|
+
if (!validation || validation.ok && validation.substitutions.length === 0) return;
|
|
26618
|
+
const logMeta = {
|
|
26619
|
+
nodeId,
|
|
26620
|
+
deviceId: config.deviceId,
|
|
26621
|
+
issues: validation.issues,
|
|
26622
|
+
substitutions: validation.substitutions
|
|
26623
|
+
};
|
|
26624
|
+
if (!validation.ok) this.ctx.logger.warn("attachOn: pre-init pipeline validation found issues", {
|
|
26625
|
+
tags: {
|
|
26626
|
+
nodeId,
|
|
26627
|
+
deviceId: config.deviceId
|
|
26628
|
+
},
|
|
26629
|
+
meta: logMeta
|
|
26630
|
+
});
|
|
26631
|
+
else this.ctx.logger.info("attachOn: pre-init pipeline validation found model substitutions", {
|
|
26632
|
+
tags: {
|
|
26633
|
+
nodeId,
|
|
26634
|
+
deviceId: config.deviceId
|
|
26635
|
+
},
|
|
26636
|
+
meta: logMeta
|
|
26637
|
+
});
|
|
26638
|
+
}).catch(() => {});
|
|
26448
26639
|
try {
|
|
26449
26640
|
await api.pipelineRunner.attachCamera.mutate({
|
|
26450
26641
|
...config,
|
|
@@ -26459,7 +26650,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26459
26650
|
audioMode: config.audioMode,
|
|
26460
26651
|
motionStreamId: config.motionStreamId,
|
|
26461
26652
|
detectionStreamId: config.detectionStreamId,
|
|
26462
|
-
hasEngine: !!config.engine,
|
|
26463
26653
|
stepsCount: config.steps?.length ?? null,
|
|
26464
26654
|
hasAudio: config.audio?.enabled ?? null
|
|
26465
26655
|
};
|
|
@@ -26564,18 +26754,31 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26564
26754
|
return this.clusterRoles.ingestNode;
|
|
26565
26755
|
}
|
|
26566
26756
|
/**
|
|
26757
|
+
* Cap surface: the cluster's single camera-source owner + its
|
|
26758
|
+
* LAN-reachable host, resolved UNCONDITIONALLY from `clusterRoles.ingestNode`
|
|
26759
|
+
* (NOT `resolveSourceOwner`/`sourceOwner`, which are gated on the
|
|
26760
|
+
* detection-decode `remoteSourcingNodes` rollout knob). Consumers
|
|
26761
|
+
* (recorder/snapshot/audio) call this to pin their broker calls to the
|
|
26762
|
+
* owner and dial `reachableHost`. Rollout-safe: defaults to
|
|
26763
|
+
* `{ ownerNodeId: 'hub' }` — byte-identical to today's hub-only behavior.
|
|
26764
|
+
*/
|
|
26765
|
+
async getIngestOwner() {
|
|
26766
|
+
return resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
26767
|
+
}
|
|
26768
|
+
/**
|
|
26567
26769
|
* The node that owns `deviceId`'s source pull, or `undefined` while
|
|
26568
26770
|
* ownership is UNMODELED — which is exactly the rollout-OFF state: with the
|
|
26569
26771
|
* `remoteSourcingNodes` knob empty (the default), `resolveSourceOwner`
|
|
26570
26772
|
* returns `undefined`, `computeFrameSourceNodes` collapses to the global
|
|
26571
26773
|
* `enabledDecoderNodes`, and `selectRunnerFrameSource` emits `local-broker`
|
|
26572
26774
|
* for every target — bit-identical to pre-Phase-2 behavior. Populating the
|
|
26573
|
-
* knob
|
|
26574
|
-
* turns eligibility per-camera and the
|
|
26775
|
+
* knob (a decode node other than the ingest owner exists) models ownership as
|
|
26776
|
+
* `clusterRoles.ingestNode` and thereby turns eligibility per-camera and the
|
|
26777
|
+
* remote-source leg dispatchable.
|
|
26575
26778
|
*/
|
|
26576
26779
|
sourceOwner(deviceId) {
|
|
26577
26780
|
return resolveSourceOwner({
|
|
26578
|
-
|
|
26781
|
+
ownerNodeId: this.clusterRoles.ingestNode,
|
|
26579
26782
|
remoteSourcingNodes: this.remoteSourcingNodes,
|
|
26580
26783
|
...deviceId !== void 0 ? { assignedOwner: this.assignSource(deviceId) } : {}
|
|
26581
26784
|
});
|
|
@@ -26688,7 +26891,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26688
26891
|
* lookup. Works for any backend (ffmpeg / node-av / future) and no longer
|
|
26689
26892
|
* returns empty on a node-av-only node (the old `decoder-ffmpeg`-keyed read
|
|
26690
26893
|
* did). Returns `null` when the value is empty/unset, `undefined` on a read
|
|
26691
|
-
* failure (keep the last known).
|
|
26894
|
+
* failure (keep the last known). Node-local — the same pattern the
|
|
26895
|
+
* orchestrator's (now-deleted) per-node engine reader used before engine
|
|
26896
|
+
* resolution moved entirely off the orchestrator.
|
|
26692
26897
|
*/
|
|
26693
26898
|
async readNodeDecodeHwaccel(nodeId) {
|
|
26694
26899
|
try {
|
|
@@ -27164,7 +27369,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27164
27369
|
if (resolved.steps.length === 0) continue;
|
|
27165
27370
|
const next = {
|
|
27166
27371
|
...cached,
|
|
27167
|
-
engine: resolved.engine,
|
|
27168
27372
|
steps: resolved.steps,
|
|
27169
27373
|
audio: resolved.audio ?? null
|
|
27170
27374
|
};
|
|
@@ -27234,7 +27438,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27234
27438
|
if (resolved.steps.length === 0) continue;
|
|
27235
27439
|
const next = {
|
|
27236
27440
|
...cached,
|
|
27237
|
-
engine: resolved.engine,
|
|
27238
27441
|
steps: resolved.steps,
|
|
27239
27442
|
audio: resolved.audio ?? null
|
|
27240
27443
|
};
|
|
@@ -27794,6 +27997,33 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27794
27997
|
this.schedulePendingRetry();
|
|
27795
27998
|
return { success: true };
|
|
27796
27999
|
}
|
|
28000
|
+
/**
|
|
28001
|
+
* Set (or clear) the per-node `reachableHost` override — the LAN host a
|
|
28002
|
+
* cross-node decoder dials to reach this node's restream. A non-empty string
|
|
28003
|
+
* overrides the auto-detected host the runner would otherwise derive from
|
|
28004
|
+
* `CAMSTACK_HUB_URL`; `null` or an empty string clears it (revert to
|
|
28005
|
+
* auto-detect). Persisted onto the node's `agentSettings` entry, then a
|
|
28006
|
+
* debounced re-dispatch so new cross-node attaches pick up the host.
|
|
28007
|
+
*/
|
|
28008
|
+
async setAgentReachableHost(input) {
|
|
28009
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
28010
|
+
const trimmed = input.reachableHost?.trim();
|
|
28011
|
+
const host = trimmed !== void 0 && trimmed.length > 0 ? trimmed : void 0;
|
|
28012
|
+
const next = {
|
|
28013
|
+
...existing ?? {
|
|
28014
|
+
addonDefaults: {},
|
|
28015
|
+
maxCameras: null
|
|
28016
|
+
},
|
|
28017
|
+
reachableHost: host
|
|
28018
|
+
};
|
|
28019
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
28020
|
+
this.ctx.logger.info("agentSettings.reachableHost updated", {
|
|
28021
|
+
tags: { nodeId: input.agentNodeId },
|
|
28022
|
+
meta: { reachableHost: host ?? null }
|
|
28023
|
+
});
|
|
28024
|
+
this.schedulePendingRetry();
|
|
28025
|
+
return { success: true };
|
|
28026
|
+
}
|
|
27797
28027
|
async getCameraSettings(input) {
|
|
27798
28028
|
return (await this.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
27799
28029
|
}
|
|
@@ -27877,6 +28107,25 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27877
28107
|
});
|
|
27878
28108
|
for (const deviceId of affected) await this.emitResolvedCameraUpdated(deviceId);
|
|
27879
28109
|
}
|
|
28110
|
+
/**
|
|
28111
|
+
* Re-dispatch EVERY assigned camera through the REAL attach path
|
|
28112
|
+
* (`redispatchSingleCamera` → `attachOn`), so each camera's `frameSource`
|
|
28113
|
+
* (`local-broker` vs `remote-restream` naming the current ingest owner) is
|
|
28114
|
+
* RECOMPUTED and re-applied to its runner. Used when the cluster ingest owner
|
|
28115
|
+
* changes: `frameSource` is computed only in `attachOn` (the single choke
|
|
28116
|
+
* point), so a plain `emitCameraUpdated` config event would NOT move the
|
|
28117
|
+
* source — the runner would keep sourcing from the OLD owner. No-op when
|
|
28118
|
+
* nothing is assigned.
|
|
28119
|
+
*/
|
|
28120
|
+
async redispatchAllActiveCameras(reason) {
|
|
28121
|
+
const deviceIds = [...this.assignments.keys()];
|
|
28122
|
+
if (deviceIds.length === 0) return;
|
|
28123
|
+
this.ctx.logger.info("re-dispatching all cameras", { meta: {
|
|
28124
|
+
reason,
|
|
28125
|
+
count: deviceIds.length
|
|
28126
|
+
} });
|
|
28127
|
+
for (const deviceId of deviceIds) await this.redispatchSingleCamera(deviceId);
|
|
28128
|
+
}
|
|
27880
28129
|
async listTemplates() {
|
|
27881
28130
|
const templates = await this.readTemplatesMap();
|
|
27882
28131
|
return Object.values(templates).slice().toSorted((a, b) => a.name.localeCompare(b.name));
|
|
@@ -27953,6 +28202,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27953
28202
|
const STAGE_TIMEOUT_MS = 3e3;
|
|
27954
28203
|
const pipelineAssignment = this.assignments.get(deviceId) ?? null;
|
|
27955
28204
|
const detectionNodeId = pipelineAssignment?.agentNodeId ?? null;
|
|
28205
|
+
const sourceNodeId = this.assignSource(deviceId);
|
|
27956
28206
|
const decoderPinRaw = (api ? await this.ctx.settings?.readDeviceStore(deviceId).catch(() => ({})) ?? {} : {})["decoderNodeId"];
|
|
27957
28207
|
const decoderPinned = typeof decoderPinRaw === "string" && decoderPinRaw !== "auto";
|
|
27958
28208
|
const advisoryDecoderNodeId = detectionNodeId ? await this.resolveDecoderNode(deviceId, detectionNodeId).catch(() => null) : null;
|
|
@@ -27966,7 +28216,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27966
28216
|
};
|
|
27967
28217
|
const detectionReason = pipelineAssignment !== null ? pipelineAssignment.reason : this.cameraConfigs.has(deviceId) ? `pending:${this.pendingReasons.get(deviceId) ?? "pending"}` : void 0;
|
|
27968
28218
|
const liveDecoder = { nodeId: null };
|
|
27969
|
-
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query() : null;
|
|
28219
|
+
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
|
|
27970
28220
|
const sourceFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then((slots) => {
|
|
27971
28221
|
return { streams: slots.filter((s) => s.deviceId === deviceId).map((s) => ({
|
|
27972
28222
|
camStreamId: s.sourceCamStreamId ?? s.brokerId,
|
|
@@ -27992,10 +28242,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27992
28242
|
const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
|
|
27993
28243
|
return {
|
|
27994
28244
|
slot,
|
|
27995
|
-
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }).catch(() => null),
|
|
27996
|
-
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }).catch(() => null)
|
|
28245
|
+
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
|
|
28246
|
+
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
|
|
27997
28247
|
};
|
|
27998
|
-
})), api.streamBroker.getAllRtspEntries.query({}).catch(() => null)]);
|
|
28248
|
+
})), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
|
|
27999
28249
|
const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
|
|
28000
28250
|
profile: slot.profile,
|
|
28001
28251
|
status: slot.status,
|
|
@@ -28100,6 +28350,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28100
28350
|
detectionNodeId,
|
|
28101
28351
|
decoderNodeId,
|
|
28102
28352
|
audioNodeId,
|
|
28353
|
+
sourceNodeId,
|
|
28103
28354
|
pinned,
|
|
28104
28355
|
reasons,
|
|
28105
28356
|
sourceResult,
|
|
@@ -28177,14 +28428,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28177
28428
|
}
|
|
28178
28429
|
/**
|
|
28179
28430
|
* Fetch `pipelineExecutor.getSchema({nodeId})` through the standard cap
|
|
28180
|
-
* router, cached per
|
|
28181
|
-
*
|
|
28182
|
-
*
|
|
28431
|
+
* router, cached per node with a short TTL. Returns `null` when the
|
|
28432
|
+
* executor can't be reached (no runner yet attached) — callers fall
|
|
28433
|
+
* back to an empty pipeline in that case.
|
|
28434
|
+
*
|
|
28435
|
+
* The cache key keeps the `${nodeId}::*` suffix (rather than a bare
|
|
28436
|
+
* `nodeId`) so `handleDetectionPipelineReadiness`'s prefix-match
|
|
28437
|
+
* invalidation (`key.startsWith(\`${nodeId}::\`)`) keeps working
|
|
28438
|
+
* unchanged; there is no longer a per-engine variant to distinguish.
|
|
28183
28439
|
*/
|
|
28184
|
-
async getCatalogForAgent(nodeId
|
|
28440
|
+
async getCatalogForAgent(nodeId) {
|
|
28185
28441
|
const api = this.ctx.api;
|
|
28186
28442
|
if (!api) return null;
|
|
28187
|
-
const engineKey =
|
|
28443
|
+
const engineKey = `${nodeId}::*`;
|
|
28188
28444
|
const now = Date.now();
|
|
28189
28445
|
const hit = this.catalogCache.get(engineKey);
|
|
28190
28446
|
if (hit && now - hit.at < PipelineOrchestratorAddon.CATALOG_CACHE_TTL_MS) return hit.schema;
|
|
@@ -28210,11 +28466,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28210
28466
|
*
|
|
28211
28467
|
* Behaviour:
|
|
28212
28468
|
* - Fetch catalog for the node.
|
|
28213
|
-
* - Call `seedAgentAddonDefaults(current.addonDefaults,
|
|
28469
|
+
* - Call `seedAgentAddonDefaults(current.addonDefaults, catalog)` — preserves
|
|
28214
28470
|
* operator customisations, adds missing addons, drops orphans.
|
|
28215
28471
|
* - Persist if anything changed.
|
|
28216
28472
|
*
|
|
28217
|
-
* Idempotent: a second call with the same catalog is a no-op.
|
|
28473
|
+
* Idempotent: a second call with the same catalog is a no-op. Data
|
|
28474
|
+
* only: no engine is resolved or stored here — the executing node
|
|
28475
|
+
* picks its own format default at runtime when a step's `modelId` is
|
|
28476
|
+
* absent.
|
|
28218
28477
|
*/
|
|
28219
28478
|
async seedAgentSettingsFromCatalog(nodeId) {
|
|
28220
28479
|
await this.ctx.acquireCapability("pipeline-executor", {
|
|
@@ -28224,71 +28483,28 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28224
28483
|
const catalog = await this.getCatalogForAgent(nodeId);
|
|
28225
28484
|
if (!catalog) return null;
|
|
28226
28485
|
const existing = (await this.readAgentSettingsMap())[nodeId];
|
|
28227
|
-
const
|
|
28228
|
-
const nextAddonDefaults = seedAgentAddonDefaults(existing?.addonDefaults ?? {}, engine, catalog);
|
|
28486
|
+
const nextAddonDefaults = seedAgentAddonDefaults(existing?.addonDefaults ?? {}, catalog);
|
|
28229
28487
|
const next = { addonDefaults: nextAddonDefaults };
|
|
28230
28488
|
if (!existing || JSON.stringify(existing.addonDefaults) !== JSON.stringify(next.addonDefaults)) {
|
|
28231
28489
|
await this.writeAgentSettings(nodeId, next);
|
|
28232
28490
|
this.ctx.logger.info("agentSettings seeded/refreshed", {
|
|
28233
28491
|
tags: { nodeId },
|
|
28234
|
-
meta: {
|
|
28235
|
-
runtime: engine.runtime,
|
|
28236
|
-
backend: engine.backend,
|
|
28237
|
-
format: engine.format,
|
|
28238
|
-
addons: Object.keys(nextAddonDefaults).length
|
|
28239
|
-
}
|
|
28492
|
+
meta: { addons: Object.keys(nextAddonDefaults).length }
|
|
28240
28493
|
});
|
|
28241
28494
|
}
|
|
28242
28495
|
return next;
|
|
28243
28496
|
}
|
|
28244
28497
|
/**
|
|
28245
|
-
* Fetch the engine triple (`runtime/backend/device`) from the
|
|
28246
|
-
* `detection-pipeline` addon's global settings on the target node.
|
|
28247
|
-
* This is the authoritative engine source since phase 2f. Returns
|
|
28248
|
-
* `null` when the addon hasn't responded yet or doesn't expose the
|
|
28249
|
-
* fields — callers fall back to `catalog.selectedEngine`.
|
|
28250
|
-
*/
|
|
28251
|
-
async readDetectionPipelineEngine(agentNodeId) {
|
|
28252
|
-
const api = this.ctx.api;
|
|
28253
|
-
if (!api?.addonSettings) return null;
|
|
28254
|
-
try {
|
|
28255
|
-
const schema = await api.addonSettings.getGlobalSettings.query({
|
|
28256
|
-
addonId: "detection-pipeline",
|
|
28257
|
-
nodeId: agentNodeId
|
|
28258
|
-
});
|
|
28259
|
-
if (!schema) return null;
|
|
28260
|
-
const leaf = (key) => {
|
|
28261
|
-
for (const s of schema.sections) for (const f of s.fields) if (f.key === key) return f.value;
|
|
28262
|
-
};
|
|
28263
|
-
const runtime = leaf("engineRuntime");
|
|
28264
|
-
const backend = leaf("engineBackend");
|
|
28265
|
-
const device = leaf("engineDevice");
|
|
28266
|
-
if (runtime !== "python" && runtime !== "node" || typeof backend !== "string" || backend.length === 0) return null;
|
|
28267
|
-
const base = {
|
|
28268
|
-
runtime,
|
|
28269
|
-
backend,
|
|
28270
|
-
format: backend === "coreml" ? "coreml" : backend === "openvino" ? "openvino" : "onnx"
|
|
28271
|
-
};
|
|
28272
|
-
if (typeof device === "string" && device.length > 0) return {
|
|
28273
|
-
...base,
|
|
28274
|
-
device
|
|
28275
|
-
};
|
|
28276
|
-
return base;
|
|
28277
|
-
} catch (err) {
|
|
28278
|
-
this.ctx.logger.debug("readDetectionPipelineEngine failed — falling back", {
|
|
28279
|
-
tags: { nodeId: agentNodeId },
|
|
28280
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
28281
|
-
});
|
|
28282
|
-
return null;
|
|
28283
|
-
}
|
|
28284
|
-
}
|
|
28285
|
-
/**
|
|
28286
28498
|
* Resolve the pipeline for a device. NEVER returns an empty/fallback
|
|
28287
28499
|
* config: if the catalog or agent settings aren't available yet, this
|
|
28288
28500
|
* blocks (with backoff) until the detection-pipeline cap is registered
|
|
28289
28501
|
* and a non-null catalog comes back. Cameras must never be dispatched
|
|
28290
|
-
* with `
|
|
28291
|
-
*
|
|
28502
|
+
* with `steps=[]` because the runner then attaches the camera with no
|
|
28503
|
+
* steps and silently never recovers.
|
|
28504
|
+
*
|
|
28505
|
+
* Data only: the returned config never carries an engine — model
|
|
28506
|
+
* selection is data only, and the executing node resolves its own
|
|
28507
|
+
* format default at runtime when a step's `modelId` is absent.
|
|
28292
28508
|
*
|
|
28293
28509
|
* Two legitimate exceptions to "never empty":
|
|
28294
28510
|
* 1. The device's `detection-pipeline` binding is INACTIVE — the
|
|
@@ -28302,39 +28518,29 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28302
28518
|
async resolvePipelineForDevice(deviceId, agentNodeIdOverride) {
|
|
28303
28519
|
const agentNodeId = agentNodeIdOverride ?? this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
28304
28520
|
if (!await this.isDetectionPipelineActive(deviceId)) {
|
|
28305
|
-
const engine = await this.waitForEngine(agentNodeId);
|
|
28306
28521
|
this.ctx.logger.info("resolvePipelineForDevice → empty (binding inactive)", {
|
|
28307
28522
|
tags: {
|
|
28308
28523
|
deviceId,
|
|
28309
28524
|
nodeId: agentNodeId
|
|
28310
28525
|
},
|
|
28311
|
-
meta: {
|
|
28312
|
-
reason: "detection-pipeline binding inactive",
|
|
28313
|
-
engine: `${engine.runtime}/${engine.backend}`
|
|
28314
|
-
}
|
|
28526
|
+
meta: { reason: "detection-pipeline binding inactive" }
|
|
28315
28527
|
});
|
|
28316
28528
|
return {
|
|
28317
|
-
engine,
|
|
28318
28529
|
steps: [],
|
|
28319
28530
|
audio: null
|
|
28320
28531
|
};
|
|
28321
28532
|
}
|
|
28322
28533
|
const cam = (await this.readCameraSettingsMap())[String(deviceId)] ?? null;
|
|
28323
28534
|
const wholesale = cam?.pipelineByAgent?.[agentNodeId];
|
|
28324
|
-
if (wholesale) {
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
|
|
28328
|
-
|
|
28329
|
-
|
|
28330
|
-
|
|
28331
|
-
|
|
28332
|
-
|
|
28333
|
-
} : null
|
|
28334
|
-
};
|
|
28335
|
-
}
|
|
28336
|
-
const { agent, catalog, engine } = await this.waitForAgentAndCatalog(agentNodeId);
|
|
28337
|
-
const resolved = resolvePipeline(agent, cam, agentNodeId, engine, catalog);
|
|
28535
|
+
if (wholesale) return {
|
|
28536
|
+
steps: wholesale.steps,
|
|
28537
|
+
audio: wholesale.audio ? {
|
|
28538
|
+
modelId: wholesale.audio.modelId,
|
|
28539
|
+
enabled: wholesale.audio.enabled
|
|
28540
|
+
} : null
|
|
28541
|
+
};
|
|
28542
|
+
const { agent, catalog } = await this.waitForAgentAndCatalog(agentNodeId);
|
|
28543
|
+
const resolved = resolvePipeline(agent, cam, agentNodeId, catalog);
|
|
28338
28544
|
if (resolved.steps.length === 0) this.ctx.logger.info("resolvePipelineForDevice → empty (all addons disabled)", {
|
|
28339
28545
|
tags: {
|
|
28340
28546
|
deviceId,
|
|
@@ -28382,51 +28588,27 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28382
28588
|
await sleep$1(2e3);
|
|
28383
28589
|
continue;
|
|
28384
28590
|
}
|
|
28385
|
-
const engine = await this.readDetectionPipelineEngine(nodeId) ?? catalog.selectedEngine;
|
|
28386
28591
|
return {
|
|
28387
28592
|
agent,
|
|
28388
|
-
catalog
|
|
28389
|
-
engine
|
|
28593
|
+
catalog
|
|
28390
28594
|
};
|
|
28391
28595
|
}
|
|
28392
28596
|
}
|
|
28393
28597
|
/**
|
|
28394
|
-
* Block until a real engine choice is available. `acquireCapability`
|
|
28395
|
-
* defaults to infinite wait; the post-acquire loop only handles the
|
|
28396
|
-
* propagation gap where the cap is `ready` but the derived calls
|
|
28397
|
-
* still return null for a tick.
|
|
28398
|
-
*/
|
|
28399
|
-
async waitForEngine(nodeId) {
|
|
28400
|
-
await this.ctx.acquireCapability("pipeline-executor", {
|
|
28401
|
-
type: "node",
|
|
28402
|
-
nodeId
|
|
28403
|
-
});
|
|
28404
|
-
while (true) {
|
|
28405
|
-
const fromAddon = await this.readDetectionPipelineEngine(nodeId);
|
|
28406
|
-
if (fromAddon) return fromAddon;
|
|
28407
|
-
const catalog = await this.getCatalogForAgent(nodeId);
|
|
28408
|
-
if (catalog?.selectedEngine) return catalog.selectedEngine;
|
|
28409
|
-
await sleep$1(2e3);
|
|
28410
|
-
}
|
|
28411
|
-
}
|
|
28412
|
-
/**
|
|
28413
28598
|
* Structural runtime check for `CameraPipelineConfig`. Persisted JSON
|
|
28414
28599
|
* may be stale or partial across upgrades, so we gate reads on shape
|
|
28415
28600
|
* rather than trusting the raw object. False = silently ignore that
|
|
28416
28601
|
* entry and fall back to defaults — the orchestrator's job is to
|
|
28417
28602
|
* never crash on corrupt store state.
|
|
28603
|
+
*
|
|
28604
|
+
* Data only: `engine` is no longer required — a persisted config may
|
|
28605
|
+
* still carry a vestigial `engine` from before the data-only refactor;
|
|
28606
|
+
* it is simply ignored.
|
|
28418
28607
|
*/
|
|
28419
28608
|
isCameraPipelineConfig(value) {
|
|
28420
28609
|
if (!value || typeof value !== "object") return false;
|
|
28421
28610
|
const v = value;
|
|
28422
|
-
|
|
28423
|
-
if (!engine || typeof engine !== "object") return false;
|
|
28424
|
-
const e = engine;
|
|
28425
|
-
if (typeof e["runtime"] !== "string") return false;
|
|
28426
|
-
if (typeof e["backend"] !== "string") return false;
|
|
28427
|
-
if (typeof e["format"] !== "string") return false;
|
|
28428
|
-
if (!Array.isArray(v["steps"])) return false;
|
|
28429
|
-
return true;
|
|
28611
|
+
return Array.isArray(v["steps"]);
|
|
28430
28612
|
}
|
|
28431
28613
|
isPipelineTemplate(value) {
|
|
28432
28614
|
if (!value || typeof value !== "object") return false;
|
|
@@ -28778,10 +28960,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28778
28960
|
] });
|
|
28779
28961
|
}
|
|
28780
28962
|
async updateGlobalSettings(patch) {
|
|
28963
|
+
const previousIngestNode = this.clusterRoles.ingestNode;
|
|
28781
28964
|
await super.updateGlobalSettings(patch);
|
|
28782
28965
|
const full = await this.resolveGlobalStore();
|
|
28783
28966
|
this.globalSettings = { ...full };
|
|
28784
28967
|
this.applyRuntimeSettings(full);
|
|
28968
|
+
if (this.clusterRoles.ingestNode !== previousIngestNode) {
|
|
28969
|
+
const { configIssue } = resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
28970
|
+
if (configIssue !== void 0) this.ctx.logger.warn("ingest node config issue", { meta: {
|
|
28971
|
+
ingestNode: this.clusterRoles.ingestNode,
|
|
28972
|
+
issue: configIssue
|
|
28973
|
+
} });
|
|
28974
|
+
this.refreshNodeCapabilities().then(() => this.redispatchAllActiveCameras("ingest-owner-changed")).catch((err) => {
|
|
28975
|
+
this.ctx.logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg(err) } });
|
|
28976
|
+
});
|
|
28977
|
+
}
|
|
28785
28978
|
const pausedIds = [...this.loadShedState.entries()].filter(([, s]) => s.pausedAt !== null).map(([id]) => id);
|
|
28786
28979
|
if (pausedIds.length > 0) {
|
|
28787
28980
|
this.loadShedState.clear();
|
|
@@ -28950,7 +29143,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28950
29143
|
async applyPipelinePatch(deviceId, pipelinePatch) {
|
|
28951
29144
|
const incoming = pipelinePatch.cameraPipeline;
|
|
28952
29145
|
if (incoming === void 0) return;
|
|
28953
|
-
if (!this.isCameraPipelineConfig(incoming)) throw new Error(`applyPipelinePatch: cameraPipeline value for device ${deviceId} is not a valid CameraPipelineConfig (expected {
|
|
29146
|
+
if (!this.isCameraPipelineConfig(incoming)) throw new Error(`applyPipelinePatch: cameraPipeline value for device ${deviceId} is not a valid CameraPipelineConfig (expected { steps:[], audio?:null|object })`);
|
|
28954
29147
|
const agentNodeId = this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
28955
29148
|
await this.setCameraPipelineForAgent({
|
|
28956
29149
|
deviceId,
|
|
@@ -29006,12 +29199,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29006
29199
|
}
|
|
29007
29200
|
/**
|
|
29008
29201
|
* Recompute the derived enabled-node sets (`enabledNodes` /
|
|
29009
|
-
* `enabledDecoderNodes` / `enabledAudioNodes`) from
|
|
29010
|
-
* store. A node is eligible for a concern iff its
|
|
29011
|
-
* the flag is unset AND the node defaults capable
|
|
29012
|
-
* Forked child nodeIds (`hub/classifier`)
|
|
29013
|
-
* excluded. `hub` is always considered even
|
|
29014
|
-
* fresh install keeps the hub-only cluster
|
|
29202
|
+
* `enabledDecoderNodes` / `enabledAudioNodes` / `enabledIngestNodes`) from
|
|
29203
|
+
* the per-node capability store. A node is eligible for a concern iff its
|
|
29204
|
+
* stored flag is `true`, or the flag is unset AND the node defaults capable
|
|
29205
|
+
* ({@link nodeCapabilityDefault}). Forked child nodeIds (`hub/classifier`)
|
|
29206
|
+
* are never dispatchable and are excluded. `hub` is always considered even
|
|
29207
|
+
* when absent from the store, so a fresh install keeps the hub-only cluster
|
|
29208
|
+
* working.
|
|
29015
29209
|
*/
|
|
29016
29210
|
async refreshNodeCapabilities() {
|
|
29017
29211
|
let blob = {};
|
|
@@ -29025,16 +29219,30 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29025
29219
|
const detect = [];
|
|
29026
29220
|
const decode = [];
|
|
29027
29221
|
const audio = [];
|
|
29222
|
+
const ingest = [];
|
|
29223
|
+
const reachableHosts = /* @__PURE__ */ new Map();
|
|
29028
29224
|
const capable = (flag, nodeId) => typeof flag === "boolean" ? flag : PipelineOrchestratorAddon.nodeCapabilityDefault(nodeId);
|
|
29029
29225
|
for (const nodeId of nodeIds) {
|
|
29030
29226
|
const settings = blob[nodeId];
|
|
29031
29227
|
if (capable(settings?.detect, nodeId)) detect.push(nodeId);
|
|
29032
29228
|
if (capable(settings?.decode, nodeId)) decode.push(nodeId);
|
|
29033
29229
|
if (capable(settings?.audio, nodeId)) audio.push(nodeId);
|
|
29230
|
+
if (capable(settings?.ingest, nodeId)) ingest.push(nodeId);
|
|
29231
|
+
const host = settings?.reachableHost?.trim();
|
|
29232
|
+
if (host !== void 0 && host.length > 0) reachableHosts.set(nodeId, host);
|
|
29034
29233
|
}
|
|
29234
|
+
this.reachableHostByNode = reachableHosts;
|
|
29035
29235
|
this.enabledNodes = detect.toSorted();
|
|
29036
29236
|
this.enabledDecoderNodes = decode.toSorted();
|
|
29037
29237
|
this.enabledAudioNodes = audio.toSorted();
|
|
29238
|
+
this.enabledIngestNodes = ingest.toSorted();
|
|
29239
|
+
this.remoteSourcingNodes = deriveRemoteSourcingNodes(this.enabledDecoderNodes, this.clusterRoles.ingestNode);
|
|
29240
|
+
const { ownerNodeId } = resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
29241
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.PipelineIngestOwnerChanged, {
|
|
29242
|
+
type: "addon",
|
|
29243
|
+
id: this.ctx.id,
|
|
29244
|
+
nodeId: "hub"
|
|
29245
|
+
}, { ownerNodeId }));
|
|
29038
29246
|
}
|
|
29039
29247
|
get api() {
|
|
29040
29248
|
return this.ctx.api ?? null;
|
|
@@ -29333,6 +29541,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29333
29541
|
await this.stopDetection(deviceId);
|
|
29334
29542
|
}
|
|
29335
29543
|
this.activeDetections.set(deviceId, config);
|
|
29544
|
+
const hydrateNodeId = this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
29336
29545
|
const pipelineConfig = await this.resolvePipelineForDevice(deviceId);
|
|
29337
29546
|
const zones = await this.zonesProvider?.listZones({ deviceId }) ?? [];
|
|
29338
29547
|
const runnerConfig = {
|
|
@@ -29344,7 +29553,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29344
29553
|
detectionStreamId: config.detectionStreamId,
|
|
29345
29554
|
motionSources: config.motionSources,
|
|
29346
29555
|
pipelineEnabled: config.pipelineEnabled,
|
|
29347
|
-
engine: pipelineConfig.engine,
|
|
29348
29556
|
steps: pipelineConfig.steps,
|
|
29349
29557
|
audio: pipelineConfig.audio ?? null,
|
|
29350
29558
|
zones,
|
|
@@ -29363,15 +29571,20 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29363
29571
|
const msg = errMsg(err);
|
|
29364
29572
|
log.error("dispatchCamera failed", { meta: { error: msg } });
|
|
29365
29573
|
}
|
|
29366
|
-
if (dispatchedNodeId && (runnerConfig.steps?.length ?? 0) === 0) try {
|
|
29574
|
+
if (dispatchedNodeId && (dispatchedNodeId !== hydrateNodeId || (runnerConfig.steps?.length ?? 0) === 0)) try {
|
|
29367
29575
|
const corrected = await this.resolvePipelineForDevice(deviceId, dispatchedNodeId);
|
|
29368
|
-
|
|
29369
|
-
|
|
29370
|
-
|
|
29371
|
-
|
|
29372
|
-
|
|
29373
|
-
|
|
29374
|
-
|
|
29576
|
+
const correctedConfig = {
|
|
29577
|
+
...runnerConfig,
|
|
29578
|
+
steps: corrected.steps,
|
|
29579
|
+
audio: corrected.audio ?? null
|
|
29580
|
+
};
|
|
29581
|
+
if (JSON.stringify({
|
|
29582
|
+
steps: runnerConfig.steps,
|
|
29583
|
+
audio: runnerConfig.audio ?? null
|
|
29584
|
+
}) !== JSON.stringify({
|
|
29585
|
+
steps: correctedConfig.steps,
|
|
29586
|
+
audio: correctedConfig.audio
|
|
29587
|
+
})) {
|
|
29375
29588
|
await this.attachOn(dispatchedNodeId, correctedConfig);
|
|
29376
29589
|
this.cameraConfigs.set(deviceId, correctedConfig);
|
|
29377
29590
|
log.info("startDetection: pipeline re-resolved after agent assignment", {
|
|
@@ -29572,7 +29785,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29572
29785
|
}
|
|
29573
29786
|
const nextConfig = {
|
|
29574
29787
|
...cached,
|
|
29575
|
-
engine: config.engine,
|
|
29576
29788
|
steps: config.steps,
|
|
29577
29789
|
audio: config.audio ?? null
|
|
29578
29790
|
};
|
|
@@ -30064,6 +30276,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
30064
30276
|
api,
|
|
30065
30277
|
brokerId: audioBrokerId,
|
|
30066
30278
|
tag: "audio-analyzer",
|
|
30279
|
+
ownerNodeId: this.clusterRoles.ingestNode,
|
|
30067
30280
|
logger: this.ctx.logger,
|
|
30068
30281
|
onChunk: async (chunk) => {
|
|
30069
30282
|
this.pipelineWatchdog?.noteSignal(deviceId, "audio");
|