@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.mjs
CHANGED
|
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4627
4627
|
return inst;
|
|
4628
4628
|
}
|
|
4629
4629
|
//#endregion
|
|
4630
|
-
//#region ../types/dist/sleep-
|
|
4630
|
+
//#region ../types/dist/sleep-Cc14_yxc.mjs
|
|
4631
4631
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4632
4632
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4633
4633
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -4813,6 +4813,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4813
4813
|
*/
|
|
4814
4814
|
EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
|
|
4815
4815
|
/**
|
|
4816
|
+
* The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
|
|
4817
|
+
* Emitted by addon-pipeline-orchestrator whenever it (re)derives node
|
|
4818
|
+
* capabilities — at boot, on agent online/offline, and on an ingest-node
|
|
4819
|
+
* flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
|
|
4820
|
+
* keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
|
|
4821
|
+
* cross-process `getIngestOwner` query (push the authority's decision instead
|
|
4822
|
+
* of polling it on the hot path). Idempotent state — re-emitted on every
|
|
4823
|
+
* topology change, so a dropped event self-heals on the next one (plus the
|
|
4824
|
+
* broker's long backstop reconcile query).
|
|
4825
|
+
*/
|
|
4826
|
+
EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
|
|
4827
|
+
/**
|
|
4816
4828
|
* Periodic snapshot of per-node pipeline-runner load
|
|
4817
4829
|
* (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
|
|
4818
4830
|
* subscribe instead of polling `pipelineRunner.getLocalLoad`.
|
|
@@ -7304,6 +7316,28 @@ var ModelFormatsSchema = object({
|
|
|
7304
7316
|
tflite: ModelFormatEntrySchema.optional(),
|
|
7305
7317
|
pt: ModelFormatEntrySchema.optional()
|
|
7306
7318
|
});
|
|
7319
|
+
/**
|
|
7320
|
+
* Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
|
|
7321
|
+
* the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
|
|
7322
|
+
* grouped Family→Tier→Variant picker renders identically in the config UI and
|
|
7323
|
+
* in the pipeline/device steppers. The flat `id` stays the source of truth for
|
|
7324
|
+
* resolution/download/persistence; this is a presentation overlay resolved back
|
|
7325
|
+
* to an `id`.
|
|
7326
|
+
*/
|
|
7327
|
+
var ModelVariantGroupSchema = object({
|
|
7328
|
+
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
7329
|
+
family: string(),
|
|
7330
|
+
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
7331
|
+
tier: string(),
|
|
7332
|
+
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
7333
|
+
precision: _enum(["fp32", "int8"]).optional(),
|
|
7334
|
+
/**
|
|
7335
|
+
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
7336
|
+
* latency-optimized export (e.g. ReLU-activation / reduced-input variant)
|
|
7337
|
+
* — the slot the future performance variants plug into.
|
|
7338
|
+
*/
|
|
7339
|
+
optimization: _enum(["standard", "fast"]).optional()
|
|
7340
|
+
});
|
|
7307
7341
|
var ModelCatalogEntrySchema = object({
|
|
7308
7342
|
id: string(),
|
|
7309
7343
|
name: string(),
|
|
@@ -7333,7 +7367,43 @@ var ModelCatalogEntrySchema = object({
|
|
|
7333
7367
|
* Auxiliary files required at runtime (labels JSON, charset dict, etc.).
|
|
7334
7368
|
* Downloaded into the same modelsDir alongside the model file.
|
|
7335
7369
|
*/
|
|
7336
|
-
extraFiles: array(ModelExtraFileSchema).readonly().optional()
|
|
7370
|
+
extraFiles: array(ModelExtraFileSchema).readonly().optional(),
|
|
7371
|
+
/**
|
|
7372
|
+
* LEGACY entry — retained in the catalog so a persisted operator selection
|
|
7373
|
+
* still RESOLVES (and can be re-activated), but hidden from the selectable
|
|
7374
|
+
* model list and excluded from the auto format-default pick. Set on the
|
|
7375
|
+
* superseded / consolidated models (older lineages, redundant fp16 IRs) so
|
|
7376
|
+
* the active lineup stays the coherent curated ladder without deleting a
|
|
7377
|
+
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
7378
|
+
* an explicit legacy id that has a build for the node's format.
|
|
7379
|
+
*/
|
|
7380
|
+
legacy: boolean().optional(),
|
|
7381
|
+
/**
|
|
7382
|
+
* Measured quality/latency metadata — populated from the benchmark addon on
|
|
7383
|
+
* the real node classes. Absent = not yet measured (most entries today; the
|
|
7384
|
+
* catalog historically carried only `sizeMB`, a poor cross-architecture
|
|
7385
|
+
* speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
|
|
7386
|
+
*/
|
|
7387
|
+
metrics: object({
|
|
7388
|
+
map50: number().optional(),
|
|
7389
|
+
p95LatencyMs: record(string(), number()).optional()
|
|
7390
|
+
}).optional(),
|
|
7391
|
+
/**
|
|
7392
|
+
* SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
|
|
7393
|
+
* YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
|
|
7394
|
+
* the retraining addon and any future commercial distribution.
|
|
7395
|
+
*/
|
|
7396
|
+
license: string().optional(),
|
|
7397
|
+
/**
|
|
7398
|
+
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
7399
|
+
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
7400
|
+
* of a family's sizes and quantizations collapse into one grouped picker
|
|
7401
|
+
* instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
|
|
7402
|
+
* (legacy / custom models) — never shown in the grouped selector. The flat
|
|
7403
|
+
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
7404
|
+
* is a presentation overlay resolved back to an `id`.
|
|
7405
|
+
*/
|
|
7406
|
+
group: ModelVariantGroupSchema.optional()
|
|
7337
7407
|
});
|
|
7338
7408
|
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
7339
7409
|
format: literal("openvino"),
|
|
@@ -7394,8 +7464,8 @@ var RecordingModeSchema = _enum([
|
|
|
7394
7464
|
"onAudioThreshold"
|
|
7395
7465
|
]);
|
|
7396
7466
|
/**
|
|
7397
|
-
* First-class, authoritative per-camera storage mode — the
|
|
7398
|
-
* reads directly (never inferred from `rules`):
|
|
7467
|
+
* First-class, authoritative per-camera storage mode — the explicit choice the
|
|
7468
|
+
* UI reads directly (never inferred from `rules`):
|
|
7399
7469
|
* - `off` — not recording.
|
|
7400
7470
|
* - `events` — record only around triggers (motion / audio threshold),
|
|
7401
7471
|
* with pre/post-buffer.
|
|
@@ -9104,26 +9174,13 @@ DeviceType.Light, method(object({
|
|
|
9104
9174
|
percentage: number().min(0).max(100),
|
|
9105
9175
|
lastChangedAt: number()
|
|
9106
9176
|
});
|
|
9177
|
+
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
9107
9178
|
var StreamFormatSchema = _enum([
|
|
9108
9179
|
"webrtc",
|
|
9109
9180
|
"hls",
|
|
9110
9181
|
"mjpeg",
|
|
9111
9182
|
"rtsp"
|
|
9112
9183
|
]);
|
|
9113
|
-
var StreamInfoSchema = object({
|
|
9114
|
-
streamId: string(),
|
|
9115
|
-
format: StreamFormatSchema,
|
|
9116
|
-
url: string().nullable(),
|
|
9117
|
-
active: boolean()
|
|
9118
|
-
});
|
|
9119
|
-
method(object({
|
|
9120
|
-
streamId: string(),
|
|
9121
|
-
sourceUrl: string(),
|
|
9122
|
-
codec: string().optional()
|
|
9123
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
9124
|
-
streamId: string(),
|
|
9125
|
-
format: StreamFormatSchema
|
|
9126
|
-
}), string().nullable()), method(_void(), array(StreamInfoSchema));
|
|
9127
9184
|
var RtspRestreamEntrySchema = object({
|
|
9128
9185
|
brokerId: string(),
|
|
9129
9186
|
url: string(),
|
|
@@ -9788,7 +9845,7 @@ var ConsumablesStatusSchema = object({
|
|
|
9788
9845
|
})),
|
|
9789
9846
|
lastChangedAt: number()
|
|
9790
9847
|
});
|
|
9791
|
-
|
|
9848
|
+
Object.values(DeviceType), method(object({
|
|
9792
9849
|
deviceId: number().int().nonnegative(),
|
|
9793
9850
|
key: string().min(1)
|
|
9794
9851
|
}), _void(), {
|
|
@@ -10838,7 +10895,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
10838
10895
|
enabled: boolean(),
|
|
10839
10896
|
modelId: string(),
|
|
10840
10897
|
children: array(PipelineDefaultStepSchema).readonly(),
|
|
10841
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
10842
10898
|
group: string().optional(),
|
|
10843
10899
|
settings: record(string(), unknown()).optional()
|
|
10844
10900
|
}));
|
|
@@ -10863,7 +10919,9 @@ var PipelineModelOptionSchema = object({
|
|
|
10863
10919
|
formats: record(string(), object({
|
|
10864
10920
|
downloaded: boolean(),
|
|
10865
10921
|
sizeMB: number()
|
|
10866
|
-
}))
|
|
10922
|
+
})),
|
|
10923
|
+
group: ModelVariantGroupSchema.optional(),
|
|
10924
|
+
legacy: boolean().optional()
|
|
10867
10925
|
});
|
|
10868
10926
|
var ConfigFieldBridge = custom();
|
|
10869
10927
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -10914,15 +10972,42 @@ var EngineProvisioningSchema = object({
|
|
|
10914
10972
|
]),
|
|
10915
10973
|
progress: number().optional(),
|
|
10916
10974
|
error: string().optional(),
|
|
10917
|
-
nextRetryAt: number().optional()
|
|
10975
|
+
nextRetryAt: number().optional(),
|
|
10976
|
+
/**
|
|
10977
|
+
* Gate A (config-correctness gate at engine change): human-readable
|
|
10978
|
+
* config issues surfaced EAGERLY when the node's engine changes — model
|
|
10979
|
+
* substitutions ("chose X, running Y") and zero-build steps ("no model
|
|
10980
|
+
* has a <format> build"). Additive/optional: informational only, never
|
|
10981
|
+
* enforced here — `assertEngineReady` (readiness) still gates inference.
|
|
10982
|
+
* Absent/empty when the node-default tree resolves cleanly.
|
|
10983
|
+
*/
|
|
10984
|
+
configIssues: array(string()).optional()
|
|
10918
10985
|
});
|
|
10919
10986
|
var PipelineStepInputSchema = lazy(() => object({
|
|
10920
10987
|
addonId: string(),
|
|
10921
|
-
modelId: string(),
|
|
10988
|
+
modelId: string().optional(),
|
|
10922
10989
|
enabled: boolean().default(true),
|
|
10923
10990
|
children: array(PipelineStepInputSchema).optional(),
|
|
10924
10991
|
settings: record(string(), unknown()).optional()
|
|
10925
10992
|
}));
|
|
10993
|
+
var ModelSubstitutionSchema = object({
|
|
10994
|
+
addonId: string(),
|
|
10995
|
+
chosen: string(),
|
|
10996
|
+
running: string(),
|
|
10997
|
+
format: string()
|
|
10998
|
+
});
|
|
10999
|
+
var PipelineValidationIssueSchema = object({
|
|
11000
|
+
addonId: string(),
|
|
11001
|
+
kind: _enum(["unknown-addon", "no-format-build"]),
|
|
11002
|
+
detail: string()
|
|
11003
|
+
});
|
|
11004
|
+
var PipelineValidationResultSchema = object({
|
|
11005
|
+
ok: boolean(),
|
|
11006
|
+
issues: array(PipelineValidationIssueSchema).readonly(),
|
|
11007
|
+
substitutions: array(ModelSubstitutionSchema).readonly(),
|
|
11008
|
+
/** The node's `currentEngine.format` this validation ran against. */
|
|
11009
|
+
format: string()
|
|
11010
|
+
});
|
|
10926
11011
|
var ReferenceImageEntrySchema = object({
|
|
10927
11012
|
filename: string(),
|
|
10928
11013
|
stepIds: array(string()).readonly().optional()
|
|
@@ -11039,6 +11124,19 @@ var pipelineExecutorCapability = {
|
|
|
11039
11124
|
getGlobalSteps: method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()),
|
|
11040
11125
|
getGlobalPipelineConfig: method(_void(), PipelineConfigBridge),
|
|
11041
11126
|
getOrchestratorConfigSchema: method(_void(), ConfigUISchemaBridge),
|
|
11127
|
+
/**
|
|
11128
|
+
* Gate B (pre-init config-correctness gate). PURE COMPUTE against this
|
|
11129
|
+
* node's `currentEngine.format` — resolves `steps` the same way the
|
|
11130
|
+
* runtime dispatch path would, and reports what WOULD happen without
|
|
11131
|
+
* touching any node-global state. Called by the orchestrator at attach
|
|
11132
|
+
* time (`attachOn`), node-pinned to the TARGET node, so config problems
|
|
11133
|
+
* surface BEFORE `pipelineRunner.attachCamera` rather than at the first
|
|
11134
|
+
* per-frame resolve. `ok` is false iff `issues` is non-empty (both
|
|
11135
|
+
* `unknown-addon` and `no-format-build` are HARD issues); `substitutions`
|
|
11136
|
+
* is informational (a degraded-but-loadable model swap) and never
|
|
11137
|
+
* affects `ok`. Never throws.
|
|
11138
|
+
*/
|
|
11139
|
+
validatePipeline: method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema),
|
|
11042
11140
|
listTemplates: method(_void(), array(PipelineTemplateSchema$1).readonly()),
|
|
11043
11141
|
saveTemplate: method(object({
|
|
11044
11142
|
name: string(),
|
|
@@ -11421,6 +11519,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
11421
11519
|
kind: literal("remote-restream"),
|
|
11422
11520
|
/** The camera's source-owner node (slice 1: always the hub). */
|
|
11423
11521
|
ownerNodeId: string(),
|
|
11522
|
+
/**
|
|
11523
|
+
* The owner's LAN-reachable host, resolved by the orchestrator from the
|
|
11524
|
+
* per-node `reachableHost` override (Cluster UI). When present the runner
|
|
11525
|
+
* dials THIS host for the owner's restream, in preference to the
|
|
11526
|
+
* `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
|
|
11527
|
+
*/
|
|
11528
|
+
ownerReachableHost: string().optional(),
|
|
11424
11529
|
/** Operator override for the owner host the runner dials. */
|
|
11425
11530
|
hubHostnameOverride: string().optional()
|
|
11426
11531
|
})]).describe("Per-camera frame-source mode for the runner (P2c)");
|
|
@@ -11429,13 +11534,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
11429
11534
|
* specific runner instance via `attachCamera`. Carries everything the
|
|
11430
11535
|
* runner needs to subscribe to the local broker and execute inference.
|
|
11431
11536
|
*
|
|
11432
|
-
* Stateless-pipeline model: the
|
|
11433
|
-
*
|
|
11434
|
-
*
|
|
11435
|
-
*
|
|
11436
|
-
*
|
|
11437
|
-
* `engine`/`steps`/`audio` are optional during the additive migration
|
|
11438
|
-
* window; once orchestrator + UI are migrated they become required.
|
|
11537
|
+
* Stateless-pipeline model: the pipeline content (`steps`, optional
|
|
11538
|
+
* `audio`) travels with the attach payload. The runner keeps it in RAM
|
|
11539
|
+
* for the lifetime of the attach — on rebalance, edit, or restart the
|
|
11540
|
+
* orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
|
|
11541
|
+
* node-local, resolved by the executing runner at dispatch time.
|
|
11439
11542
|
*/
|
|
11440
11543
|
var RunnerCameraConfigSchema = object({
|
|
11441
11544
|
deviceId: number(),
|
|
@@ -11486,14 +11589,11 @@ var RunnerCameraConfigSchema = object({
|
|
|
11486
11589
|
*/
|
|
11487
11590
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
11488
11591
|
pipelineEnabled: boolean().default(true),
|
|
11489
|
-
/** Engine choice for video steps (runtime+backend+format). */
|
|
11490
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
11491
11592
|
/** Ordered tree of video steps. Absent → runner skips video detection. */
|
|
11492
11593
|
steps: array(PipelineStepInputSchema).readonly().optional(),
|
|
11493
11594
|
/** Audio classification branch. `enabled:false` disables, null skips. */
|
|
11494
11595
|
audio: object({
|
|
11495
|
-
|
|
11496
|
-
modelId: string(),
|
|
11596
|
+
modelId: string().optional(),
|
|
11497
11597
|
enabled: boolean()
|
|
11498
11598
|
}).nullable().optional(),
|
|
11499
11599
|
/**
|
|
@@ -15949,11 +16049,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
15949
16049
|
timestamp: number()
|
|
15950
16050
|
});
|
|
15951
16051
|
var CameraPipelineConfigSchema = object({
|
|
15952
|
-
engine: PipelineEngineChoiceSchema,
|
|
16052
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
15953
16053
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
15954
16054
|
audio: object({
|
|
15955
|
-
engine: PipelineEngineChoiceSchema,
|
|
15956
|
-
modelId: string(),
|
|
16055
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
16056
|
+
modelId: string().optional(),
|
|
15957
16057
|
enabled: boolean(),
|
|
15958
16058
|
settings: record(string(), unknown()).readonly().optional()
|
|
15959
16059
|
}).nullable().optional()
|
|
@@ -15968,7 +16068,7 @@ var PipelineTemplateSchema = object({
|
|
|
15968
16068
|
});
|
|
15969
16069
|
var AgentAddonConfigSchema = object({
|
|
15970
16070
|
enabled: boolean(),
|
|
15971
|
-
modelId: string(),
|
|
16071
|
+
modelId: string().optional(),
|
|
15972
16072
|
settings: record(string(), unknown()).readonly()
|
|
15973
16073
|
});
|
|
15974
16074
|
var AgentPipelineSettingsSchema = object({
|
|
@@ -15983,7 +16083,15 @@ var AgentPipelineSettingsSchema = object({
|
|
|
15983
16083
|
/** Node is eligible to run audio-analyzer sessions. */
|
|
15984
16084
|
audio: boolean().optional(),
|
|
15985
16085
|
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
15986
|
-
ingest: boolean().optional()
|
|
16086
|
+
ingest: boolean().optional(),
|
|
16087
|
+
/**
|
|
16088
|
+
* Operator override for the LAN host a cross-node decoder dials to reach
|
|
16089
|
+
* THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
|
|
16090
|
+
* falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
|
|
16091
|
+
* it already uses to reach the hub). Set this only when the auto-detected
|
|
16092
|
+
* address is wrong (multi-homed host, NAT, custom interface).
|
|
16093
|
+
*/
|
|
16094
|
+
reachableHost: string().optional()
|
|
15987
16095
|
});
|
|
15988
16096
|
var CameraPipelineForAgentSchema = object({
|
|
15989
16097
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -16089,6 +16197,15 @@ var GlobalMetricsSchema = object({
|
|
|
16089
16197
|
* capability providers.
|
|
16090
16198
|
*/
|
|
16091
16199
|
var CapabilityBindingsSchema = record(string(), string());
|
|
16200
|
+
/**
|
|
16201
|
+
* The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
|
|
16202
|
+
* its LAN-reachable host, if one is registered. See `getIngestOwner`.
|
|
16203
|
+
*/
|
|
16204
|
+
var IngestOwnerSchema = object({
|
|
16205
|
+
ownerNodeId: string(),
|
|
16206
|
+
reachableHost: string().optional(),
|
|
16207
|
+
configIssue: string().optional()
|
|
16208
|
+
});
|
|
16092
16209
|
/** Source block — always present; derives from the stream catalog. */
|
|
16093
16210
|
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
16094
16211
|
camStreamId: string(),
|
|
@@ -16103,6 +16220,14 @@ var CameraAssignmentStatusSchema = object({
|
|
|
16103
16220
|
detectionNodeId: string().nullable(),
|
|
16104
16221
|
decoderNodeId: string().nullable(),
|
|
16105
16222
|
audioNodeId: string().nullable(),
|
|
16223
|
+
/**
|
|
16224
|
+
* The node that OWNS this camera's physical source pull (dials the RTSP and
|
|
16225
|
+
* hosts the broker/restream) — the cluster ingest owner today
|
|
16226
|
+
* (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
|
|
16227
|
+
* the UI show WHERE a camera is sourced without SSH/logs, and is the node the
|
|
16228
|
+
* broker block below was read from (pinned). Nullable only pre-wiring.
|
|
16229
|
+
*/
|
|
16230
|
+
sourceNodeId: string().nullable(),
|
|
16106
16231
|
pinned: object({
|
|
16107
16232
|
detection: boolean(),
|
|
16108
16233
|
decoder: boolean(),
|
|
@@ -16303,6 +16428,14 @@ var pipelineOrchestratorCapability = {
|
|
|
16303
16428
|
kind: "mutation",
|
|
16304
16429
|
auth: "admin"
|
|
16305
16430
|
}),
|
|
16431
|
+
/**
|
|
16432
|
+
* The cluster's single camera-source owner — resolved UNCONDITIONALLY
|
|
16433
|
+
* from `clusterRoles.ingestNode` (NOT the detection-decode
|
|
16434
|
+
* `remoteSourcingNodes` knob). Consumers (recorder/snapshot/audio) pin
|
|
16435
|
+
* their broker calls to `ownerNodeId` and dial `reachableHost` when
|
|
16436
|
+
* present. Defaults to `{ ownerNodeId: 'hub' }`.
|
|
16437
|
+
*/
|
|
16438
|
+
getIngestOwner: method(_void(), IngestOwnerSchema),
|
|
16306
16439
|
/** Pin a device's decoder to a specific node. */
|
|
16307
16440
|
assignDecoder: method(object({
|
|
16308
16441
|
deviceId: number(),
|
|
@@ -16449,6 +16582,22 @@ var pipelineOrchestratorCapability = {
|
|
|
16449
16582
|
kind: "mutation",
|
|
16450
16583
|
auth: "admin"
|
|
16451
16584
|
}),
|
|
16585
|
+
/**
|
|
16586
|
+
* Set (or clear) the operator override for the LAN host a cross-node
|
|
16587
|
+
* decoder dials to reach this node's restream. Pass a non-empty string to
|
|
16588
|
+
* override the auto-detected address (the runner's `CAMSTACK_HUB_URL`
|
|
16589
|
+
* default); pass `null` or an empty string to clear it and revert to
|
|
16590
|
+
* auto-detect. Applied on the next dispatch cycle — takes effect for new
|
|
16591
|
+
* cross-node attaches; existing attaches keep their current host until
|
|
16592
|
+
* re-dispatched.
|
|
16593
|
+
*/
|
|
16594
|
+
setAgentReachableHost: method(object({
|
|
16595
|
+
agentNodeId: string(),
|
|
16596
|
+
reachableHost: string().nullable()
|
|
16597
|
+
}), object({ success: literal(true) }), {
|
|
16598
|
+
kind: "mutation",
|
|
16599
|
+
auth: "admin"
|
|
16600
|
+
}),
|
|
16452
16601
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
16453
16602
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
16454
16603
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -16540,22 +16689,6 @@ var pipelineOrchestratorCapability = {
|
|
|
16540
16689
|
})
|
|
16541
16690
|
}
|
|
16542
16691
|
};
|
|
16543
|
-
var RegisteredStreamSchema = object({
|
|
16544
|
-
streamId: string(),
|
|
16545
|
-
label: string().optional(),
|
|
16546
|
-
codec: string(),
|
|
16547
|
-
type: _enum(["video", "audio"]),
|
|
16548
|
-
sourceUrl: string()
|
|
16549
|
-
});
|
|
16550
|
-
var ExposedResourceSchema = object({
|
|
16551
|
-
streamId: string(),
|
|
16552
|
-
format: string(),
|
|
16553
|
-
value: string()
|
|
16554
|
-
});
|
|
16555
|
-
method(object({
|
|
16556
|
-
deviceId: number(),
|
|
16557
|
-
streams: array(RegisteredStreamSchema).readonly()
|
|
16558
|
-
}), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
|
|
16559
16692
|
/**
|
|
16560
16693
|
* Query filter for settings-store collections.
|
|
16561
16694
|
*/
|
|
@@ -16708,9 +16841,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
16708
16841
|
/**
|
|
16709
16842
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
16710
16843
|
*
|
|
16711
|
-
*
|
|
16712
|
-
*
|
|
16713
|
-
*
|
|
16844
|
+
* The `SnapshotAddon` wrapper returns this shape whether the frame came from
|
|
16845
|
+
* the device-native provider (onboard capture) or from the stream-broker
|
|
16846
|
+
* prebuffer fallback.
|
|
16714
16847
|
*/
|
|
16715
16848
|
var SnapshotImageSchema = object({
|
|
16716
16849
|
base64: string(),
|
|
@@ -16742,10 +16875,6 @@ DeviceType.Camera, method(object({
|
|
|
16742
16875
|
kind: "mutation",
|
|
16743
16876
|
auth: "admin"
|
|
16744
16877
|
});
|
|
16745
|
-
method(object({ deviceId: number() }), boolean()), method(object({
|
|
16746
|
-
deviceId: number(),
|
|
16747
|
-
streamId: string().optional()
|
|
16748
|
-
}), SnapshotImageSchema.nullable());
|
|
16749
16878
|
/**
|
|
16750
16879
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
16751
16880
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -17106,9 +17235,10 @@ method(object({
|
|
|
17106
17235
|
auth: "admin"
|
|
17107
17236
|
});
|
|
17108
17237
|
/**
|
|
17109
|
-
* Optional client-side hints sent at session creation to help the
|
|
17110
|
-
*
|
|
17111
|
-
*
|
|
17238
|
+
* Optional client-side hints sent at session creation to help the provider
|
|
17239
|
+
* pick the best native source. All fields optional — a viewer that knows
|
|
17240
|
+
* nothing still gets a sane default. (Relocated from the retired `webrtc`
|
|
17241
|
+
* collection cap; this `webrtc-session` cap is the live signaling surface.)
|
|
17112
17242
|
*/
|
|
17113
17243
|
var webrtcClientHintsSchema = object({
|
|
17114
17244
|
viewportWidth: number().int().positive().optional(),
|
|
@@ -17119,22 +17249,6 @@ var webrtcClientHintsSchema = object({
|
|
|
17119
17249
|
/** Hard tier override; takes precedence over scoring when registered. */
|
|
17120
17250
|
prefersTier: string().optional()
|
|
17121
17251
|
}).partial();
|
|
17122
|
-
method(object({
|
|
17123
|
-
streamId: string(),
|
|
17124
|
-
sdpOffer: string()
|
|
17125
|
-
}), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
|
|
17126
|
-
streamId: string(),
|
|
17127
|
-
codec: string()
|
|
17128
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
17129
|
-
streamId: string(),
|
|
17130
|
-
hints: webrtcClientHintsSchema.optional()
|
|
17131
|
-
}), object({
|
|
17132
|
-
sessionId: string(),
|
|
17133
|
-
sdpOffer: string()
|
|
17134
|
-
}), { kind: "mutation" }), method(object({
|
|
17135
|
-
sessionId: string(),
|
|
17136
|
-
sdpAnswer: string()
|
|
17137
|
-
}), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
|
|
17138
17252
|
/**
|
|
17139
17253
|
* Discriminated target for a WebRTC session. The client sends this
|
|
17140
17254
|
* structured object instead of building / parsing brokerId strings;
|
|
@@ -22225,6 +22339,12 @@ Object.freeze({
|
|
|
22225
22339
|
addonId: null,
|
|
22226
22340
|
access: "create"
|
|
22227
22341
|
},
|
|
22342
|
+
"pipelineExecutor.validatePipeline": {
|
|
22343
|
+
capName: "pipeline-executor",
|
|
22344
|
+
capScope: "system",
|
|
22345
|
+
addonId: null,
|
|
22346
|
+
access: "view"
|
|
22347
|
+
},
|
|
22228
22348
|
"pipelineOrchestrator.assignAudio": {
|
|
22229
22349
|
capName: "pipeline-orchestrator",
|
|
22230
22350
|
capScope: "system",
|
|
@@ -22333,6 +22453,12 @@ Object.freeze({
|
|
|
22333
22453
|
addonId: null,
|
|
22334
22454
|
access: "view"
|
|
22335
22455
|
},
|
|
22456
|
+
"pipelineOrchestrator.getIngestOwner": {
|
|
22457
|
+
capName: "pipeline-orchestrator",
|
|
22458
|
+
capScope: "system",
|
|
22459
|
+
addonId: null,
|
|
22460
|
+
access: "view"
|
|
22461
|
+
},
|
|
22336
22462
|
"pipelineOrchestrator.getPipelineAssignment": {
|
|
22337
22463
|
capName: "pipeline-orchestrator",
|
|
22338
22464
|
capScope: "system",
|
|
@@ -22405,6 +22531,12 @@ Object.freeze({
|
|
|
22405
22531
|
addonId: null,
|
|
22406
22532
|
access: "create"
|
|
22407
22533
|
},
|
|
22534
|
+
"pipelineOrchestrator.setAgentReachableHost": {
|
|
22535
|
+
capName: "pipeline-orchestrator",
|
|
22536
|
+
capScope: "system",
|
|
22537
|
+
addonId: null,
|
|
22538
|
+
access: "create"
|
|
22539
|
+
},
|
|
22408
22540
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
22409
22541
|
capName: "pipeline-orchestrator",
|
|
22410
22542
|
capScope: "system",
|
|
@@ -22735,24 +22867,6 @@ Object.freeze({
|
|
|
22735
22867
|
addonId: null,
|
|
22736
22868
|
access: "create"
|
|
22737
22869
|
},
|
|
22738
|
-
"restreamer.getExposedResources": {
|
|
22739
|
-
capName: "restreamer",
|
|
22740
|
-
capScope: "system",
|
|
22741
|
-
addonId: null,
|
|
22742
|
-
access: "view"
|
|
22743
|
-
},
|
|
22744
|
-
"restreamer.registerDevice": {
|
|
22745
|
-
capName: "restreamer",
|
|
22746
|
-
capScope: "system",
|
|
22747
|
-
addonId: null,
|
|
22748
|
-
access: "create"
|
|
22749
|
-
},
|
|
22750
|
-
"restreamer.unregisterDevice": {
|
|
22751
|
-
capName: "restreamer",
|
|
22752
|
-
capScope: "system",
|
|
22753
|
-
addonId: null,
|
|
22754
|
-
access: "delete"
|
|
22755
|
-
},
|
|
22756
22870
|
"scriptRunner.run": {
|
|
22757
22871
|
capName: "script-runner",
|
|
22758
22872
|
capScope: "device",
|
|
@@ -22855,18 +22969,6 @@ Object.freeze({
|
|
|
22855
22969
|
addonId: null,
|
|
22856
22970
|
access: "create"
|
|
22857
22971
|
},
|
|
22858
|
-
"snapshotProvider.getSnapshot": {
|
|
22859
|
-
capName: "snapshot-provider",
|
|
22860
|
-
capScope: "system",
|
|
22861
|
-
addonId: null,
|
|
22862
|
-
access: "view"
|
|
22863
|
-
},
|
|
22864
|
-
"snapshotProvider.supportsDevice": {
|
|
22865
|
-
capName: "snapshot-provider",
|
|
22866
|
-
capScope: "system",
|
|
22867
|
-
addonId: null,
|
|
22868
|
-
access: "view"
|
|
22869
|
-
},
|
|
22870
22972
|
"ssoBridge.signBridgeToken": {
|
|
22871
22973
|
capName: "sso-bridge",
|
|
22872
22974
|
capScope: "system",
|
|
@@ -23293,30 +23395,6 @@ Object.freeze({
|
|
|
23293
23395
|
addonId: null,
|
|
23294
23396
|
access: "view"
|
|
23295
23397
|
},
|
|
23296
|
-
"streamingEngine.getStreamUrl": {
|
|
23297
|
-
capName: "streaming-engine",
|
|
23298
|
-
capScope: "system",
|
|
23299
|
-
addonId: null,
|
|
23300
|
-
access: "view"
|
|
23301
|
-
},
|
|
23302
|
-
"streamingEngine.listStreams": {
|
|
23303
|
-
capName: "streaming-engine",
|
|
23304
|
-
capScope: "system",
|
|
23305
|
-
addonId: null,
|
|
23306
|
-
access: "view"
|
|
23307
|
-
},
|
|
23308
|
-
"streamingEngine.registerStream": {
|
|
23309
|
-
capName: "streaming-engine",
|
|
23310
|
-
capScope: "system",
|
|
23311
|
-
addonId: null,
|
|
23312
|
-
access: "create"
|
|
23313
|
-
},
|
|
23314
|
-
"streamingEngine.unregisterStream": {
|
|
23315
|
-
capName: "streaming-engine",
|
|
23316
|
-
capScope: "system",
|
|
23317
|
-
addonId: null,
|
|
23318
|
-
access: "delete"
|
|
23319
|
-
},
|
|
23320
23398
|
"streamParams.getConfigSchema": {
|
|
23321
23399
|
capName: "stream-params",
|
|
23322
23400
|
capScope: "device",
|
|
@@ -23683,54 +23761,6 @@ Object.freeze({
|
|
|
23683
23761
|
addonId: null,
|
|
23684
23762
|
access: "create"
|
|
23685
23763
|
},
|
|
23686
|
-
"webrtc.closeSession": {
|
|
23687
|
-
capName: "webrtc",
|
|
23688
|
-
capScope: "system",
|
|
23689
|
-
addonId: null,
|
|
23690
|
-
access: "create"
|
|
23691
|
-
},
|
|
23692
|
-
"webrtc.createSession": {
|
|
23693
|
-
capName: "webrtc",
|
|
23694
|
-
capScope: "system",
|
|
23695
|
-
addonId: null,
|
|
23696
|
-
access: "create"
|
|
23697
|
-
},
|
|
23698
|
-
"webrtc.handleAnswer": {
|
|
23699
|
-
capName: "webrtc",
|
|
23700
|
-
capScope: "system",
|
|
23701
|
-
addonId: null,
|
|
23702
|
-
access: "create"
|
|
23703
|
-
},
|
|
23704
|
-
"webrtc.handleOffer": {
|
|
23705
|
-
capName: "webrtc",
|
|
23706
|
-
capScope: "system",
|
|
23707
|
-
addonId: null,
|
|
23708
|
-
access: "create"
|
|
23709
|
-
},
|
|
23710
|
-
"webrtc.hasAdaptiveBitrate": {
|
|
23711
|
-
capName: "webrtc",
|
|
23712
|
-
capScope: "system",
|
|
23713
|
-
addonId: null,
|
|
23714
|
-
access: "view"
|
|
23715
|
-
},
|
|
23716
|
-
"webrtc.registerStream": {
|
|
23717
|
-
capName: "webrtc",
|
|
23718
|
-
capScope: "system",
|
|
23719
|
-
addonId: null,
|
|
23720
|
-
access: "create"
|
|
23721
|
-
},
|
|
23722
|
-
"webrtc.supportsStream": {
|
|
23723
|
-
capName: "webrtc",
|
|
23724
|
-
capScope: "system",
|
|
23725
|
-
addonId: null,
|
|
23726
|
-
access: "view"
|
|
23727
|
-
},
|
|
23728
|
-
"webrtc.unregisterStream": {
|
|
23729
|
-
capName: "webrtc",
|
|
23730
|
-
capScope: "system",
|
|
23731
|
-
addonId: null,
|
|
23732
|
-
access: "delete"
|
|
23733
|
-
},
|
|
23734
23764
|
"webrtcSession.addIceCandidate": {
|
|
23735
23765
|
capName: "webrtc-session",
|
|
23736
23766
|
capScope: "device",
|
|
@@ -23893,18 +23923,21 @@ function applyStepPatch(base, patch) {
|
|
|
23893
23923
|
/**
|
|
23894
23924
|
* Seed an agent's addonDefaults map from the catalog. For each addonId
|
|
23895
23925
|
* advertised by any slot, populate a sensible entry:
|
|
23896
|
-
* - `modelId` = addon's `defaultModelIdByFormat[engine.format]` if
|
|
23897
|
-
* present, else `defaultModelId` (from StepDefinition).
|
|
23898
23926
|
* - `enabled` = addon's `enabledByDefault` (absent = true).
|
|
23899
23927
|
* - `settings` = catalog's default settings (empty for now — the
|
|
23900
23928
|
* runtime applies step-level defaults when settings[key] is absent).
|
|
23901
23929
|
*
|
|
23930
|
+
* `modelId` is intentionally ABSENT on a fresh entry — model selection is
|
|
23931
|
+
* data only now. When the operator hasn't explicitly chosen a model, the
|
|
23932
|
+
* step carries no `modelId` and the executing NODE resolves its own
|
|
23933
|
+
* format default at runtime (the resolver has no notion of engines).
|
|
23934
|
+
*
|
|
23902
23935
|
* Entries already present in `current` are LEFT ALONE — operators who
|
|
23903
23936
|
* customised a model keep their choice after catalog growth adds new
|
|
23904
23937
|
* addons. Entries for addonIds that no longer exist in the catalog are
|
|
23905
23938
|
* dropped (the addon was removed; its config is dead weight).
|
|
23906
23939
|
*/
|
|
23907
|
-
function seedAgentAddonDefaults(current,
|
|
23940
|
+
function seedAgentAddonDefaults(current, catalog) {
|
|
23908
23941
|
const next = {};
|
|
23909
23942
|
const liveAddonIds = /* @__PURE__ */ new Set();
|
|
23910
23943
|
for (const slot of catalog.slots) for (const addon of slot.addons) {
|
|
@@ -23915,7 +23948,6 @@ function seedAgentAddonDefaults(current, engine, catalog) {
|
|
|
23915
23948
|
}
|
|
23916
23949
|
next[addon.id] = {
|
|
23917
23950
|
enabled: addon.enabledByDefault ?? true,
|
|
23918
|
-
modelId: pickDefaultModelId(addon, engine.format),
|
|
23919
23951
|
settings: {}
|
|
23920
23952
|
};
|
|
23921
23953
|
}
|
|
@@ -23923,27 +23955,11 @@ function seedAgentAddonDefaults(current, engine, catalog) {
|
|
|
23923
23955
|
return next;
|
|
23924
23956
|
}
|
|
23925
23957
|
/**
|
|
23926
|
-
* Pick the best model ID for `(addon, agent.format)`:
|
|
23927
|
-
* 1. `defaultModelIdByFormat[agent.format]` — explicit per-format choice
|
|
23928
|
-
* 2. `defaultModelId` from the StepDefinition — format-agnostic default
|
|
23929
|
-
* 3. First model in `addon.models[]` that has `formats[agent.format]` — desperate fallback
|
|
23930
|
-
*
|
|
23931
|
-
* Returns the first candidate whose model exists in the catalog entry's
|
|
23932
|
-
* `formats` map for the target engine format. If no candidate matches,
|
|
23933
|
-
* falls through to `addon.defaultModelId` (even if incompatible — let
|
|
23934
|
-
* the runtime error explicitly rather than silently substitute).
|
|
23935
|
-
*/
|
|
23936
|
-
function pickDefaultModelId(addon, format) {
|
|
23937
|
-
const byFormat = addon.defaultModelIdByFormat?.[format];
|
|
23938
|
-
if (byFormat && addon.models.some((m) => m.id === byFormat)) return byFormat;
|
|
23939
|
-
if (addon.models.some((m) => m.id === addon.defaultModelId && m.formats?.[format])) return addon.defaultModelId;
|
|
23940
|
-
const compat = addon.models.find((m) => m.formats?.[format]);
|
|
23941
|
-
if (compat) return compat.id;
|
|
23942
|
-
return addon.defaultModelId;
|
|
23943
|
-
}
|
|
23944
|
-
/**
|
|
23945
23958
|
* Main resolver — produces the `CameraPipelineConfig` the runner should
|
|
23946
23959
|
* receive for `(deviceId, agent)`. Pure: zero I/O, zero side effects.
|
|
23960
|
+
* DATA ONLY: this resolver has no notion of engines. A step carries a
|
|
23961
|
+
* `modelId` only when the operator explicitly chose one; when absent,
|
|
23962
|
+
* the executing node resolves its own format default at runtime.
|
|
23947
23963
|
*
|
|
23948
23964
|
* Step chain:
|
|
23949
23965
|
* 1. `cameraSettings.pipelineByAgent[agentNodeId]` → wholesale override
|
|
@@ -23951,16 +23967,14 @@ function pickDefaultModelId(addon, format) {
|
|
|
23951
23967
|
*
|
|
23952
23968
|
* "Inference disabled for this camera" is NOT handled here — the caller
|
|
23953
23969
|
* (resolvePipelineForDevice) consults the detection-pipeline binding and
|
|
23954
|
-
* short-circuits to
|
|
23970
|
+
* short-circuits to an empty pipeline before calling this resolver.
|
|
23955
23971
|
*/
|
|
23956
|
-
function resolvePipeline(agentSettings, cameraSettings, agentNodeId,
|
|
23972
|
+
function resolvePipeline(agentSettings, cameraSettings, agentNodeId, catalog) {
|
|
23957
23973
|
const cam = cameraSettings ?? {};
|
|
23958
23974
|
const wholesale = cam.pipelineByAgent?.[agentNodeId];
|
|
23959
23975
|
if (wholesale) return {
|
|
23960
|
-
engine,
|
|
23961
23976
|
steps: wholesale.steps,
|
|
23962
23977
|
audio: wholesale.audio ? {
|
|
23963
|
-
engine,
|
|
23964
23978
|
modelId: wholesale.audio.modelId,
|
|
23965
23979
|
enabled: wholesale.audio.enabled
|
|
23966
23980
|
} : null
|
|
@@ -23982,11 +23996,9 @@ function resolvePipeline(agentSettings, cameraSettings, agentNodeId, engine, cat
|
|
|
23982
23996
|
for (const [addonId, override] of Object.entries(toggles)) {
|
|
23983
23997
|
if (override !== true) continue;
|
|
23984
23998
|
if (addonDefaults[addonId]) continue;
|
|
23985
|
-
|
|
23986
|
-
if (!catalogAddon) continue;
|
|
23999
|
+
if (!findInCatalog(catalog, addonId)) continue;
|
|
23987
24000
|
const patched = applyStepPatch({
|
|
23988
24001
|
enabled: true,
|
|
23989
|
-
modelId: pickDefaultModelId(catalogAddon, engine.format),
|
|
23990
24002
|
settings: {}
|
|
23991
24003
|
}, overridesForAgent[addonId]);
|
|
23992
24004
|
if (!patched.enabled) continue;
|
|
@@ -24004,10 +24016,8 @@ function resolvePipeline(agentSettings, cameraSettings, agentNodeId, engine, cat
|
|
|
24004
24016
|
else videoAddons.push(entry);
|
|
24005
24017
|
}
|
|
24006
24018
|
return {
|
|
24007
|
-
engine,
|
|
24008
24019
|
steps: buildTreeFromAddons(videoAddons, catalog),
|
|
24009
24020
|
audio: audioAddons.length > 0 ? {
|
|
24010
|
-
engine,
|
|
24011
24021
|
modelId: audioAddons[0].cfg.modelId,
|
|
24012
24022
|
enabled: true,
|
|
24013
24023
|
...Object.keys(audioAddons[0].cfg.settings).length > 0 ? { settings: audioAddons[0].cfg.settings } : {}
|
|
@@ -24045,9 +24055,9 @@ function buildTreeFromAddons(enabled, catalog) {
|
|
|
24045
24055
|
metaById.set(addonId, meta);
|
|
24046
24056
|
nodes.set(addonId, {
|
|
24047
24057
|
addonId,
|
|
24048
|
-
modelId: cfg.modelId,
|
|
24049
24058
|
enabled: true,
|
|
24050
24059
|
children: [],
|
|
24060
|
+
...cfg.modelId ? { modelId: cfg.modelId } : {},
|
|
24051
24061
|
...Object.keys(cfg.settings).length > 0 ? { settings: cfg.settings } : {}
|
|
24052
24062
|
});
|
|
24053
24063
|
}
|
|
@@ -24155,7 +24165,7 @@ function startAudioChunkPoller(options) {
|
|
|
24155
24165
|
const subId = lifecycle.activeSubscriptionId;
|
|
24156
24166
|
if (subId) {
|
|
24157
24167
|
lifecycle.activeSubscriptionId = null;
|
|
24158
|
-
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }).catch((err) => {
|
|
24168
|
+
options.api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: subId }, nodePin(options.ownerNodeId)).catch((err) => {
|
|
24159
24169
|
options.logger.warn("audio-chunk poller: unsubscribeAudioChunks failed", { meta: {
|
|
24160
24170
|
brokerId: options.brokerId,
|
|
24161
24171
|
subscriptionId: subId,
|
|
@@ -24173,7 +24183,8 @@ function startAudioChunkPoller(options) {
|
|
|
24173
24183
|
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
24174
24184
|
*/
|
|
24175
24185
|
async function subscribeWithRetry(options, lifecycle) {
|
|
24176
|
-
const { api, brokerId, tag, logger } = options;
|
|
24186
|
+
const { api, brokerId, tag, ownerNodeId, logger } = options;
|
|
24187
|
+
const pin = nodePin(ownerNodeId);
|
|
24177
24188
|
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
24178
24189
|
let attempt = 0;
|
|
24179
24190
|
while (!lifecycle.stopped) {
|
|
@@ -24182,9 +24193,9 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
24182
24193
|
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
24183
24194
|
brokerId,
|
|
24184
24195
|
tag
|
|
24185
|
-
});
|
|
24196
|
+
}, pin);
|
|
24186
24197
|
if (lifecycle.stopped) {
|
|
24187
|
-
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
24198
|
+
await api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId: result.subscriptionId }, pin).catch((err) => {
|
|
24188
24199
|
logger.warn("audio-chunk poller: late unsubscribe failed", { meta: {
|
|
24189
24200
|
brokerId,
|
|
24190
24201
|
subscriptionId: result.subscriptionId,
|
|
@@ -24228,14 +24239,15 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
24228
24239
|
* disowned.
|
|
24229
24240
|
*/
|
|
24230
24241
|
function startPolling(options, lifecycle) {
|
|
24231
|
-
const { api, brokerId, tag, onChunk, logger } = options;
|
|
24242
|
+
const { api, brokerId, tag, ownerNodeId, onChunk, logger } = options;
|
|
24243
|
+
const pin = nodePin(ownerNodeId);
|
|
24232
24244
|
let consecutiveFailures = 0;
|
|
24233
24245
|
const resubscribe = async () => {
|
|
24234
24246
|
try {
|
|
24235
24247
|
const result = await api.streamBroker.subscribeAudioChunks.mutate({
|
|
24236
24248
|
brokerId,
|
|
24237
24249
|
tag
|
|
24238
|
-
});
|
|
24250
|
+
}, pin);
|
|
24239
24251
|
lifecycle.activeSubscriptionId = result.subscriptionId;
|
|
24240
24252
|
logger.info("audio-chunk poller: re-subscribed after broker outage", { meta: {
|
|
24241
24253
|
brokerId,
|
|
@@ -24256,7 +24268,7 @@ function startPolling(options, lifecycle) {
|
|
|
24256
24268
|
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
24257
24269
|
subscriptionId: subId,
|
|
24258
24270
|
maxCount: PULL_MAX_COUNT
|
|
24259
|
-
});
|
|
24271
|
+
}, pin);
|
|
24260
24272
|
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
24261
24273
|
brokerId,
|
|
24262
24274
|
subscriptionId: subId
|
|
@@ -24321,6 +24333,7 @@ function mapAssignment(input) {
|
|
|
24321
24333
|
detectionNodeId: input.detectionNodeId,
|
|
24322
24334
|
decoderNodeId: input.decoderNodeId,
|
|
24323
24335
|
audioNodeId: input.audioNodeId,
|
|
24336
|
+
sourceNodeId: input.sourceNodeId,
|
|
24324
24337
|
pinned: {
|
|
24325
24338
|
detection: input.pinned.detection,
|
|
24326
24339
|
decoder: input.pinned.decoder,
|
|
@@ -24494,14 +24507,58 @@ function computeFrameSourceNodes(input) {
|
|
|
24494
24507
|
return enabledDecoderNodes.filter((nodeId) => nodeId === sourceOwnerNodeId || remote.includes(nodeId));
|
|
24495
24508
|
}
|
|
24496
24509
|
//#endregion
|
|
24510
|
+
//#region src/ingest-owner.ts
|
|
24511
|
+
/**
|
|
24512
|
+
* The cluster's single camera-source owner + its LAN-reachable host, resolved
|
|
24513
|
+
* UNCONDITIONALLY from `clusterRoles.ingestNode` (NOT `resolveSourceOwner`,
|
|
24514
|
+
* which is gated on the detection-decode `remoteSourcingNodes` knob). Consumers
|
|
24515
|
+
* (recorder/snapshot/audio) pin their broker calls to `ownerNodeId` and dial
|
|
24516
|
+
* `reachableHost`. Defaults to `{ ownerNodeId: 'hub' }` — byte-identical to
|
|
24517
|
+
* today's hub-only behavior.
|
|
24518
|
+
*
|
|
24519
|
+
* `configIssue` is a NON-FATAL guard: when the configured ingest owner is an
|
|
24520
|
+
* agent that's unreachable or not ingest-capable, it surfaces a human-readable
|
|
24521
|
+
* warning string. The hub is always assumed reachable and ingest-capable, so
|
|
24522
|
+
* `ownerNodeId === 'hub'` never produces a `configIssue`.
|
|
24523
|
+
*/
|
|
24524
|
+
function resolveIngestOwner(clusterRoles, reachableHostByNode, ingestCapableNodes) {
|
|
24525
|
+
const ownerNodeId = clusterRoles.ingestNode;
|
|
24526
|
+
const reachableHost = reachableHostByNode.get(ownerNodeId);
|
|
24527
|
+
const configIssue = computeIngestConfigIssue(ownerNodeId, reachableHost, ingestCapableNodes);
|
|
24528
|
+
return {
|
|
24529
|
+
ownerNodeId,
|
|
24530
|
+
...reachableHost !== void 0 ? { reachableHost } : {},
|
|
24531
|
+
...configIssue !== void 0 ? { configIssue } : {}
|
|
24532
|
+
};
|
|
24533
|
+
}
|
|
24534
|
+
function computeIngestConfigIssue(ownerNodeId, reachableHost, ingestCapableNodes) {
|
|
24535
|
+
if (ownerNodeId === "hub") return void 0;
|
|
24536
|
+
if (reachableHost === void 0) return `ingest node '${ownerNodeId}' has no reachable host — set its reachableHost or cameras cannot be sourced from it`;
|
|
24537
|
+
if (!ingestCapableNodes.includes(ownerNodeId)) return `ingest node '${ownerNodeId}' is not ingest-capable — enable its ingest capability`;
|
|
24538
|
+
}
|
|
24539
|
+
//#endregion
|
|
24497
24540
|
//#region src/source-owner.ts
|
|
24498
24541
|
/**
|
|
24542
|
+
* The set of nodes allowed to run the REMOTE-SOURCE leg — every decode-enabled
|
|
24543
|
+
* node EXCEPT the camera-source OWNER (the node that dials the camera). Derived
|
|
24544
|
+
* from `enabledDecoderNodes`. When the owner is the hub this equals the old
|
|
24545
|
+
* hub-exclusion (byte-identical to Phase 1); when the owner is an agent the hub
|
|
24546
|
+
* itself becomes a valid remote-source target.
|
|
24547
|
+
*
|
|
24548
|
+
* Rollout-safe: when the owner is the only decode node the result is `[]`, which
|
|
24549
|
+
* keeps `resolveSourceOwner` unmodeled and every frame source `local-broker`.
|
|
24550
|
+
* Pure: preserves input order, never mutates.
|
|
24551
|
+
*/
|
|
24552
|
+
function deriveRemoteSourcingNodes(enabledDecoderNodes, ownerNodeId) {
|
|
24553
|
+
return enabledDecoderNodes.filter((nodeId) => nodeId !== ownerNodeId);
|
|
24554
|
+
}
|
|
24555
|
+
/**
|
|
24499
24556
|
* The node that owns a camera's source pull (dials the real RTSP, hosts its
|
|
24500
24557
|
* broker), or `undefined` while ownership is unmodeled (rollout OFF).
|
|
24501
24558
|
*/
|
|
24502
24559
|
function resolveSourceOwner(input) {
|
|
24503
24560
|
if (input.remoteSourcingNodes.length === 0) return void 0;
|
|
24504
|
-
return input.assignedOwner ?? input.
|
|
24561
|
+
return input.assignedOwner ?? input.ownerNodeId;
|
|
24505
24562
|
}
|
|
24506
24563
|
/**
|
|
24507
24564
|
* The `frameSource` the attach payload carries for a target node:
|
|
@@ -24516,13 +24573,14 @@ function resolveSourceOwner(input) {
|
|
|
24516
24573
|
* degrade to the safe local path, never to an unauthorized cross-node pull.
|
|
24517
24574
|
*/
|
|
24518
24575
|
function selectRunnerFrameSource(input) {
|
|
24519
|
-
const { targetNodeId, sourceOwnerNodeId, remoteSourcingNodes, hubHostnameOverride } = input;
|
|
24576
|
+
const { targetNodeId, sourceOwnerNodeId, remoteSourcingNodes, ownerReachableHost, hubHostnameOverride } = input;
|
|
24520
24577
|
if (sourceOwnerNodeId === void 0) return { kind: "local-broker" };
|
|
24521
24578
|
if (targetNodeId === sourceOwnerNodeId) return { kind: "local-broker" };
|
|
24522
24579
|
if (!remoteSourcingNodes.includes(targetNodeId)) return { kind: "local-broker" };
|
|
24523
24580
|
return {
|
|
24524
24581
|
kind: "remote-restream",
|
|
24525
24582
|
ownerNodeId: sourceOwnerNodeId,
|
|
24583
|
+
...ownerReachableHost !== void 0 ? { ownerReachableHost } : {},
|
|
24526
24584
|
...hubHostnameOverride !== void 0 ? { hubHostnameOverride } : {}
|
|
24527
24585
|
};
|
|
24528
24586
|
}
|
|
@@ -24530,16 +24588,29 @@ function selectRunnerFrameSource(input) {
|
|
|
24530
24588
|
//#region src/load-balancer.ts
|
|
24531
24589
|
/**
|
|
24532
24590
|
* Compute the L2 capacity score for a runner node. Lower is better.
|
|
24533
|
-
* The score is a weighted sum of the runner's active workload so the balancer
|
|
24534
|
-
* prefers agents that are serving fewer cameras OR draining queues quickly.
|
|
24535
24591
|
*
|
|
24536
|
-
*
|
|
24537
|
-
*
|
|
24538
|
-
*
|
|
24539
|
-
*
|
|
24592
|
+
* The score is **committed standing load + real-time backpressure**:
|
|
24593
|
+
*
|
|
24594
|
+
* - `attachedCameras` — the primary signal. Every attached camera is a
|
|
24595
|
+
* standing commitment: an `on-motion` camera costs ≈ zero while idle but
|
|
24596
|
+
* WILL fork a decode+inference session on its node the moment motion fires.
|
|
24597
|
+
* Counting *attachments* (not current inference activity) is what makes the
|
|
24598
|
+
* balancer honest about steady-state load and gives the per-node `weights`
|
|
24599
|
+
* sliders camera-count-share semantics.
|
|
24600
|
+
* - `queueDepthTotal` — real-time pressure: a node whose frame queues are
|
|
24601
|
+
* backing up is falling behind NOW and must be deprioritised beyond its
|
|
24602
|
+
* camera count.
|
|
24603
|
+
*
|
|
24604
|
+
* The previous formula multiplied `attachedCameras * avgInferenceFps`, which
|
|
24605
|
+
* scored a node carrying many IDLE on-motion cameras (fps 0) identically to an
|
|
24606
|
+
* empty node — the balancer then piled unpinned cameras onto an already-loaded
|
|
24607
|
+
* node until `maxCameras` bit. `avgInferenceFps` (in practice a mislabeled SUM
|
|
24608
|
+
* of per-camera fps) is deliberately dropped: transient inference activity is
|
|
24609
|
+
* not steady-state load, and a genuinely overloaded node surfaces through
|
|
24610
|
+
* `queueDepthTotal` instead.
|
|
24540
24611
|
*/
|
|
24541
24612
|
function computeCapacityScore(load) {
|
|
24542
|
-
return
|
|
24613
|
+
return Math.max(load.attachedCameras, 0) + Math.max(load.queueDepthTotal, 0);
|
|
24543
24614
|
}
|
|
24544
24615
|
/**
|
|
24545
24616
|
* Node detection weight, defaulting to 1 when absent/invalid. A higher weight
|
|
@@ -24559,6 +24630,18 @@ function weightedScore(load, weights) {
|
|
|
24559
24630
|
return computeCapacityScore(load) / nodeWeight(load.nodeId, weights);
|
|
24560
24631
|
}
|
|
24561
24632
|
/**
|
|
24633
|
+
* Weighted-score improvement of moving one camera off `current` onto `target`
|
|
24634
|
+
* (both weight-adjusted). Positive = `target` is less loaded than `current`.
|
|
24635
|
+
*
|
|
24636
|
+
* Used by the auto-rebalance hysteresis: a migration is applied only when this
|
|
24637
|
+
* clears a margin (> 1), so equalizing a mere 1-camera gap — which would just
|
|
24638
|
+
* reverse the imbalance — is skipped and a periodic pass never churns an
|
|
24639
|
+
* already-balanced cluster.
|
|
24640
|
+
*/
|
|
24641
|
+
function capacityImprovement(current, target, weights) {
|
|
24642
|
+
return weightedScore(current, weights) - weightedScore(target, weights);
|
|
24643
|
+
}
|
|
24644
|
+
/**
|
|
24562
24645
|
* Returns true when the node has remaining capacity.
|
|
24563
24646
|
* A node is eligible iff `cap` is unlimited (null/absent/<=0) OR
|
|
24564
24647
|
* its `attachedCameras` count is strictly less than the cap.
|
|
@@ -24703,11 +24786,11 @@ var EngineChoiceSchema = object({
|
|
|
24703
24786
|
});
|
|
24704
24787
|
var NodeBindingsSchema = record(string(), record(string(), string()));
|
|
24705
24788
|
var StoredPipelineConfigSchema = object({
|
|
24706
|
-
engine: EngineChoiceSchema,
|
|
24789
|
+
engine: EngineChoiceSchema.optional(),
|
|
24707
24790
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
24708
24791
|
audio: object({
|
|
24709
|
-
engine: EngineChoiceSchema,
|
|
24710
|
-
modelId: string(),
|
|
24792
|
+
engine: EngineChoiceSchema.optional(),
|
|
24793
|
+
modelId: string().optional(),
|
|
24711
24794
|
enabled: boolean(),
|
|
24712
24795
|
settings: record(string(), unknown()).readonly().optional()
|
|
24713
24796
|
}).nullable().optional()
|
|
@@ -24723,7 +24806,7 @@ var StoredPipelineTemplateSchema = object({
|
|
|
24723
24806
|
var TemplatesSchema = record(string(), StoredPipelineTemplateSchema);
|
|
24724
24807
|
var StoredAgentAddonConfigSchema = object({
|
|
24725
24808
|
enabled: boolean(),
|
|
24726
|
-
modelId: string(),
|
|
24809
|
+
modelId: string().optional(),
|
|
24727
24810
|
settings: record(string(), unknown()).readonly()
|
|
24728
24811
|
});
|
|
24729
24812
|
var StoredAgentPipelineSettingsSchema = object({
|
|
@@ -24733,7 +24816,8 @@ var StoredAgentPipelineSettingsSchema = object({
|
|
|
24733
24816
|
detect: boolean().optional(),
|
|
24734
24817
|
decode: boolean().optional(),
|
|
24735
24818
|
audio: boolean().optional(),
|
|
24736
|
-
ingest: boolean().optional()
|
|
24819
|
+
ingest: boolean().optional(),
|
|
24820
|
+
reachableHost: string().optional()
|
|
24737
24821
|
});
|
|
24738
24822
|
var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
|
|
24739
24823
|
var StoredCameraStepOverridePatchSchema = object({
|
|
@@ -24744,7 +24828,7 @@ var StoredCameraStepOverridePatchSchema = object({
|
|
|
24744
24828
|
var StoredCameraPipelineForAgentSchema = object({
|
|
24745
24829
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
24746
24830
|
audio: object({
|
|
24747
|
-
modelId: string(),
|
|
24831
|
+
modelId: string().optional(),
|
|
24748
24832
|
enabled: boolean()
|
|
24749
24833
|
}).nullable()
|
|
24750
24834
|
});
|
|
@@ -24826,8 +24910,8 @@ var PipelineWatchdog = class {
|
|
|
24826
24910
|
const now = this.deps.now();
|
|
24827
24911
|
for (const cam of this.cameras.values()) {
|
|
24828
24912
|
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
24829
|
-
if (stalled) this.deps.logger.warn(line);
|
|
24830
|
-
else this.deps.logger.info(line);
|
|
24913
|
+
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
24914
|
+
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
24831
24915
|
for (const r of recoveries) {
|
|
24832
24916
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
24833
24917
|
if (rt) {
|
|
@@ -25266,6 +25350,21 @@ var PENDING_RETRY_INTERVAL_MS = 6e4;
|
|
|
25266
25350
|
/** Debounce window for `schedulePendingRetry` — coalesces capacity/eligibility/readiness signals. */
|
|
25267
25351
|
var PENDING_RETRY_DEBOUNCE_MS = 2e3;
|
|
25268
25352
|
/**
|
|
25353
|
+
* Periodic auto-rebalance sweep. New attaches are already load-balanced at
|
|
25354
|
+
* dispatch time; this corrects DRIFT that accumulates over time (uneven
|
|
25355
|
+
* detach, a node returning online, a weight change) so the steady-state
|
|
25356
|
+
* distribution tracks the per-node weights. Only runs with ≥2 enabled detect
|
|
25357
|
+
* nodes, and migrates under hysteresis so a balanced cluster is a no-op.
|
|
25358
|
+
*/
|
|
25359
|
+
var AUTO_REBALANCE_INTERVAL_MS = 6e4;
|
|
25360
|
+
/**
|
|
25361
|
+
* Hysteresis margin (weight-adjusted cameras) for the periodic auto-rebalance:
|
|
25362
|
+
* migrate a camera only when its target node is at least this much less loaded
|
|
25363
|
+
* than its current node. > 1 so equalizing a single-camera gap (which would
|
|
25364
|
+
* only reverse the imbalance) is skipped — prevents periodic churn.
|
|
25365
|
+
*/
|
|
25366
|
+
var AUTO_REBALANCE_MIN_IMPROVEMENT = 1.5;
|
|
25367
|
+
/**
|
|
25269
25368
|
* Remote-assignment health loop (T7) bounded backoff. A remote camera flagged
|
|
25270
25369
|
* as unhealthy (0-fps / metrics-stale) is re-placed at most
|
|
25271
25370
|
* `REMOTE_HEALTH_MAX_ATTEMPTS` times inside a rolling `REMOTE_HEALTH_WINDOW_MS`
|
|
@@ -25303,6 +25402,7 @@ var OrchestratorDiagnosticsSchema = object({
|
|
|
25303
25402
|
enabledNodes: array(string()),
|
|
25304
25403
|
enabledDecoderNodes: array(string()),
|
|
25305
25404
|
enabledAudioNodes: array(string()),
|
|
25405
|
+
enabledIngestNodes: array(string()),
|
|
25306
25406
|
clusterRoles: object({
|
|
25307
25407
|
ingestNode: string(),
|
|
25308
25408
|
audioNode: string()
|
|
@@ -25448,21 +25548,38 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25448
25548
|
enabledDecoderNodes = ["hub"];
|
|
25449
25549
|
/**
|
|
25450
25550
|
* Nodes allowed to run the REMOTE-SOURCE leg — dial a camera's source-owner
|
|
25451
|
-
* restream over the LAN and decode it locally.
|
|
25452
|
-
*
|
|
25453
|
-
*
|
|
25454
|
-
*
|
|
25455
|
-
* `
|
|
25456
|
-
* pre-
|
|
25551
|
+
* restream over the LAN and decode it locally. DERIVED (Phase-1 activation,
|
|
25552
|
+
* `deriveRemoteSourcingNodes`) from `enabledDecoderNodes` minus the hub, in
|
|
25553
|
+
* `refreshNodeCapabilities`: every decode-enabled AGENT is a remote-sourcing
|
|
25554
|
+
* node. When only the hub decodes this stays `[]`, so `resolveSourceOwner`
|
|
25555
|
+
* returns `undefined` and every `selectRunnerFrameSource` emits
|
|
25556
|
+
* `local-broker` — bit-identical to pre-activation (rollout-safe).
|
|
25457
25557
|
*/
|
|
25458
25558
|
remoteSourcingNodes = [];
|
|
25459
25559
|
/**
|
|
25560
|
+
* Per-node `reachableHost` override (the LAN host a cross-node decoder dials
|
|
25561
|
+
* to reach that node's restream), from `agentSettings[node].reachableHost`.
|
|
25562
|
+
* DERIVED alongside the enabled-node sets in {@link refreshNodeCapabilities}
|
|
25563
|
+
* so `attachOn` resolves the owner host synchronously (no per-attach durable
|
|
25564
|
+
* read). Absent entry → the runner auto-detects via `CAMSTACK_HUB_URL`.
|
|
25565
|
+
*/
|
|
25566
|
+
reachableHostByNode = /* @__PURE__ */ new Map();
|
|
25567
|
+
/**
|
|
25460
25568
|
* Hub-wide allow-list of node ids eligible to run audio-analyzer sessions.
|
|
25461
25569
|
* DERIVED from the per-node capability store (`agentSettings[node].audio`).
|
|
25462
25570
|
* Defaults to `['hub']`.
|
|
25463
25571
|
*/
|
|
25464
25572
|
enabledAudioNodes = ["hub"];
|
|
25465
25573
|
/**
|
|
25574
|
+
* Hub-wide allow-list of node ids eligible to run the INGEST leg (dial the
|
|
25575
|
+
* camera's real source, host its broker). DERIVED from the per-node
|
|
25576
|
+
* capability store (`agentSettings[node].ingest`), mirroring
|
|
25577
|
+
* `enabledAudioNodes`. Defaults to `['hub']`. Not yet consumed by any
|
|
25578
|
+
* placement logic — this field only makes the persisted `ingest` flag
|
|
25579
|
+
* readable for a later config guard.
|
|
25580
|
+
*/
|
|
25581
|
+
enabledIngestNodes = ["hub"];
|
|
25582
|
+
/**
|
|
25466
25583
|
* Cluster-wide singleton role assignments (placement-model §6). Each role
|
|
25467
25584
|
* is the ONE node that serves it for every camera. Driven by the
|
|
25468
25585
|
* `ingestNode` / `audioNode` node-selects in the addon global schema;
|
|
@@ -25570,6 +25687,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25570
25687
|
pendingRetryInFlight = false;
|
|
25571
25688
|
/** Set when a pending-retry is requested while one is already in-flight; triggers a follow-up pass. */
|
|
25572
25689
|
pendingRetryRerunRequested = false;
|
|
25690
|
+
/** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
|
|
25691
|
+
autoRebalanceTimer = null;
|
|
25692
|
+
/** True while an auto-rebalance pass is running (skip overlap). */
|
|
25693
|
+
autoRebalanceInFlight = false;
|
|
25573
25694
|
initTimestamp = 0;
|
|
25574
25695
|
constructor() {
|
|
25575
25696
|
super({});
|
|
@@ -25672,7 +25793,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25672
25793
|
if (!isEvent(event, EventCategory.PipelineInferenceResult)) return;
|
|
25673
25794
|
this.handleInferenceResult(event.data).catch((err) => {
|
|
25674
25795
|
const msg = errMsg(err);
|
|
25675
|
-
this.ctx.logger.error("PipelineInferenceResult post-processing failed", {
|
|
25796
|
+
this.ctx.logger.error("PipelineInferenceResult post-processing failed", {
|
|
25797
|
+
tags: { deviceId: event.data.deviceId },
|
|
25798
|
+
meta: { error: msg }
|
|
25799
|
+
});
|
|
25676
25800
|
});
|
|
25677
25801
|
});
|
|
25678
25802
|
const PROFILE_SLOTS_DEBOUNCE_MS = 200;
|
|
@@ -25788,6 +25912,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25788
25912
|
});
|
|
25789
25913
|
this.pipelineWatchdog.start(PipelineOrchestratorAddon.WATCHDOG_INTERVAL_MS);
|
|
25790
25914
|
this.pendingRetryTimer = setInterval(() => void this.retryPendingDispatches(), PENDING_RETRY_INTERVAL_MS);
|
|
25915
|
+
this.autoRebalanceTimer = setInterval(() => void this.runAutoRebalance(), AUTO_REBALANCE_INTERVAL_MS);
|
|
25791
25916
|
this.ctx.logger.info("Pipeline orchestrator detection-wiring subscriptions installed");
|
|
25792
25917
|
this.migrateLegacyFlagsToBindings().catch((err) => {
|
|
25793
25918
|
this.ctxIfReady?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg(err) } });
|
|
@@ -25998,6 +26123,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25998
26123
|
enabledNodes: [...this.enabledNodes],
|
|
25999
26124
|
enabledDecoderNodes: [...this.enabledDecoderNodes],
|
|
26000
26125
|
enabledAudioNodes: [...this.enabledAudioNodes],
|
|
26126
|
+
enabledIngestNodes: [...this.enabledIngestNodes],
|
|
26001
26127
|
clusterRoles: { ...this.clusterRoles },
|
|
26002
26128
|
assignedDeviceCount: this.assignments.size,
|
|
26003
26129
|
cameraConfigCount: this.cameraConfigs.size,
|
|
@@ -26023,6 +26149,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26023
26149
|
clearTimeout(this.pendingRetryDebounceTimer);
|
|
26024
26150
|
this.pendingRetryDebounceTimer = null;
|
|
26025
26151
|
}
|
|
26152
|
+
if (this.autoRebalanceTimer !== null) {
|
|
26153
|
+
clearInterval(this.autoRebalanceTimer);
|
|
26154
|
+
this.autoRebalanceTimer = null;
|
|
26155
|
+
}
|
|
26026
26156
|
for (const t of this.slotReadRetryTimers.values()) clearTimeout(t);
|
|
26027
26157
|
this.slotReadRetryTimers.clear();
|
|
26028
26158
|
this.slotReadRetryAttempts.clear();
|
|
@@ -26288,8 +26418,30 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26288
26418
|
this.assignments.delete(input.deviceId);
|
|
26289
26419
|
return { success: true };
|
|
26290
26420
|
}
|
|
26291
|
-
|
|
26421
|
+
/**
|
|
26422
|
+
* Periodic auto-rebalance tick. Corrects placement drift toward the per-node
|
|
26423
|
+
* weights without operator action. Guards: never overlaps itself; skips when
|
|
26424
|
+
* fewer than two nodes can run detection (a hub-only cluster has nothing to
|
|
26425
|
+
* balance); migrates under {@link AUTO_REBALANCE_MIN_IMPROVEMENT} hysteresis
|
|
26426
|
+
* so a balanced cluster is a no-op. Errors are swallowed (best-effort net).
|
|
26427
|
+
*/
|
|
26428
|
+
async runAutoRebalance() {
|
|
26429
|
+
if (!this.ctxIfReady) return;
|
|
26430
|
+
if (this.autoRebalanceInFlight) return;
|
|
26431
|
+
if (this.enabledNodes.length < 2) return;
|
|
26432
|
+
this.autoRebalanceInFlight = true;
|
|
26433
|
+
try {
|
|
26434
|
+
const { migrated } = await this.rebalance({ minImprovement: AUTO_REBALANCE_MIN_IMPROVEMENT });
|
|
26435
|
+
if (migrated > 0) this.ctx.logger.info("auto-rebalance: corrected placement drift", { meta: { migrated } });
|
|
26436
|
+
} catch (err) {
|
|
26437
|
+
this.ctx.logger.debug("auto-rebalance pass failed", { meta: { error: errMsg(err) } });
|
|
26438
|
+
} finally {
|
|
26439
|
+
this.autoRebalanceInFlight = false;
|
|
26440
|
+
}
|
|
26441
|
+
}
|
|
26442
|
+
async rebalance(opts) {
|
|
26292
26443
|
if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
|
|
26444
|
+
const minImprovement = Math.max(opts?.minImprovement ?? 0, 0);
|
|
26293
26445
|
const loads = await this.collectAgentLoad({ onlyEnabled: true });
|
|
26294
26446
|
const nodeCaps = await this.buildNodeCaps();
|
|
26295
26447
|
const nodeWeights = await this.buildNodeWeights();
|
|
@@ -26327,6 +26479,12 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26327
26479
|
continue;
|
|
26328
26480
|
}
|
|
26329
26481
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
26482
|
+
if (current && minImprovement > 0) {
|
|
26483
|
+
const tallied = talliedLoads();
|
|
26484
|
+
const currentLoad = tallied.find((l) => l.nodeId === current.agentNodeId);
|
|
26485
|
+
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
26486
|
+
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
26487
|
+
}
|
|
26330
26488
|
if (current) {
|
|
26331
26489
|
await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
|
|
26332
26490
|
const msg = errMsg(err);
|
|
@@ -26337,7 +26495,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26337
26495
|
});
|
|
26338
26496
|
bumpAttached(current.agentNodeId, -1);
|
|
26339
26497
|
}
|
|
26340
|
-
await this.
|
|
26498
|
+
const reResolved = await this.resolvePipelineForDevice(deviceId, decision.agentNodeId);
|
|
26499
|
+
const targetConfig = reResolved.steps.length > 0 ? {
|
|
26500
|
+
...config,
|
|
26501
|
+
steps: reResolved.steps,
|
|
26502
|
+
audio: reResolved.audio ?? null
|
|
26503
|
+
} : config;
|
|
26504
|
+
this.cameraConfigs.set(deviceId, targetConfig);
|
|
26505
|
+
await this.attachOn(decision.agentNodeId, targetConfig);
|
|
26341
26506
|
bumpAttached(decision.agentNodeId, 1);
|
|
26342
26507
|
this.recordAssignment(deviceId, decision.agentNodeId, "rebalance", false);
|
|
26343
26508
|
migrated++;
|
|
@@ -26436,11 +26601,37 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26436
26601
|
async attachOn(nodeId, config) {
|
|
26437
26602
|
const api = this.ctx.api;
|
|
26438
26603
|
if (!api) throw new Error(`attachOn(${nodeId}, ${config.deviceId}): this.ctx.api not available`);
|
|
26604
|
+
const sourceOwnerNodeId = this.sourceOwner(config.deviceId);
|
|
26605
|
+
const ownerReachableHost = sourceOwnerNodeId !== void 0 ? this.reachableHostByNode.get(sourceOwnerNodeId) : void 0;
|
|
26439
26606
|
const frameSource = selectRunnerFrameSource({
|
|
26440
26607
|
targetNodeId: nodeId,
|
|
26441
|
-
sourceOwnerNodeId
|
|
26442
|
-
remoteSourcingNodes: this.remoteSourcingNodes
|
|
26608
|
+
sourceOwnerNodeId,
|
|
26609
|
+
remoteSourcingNodes: this.remoteSourcingNodes,
|
|
26610
|
+
...ownerReachableHost !== void 0 ? { ownerReachableHost } : {}
|
|
26443
26611
|
});
|
|
26612
|
+
api.pipelineExecutor.validatePipeline.query({ steps: [...config.steps ?? []] }, nodePin(nodeId)).then((validation) => {
|
|
26613
|
+
if (!validation || validation.ok && validation.substitutions.length === 0) return;
|
|
26614
|
+
const logMeta = {
|
|
26615
|
+
nodeId,
|
|
26616
|
+
deviceId: config.deviceId,
|
|
26617
|
+
issues: validation.issues,
|
|
26618
|
+
substitutions: validation.substitutions
|
|
26619
|
+
};
|
|
26620
|
+
if (!validation.ok) this.ctx.logger.warn("attachOn: pre-init pipeline validation found issues", {
|
|
26621
|
+
tags: {
|
|
26622
|
+
nodeId,
|
|
26623
|
+
deviceId: config.deviceId
|
|
26624
|
+
},
|
|
26625
|
+
meta: logMeta
|
|
26626
|
+
});
|
|
26627
|
+
else this.ctx.logger.info("attachOn: pre-init pipeline validation found model substitutions", {
|
|
26628
|
+
tags: {
|
|
26629
|
+
nodeId,
|
|
26630
|
+
deviceId: config.deviceId
|
|
26631
|
+
},
|
|
26632
|
+
meta: logMeta
|
|
26633
|
+
});
|
|
26634
|
+
}).catch(() => {});
|
|
26444
26635
|
try {
|
|
26445
26636
|
await api.pipelineRunner.attachCamera.mutate({
|
|
26446
26637
|
...config,
|
|
@@ -26455,7 +26646,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26455
26646
|
audioMode: config.audioMode,
|
|
26456
26647
|
motionStreamId: config.motionStreamId,
|
|
26457
26648
|
detectionStreamId: config.detectionStreamId,
|
|
26458
|
-
hasEngine: !!config.engine,
|
|
26459
26649
|
stepsCount: config.steps?.length ?? null,
|
|
26460
26650
|
hasAudio: config.audio?.enabled ?? null
|
|
26461
26651
|
};
|
|
@@ -26560,18 +26750,31 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26560
26750
|
return this.clusterRoles.ingestNode;
|
|
26561
26751
|
}
|
|
26562
26752
|
/**
|
|
26753
|
+
* Cap surface: the cluster's single camera-source owner + its
|
|
26754
|
+
* LAN-reachable host, resolved UNCONDITIONALLY from `clusterRoles.ingestNode`
|
|
26755
|
+
* (NOT `resolveSourceOwner`/`sourceOwner`, which are gated on the
|
|
26756
|
+
* detection-decode `remoteSourcingNodes` rollout knob). Consumers
|
|
26757
|
+
* (recorder/snapshot/audio) call this to pin their broker calls to the
|
|
26758
|
+
* owner and dial `reachableHost`. Rollout-safe: defaults to
|
|
26759
|
+
* `{ ownerNodeId: 'hub' }` — byte-identical to today's hub-only behavior.
|
|
26760
|
+
*/
|
|
26761
|
+
async getIngestOwner() {
|
|
26762
|
+
return resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
26763
|
+
}
|
|
26764
|
+
/**
|
|
26563
26765
|
* The node that owns `deviceId`'s source pull, or `undefined` while
|
|
26564
26766
|
* ownership is UNMODELED — which is exactly the rollout-OFF state: with the
|
|
26565
26767
|
* `remoteSourcingNodes` knob empty (the default), `resolveSourceOwner`
|
|
26566
26768
|
* returns `undefined`, `computeFrameSourceNodes` collapses to the global
|
|
26567
26769
|
* `enabledDecoderNodes`, and `selectRunnerFrameSource` emits `local-broker`
|
|
26568
26770
|
* for every target — bit-identical to pre-Phase-2 behavior. Populating the
|
|
26569
|
-
* knob
|
|
26570
|
-
* turns eligibility per-camera and the
|
|
26771
|
+
* knob (a decode node other than the ingest owner exists) models ownership as
|
|
26772
|
+
* `clusterRoles.ingestNode` and thereby turns eligibility per-camera and the
|
|
26773
|
+
* remote-source leg dispatchable.
|
|
26571
26774
|
*/
|
|
26572
26775
|
sourceOwner(deviceId) {
|
|
26573
26776
|
return resolveSourceOwner({
|
|
26574
|
-
|
|
26777
|
+
ownerNodeId: this.clusterRoles.ingestNode,
|
|
26575
26778
|
remoteSourcingNodes: this.remoteSourcingNodes,
|
|
26576
26779
|
...deviceId !== void 0 ? { assignedOwner: this.assignSource(deviceId) } : {}
|
|
26577
26780
|
});
|
|
@@ -26684,7 +26887,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
26684
26887
|
* lookup. Works for any backend (ffmpeg / node-av / future) and no longer
|
|
26685
26888
|
* returns empty on a node-av-only node (the old `decoder-ffmpeg`-keyed read
|
|
26686
26889
|
* did). Returns `null` when the value is empty/unset, `undefined` on a read
|
|
26687
|
-
* failure (keep the last known).
|
|
26890
|
+
* failure (keep the last known). Node-local — the same pattern the
|
|
26891
|
+
* orchestrator's (now-deleted) per-node engine reader used before engine
|
|
26892
|
+
* resolution moved entirely off the orchestrator.
|
|
26688
26893
|
*/
|
|
26689
26894
|
async readNodeDecodeHwaccel(nodeId) {
|
|
26690
26895
|
try {
|
|
@@ -27160,7 +27365,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27160
27365
|
if (resolved.steps.length === 0) continue;
|
|
27161
27366
|
const next = {
|
|
27162
27367
|
...cached,
|
|
27163
|
-
engine: resolved.engine,
|
|
27164
27368
|
steps: resolved.steps,
|
|
27165
27369
|
audio: resolved.audio ?? null
|
|
27166
27370
|
};
|
|
@@ -27230,7 +27434,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27230
27434
|
if (resolved.steps.length === 0) continue;
|
|
27231
27435
|
const next = {
|
|
27232
27436
|
...cached,
|
|
27233
|
-
engine: resolved.engine,
|
|
27234
27437
|
steps: resolved.steps,
|
|
27235
27438
|
audio: resolved.audio ?? null
|
|
27236
27439
|
};
|
|
@@ -27790,6 +27993,33 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27790
27993
|
this.schedulePendingRetry();
|
|
27791
27994
|
return { success: true };
|
|
27792
27995
|
}
|
|
27996
|
+
/**
|
|
27997
|
+
* Set (or clear) the per-node `reachableHost` override — the LAN host a
|
|
27998
|
+
* cross-node decoder dials to reach this node's restream. A non-empty string
|
|
27999
|
+
* overrides the auto-detected host the runner would otherwise derive from
|
|
28000
|
+
* `CAMSTACK_HUB_URL`; `null` or an empty string clears it (revert to
|
|
28001
|
+
* auto-detect). Persisted onto the node's `agentSettings` entry, then a
|
|
28002
|
+
* debounced re-dispatch so new cross-node attaches pick up the host.
|
|
28003
|
+
*/
|
|
28004
|
+
async setAgentReachableHost(input) {
|
|
28005
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
28006
|
+
const trimmed = input.reachableHost?.trim();
|
|
28007
|
+
const host = trimmed !== void 0 && trimmed.length > 0 ? trimmed : void 0;
|
|
28008
|
+
const next = {
|
|
28009
|
+
...existing ?? {
|
|
28010
|
+
addonDefaults: {},
|
|
28011
|
+
maxCameras: null
|
|
28012
|
+
},
|
|
28013
|
+
reachableHost: host
|
|
28014
|
+
};
|
|
28015
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
28016
|
+
this.ctx.logger.info("agentSettings.reachableHost updated", {
|
|
28017
|
+
tags: { nodeId: input.agentNodeId },
|
|
28018
|
+
meta: { reachableHost: host ?? null }
|
|
28019
|
+
});
|
|
28020
|
+
this.schedulePendingRetry();
|
|
28021
|
+
return { success: true };
|
|
28022
|
+
}
|
|
27793
28023
|
async getCameraSettings(input) {
|
|
27794
28024
|
return (await this.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
27795
28025
|
}
|
|
@@ -27873,6 +28103,25 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27873
28103
|
});
|
|
27874
28104
|
for (const deviceId of affected) await this.emitResolvedCameraUpdated(deviceId);
|
|
27875
28105
|
}
|
|
28106
|
+
/**
|
|
28107
|
+
* Re-dispatch EVERY assigned camera through the REAL attach path
|
|
28108
|
+
* (`redispatchSingleCamera` → `attachOn`), so each camera's `frameSource`
|
|
28109
|
+
* (`local-broker` vs `remote-restream` naming the current ingest owner) is
|
|
28110
|
+
* RECOMPUTED and re-applied to its runner. Used when the cluster ingest owner
|
|
28111
|
+
* changes: `frameSource` is computed only in `attachOn` (the single choke
|
|
28112
|
+
* point), so a plain `emitCameraUpdated` config event would NOT move the
|
|
28113
|
+
* source — the runner would keep sourcing from the OLD owner. No-op when
|
|
28114
|
+
* nothing is assigned.
|
|
28115
|
+
*/
|
|
28116
|
+
async redispatchAllActiveCameras(reason) {
|
|
28117
|
+
const deviceIds = [...this.assignments.keys()];
|
|
28118
|
+
if (deviceIds.length === 0) return;
|
|
28119
|
+
this.ctx.logger.info("re-dispatching all cameras", { meta: {
|
|
28120
|
+
reason,
|
|
28121
|
+
count: deviceIds.length
|
|
28122
|
+
} });
|
|
28123
|
+
for (const deviceId of deviceIds) await this.redispatchSingleCamera(deviceId);
|
|
28124
|
+
}
|
|
27876
28125
|
async listTemplates() {
|
|
27877
28126
|
const templates = await this.readTemplatesMap();
|
|
27878
28127
|
return Object.values(templates).slice().toSorted((a, b) => a.name.localeCompare(b.name));
|
|
@@ -27949,6 +28198,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27949
28198
|
const STAGE_TIMEOUT_MS = 3e3;
|
|
27950
28199
|
const pipelineAssignment = this.assignments.get(deviceId) ?? null;
|
|
27951
28200
|
const detectionNodeId = pipelineAssignment?.agentNodeId ?? null;
|
|
28201
|
+
const sourceNodeId = this.assignSource(deviceId);
|
|
27952
28202
|
const decoderPinRaw = (api ? await this.ctx.settings?.readDeviceStore(deviceId).catch(() => ({})) ?? {} : {})["decoderNodeId"];
|
|
27953
28203
|
const decoderPinned = typeof decoderPinRaw === "string" && decoderPinRaw !== "auto";
|
|
27954
28204
|
const advisoryDecoderNodeId = detectionNodeId ? await this.resolveDecoderNode(deviceId, detectionNodeId).catch(() => null) : null;
|
|
@@ -27962,7 +28212,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27962
28212
|
};
|
|
27963
28213
|
const detectionReason = pipelineAssignment !== null ? pipelineAssignment.reason : this.cameraConfigs.has(deviceId) ? `pending:${this.pendingReasons.get(deviceId) ?? "pending"}` : void 0;
|
|
27964
28214
|
const liveDecoder = { nodeId: null };
|
|
27965
|
-
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query() : null;
|
|
28215
|
+
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query(void 0, nodePin(sourceNodeId)) : null;
|
|
27966
28216
|
const sourceFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then((slots) => {
|
|
27967
28217
|
return { streams: slots.filter((s) => s.deviceId === deviceId).map((s) => ({
|
|
27968
28218
|
camStreamId: s.sourceCamStreamId ?? s.brokerId,
|
|
@@ -27988,10 +28238,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
27988
28238
|
const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
|
|
27989
28239
|
return {
|
|
27990
28240
|
slot,
|
|
27991
|
-
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }).catch(() => null),
|
|
27992
|
-
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }).catch(() => null)
|
|
28241
|
+
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null),
|
|
28242
|
+
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }, nodePin(sourceNodeId)).catch(() => null)
|
|
27993
28243
|
};
|
|
27994
|
-
})), api.streamBroker.getAllRtspEntries.query({}).catch(() => null)]);
|
|
28244
|
+
})), api.streamBroker.getAllRtspEntries.query({}, nodePin(sourceNodeId)).catch(() => null)]);
|
|
27995
28245
|
const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
|
|
27996
28246
|
profile: slot.profile,
|
|
27997
28247
|
status: slot.status,
|
|
@@ -28096,6 +28346,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28096
28346
|
detectionNodeId,
|
|
28097
28347
|
decoderNodeId,
|
|
28098
28348
|
audioNodeId,
|
|
28349
|
+
sourceNodeId,
|
|
28099
28350
|
pinned,
|
|
28100
28351
|
reasons,
|
|
28101
28352
|
sourceResult,
|
|
@@ -28173,14 +28424,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28173
28424
|
}
|
|
28174
28425
|
/**
|
|
28175
28426
|
* Fetch `pipelineExecutor.getSchema({nodeId})` through the standard cap
|
|
28176
|
-
* router, cached per
|
|
28177
|
-
*
|
|
28178
|
-
*
|
|
28427
|
+
* router, cached per node with a short TTL. Returns `null` when the
|
|
28428
|
+
* executor can't be reached (no runner yet attached) — callers fall
|
|
28429
|
+
* back to an empty pipeline in that case.
|
|
28430
|
+
*
|
|
28431
|
+
* The cache key keeps the `${nodeId}::*` suffix (rather than a bare
|
|
28432
|
+
* `nodeId`) so `handleDetectionPipelineReadiness`'s prefix-match
|
|
28433
|
+
* invalidation (`key.startsWith(\`${nodeId}::\`)`) keeps working
|
|
28434
|
+
* unchanged; there is no longer a per-engine variant to distinguish.
|
|
28179
28435
|
*/
|
|
28180
|
-
async getCatalogForAgent(nodeId
|
|
28436
|
+
async getCatalogForAgent(nodeId) {
|
|
28181
28437
|
const api = this.ctx.api;
|
|
28182
28438
|
if (!api) return null;
|
|
28183
|
-
const engineKey =
|
|
28439
|
+
const engineKey = `${nodeId}::*`;
|
|
28184
28440
|
const now = Date.now();
|
|
28185
28441
|
const hit = this.catalogCache.get(engineKey);
|
|
28186
28442
|
if (hit && now - hit.at < PipelineOrchestratorAddon.CATALOG_CACHE_TTL_MS) return hit.schema;
|
|
@@ -28206,11 +28462,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28206
28462
|
*
|
|
28207
28463
|
* Behaviour:
|
|
28208
28464
|
* - Fetch catalog for the node.
|
|
28209
|
-
* - Call `seedAgentAddonDefaults(current.addonDefaults,
|
|
28465
|
+
* - Call `seedAgentAddonDefaults(current.addonDefaults, catalog)` — preserves
|
|
28210
28466
|
* operator customisations, adds missing addons, drops orphans.
|
|
28211
28467
|
* - Persist if anything changed.
|
|
28212
28468
|
*
|
|
28213
|
-
* Idempotent: a second call with the same catalog is a no-op.
|
|
28469
|
+
* Idempotent: a second call with the same catalog is a no-op. Data
|
|
28470
|
+
* only: no engine is resolved or stored here — the executing node
|
|
28471
|
+
* picks its own format default at runtime when a step's `modelId` is
|
|
28472
|
+
* absent.
|
|
28214
28473
|
*/
|
|
28215
28474
|
async seedAgentSettingsFromCatalog(nodeId) {
|
|
28216
28475
|
await this.ctx.acquireCapability("pipeline-executor", {
|
|
@@ -28220,71 +28479,28 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28220
28479
|
const catalog = await this.getCatalogForAgent(nodeId);
|
|
28221
28480
|
if (!catalog) return null;
|
|
28222
28481
|
const existing = (await this.readAgentSettingsMap())[nodeId];
|
|
28223
|
-
const
|
|
28224
|
-
const nextAddonDefaults = seedAgentAddonDefaults(existing?.addonDefaults ?? {}, engine, catalog);
|
|
28482
|
+
const nextAddonDefaults = seedAgentAddonDefaults(existing?.addonDefaults ?? {}, catalog);
|
|
28225
28483
|
const next = { addonDefaults: nextAddonDefaults };
|
|
28226
28484
|
if (!existing || JSON.stringify(existing.addonDefaults) !== JSON.stringify(next.addonDefaults)) {
|
|
28227
28485
|
await this.writeAgentSettings(nodeId, next);
|
|
28228
28486
|
this.ctx.logger.info("agentSettings seeded/refreshed", {
|
|
28229
28487
|
tags: { nodeId },
|
|
28230
|
-
meta: {
|
|
28231
|
-
runtime: engine.runtime,
|
|
28232
|
-
backend: engine.backend,
|
|
28233
|
-
format: engine.format,
|
|
28234
|
-
addons: Object.keys(nextAddonDefaults).length
|
|
28235
|
-
}
|
|
28488
|
+
meta: { addons: Object.keys(nextAddonDefaults).length }
|
|
28236
28489
|
});
|
|
28237
28490
|
}
|
|
28238
28491
|
return next;
|
|
28239
28492
|
}
|
|
28240
28493
|
/**
|
|
28241
|
-
* Fetch the engine triple (`runtime/backend/device`) from the
|
|
28242
|
-
* `detection-pipeline` addon's global settings on the target node.
|
|
28243
|
-
* This is the authoritative engine source since phase 2f. Returns
|
|
28244
|
-
* `null` when the addon hasn't responded yet or doesn't expose the
|
|
28245
|
-
* fields — callers fall back to `catalog.selectedEngine`.
|
|
28246
|
-
*/
|
|
28247
|
-
async readDetectionPipelineEngine(agentNodeId) {
|
|
28248
|
-
const api = this.ctx.api;
|
|
28249
|
-
if (!api?.addonSettings) return null;
|
|
28250
|
-
try {
|
|
28251
|
-
const schema = await api.addonSettings.getGlobalSettings.query({
|
|
28252
|
-
addonId: "detection-pipeline",
|
|
28253
|
-
nodeId: agentNodeId
|
|
28254
|
-
});
|
|
28255
|
-
if (!schema) return null;
|
|
28256
|
-
const leaf = (key) => {
|
|
28257
|
-
for (const s of schema.sections) for (const f of s.fields) if (f.key === key) return f.value;
|
|
28258
|
-
};
|
|
28259
|
-
const runtime = leaf("engineRuntime");
|
|
28260
|
-
const backend = leaf("engineBackend");
|
|
28261
|
-
const device = leaf("engineDevice");
|
|
28262
|
-
if (runtime !== "python" && runtime !== "node" || typeof backend !== "string" || backend.length === 0) return null;
|
|
28263
|
-
const base = {
|
|
28264
|
-
runtime,
|
|
28265
|
-
backend,
|
|
28266
|
-
format: backend === "coreml" ? "coreml" : backend === "openvino" ? "openvino" : "onnx"
|
|
28267
|
-
};
|
|
28268
|
-
if (typeof device === "string" && device.length > 0) return {
|
|
28269
|
-
...base,
|
|
28270
|
-
device
|
|
28271
|
-
};
|
|
28272
|
-
return base;
|
|
28273
|
-
} catch (err) {
|
|
28274
|
-
this.ctx.logger.debug("readDetectionPipelineEngine failed — falling back", {
|
|
28275
|
-
tags: { nodeId: agentNodeId },
|
|
28276
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
28277
|
-
});
|
|
28278
|
-
return null;
|
|
28279
|
-
}
|
|
28280
|
-
}
|
|
28281
|
-
/**
|
|
28282
28494
|
* Resolve the pipeline for a device. NEVER returns an empty/fallback
|
|
28283
28495
|
* config: if the catalog or agent settings aren't available yet, this
|
|
28284
28496
|
* blocks (with backoff) until the detection-pipeline cap is registered
|
|
28285
28497
|
* and a non-null catalog comes back. Cameras must never be dispatched
|
|
28286
|
-
* with `
|
|
28287
|
-
*
|
|
28498
|
+
* with `steps=[]` because the runner then attaches the camera with no
|
|
28499
|
+
* steps and silently never recovers.
|
|
28500
|
+
*
|
|
28501
|
+
* Data only: the returned config never carries an engine — model
|
|
28502
|
+
* selection is data only, and the executing node resolves its own
|
|
28503
|
+
* format default at runtime when a step's `modelId` is absent.
|
|
28288
28504
|
*
|
|
28289
28505
|
* Two legitimate exceptions to "never empty":
|
|
28290
28506
|
* 1. The device's `detection-pipeline` binding is INACTIVE — the
|
|
@@ -28298,39 +28514,29 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28298
28514
|
async resolvePipelineForDevice(deviceId, agentNodeIdOverride) {
|
|
28299
28515
|
const agentNodeId = agentNodeIdOverride ?? this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
28300
28516
|
if (!await this.isDetectionPipelineActive(deviceId)) {
|
|
28301
|
-
const engine = await this.waitForEngine(agentNodeId);
|
|
28302
28517
|
this.ctx.logger.info("resolvePipelineForDevice → empty (binding inactive)", {
|
|
28303
28518
|
tags: {
|
|
28304
28519
|
deviceId,
|
|
28305
28520
|
nodeId: agentNodeId
|
|
28306
28521
|
},
|
|
28307
|
-
meta: {
|
|
28308
|
-
reason: "detection-pipeline binding inactive",
|
|
28309
|
-
engine: `${engine.runtime}/${engine.backend}`
|
|
28310
|
-
}
|
|
28522
|
+
meta: { reason: "detection-pipeline binding inactive" }
|
|
28311
28523
|
});
|
|
28312
28524
|
return {
|
|
28313
|
-
engine,
|
|
28314
28525
|
steps: [],
|
|
28315
28526
|
audio: null
|
|
28316
28527
|
};
|
|
28317
28528
|
}
|
|
28318
28529
|
const cam = (await this.readCameraSettingsMap())[String(deviceId)] ?? null;
|
|
28319
28530
|
const wholesale = cam?.pipelineByAgent?.[agentNodeId];
|
|
28320
|
-
if (wholesale) {
|
|
28321
|
-
|
|
28322
|
-
|
|
28323
|
-
|
|
28324
|
-
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
|
|
28328
|
-
|
|
28329
|
-
} : null
|
|
28330
|
-
};
|
|
28331
|
-
}
|
|
28332
|
-
const { agent, catalog, engine } = await this.waitForAgentAndCatalog(agentNodeId);
|
|
28333
|
-
const resolved = resolvePipeline(agent, cam, agentNodeId, engine, catalog);
|
|
28531
|
+
if (wholesale) return {
|
|
28532
|
+
steps: wholesale.steps,
|
|
28533
|
+
audio: wholesale.audio ? {
|
|
28534
|
+
modelId: wholesale.audio.modelId,
|
|
28535
|
+
enabled: wholesale.audio.enabled
|
|
28536
|
+
} : null
|
|
28537
|
+
};
|
|
28538
|
+
const { agent, catalog } = await this.waitForAgentAndCatalog(agentNodeId);
|
|
28539
|
+
const resolved = resolvePipeline(agent, cam, agentNodeId, catalog);
|
|
28334
28540
|
if (resolved.steps.length === 0) this.ctx.logger.info("resolvePipelineForDevice → empty (all addons disabled)", {
|
|
28335
28541
|
tags: {
|
|
28336
28542
|
deviceId,
|
|
@@ -28378,51 +28584,27 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28378
28584
|
await sleep$1(2e3);
|
|
28379
28585
|
continue;
|
|
28380
28586
|
}
|
|
28381
|
-
const engine = await this.readDetectionPipelineEngine(nodeId) ?? catalog.selectedEngine;
|
|
28382
28587
|
return {
|
|
28383
28588
|
agent,
|
|
28384
|
-
catalog
|
|
28385
|
-
engine
|
|
28589
|
+
catalog
|
|
28386
28590
|
};
|
|
28387
28591
|
}
|
|
28388
28592
|
}
|
|
28389
28593
|
/**
|
|
28390
|
-
* Block until a real engine choice is available. `acquireCapability`
|
|
28391
|
-
* defaults to infinite wait; the post-acquire loop only handles the
|
|
28392
|
-
* propagation gap where the cap is `ready` but the derived calls
|
|
28393
|
-
* still return null for a tick.
|
|
28394
|
-
*/
|
|
28395
|
-
async waitForEngine(nodeId) {
|
|
28396
|
-
await this.ctx.acquireCapability("pipeline-executor", {
|
|
28397
|
-
type: "node",
|
|
28398
|
-
nodeId
|
|
28399
|
-
});
|
|
28400
|
-
while (true) {
|
|
28401
|
-
const fromAddon = await this.readDetectionPipelineEngine(nodeId);
|
|
28402
|
-
if (fromAddon) return fromAddon;
|
|
28403
|
-
const catalog = await this.getCatalogForAgent(nodeId);
|
|
28404
|
-
if (catalog?.selectedEngine) return catalog.selectedEngine;
|
|
28405
|
-
await sleep$1(2e3);
|
|
28406
|
-
}
|
|
28407
|
-
}
|
|
28408
|
-
/**
|
|
28409
28594
|
* Structural runtime check for `CameraPipelineConfig`. Persisted JSON
|
|
28410
28595
|
* may be stale or partial across upgrades, so we gate reads on shape
|
|
28411
28596
|
* rather than trusting the raw object. False = silently ignore that
|
|
28412
28597
|
* entry and fall back to defaults — the orchestrator's job is to
|
|
28413
28598
|
* never crash on corrupt store state.
|
|
28599
|
+
*
|
|
28600
|
+
* Data only: `engine` is no longer required — a persisted config may
|
|
28601
|
+
* still carry a vestigial `engine` from before the data-only refactor;
|
|
28602
|
+
* it is simply ignored.
|
|
28414
28603
|
*/
|
|
28415
28604
|
isCameraPipelineConfig(value) {
|
|
28416
28605
|
if (!value || typeof value !== "object") return false;
|
|
28417
28606
|
const v = value;
|
|
28418
|
-
|
|
28419
|
-
if (!engine || typeof engine !== "object") return false;
|
|
28420
|
-
const e = engine;
|
|
28421
|
-
if (typeof e["runtime"] !== "string") return false;
|
|
28422
|
-
if (typeof e["backend"] !== "string") return false;
|
|
28423
|
-
if (typeof e["format"] !== "string") return false;
|
|
28424
|
-
if (!Array.isArray(v["steps"])) return false;
|
|
28425
|
-
return true;
|
|
28607
|
+
return Array.isArray(v["steps"]);
|
|
28426
28608
|
}
|
|
28427
28609
|
isPipelineTemplate(value) {
|
|
28428
28610
|
if (!value || typeof value !== "object") return false;
|
|
@@ -28774,10 +28956,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28774
28956
|
] });
|
|
28775
28957
|
}
|
|
28776
28958
|
async updateGlobalSettings(patch) {
|
|
28959
|
+
const previousIngestNode = this.clusterRoles.ingestNode;
|
|
28777
28960
|
await super.updateGlobalSettings(patch);
|
|
28778
28961
|
const full = await this.resolveGlobalStore();
|
|
28779
28962
|
this.globalSettings = { ...full };
|
|
28780
28963
|
this.applyRuntimeSettings(full);
|
|
28964
|
+
if (this.clusterRoles.ingestNode !== previousIngestNode) {
|
|
28965
|
+
const { configIssue } = resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
28966
|
+
if (configIssue !== void 0) this.ctx.logger.warn("ingest node config issue", { meta: {
|
|
28967
|
+
ingestNode: this.clusterRoles.ingestNode,
|
|
28968
|
+
issue: configIssue
|
|
28969
|
+
} });
|
|
28970
|
+
this.refreshNodeCapabilities().then(() => this.redispatchAllActiveCameras("ingest-owner-changed")).catch((err) => {
|
|
28971
|
+
this.ctx.logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg(err) } });
|
|
28972
|
+
});
|
|
28973
|
+
}
|
|
28781
28974
|
const pausedIds = [...this.loadShedState.entries()].filter(([, s]) => s.pausedAt !== null).map(([id]) => id);
|
|
28782
28975
|
if (pausedIds.length > 0) {
|
|
28783
28976
|
this.loadShedState.clear();
|
|
@@ -28946,7 +29139,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
28946
29139
|
async applyPipelinePatch(deviceId, pipelinePatch) {
|
|
28947
29140
|
const incoming = pipelinePatch.cameraPipeline;
|
|
28948
29141
|
if (incoming === void 0) return;
|
|
28949
|
-
if (!this.isCameraPipelineConfig(incoming)) throw new Error(`applyPipelinePatch: cameraPipeline value for device ${deviceId} is not a valid CameraPipelineConfig (expected {
|
|
29142
|
+
if (!this.isCameraPipelineConfig(incoming)) throw new Error(`applyPipelinePatch: cameraPipeline value for device ${deviceId} is not a valid CameraPipelineConfig (expected { steps:[], audio?:null|object })`);
|
|
28950
29143
|
const agentNodeId = this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
28951
29144
|
await this.setCameraPipelineForAgent({
|
|
28952
29145
|
deviceId,
|
|
@@ -29002,12 +29195,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29002
29195
|
}
|
|
29003
29196
|
/**
|
|
29004
29197
|
* Recompute the derived enabled-node sets (`enabledNodes` /
|
|
29005
|
-
* `enabledDecoderNodes` / `enabledAudioNodes`) from
|
|
29006
|
-
* store. A node is eligible for a concern iff its
|
|
29007
|
-
* the flag is unset AND the node defaults capable
|
|
29008
|
-
* Forked child nodeIds (`hub/classifier`)
|
|
29009
|
-
* excluded. `hub` is always considered even
|
|
29010
|
-
* fresh install keeps the hub-only cluster
|
|
29198
|
+
* `enabledDecoderNodes` / `enabledAudioNodes` / `enabledIngestNodes`) from
|
|
29199
|
+
* the per-node capability store. A node is eligible for a concern iff its
|
|
29200
|
+
* stored flag is `true`, or the flag is unset AND the node defaults capable
|
|
29201
|
+
* ({@link nodeCapabilityDefault}). Forked child nodeIds (`hub/classifier`)
|
|
29202
|
+
* are never dispatchable and are excluded. `hub` is always considered even
|
|
29203
|
+
* when absent from the store, so a fresh install keeps the hub-only cluster
|
|
29204
|
+
* working.
|
|
29011
29205
|
*/
|
|
29012
29206
|
async refreshNodeCapabilities() {
|
|
29013
29207
|
let blob = {};
|
|
@@ -29021,16 +29215,30 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29021
29215
|
const detect = [];
|
|
29022
29216
|
const decode = [];
|
|
29023
29217
|
const audio = [];
|
|
29218
|
+
const ingest = [];
|
|
29219
|
+
const reachableHosts = /* @__PURE__ */ new Map();
|
|
29024
29220
|
const capable = (flag, nodeId) => typeof flag === "boolean" ? flag : PipelineOrchestratorAddon.nodeCapabilityDefault(nodeId);
|
|
29025
29221
|
for (const nodeId of nodeIds) {
|
|
29026
29222
|
const settings = blob[nodeId];
|
|
29027
29223
|
if (capable(settings?.detect, nodeId)) detect.push(nodeId);
|
|
29028
29224
|
if (capable(settings?.decode, nodeId)) decode.push(nodeId);
|
|
29029
29225
|
if (capable(settings?.audio, nodeId)) audio.push(nodeId);
|
|
29226
|
+
if (capable(settings?.ingest, nodeId)) ingest.push(nodeId);
|
|
29227
|
+
const host = settings?.reachableHost?.trim();
|
|
29228
|
+
if (host !== void 0 && host.length > 0) reachableHosts.set(nodeId, host);
|
|
29030
29229
|
}
|
|
29230
|
+
this.reachableHostByNode = reachableHosts;
|
|
29031
29231
|
this.enabledNodes = detect.toSorted();
|
|
29032
29232
|
this.enabledDecoderNodes = decode.toSorted();
|
|
29033
29233
|
this.enabledAudioNodes = audio.toSorted();
|
|
29234
|
+
this.enabledIngestNodes = ingest.toSorted();
|
|
29235
|
+
this.remoteSourcingNodes = deriveRemoteSourcingNodes(this.enabledDecoderNodes, this.clusterRoles.ingestNode);
|
|
29236
|
+
const { ownerNodeId } = resolveIngestOwner(this.clusterRoles, this.reachableHostByNode, this.enabledIngestNodes);
|
|
29237
|
+
this.ctx.eventBus.emit(createEvent(EventCategory.PipelineIngestOwnerChanged, {
|
|
29238
|
+
type: "addon",
|
|
29239
|
+
id: this.ctx.id,
|
|
29240
|
+
nodeId: "hub"
|
|
29241
|
+
}, { ownerNodeId }));
|
|
29034
29242
|
}
|
|
29035
29243
|
get api() {
|
|
29036
29244
|
return this.ctx.api ?? null;
|
|
@@ -29329,6 +29537,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29329
29537
|
await this.stopDetection(deviceId);
|
|
29330
29538
|
}
|
|
29331
29539
|
this.activeDetections.set(deviceId, config);
|
|
29540
|
+
const hydrateNodeId = this.assignments.get(deviceId)?.agentNodeId ?? this.localNodeId;
|
|
29332
29541
|
const pipelineConfig = await this.resolvePipelineForDevice(deviceId);
|
|
29333
29542
|
const zones = await this.zonesProvider?.listZones({ deviceId }) ?? [];
|
|
29334
29543
|
const runnerConfig = {
|
|
@@ -29340,7 +29549,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29340
29549
|
detectionStreamId: config.detectionStreamId,
|
|
29341
29550
|
motionSources: config.motionSources,
|
|
29342
29551
|
pipelineEnabled: config.pipelineEnabled,
|
|
29343
|
-
engine: pipelineConfig.engine,
|
|
29344
29552
|
steps: pipelineConfig.steps,
|
|
29345
29553
|
audio: pipelineConfig.audio ?? null,
|
|
29346
29554
|
zones,
|
|
@@ -29359,15 +29567,20 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29359
29567
|
const msg = errMsg(err);
|
|
29360
29568
|
log.error("dispatchCamera failed", { meta: { error: msg } });
|
|
29361
29569
|
}
|
|
29362
|
-
if (dispatchedNodeId && (runnerConfig.steps?.length ?? 0) === 0) try {
|
|
29570
|
+
if (dispatchedNodeId && (dispatchedNodeId !== hydrateNodeId || (runnerConfig.steps?.length ?? 0) === 0)) try {
|
|
29363
29571
|
const corrected = await this.resolvePipelineForDevice(deviceId, dispatchedNodeId);
|
|
29364
|
-
|
|
29365
|
-
|
|
29366
|
-
|
|
29367
|
-
|
|
29368
|
-
|
|
29369
|
-
|
|
29370
|
-
|
|
29572
|
+
const correctedConfig = {
|
|
29573
|
+
...runnerConfig,
|
|
29574
|
+
steps: corrected.steps,
|
|
29575
|
+
audio: corrected.audio ?? null
|
|
29576
|
+
};
|
|
29577
|
+
if (JSON.stringify({
|
|
29578
|
+
steps: runnerConfig.steps,
|
|
29579
|
+
audio: runnerConfig.audio ?? null
|
|
29580
|
+
}) !== JSON.stringify({
|
|
29581
|
+
steps: correctedConfig.steps,
|
|
29582
|
+
audio: correctedConfig.audio
|
|
29583
|
+
})) {
|
|
29371
29584
|
await this.attachOn(dispatchedNodeId, correctedConfig);
|
|
29372
29585
|
this.cameraConfigs.set(deviceId, correctedConfig);
|
|
29373
29586
|
log.info("startDetection: pipeline re-resolved after agent assignment", {
|
|
@@ -29568,7 +29781,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
29568
29781
|
}
|
|
29569
29782
|
const nextConfig = {
|
|
29570
29783
|
...cached,
|
|
29571
|
-
engine: config.engine,
|
|
29572
29784
|
steps: config.steps,
|
|
29573
29785
|
audio: config.audio ?? null
|
|
29574
29786
|
};
|
|
@@ -30060,6 +30272,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
30060
30272
|
api,
|
|
30061
30273
|
brokerId: audioBrokerId,
|
|
30062
30274
|
tag: "audio-analyzer",
|
|
30275
|
+
ownerNodeId: this.clusterRoles.ingestNode,
|
|
30063
30276
|
logger: this.ctx.logger,
|
|
30064
30277
|
onChunk: async (chunk) => {
|
|
30065
30278
|
this.pipelineWatchdog?.noteSignal(deviceId, "audio");
|