@camstack/addon-pipeline-orchestrator 1.0.4 → 1.0.5
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--jkY2PwT.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-B-Qt-xEj.mjs} +3 -3
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DXnipYJF.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-DUkBS-o2.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-KklTiJaw.mjs} +1 -1
- package/dist/{hostInit-CSC3mFBH.mjs → hostInit-DUWCx6dX.mjs} +3 -3
- package/dist/index.js +691 -41
- package/dist/index.mjs +691 -41
- 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-TYiNYNvp.mjs +0 -26
package/dist/index.mjs
CHANGED
|
@@ -4981,6 +4981,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4981
4981
|
*/
|
|
4982
4982
|
EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
|
|
4983
4983
|
/**
|
|
4984
|
+
* Per-node detection-engine runtime-provisioning transition. Emitted by
|
|
4985
|
+
* the detection-pipeline provider on every state change of its lazy
|
|
4986
|
+
* engine-provisioning machine (idle → installing → verifying → ready,
|
|
4987
|
+
* or → failed with a `nextRetryAt`). Payload is the
|
|
4988
|
+
* `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
|
|
4989
|
+
* node. The Pipeline page subscribes to drive a live "installing
|
|
4990
|
+
* OpenVINO… / ready" indicator per node without polling
|
|
4991
|
+
* `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
|
|
4992
|
+
* also reads the cap snapshot on mount / reconnect. Phase 2.
|
|
4993
|
+
*/
|
|
4994
|
+
EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
|
|
4995
|
+
/**
|
|
4984
4996
|
* Cluster topology snapshot. Carries the same payload returned by
|
|
4985
4997
|
* `nodes.topology` (every reachable node + addons + processes).
|
|
4986
4998
|
* Emitted by the hub on any agent / addon lifecycle change
|
|
@@ -9562,6 +9574,24 @@ var DetectorOutputSchema = object({
|
|
|
9562
9574
|
inferenceMs: number(),
|
|
9563
9575
|
modelId: string()
|
|
9564
9576
|
});
|
|
9577
|
+
var EngineProvisioningSchema = object({
|
|
9578
|
+
runtimeId: _enum([
|
|
9579
|
+
"onnx",
|
|
9580
|
+
"openvino",
|
|
9581
|
+
"coreml"
|
|
9582
|
+
]).nullable(),
|
|
9583
|
+
device: string().nullable(),
|
|
9584
|
+
state: _enum([
|
|
9585
|
+
"idle",
|
|
9586
|
+
"installing",
|
|
9587
|
+
"verifying",
|
|
9588
|
+
"ready",
|
|
9589
|
+
"failed"
|
|
9590
|
+
]),
|
|
9591
|
+
progress: number().optional(),
|
|
9592
|
+
error: string().optional(),
|
|
9593
|
+
nextRetryAt: number().optional()
|
|
9594
|
+
});
|
|
9565
9595
|
var PipelineStepInputSchema = lazy(() => object({
|
|
9566
9596
|
addonId: string(),
|
|
9567
9597
|
modelId: string(),
|
|
@@ -9661,6 +9691,15 @@ var pipelineExecutorCapability = {
|
|
|
9661
9691
|
kind: "mutation",
|
|
9662
9692
|
auth: "admin"
|
|
9663
9693
|
}),
|
|
9694
|
+
/**
|
|
9695
|
+
* Per-node detection-engine provisioning snapshot. Returns the live
|
|
9696
|
+
* state of the lazy runtime-provisioning machine on `nodeId`
|
|
9697
|
+
* (idle / installing / verifying / ready / failed). The UI pairs this
|
|
9698
|
+
* one-shot query with the `pipeline.engine-provisioning` live event
|
|
9699
|
+
* (emitted on every transition) to drive a per-node "engine ready?"
|
|
9700
|
+
* indicator without polling. Phase 2.
|
|
9701
|
+
*/
|
|
9702
|
+
getEngineProvisioning: method(object({ nodeId: string() }), EngineProvisioningSchema),
|
|
9664
9703
|
getVideoPipelineSteps: method(_void(), record(string(), object({
|
|
9665
9704
|
modelId: string(),
|
|
9666
9705
|
settings: record(string(), unknown()).readonly()
|
|
@@ -13537,7 +13576,10 @@ var AgentAddonConfigSchema = object({
|
|
|
13537
13576
|
modelId: string(),
|
|
13538
13577
|
settings: record(string(), unknown()).readonly()
|
|
13539
13578
|
});
|
|
13540
|
-
var AgentPipelineSettingsSchema = object({
|
|
13579
|
+
var AgentPipelineSettingsSchema = object({
|
|
13580
|
+
addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
|
|
13581
|
+
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
13582
|
+
});
|
|
13541
13583
|
var CameraPipelineForAgentSchema = object({
|
|
13542
13584
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
13543
13585
|
audio: object({
|
|
@@ -13639,6 +13681,133 @@ var GlobalMetricsSchema = object({
|
|
|
13639
13681
|
* capability providers.
|
|
13640
13682
|
*/
|
|
13641
13683
|
var CapabilityBindingsSchema = record(string(), string());
|
|
13684
|
+
/** Source block — always present; derives from the stream catalog. */
|
|
13685
|
+
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
13686
|
+
camStreamId: string(),
|
|
13687
|
+
codec: string(),
|
|
13688
|
+
width: number(),
|
|
13689
|
+
height: number(),
|
|
13690
|
+
fps: number(),
|
|
13691
|
+
kind: string()
|
|
13692
|
+
})).readonly() });
|
|
13693
|
+
/** Assignment block — always present (orchestrator-local, no remote call). */
|
|
13694
|
+
var CameraAssignmentStatusSchema = object({
|
|
13695
|
+
detectionNodeId: string().nullable(),
|
|
13696
|
+
decoderNodeId: string().nullable(),
|
|
13697
|
+
audioNodeId: string().nullable(),
|
|
13698
|
+
pinned: object({
|
|
13699
|
+
detection: boolean(),
|
|
13700
|
+
decoder: boolean(),
|
|
13701
|
+
audio: boolean()
|
|
13702
|
+
}),
|
|
13703
|
+
reasons: object({
|
|
13704
|
+
detection: string().optional(),
|
|
13705
|
+
decoder: string().optional(),
|
|
13706
|
+
audio: string().optional()
|
|
13707
|
+
})
|
|
13708
|
+
});
|
|
13709
|
+
/** Broker block — null when the broker stage is unreachable or inactive. */
|
|
13710
|
+
var CameraBrokerStatusSchema = object({
|
|
13711
|
+
profiles: array(object({
|
|
13712
|
+
profile: string(),
|
|
13713
|
+
status: string(),
|
|
13714
|
+
codec: string(),
|
|
13715
|
+
width: number(),
|
|
13716
|
+
height: number(),
|
|
13717
|
+
subscribers: number(),
|
|
13718
|
+
inFps: number(),
|
|
13719
|
+
outFps: number()
|
|
13720
|
+
})).readonly(),
|
|
13721
|
+
webrtcSessions: number(),
|
|
13722
|
+
rtspRestream: boolean()
|
|
13723
|
+
});
|
|
13724
|
+
/** Shared-memory ring statistics within the decoder block. */
|
|
13725
|
+
var CameraDecoderShmSchema = object({
|
|
13726
|
+
framesWritten: number(),
|
|
13727
|
+
getFrameHits: number(),
|
|
13728
|
+
getFrameMisses: number(),
|
|
13729
|
+
budgetMb: number()
|
|
13730
|
+
});
|
|
13731
|
+
/** Decoder block — null when the decoder stage is unreachable or inactive. */
|
|
13732
|
+
var CameraDecoderStatusSchema = object({
|
|
13733
|
+
nodeId: string(),
|
|
13734
|
+
formats: array(string()).readonly(),
|
|
13735
|
+
sessionCount: number(),
|
|
13736
|
+
shm: CameraDecoderShmSchema
|
|
13737
|
+
});
|
|
13738
|
+
/** Motion block — null when motion detection is not active for this device. */
|
|
13739
|
+
var CameraMotionStatusSchema = object({
|
|
13740
|
+
enabled: boolean(),
|
|
13741
|
+
fps: number()
|
|
13742
|
+
});
|
|
13743
|
+
/** Detection provisioning sub-block. */
|
|
13744
|
+
var CameraDetectionProvisioningSchema = object({
|
|
13745
|
+
state: _enum([
|
|
13746
|
+
"idle",
|
|
13747
|
+
"installing",
|
|
13748
|
+
"verifying",
|
|
13749
|
+
"ready",
|
|
13750
|
+
"failed"
|
|
13751
|
+
]),
|
|
13752
|
+
error: string().optional()
|
|
13753
|
+
});
|
|
13754
|
+
/** Detection phase — derived from the runner's engine phase. */
|
|
13755
|
+
var CameraDetectionPhaseSchema = _enum([
|
|
13756
|
+
"idle",
|
|
13757
|
+
"watching",
|
|
13758
|
+
"active"
|
|
13759
|
+
]);
|
|
13760
|
+
/** Detection block — null when no detection node is assigned or reachable. */
|
|
13761
|
+
var CameraDetectionStatusSchema = object({
|
|
13762
|
+
nodeId: string(),
|
|
13763
|
+
engine: object({
|
|
13764
|
+
backend: string(),
|
|
13765
|
+
device: string()
|
|
13766
|
+
}),
|
|
13767
|
+
phase: CameraDetectionPhaseSchema,
|
|
13768
|
+
configuredFps: number(),
|
|
13769
|
+
actualFps: number(),
|
|
13770
|
+
queueDepth: number(),
|
|
13771
|
+
avgInferenceMs: number(),
|
|
13772
|
+
provisioning: CameraDetectionProvisioningSchema
|
|
13773
|
+
});
|
|
13774
|
+
/** Audio block — null when no audio node is assigned or reachable. */
|
|
13775
|
+
var CameraAudioStatusSchema = object({
|
|
13776
|
+
nodeId: string(),
|
|
13777
|
+
enabled: boolean()
|
|
13778
|
+
});
|
|
13779
|
+
/** Recording block — null when no recording cap is active for this device. */
|
|
13780
|
+
var CameraRecordingStatusSchema = object({
|
|
13781
|
+
mode: _enum([
|
|
13782
|
+
"off",
|
|
13783
|
+
"continuous",
|
|
13784
|
+
"events"
|
|
13785
|
+
]),
|
|
13786
|
+
active: boolean(),
|
|
13787
|
+
storageBytes: number()
|
|
13788
|
+
});
|
|
13789
|
+
/**
|
|
13790
|
+
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
13791
|
+
*
|
|
13792
|
+
* The `assignment` and `source` blocks are always present.
|
|
13793
|
+
* Every other block is `null` when the stage is inactive or unreachable
|
|
13794
|
+
* during the bounded parallel fan-out in the orchestrator implementation.
|
|
13795
|
+
*
|
|
13796
|
+
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
13797
|
+
*/
|
|
13798
|
+
var CameraStatusSchema = object({
|
|
13799
|
+
deviceId: number(),
|
|
13800
|
+
assignment: CameraAssignmentStatusSchema,
|
|
13801
|
+
source: CameraSourceStatusSchema,
|
|
13802
|
+
broker: CameraBrokerStatusSchema.nullable(),
|
|
13803
|
+
decoder: CameraDecoderStatusSchema.nullable(),
|
|
13804
|
+
motion: CameraMotionStatusSchema.nullable(),
|
|
13805
|
+
detection: CameraDetectionStatusSchema.nullable(),
|
|
13806
|
+
audio: CameraAudioStatusSchema.nullable(),
|
|
13807
|
+
recording: CameraRecordingStatusSchema.nullable(),
|
|
13808
|
+
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
13809
|
+
fetchedAt: number()
|
|
13810
|
+
});
|
|
13642
13811
|
/**
|
|
13643
13812
|
* Pipeline Orchestrator capability — global load balancer + camera dispatcher.
|
|
13644
13813
|
*
|
|
@@ -13815,6 +13984,25 @@ var pipelineOrchestratorCapability = {
|
|
|
13815
13984
|
kind: "mutation",
|
|
13816
13985
|
auth: "admin"
|
|
13817
13986
|
}),
|
|
13987
|
+
/**
|
|
13988
|
+
* Set the per-node camera cap for one agent.
|
|
13989
|
+
*
|
|
13990
|
+
* `maxCameras: 0` and `maxCameras: null` are both treated as unlimited
|
|
13991
|
+
* by the load balancer (0 is stored as-is; the balancer treats ≤0 as
|
|
13992
|
+
* unlimited alongside null). Pass `null` to clear an existing cap.
|
|
13993
|
+
* Note: the admin UI normalises 0→null before calling this method, so
|
|
13994
|
+
* `maxCameras: 0` only reaches the store via direct SDK or CLI calls.
|
|
13995
|
+
* Changes take effect immediately on the next dispatch cycle — cameras
|
|
13996
|
+
* currently assigned over-cap are left in place (no forced eviction),
|
|
13997
|
+
* but new assignments obey the updated cap.
|
|
13998
|
+
*/
|
|
13999
|
+
setAgentMaxCameras: method(object({
|
|
14000
|
+
agentNodeId: string(),
|
|
14001
|
+
maxCameras: number().int().nonnegative().nullable()
|
|
14002
|
+
}), object({ success: literal(true) }), {
|
|
14003
|
+
kind: "mutation",
|
|
14004
|
+
auth: "admin"
|
|
14005
|
+
}),
|
|
13818
14006
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
13819
14007
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
13820
14008
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -13855,6 +14043,29 @@ var pipelineOrchestratorCapability = {
|
|
|
13855
14043
|
deviceId: number(),
|
|
13856
14044
|
agentNodeId: string().optional()
|
|
13857
14045
|
}), CameraPipelineConfigSchema),
|
|
14046
|
+
/**
|
|
14047
|
+
* Server-composed aggregated status for a single camera.
|
|
14048
|
+
*
|
|
14049
|
+
* Fans out in parallel (bounded, per-stage graceful degradation) to
|
|
14050
|
+
* broker / decoder / motion / detection / audio / recording source caps
|
|
14051
|
+
* via `ctx.api`. A stage whose source errors or times out becomes `null`
|
|
14052
|
+
* in the returned payload — one slow agent never breaks the whole call.
|
|
14053
|
+
*
|
|
14054
|
+
* Intended for "on open / on camera select" snapshots. Live deltas
|
|
14055
|
+
* keep arriving from the existing ~1Hz events the UI already subscribes to.
|
|
14056
|
+
*/
|
|
14057
|
+
getCameraStatus: method(object({ deviceId: number() }), CameraStatusSchema),
|
|
14058
|
+
/**
|
|
14059
|
+
* Server-composed aggregated status for multiple cameras in one call.
|
|
14060
|
+
*
|
|
14061
|
+
* `deviceIds` defaults to all cameras currently tracked by the
|
|
14062
|
+
* orchestrator's assignment map when omitted.
|
|
14063
|
+
*
|
|
14064
|
+
* Runs per-device composition in parallel (bounded). Use this to
|
|
14065
|
+
* populate the cluster assignments table and the pipeline flow overview
|
|
14066
|
+
* rail without issuing N parallel browser round-trips.
|
|
14067
|
+
*/
|
|
14068
|
+
getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
|
|
13858
14069
|
/** List every template the operator has saved. */
|
|
13859
14070
|
listTemplates: method(_void(), array(PipelineTemplateSchema).readonly()),
|
|
13860
14071
|
/** Create a new named preset from a given CameraPipelineConfig. */
|
|
@@ -19694,6 +19905,12 @@ Object.freeze({
|
|
|
19694
19905
|
addonId: null,
|
|
19695
19906
|
access: "view"
|
|
19696
19907
|
},
|
|
19908
|
+
"pipelineExecutor.getEngineProvisioning": {
|
|
19909
|
+
capName: "pipeline-executor",
|
|
19910
|
+
capScope: "system",
|
|
19911
|
+
addonId: null,
|
|
19912
|
+
access: "view"
|
|
19913
|
+
},
|
|
19697
19914
|
"pipelineExecutor.getGlobalPipelineConfig": {
|
|
19698
19915
|
capName: "pipeline-executor",
|
|
19699
19916
|
capScope: "system",
|
|
@@ -19898,6 +20115,18 @@ Object.freeze({
|
|
|
19898
20115
|
addonId: null,
|
|
19899
20116
|
access: "view"
|
|
19900
20117
|
},
|
|
20118
|
+
"pipelineOrchestrator.getCameraStatus": {
|
|
20119
|
+
capName: "pipeline-orchestrator",
|
|
20120
|
+
capScope: "system",
|
|
20121
|
+
addonId: null,
|
|
20122
|
+
access: "view"
|
|
20123
|
+
},
|
|
20124
|
+
"pipelineOrchestrator.getCameraStatuses": {
|
|
20125
|
+
capName: "pipeline-orchestrator",
|
|
20126
|
+
capScope: "system",
|
|
20127
|
+
addonId: null,
|
|
20128
|
+
access: "view"
|
|
20129
|
+
},
|
|
19901
20130
|
"pipelineOrchestrator.getCameraStepOverrides": {
|
|
19902
20131
|
capName: "pipeline-orchestrator",
|
|
19903
20132
|
capScope: "system",
|
|
@@ -19982,6 +20211,12 @@ Object.freeze({
|
|
|
19982
20211
|
addonId: null,
|
|
19983
20212
|
access: "create"
|
|
19984
20213
|
},
|
|
20214
|
+
"pipelineOrchestrator.setAgentMaxCameras": {
|
|
20215
|
+
capName: "pipeline-orchestrator",
|
|
20216
|
+
capScope: "system",
|
|
20217
|
+
addonId: null,
|
|
20218
|
+
access: "create"
|
|
20219
|
+
},
|
|
19985
20220
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
19986
20221
|
capName: "pipeline-orchestrator",
|
|
19987
20222
|
capScope: "system",
|
|
@@ -22012,7 +22247,10 @@ var StoredAgentAddonConfigSchema = object({
|
|
|
22012
22247
|
modelId: string(),
|
|
22013
22248
|
settings: record(string(), unknown()).readonly()
|
|
22014
22249
|
});
|
|
22015
|
-
var StoredAgentPipelineSettingsSchema = object({
|
|
22250
|
+
var StoredAgentPipelineSettingsSchema = object({
|
|
22251
|
+
addonDefaults: record(string(), StoredAgentAddonConfigSchema).readonly(),
|
|
22252
|
+
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
22253
|
+
});
|
|
22016
22254
|
var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
|
|
22017
22255
|
var StoredCameraStepOverridePatchSchema = object({
|
|
22018
22256
|
enabled: boolean().optional(),
|
|
@@ -22057,10 +22295,26 @@ function computeCapacityScore(load) {
|
|
|
22057
22295
|
return load.attachedCameras * Math.max(load.avgInferenceFps, 0) + Math.max(load.queueDepthTotal, 0);
|
|
22058
22296
|
}
|
|
22059
22297
|
/**
|
|
22298
|
+
* Returns true when the node has remaining capacity.
|
|
22299
|
+
* A node is eligible iff `cap` is unlimited (null/absent/<=0) OR
|
|
22300
|
+
* its `attachedCameras` count is strictly less than the cap.
|
|
22301
|
+
* Pins count toward the cap via `attachedCameras`.
|
|
22302
|
+
*/
|
|
22303
|
+
function isEligible(node, caps) {
|
|
22304
|
+
const cap = caps?.[node.nodeId];
|
|
22305
|
+
if (cap === null || cap === void 0 || cap <= 0) return true;
|
|
22306
|
+
return node.attachedCameras < cap;
|
|
22307
|
+
}
|
|
22308
|
+
/**
|
|
22060
22309
|
* Run the two-level camera balancer.
|
|
22061
22310
|
*
|
|
22062
|
-
* L1 (manual affinity): if `preferredAgent` names an online node
|
|
22063
|
-
*
|
|
22311
|
+
* L1 (manual affinity): if `preferredAgent` names an online node AND that
|
|
22312
|
+
* node is under its `maxCameras` cap, return it. If the node is online but at
|
|
22313
|
+
* or over cap, return `{kind:'pending'}` — a pinned camera is never silently
|
|
22314
|
+
* over-assigned.
|
|
22315
|
+
*
|
|
22316
|
+
* L2 (capacity): filter to eligible nodes and pick the lowest capacity score.
|
|
22317
|
+
* If all nodes are at/over cap, return `{kind:'pending'}`.
|
|
22064
22318
|
*
|
|
22065
22319
|
* Returns `null` when no runners are online. The orchestrator decides how to
|
|
22066
22320
|
* react — typically by logging and deferring the assignment until a runner
|
|
@@ -22069,19 +22323,32 @@ function computeCapacityScore(load) {
|
|
|
22069
22323
|
function balance(input) {
|
|
22070
22324
|
const online = input.nodes.filter((n) => n.nodeId.length > 0);
|
|
22071
22325
|
if (online.length === 0) return null;
|
|
22326
|
+
const eligible = online.filter((n) => isEligible(n, input.nodeCaps));
|
|
22072
22327
|
if (input.preferredAgent) {
|
|
22073
|
-
const
|
|
22074
|
-
if (
|
|
22075
|
-
|
|
22076
|
-
|
|
22077
|
-
|
|
22078
|
-
|
|
22328
|
+
const pinnedOnline = online.find((n) => n.nodeId === input.preferredAgent);
|
|
22329
|
+
if (pinnedOnline) {
|
|
22330
|
+
if (eligible.some((n) => n.nodeId === pinnedOnline.nodeId)) return {
|
|
22331
|
+
kind: "assigned",
|
|
22332
|
+
agentNodeId: pinnedOnline.nodeId,
|
|
22333
|
+
reason: "manual",
|
|
22334
|
+
score: computeCapacityScore(pinnedOnline)
|
|
22335
|
+
};
|
|
22336
|
+
return {
|
|
22337
|
+
kind: "pending",
|
|
22338
|
+
reason: "over-cap"
|
|
22339
|
+
};
|
|
22340
|
+
}
|
|
22079
22341
|
}
|
|
22080
|
-
|
|
22342
|
+
if (eligible.length === 0) return {
|
|
22343
|
+
kind: "pending",
|
|
22344
|
+
reason: "over-cap"
|
|
22345
|
+
};
|
|
22346
|
+
const best = eligible.map((node) => ({
|
|
22081
22347
|
node,
|
|
22082
22348
|
score: computeCapacityScore(node)
|
|
22083
22349
|
})).toSorted((a, b) => a.score - b.score)[0];
|
|
22084
22350
|
return {
|
|
22351
|
+
kind: "assigned",
|
|
22085
22352
|
agentNodeId: best.node.nodeId,
|
|
22086
22353
|
reason: "capacity",
|
|
22087
22354
|
score: best.score
|
|
@@ -22500,6 +22767,139 @@ var PipelineWatchdog = class {
|
|
|
22500
22767
|
}
|
|
22501
22768
|
};
|
|
22502
22769
|
//#endregion
|
|
22770
|
+
//#region src/camera-status/compose-camera-status.ts
|
|
22771
|
+
function mapAssignment(input) {
|
|
22772
|
+
return {
|
|
22773
|
+
detectionNodeId: input.detectionNodeId,
|
|
22774
|
+
decoderNodeId: input.decoderNodeId,
|
|
22775
|
+
audioNodeId: input.audioNodeId,
|
|
22776
|
+
pinned: {
|
|
22777
|
+
detection: input.pinned.detection,
|
|
22778
|
+
decoder: input.pinned.decoder,
|
|
22779
|
+
audio: input.pinned.audio
|
|
22780
|
+
},
|
|
22781
|
+
reasons: {
|
|
22782
|
+
detection: input.reasons.detection,
|
|
22783
|
+
decoder: input.reasons.decoder,
|
|
22784
|
+
audio: input.reasons.audio
|
|
22785
|
+
}
|
|
22786
|
+
};
|
|
22787
|
+
}
|
|
22788
|
+
function mapSource(sourceResult) {
|
|
22789
|
+
if (sourceResult === null) return { streams: [] };
|
|
22790
|
+
return { streams: sourceResult.streams.map((s) => ({
|
|
22791
|
+
camStreamId: s.camStreamId,
|
|
22792
|
+
codec: s.codec,
|
|
22793
|
+
width: s.width,
|
|
22794
|
+
height: s.height,
|
|
22795
|
+
fps: s.fps,
|
|
22796
|
+
kind: s.kind
|
|
22797
|
+
})) };
|
|
22798
|
+
}
|
|
22799
|
+
function mapBroker(brokerResult) {
|
|
22800
|
+
if (brokerResult === null) return null;
|
|
22801
|
+
return {
|
|
22802
|
+
profiles: brokerResult.profiles.map((p) => ({
|
|
22803
|
+
profile: p.profile,
|
|
22804
|
+
status: p.status,
|
|
22805
|
+
codec: p.codec,
|
|
22806
|
+
width: p.width,
|
|
22807
|
+
height: p.height,
|
|
22808
|
+
subscribers: p.subscribers,
|
|
22809
|
+
inFps: p.inFps,
|
|
22810
|
+
outFps: p.outFps
|
|
22811
|
+
})),
|
|
22812
|
+
webrtcSessions: brokerResult.webrtcSessions,
|
|
22813
|
+
rtspRestream: brokerResult.rtspRestream
|
|
22814
|
+
};
|
|
22815
|
+
}
|
|
22816
|
+
function mapDecoderShm(shm) {
|
|
22817
|
+
return {
|
|
22818
|
+
framesWritten: shm.framesWritten,
|
|
22819
|
+
getFrameHits: shm.getFrameHits,
|
|
22820
|
+
getFrameMisses: shm.getFrameMisses,
|
|
22821
|
+
budgetMb: shm.budgetMb
|
|
22822
|
+
};
|
|
22823
|
+
}
|
|
22824
|
+
function mapDecoder(decoderResult) {
|
|
22825
|
+
if (decoderResult === null) return null;
|
|
22826
|
+
return {
|
|
22827
|
+
nodeId: decoderResult.nodeId,
|
|
22828
|
+
formats: [...decoderResult.formats],
|
|
22829
|
+
sessionCount: decoderResult.sessionCount,
|
|
22830
|
+
shm: mapDecoderShm(decoderResult.shm)
|
|
22831
|
+
};
|
|
22832
|
+
}
|
|
22833
|
+
function mapMotion(motionResult) {
|
|
22834
|
+
if (motionResult === null) return null;
|
|
22835
|
+
return {
|
|
22836
|
+
enabled: motionResult.enabled,
|
|
22837
|
+
fps: motionResult.fps
|
|
22838
|
+
};
|
|
22839
|
+
}
|
|
22840
|
+
function mapProvisioning(p) {
|
|
22841
|
+
if (p.error !== void 0) return {
|
|
22842
|
+
state: p.state,
|
|
22843
|
+
error: p.error
|
|
22844
|
+
};
|
|
22845
|
+
return { state: p.state };
|
|
22846
|
+
}
|
|
22847
|
+
function mapDetection(detectionResult) {
|
|
22848
|
+
if (detectionResult === null) return null;
|
|
22849
|
+
const phase = detectionResult.phase;
|
|
22850
|
+
return {
|
|
22851
|
+
nodeId: detectionResult.nodeId,
|
|
22852
|
+
engine: {
|
|
22853
|
+
backend: detectionResult.engine.backend,
|
|
22854
|
+
device: detectionResult.engine.device
|
|
22855
|
+
},
|
|
22856
|
+
phase,
|
|
22857
|
+
configuredFps: detectionResult.configuredFps,
|
|
22858
|
+
actualFps: detectionResult.actualFps,
|
|
22859
|
+
queueDepth: detectionResult.queueDepth,
|
|
22860
|
+
avgInferenceMs: detectionResult.avgInferenceMs,
|
|
22861
|
+
provisioning: mapProvisioning(detectionResult.provisioning)
|
|
22862
|
+
};
|
|
22863
|
+
}
|
|
22864
|
+
function mapAudio(audioResult) {
|
|
22865
|
+
if (audioResult === null) return null;
|
|
22866
|
+
return {
|
|
22867
|
+
nodeId: audioResult.nodeId,
|
|
22868
|
+
enabled: audioResult.enabled
|
|
22869
|
+
};
|
|
22870
|
+
}
|
|
22871
|
+
function mapRecording(recordingResult) {
|
|
22872
|
+
if (recordingResult === null) return null;
|
|
22873
|
+
return {
|
|
22874
|
+
mode: recordingResult.mode,
|
|
22875
|
+
active: recordingResult.active,
|
|
22876
|
+
storageBytes: recordingResult.storageBytes
|
|
22877
|
+
};
|
|
22878
|
+
}
|
|
22879
|
+
/**
|
|
22880
|
+
* Pure function that composes a `CameraStatus` from per-stage fetch results.
|
|
22881
|
+
*
|
|
22882
|
+
* - `assignment` is always built from orchestrator-local data (never null).
|
|
22883
|
+
* - `source` always present: defaults to `{ streams: [] }` when sourceResult is null.
|
|
22884
|
+
* - Every other block is null when its stage result is null (graceful degradation).
|
|
22885
|
+
* - `fetchedAt` is stamped exactly as provided — never calls `Date.now()`.
|
|
22886
|
+
* - No mutation of the input.
|
|
22887
|
+
*/
|
|
22888
|
+
function composeCameraStatus(input) {
|
|
22889
|
+
return {
|
|
22890
|
+
deviceId: input.deviceId,
|
|
22891
|
+
assignment: mapAssignment(input),
|
|
22892
|
+
source: mapSource(input.sourceResult),
|
|
22893
|
+
broker: mapBroker(input.brokerResult),
|
|
22894
|
+
decoder: mapDecoder(input.decoderResult),
|
|
22895
|
+
motion: mapMotion(input.motionResult),
|
|
22896
|
+
detection: mapDetection(input.detectionResult),
|
|
22897
|
+
audio: mapAudio(input.audioResult),
|
|
22898
|
+
recording: mapRecording(input.recordingResult),
|
|
22899
|
+
fetchedAt: input.fetchedAt
|
|
22900
|
+
};
|
|
22901
|
+
}
|
|
22902
|
+
//#endregion
|
|
22503
22903
|
//#region src/index.ts
|
|
22504
22904
|
var PHASE_MODE_VALUES = new Set([
|
|
22505
22905
|
"disabled",
|
|
@@ -23257,8 +23657,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23257
23657
|
});
|
|
23258
23658
|
return {
|
|
23259
23659
|
success: true,
|
|
23260
|
-
|
|
23261
|
-
reason: "capacity"
|
|
23660
|
+
kind: "pending"
|
|
23262
23661
|
};
|
|
23263
23662
|
}
|
|
23264
23663
|
}
|
|
@@ -23268,14 +23667,22 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23268
23667
|
const preferredAgent = typeof pipelinePin === "string" && pipelinePin !== "auto" ? pipelinePin : legacyPreferred;
|
|
23269
23668
|
const decision = balance({
|
|
23270
23669
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
23271
|
-
preferredAgent
|
|
23670
|
+
preferredAgent,
|
|
23671
|
+
nodeCaps: this.buildNodeCaps()
|
|
23272
23672
|
});
|
|
23273
|
-
|
|
23274
|
-
if (
|
|
23673
|
+
if (!decision) throw new Error(`dispatchCamera: no runner available for ${runnerConfig.deviceId}`);
|
|
23674
|
+
if (decision.kind === "pending") {
|
|
23675
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId: runnerConfig.deviceId } });
|
|
23676
|
+
return {
|
|
23677
|
+
success: true,
|
|
23678
|
+
kind: "pending"
|
|
23679
|
+
};
|
|
23680
|
+
}
|
|
23681
|
+
const targetNodeId = decision.agentNodeId;
|
|
23275
23682
|
await this.attachOn(targetNodeId, runnerConfig);
|
|
23276
23683
|
if (targetNodeId === this.localNodeId) this.pipelineWatchdog?.register(this.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId)));
|
|
23277
|
-
const reason = decision
|
|
23278
|
-
const pinned = decision
|
|
23684
|
+
const reason = decision.reason;
|
|
23685
|
+
const pinned = decision.reason === "manual";
|
|
23279
23686
|
this.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, pinned);
|
|
23280
23687
|
const decoderNodeId = await this.resolveDecoderNode(runnerConfig.deviceId, targetNodeId);
|
|
23281
23688
|
this.ctx.logger.info("dispatchCamera", {
|
|
@@ -23285,12 +23692,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23285
23692
|
},
|
|
23286
23693
|
meta: {
|
|
23287
23694
|
reason,
|
|
23288
|
-
score: decision
|
|
23695
|
+
score: decision.score,
|
|
23289
23696
|
decoderNodeId
|
|
23290
23697
|
}
|
|
23291
23698
|
});
|
|
23292
23699
|
return {
|
|
23293
23700
|
success: true,
|
|
23701
|
+
kind: "assigned",
|
|
23294
23702
|
agentNodeId: targetNodeId,
|
|
23295
23703
|
reason
|
|
23296
23704
|
};
|
|
@@ -23364,10 +23772,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23364
23772
|
if (cached) {
|
|
23365
23773
|
const decision = balance({
|
|
23366
23774
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
23367
|
-
preferredAgent: null
|
|
23775
|
+
preferredAgent: null,
|
|
23776
|
+
nodeCaps: this.buildNodeCaps()
|
|
23368
23777
|
});
|
|
23369
|
-
|
|
23370
|
-
if (
|
|
23778
|
+
if (!decision) this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
|
|
23779
|
+
else if (decision.kind === "pending") this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId: input.deviceId } });
|
|
23780
|
+
else {
|
|
23781
|
+
const targetNodeId = decision.agentNodeId;
|
|
23371
23782
|
if (current && current.agentNodeId !== targetNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
23372
23783
|
const msg = errMsg(err);
|
|
23373
23784
|
this.ctx.logger.debug("unassignPipeline detach-old failed", {
|
|
@@ -23376,7 +23787,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23376
23787
|
});
|
|
23377
23788
|
});
|
|
23378
23789
|
if (!current || current.agentNodeId !== targetNodeId) await this.attachOn(targetNodeId, cached);
|
|
23379
|
-
const reason = decision
|
|
23790
|
+
const reason = decision.reason === "manual" ? "capacity" : decision.reason;
|
|
23380
23791
|
this.recordAssignment(input.deviceId, targetNodeId, reason, false);
|
|
23381
23792
|
this.ctx.logger.info("unassignPipeline: re-dispatched via auto", {
|
|
23382
23793
|
tags: {
|
|
@@ -23387,7 +23798,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23387
23798
|
});
|
|
23388
23799
|
return { success: true };
|
|
23389
23800
|
}
|
|
23390
|
-
this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
|
|
23391
23801
|
}
|
|
23392
23802
|
if (current) {
|
|
23393
23803
|
await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
@@ -23405,15 +23815,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23405
23815
|
async rebalance() {
|
|
23406
23816
|
if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
|
|
23407
23817
|
const loads = await this.collectAgentLoad({ onlyEnabled: true });
|
|
23818
|
+
const nodeCaps = this.buildNodeCaps();
|
|
23408
23819
|
let migrated = 0;
|
|
23409
23820
|
for (const [deviceId, config] of this.cameraConfigs) {
|
|
23410
23821
|
const current = this.assignments.get(deviceId);
|
|
23411
23822
|
if (current?.pinned) continue;
|
|
23412
23823
|
const decision = balance({
|
|
23413
23824
|
nodes: loads,
|
|
23414
|
-
preferredAgent: await this.readPreferredAgent(deviceId)
|
|
23825
|
+
preferredAgent: await this.readPreferredAgent(deviceId),
|
|
23826
|
+
nodeCaps
|
|
23415
23827
|
});
|
|
23416
23828
|
if (!decision) continue;
|
|
23829
|
+
if (decision.kind === "pending") {
|
|
23830
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId } });
|
|
23831
|
+
continue;
|
|
23832
|
+
}
|
|
23417
23833
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
23418
23834
|
if (current) await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
|
|
23419
23835
|
const msg = errMsg(err);
|
|
@@ -23551,15 +23967,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23551
23967
|
nodeId
|
|
23552
23968
|
});
|
|
23553
23969
|
}
|
|
23554
|
-
async localRunnerNodeId() {
|
|
23555
|
-
const api = this.ctx.api;
|
|
23556
|
-
if (!api) return null;
|
|
23557
|
-
try {
|
|
23558
|
-
return (await api.pipelineRunner.getLocalLoad.query({ nodeId: this.localNodeId }))?.nodeId ?? this.localNodeId;
|
|
23559
|
-
} catch {
|
|
23560
|
-
return this.localNodeId;
|
|
23561
|
-
}
|
|
23562
|
-
}
|
|
23563
23970
|
/**
|
|
23564
23971
|
* Enumerate every runner-capable node currently known (populated via
|
|
23565
23972
|
* AgentOnline/AgentOffline events). Used to populate the `enabledNodes`
|
|
@@ -23714,6 +24121,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23714
24121
|
return null;
|
|
23715
24122
|
}
|
|
23716
24123
|
}
|
|
24124
|
+
/**
|
|
24125
|
+
* Build a per-node camera cap map from the persisted agent settings.
|
|
24126
|
+
* Returns `null` for nodes where `maxCameras` is null (unlimited).
|
|
24127
|
+
* Used by every `balance()` call so the balancer can honour operator-set
|
|
24128
|
+
* per-node maximums without polling the store on each decision.
|
|
24129
|
+
*/
|
|
24130
|
+
buildNodeCaps() {
|
|
24131
|
+
const blob = this.agentSettingsState.get();
|
|
24132
|
+
const caps = {};
|
|
24133
|
+
for (const [nodeId, settings] of Object.entries(blob)) caps[nodeId] = settings.maxCameras ?? null;
|
|
24134
|
+
return caps;
|
|
24135
|
+
}
|
|
23717
24136
|
async readAudioNodePin(deviceId) {
|
|
23718
24137
|
if (!this.ctx?.settings) return null;
|
|
23719
24138
|
try {
|
|
@@ -23856,13 +24275,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23856
24275
|
for (const { deviceId, config } of affected) {
|
|
23857
24276
|
const decision = balance({
|
|
23858
24277
|
nodes: loads,
|
|
23859
|
-
preferredAgent: null
|
|
24278
|
+
preferredAgent: null,
|
|
24279
|
+
nodeCaps: this.buildNodeCaps()
|
|
23860
24280
|
});
|
|
23861
24281
|
if (!decision) {
|
|
23862
24282
|
this.ctx.logger.error("Failover: no online runner", { tags: { deviceId } });
|
|
23863
24283
|
this.assignments.delete(deviceId);
|
|
23864
24284
|
continue;
|
|
23865
24285
|
}
|
|
24286
|
+
if (decision.kind === "pending") {
|
|
24287
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId } });
|
|
24288
|
+
this.assignments.delete(deviceId);
|
|
24289
|
+
continue;
|
|
24290
|
+
}
|
|
23866
24291
|
try {
|
|
23867
24292
|
await this.attachOn(decision.agentNodeId, config);
|
|
23868
24293
|
this.recordAssignment(deviceId, decision.agentNodeId, "failover", false);
|
|
@@ -24342,7 +24767,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24342
24767
|
}
|
|
24343
24768
|
async setAgentAddonDefaults(input) {
|
|
24344
24769
|
let existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24345
|
-
if (!existing)
|
|
24770
|
+
if (!existing) {
|
|
24771
|
+
await this.seedAgentSettingsFromCatalog(input.agentNodeId);
|
|
24772
|
+
existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24773
|
+
}
|
|
24346
24774
|
if (!existing) await this.writeAgentSettings(input.agentNodeId, { addonDefaults: { ...input.defaults } });
|
|
24347
24775
|
else await this.writeAgentSettings(input.agentNodeId, {
|
|
24348
24776
|
...existing,
|
|
@@ -24380,6 +24808,22 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24380
24808
|
removed: true
|
|
24381
24809
|
};
|
|
24382
24810
|
}
|
|
24811
|
+
async setAgentMaxCameras(input) {
|
|
24812
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24813
|
+
const next = existing ? {
|
|
24814
|
+
...existing,
|
|
24815
|
+
maxCameras: input.maxCameras
|
|
24816
|
+
} : {
|
|
24817
|
+
addonDefaults: {},
|
|
24818
|
+
maxCameras: input.maxCameras
|
|
24819
|
+
};
|
|
24820
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
24821
|
+
this.ctx.logger.info("agentSettings.maxCameras updated", {
|
|
24822
|
+
tags: { nodeId: input.agentNodeId },
|
|
24823
|
+
meta: { maxCameras: input.maxCameras }
|
|
24824
|
+
});
|
|
24825
|
+
return { success: true };
|
|
24826
|
+
}
|
|
24383
24827
|
async getCameraSettings(input) {
|
|
24384
24828
|
return (await this.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
24385
24829
|
}
|
|
@@ -24509,6 +24953,204 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24509
24953
|
this.ctx.logger.info("template deleted", { meta: { templateId: input.id } });
|
|
24510
24954
|
return { success: true };
|
|
24511
24955
|
}
|
|
24956
|
+
/**
|
|
24957
|
+
* Races a promise against a timeout. Returns `null` on timeout OR rejection.
|
|
24958
|
+
* Never throws — individual stage failures become `null` in the aggregate.
|
|
24959
|
+
*
|
|
24960
|
+
* @param p The stage fetch promise.
|
|
24961
|
+
* @param ms Timeout in milliseconds.
|
|
24962
|
+
*/
|
|
24963
|
+
boundedStage(p, ms) {
|
|
24964
|
+
let timer;
|
|
24965
|
+
const timeout = new Promise((resolve) => {
|
|
24966
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
24967
|
+
});
|
|
24968
|
+
return Promise.race([p, timeout]).catch(() => null).finally(() => {
|
|
24969
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
24970
|
+
});
|
|
24971
|
+
}
|
|
24972
|
+
/**
|
|
24973
|
+
* Server-composed aggregated status for a single camera.
|
|
24974
|
+
*
|
|
24975
|
+
* Fans out in parallel (bounded, per-stage graceful degradation) to
|
|
24976
|
+
* broker / decoder / motion / detection / audio / recording source caps
|
|
24977
|
+
* via `ctx.api`. A stage whose source errors or times out becomes `null`
|
|
24978
|
+
* in the returned payload — one slow agent never breaks the whole call.
|
|
24979
|
+
*/
|
|
24980
|
+
async getCameraStatus(input) {
|
|
24981
|
+
const { deviceId } = input;
|
|
24982
|
+
const api = this.ctx.api;
|
|
24983
|
+
const STAGE_TIMEOUT_MS = 3e3;
|
|
24984
|
+
const pipelineAssignment = this.assignments.get(deviceId) ?? null;
|
|
24985
|
+
const detectionNodeId = pipelineAssignment?.agentNodeId ?? null;
|
|
24986
|
+
const decoderPinRaw = (api ? await this.ctx.settings?.readDeviceStore(deviceId).catch(() => ({})) ?? {} : {})["decoderNodeId"];
|
|
24987
|
+
const decoderPinned = typeof decoderPinRaw === "string" && decoderPinRaw !== "auto";
|
|
24988
|
+
const decoderNodeId = detectionNodeId ? await this.resolveDecoderNode(deviceId, detectionNodeId).catch(() => null) : null;
|
|
24989
|
+
const audioAssignment = this.audioAssignments.get(deviceId) ?? null;
|
|
24990
|
+
const audioNodeId = audioAssignment?.nodeId ?? null;
|
|
24991
|
+
const audioPinned = audioAssignment?.pinned ?? false;
|
|
24992
|
+
const pinned = {
|
|
24993
|
+
detection: pipelineAssignment?.pinned ?? false,
|
|
24994
|
+
decoder: decoderPinned,
|
|
24995
|
+
audio: audioPinned
|
|
24996
|
+
};
|
|
24997
|
+
const reasons = {
|
|
24998
|
+
detection: pipelineAssignment?.reason,
|
|
24999
|
+
decoder: decoderPinned ? "manual" : "co-located",
|
|
25000
|
+
audio: audioPinned ? "manual" : void 0
|
|
25001
|
+
};
|
|
25002
|
+
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query() : null;
|
|
25003
|
+
const sourceFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then((slots) => {
|
|
25004
|
+
return { streams: slots.filter((s) => s.deviceId === deviceId).map((s) => ({
|
|
25005
|
+
camStreamId: s.sourceCamStreamId ?? s.brokerId,
|
|
25006
|
+
codec: s.codec ?? "",
|
|
25007
|
+
width: s.resolution?.width ?? 0,
|
|
25008
|
+
height: s.resolution?.height ?? 0,
|
|
25009
|
+
fps: 0,
|
|
25010
|
+
kind: s.profile
|
|
25011
|
+
})) };
|
|
25012
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25013
|
+
const WEBRTC_KINDS = new Set([
|
|
25014
|
+
"webrtc-browser",
|
|
25015
|
+
"webrtc-mobile",
|
|
25016
|
+
"webrtc-whep"
|
|
25017
|
+
]);
|
|
25018
|
+
const brokerFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then(async (slots) => {
|
|
25019
|
+
const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
|
|
25020
|
+
if (deviceSlots.length === 0) return {
|
|
25021
|
+
profiles: [],
|
|
25022
|
+
webrtcSessions: 0,
|
|
25023
|
+
rtspRestream: false
|
|
25024
|
+
};
|
|
25025
|
+
const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
|
|
25026
|
+
return {
|
|
25027
|
+
slot,
|
|
25028
|
+
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }).catch(() => null),
|
|
25029
|
+
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }).catch(() => null)
|
|
25030
|
+
};
|
|
25031
|
+
})), api.streamBroker.getAllRtspEntries.query({}).catch(() => null)]);
|
|
25032
|
+
return {
|
|
25033
|
+
profiles: statsAndClients.map(({ slot, stats, clients }) => ({
|
|
25034
|
+
profile: slot.profile,
|
|
25035
|
+
status: slot.status,
|
|
25036
|
+
codec: stats?.codec ?? slot.codec ?? "",
|
|
25037
|
+
width: slot.resolution?.width ?? 0,
|
|
25038
|
+
height: slot.resolution?.height ?? 0,
|
|
25039
|
+
subscribers: clients?.encodedSubscribers ?? 0,
|
|
25040
|
+
inFps: stats?.inputFps ?? 0,
|
|
25041
|
+
outFps: stats?.decodeFps ?? 0
|
|
25042
|
+
})),
|
|
25043
|
+
webrtcSessions: statsAndClients.reduce((total, { clients }) => {
|
|
25044
|
+
if (!clients) return total;
|
|
25045
|
+
return total + clients.encoded.filter((c) => WEBRTC_KINDS.has(c.attribution.kind)).length;
|
|
25046
|
+
}, 0),
|
|
25047
|
+
rtspRestream: rtspEntry?.some((e) => {
|
|
25048
|
+
return e.brokerId.split("/")[0] === String(deviceId) && e.enabled;
|
|
25049
|
+
}) ?? false
|
|
25050
|
+
};
|
|
25051
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25052
|
+
const decoderFetch = decoderNodeId ? Promise.resolve({
|
|
25053
|
+
nodeId: decoderNodeId,
|
|
25054
|
+
formats: [],
|
|
25055
|
+
sessionCount: 0,
|
|
25056
|
+
shm: {
|
|
25057
|
+
framesWritten: 0,
|
|
25058
|
+
getFrameHits: 0,
|
|
25059
|
+
getFrameMisses: 0,
|
|
25060
|
+
budgetMb: 0
|
|
25061
|
+
}
|
|
25062
|
+
}) : Promise.resolve(null);
|
|
25063
|
+
const motionResult = (() => {
|
|
25064
|
+
const config = this.cameraConfigs.get(deviceId);
|
|
25065
|
+
if (!config) return null;
|
|
25066
|
+
return {
|
|
25067
|
+
enabled: config.motionSources.includes("analyzer") || config.motionSources.includes("onboard"),
|
|
25068
|
+
fps: config.motionFps
|
|
25069
|
+
};
|
|
25070
|
+
})();
|
|
25071
|
+
const detectionFetch = api && detectionNodeId ? this.boundedStage(Promise.all([api.pipelineExecutor.getEngineProvisioning.query({ nodeId: detectionNodeId }).catch(() => null), api.pipelineExecutor.getSelectedEngine.query({ nodeId: detectionNodeId }).catch(() => null)]).then(async ([provisioning, engine]) => {
|
|
25072
|
+
const metrics = await api.pipelineRunner.getCameraMetrics.query({
|
|
25073
|
+
deviceId,
|
|
25074
|
+
nodeId: detectionNodeId
|
|
25075
|
+
}).catch(() => null);
|
|
25076
|
+
const phase = (() => {
|
|
25077
|
+
const p = metrics?.phase;
|
|
25078
|
+
if (p === "active") return "active";
|
|
25079
|
+
if (p === "idle") return "idle";
|
|
25080
|
+
return "watching";
|
|
25081
|
+
})();
|
|
25082
|
+
return {
|
|
25083
|
+
nodeId: detectionNodeId,
|
|
25084
|
+
engine: {
|
|
25085
|
+
backend: engine?.backend ?? "",
|
|
25086
|
+
device: engine?.device ?? ""
|
|
25087
|
+
},
|
|
25088
|
+
phase,
|
|
25089
|
+
configuredFps: metrics?.configuredFps ?? 0,
|
|
25090
|
+
actualFps: metrics?.actualFps ?? 0,
|
|
25091
|
+
queueDepth: metrics?.queueDepth ?? 0,
|
|
25092
|
+
avgInferenceMs: metrics?.avgInferenceTimeMs ?? 0,
|
|
25093
|
+
provisioning: {
|
|
25094
|
+
state: provisioning?.state ?? "idle",
|
|
25095
|
+
...provisioning?.error !== void 0 ? { error: provisioning.error } : {}
|
|
25096
|
+
}
|
|
25097
|
+
};
|
|
25098
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25099
|
+
const audioResult = audioNodeId ? {
|
|
25100
|
+
nodeId: audioNodeId,
|
|
25101
|
+
enabled: true
|
|
25102
|
+
} : null;
|
|
25103
|
+
function isRecordingStatus(v) {
|
|
25104
|
+
return v !== null && typeof v === "object" && "activeMode" in v && (v.activeMode === "off" || v.activeMode === "continuous" || v.activeMode === "events") && "enabled" in v && typeof v.enabled === "boolean" && "storageBytes" in v && typeof v.storageBytes === "number";
|
|
25105
|
+
}
|
|
25106
|
+
const recordingFetch = api ? this.boundedStage(api.recording.getStatus.query({ deviceId }).then((rawStatus) => {
|
|
25107
|
+
if (!isRecordingStatus(rawStatus)) return null;
|
|
25108
|
+
return {
|
|
25109
|
+
mode: rawStatus.activeMode,
|
|
25110
|
+
active: rawStatus.enabled && rawStatus.activeMode !== "off",
|
|
25111
|
+
storageBytes: rawStatus.storageBytes
|
|
25112
|
+
};
|
|
25113
|
+
}).catch(() => null), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25114
|
+
const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult] = await Promise.all([
|
|
25115
|
+
sourceFetch,
|
|
25116
|
+
brokerFetch,
|
|
25117
|
+
decoderFetch,
|
|
25118
|
+
detectionFetch,
|
|
25119
|
+
recordingFetch
|
|
25120
|
+
]);
|
|
25121
|
+
return composeCameraStatus({
|
|
25122
|
+
deviceId,
|
|
25123
|
+
fetchedAt: Date.now(),
|
|
25124
|
+
detectionNodeId,
|
|
25125
|
+
decoderNodeId,
|
|
25126
|
+
audioNodeId,
|
|
25127
|
+
pinned,
|
|
25128
|
+
reasons,
|
|
25129
|
+
sourceResult,
|
|
25130
|
+
brokerResult,
|
|
25131
|
+
decoderResult,
|
|
25132
|
+
motionResult,
|
|
25133
|
+
detectionResult,
|
|
25134
|
+
audioResult,
|
|
25135
|
+
recordingResult
|
|
25136
|
+
});
|
|
25137
|
+
}
|
|
25138
|
+
/**
|
|
25139
|
+
* Server-composed aggregated status for multiple cameras in one call.
|
|
25140
|
+
*
|
|
25141
|
+
* `deviceIds` defaults to all cameras currently tracked by the
|
|
25142
|
+
* orchestrator's assignment map when omitted.
|
|
25143
|
+
*
|
|
25144
|
+
* v1: `Promise.all` over per-device composition (no concurrency cap).
|
|
25145
|
+
* Note: for large fleets (hundreds of cameras) this may fan out many
|
|
25146
|
+
* parallel calls. A concurrency limiter (p-limit / semaphore) should be
|
|
25147
|
+
* added if latency measurements show it's necessary — deliberately
|
|
25148
|
+
* deferred per the YAGNI constraint in the spec.
|
|
25149
|
+
*/
|
|
25150
|
+
async getCameraStatuses(input) {
|
|
25151
|
+
const ids = input.deviceIds !== void 0 && input.deviceIds.length > 0 ? input.deviceIds : [...this.assignments.keys()];
|
|
25152
|
+
return Promise.all(ids.map((deviceId) => this.getCameraStatus({ deviceId })));
|
|
25153
|
+
}
|
|
24512
25154
|
/** Read the templates map from the addon store via the durable handle. */
|
|
24513
25155
|
async readTemplatesMap() {
|
|
24514
25156
|
const raw = await this.templatesState.get();
|
|
@@ -24539,7 +25181,11 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24539
25181
|
return;
|
|
24540
25182
|
}
|
|
24541
25183
|
const all = await this.readAgentSettingsMap();
|
|
24542
|
-
all[nodeId]
|
|
25184
|
+
const existing = all[nodeId];
|
|
25185
|
+
all[nodeId] = {
|
|
25186
|
+
addonDefaults: settings.addonDefaults,
|
|
25187
|
+
maxCameras: settings.maxCameras !== void 0 ? settings.maxCameras ?? null : existing?.maxCameras ?? null
|
|
25188
|
+
};
|
|
24543
25189
|
await this.agentSettingsState.set(all);
|
|
24544
25190
|
}
|
|
24545
25191
|
/** Read the `cameraSettings` map (keyed on `deviceId` as string) via the durable handle. */
|
|
@@ -24749,12 +25395,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24749
25395
|
}
|
|
24750
25396
|
let agent = (await this.readAgentSettingsMap())[nodeId];
|
|
24751
25397
|
if (!agent || Object.keys(agent.addonDefaults ?? {}).length === 0) {
|
|
24752
|
-
|
|
24753
|
-
if (!seeded) {
|
|
25398
|
+
if (!await this.seedAgentSettingsFromCatalog(nodeId)) {
|
|
24754
25399
|
await sleep$1(2e3);
|
|
24755
25400
|
continue;
|
|
24756
25401
|
}
|
|
24757
|
-
agent =
|
|
25402
|
+
agent = (await this.readAgentSettingsMap())[nodeId];
|
|
25403
|
+
}
|
|
25404
|
+
if (!agent) {
|
|
25405
|
+
await sleep$1(2e3);
|
|
25406
|
+
continue;
|
|
24758
25407
|
}
|
|
24759
25408
|
const engine = await this.readDetectionPipelineEngine(nodeId) ?? catalog.selectedEngine;
|
|
24760
25409
|
return {
|
|
@@ -25661,7 +26310,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25661
26310
|
};
|
|
25662
26311
|
let dispatchedNodeId = null;
|
|
25663
26312
|
try {
|
|
25664
|
-
|
|
26313
|
+
const result = await this.dispatchCamera(runnerConfig);
|
|
26314
|
+
if (result.kind === "assigned") dispatchedNodeId = result.agentNodeId;
|
|
25665
26315
|
} catch (err) {
|
|
25666
26316
|
const msg = errMsg(err);
|
|
25667
26317
|
log.error("dispatchCamera failed", { meta: { error: msg } });
|