@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.js
CHANGED
|
@@ -4985,6 +4985,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4985
4985
|
*/
|
|
4986
4986
|
EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
|
|
4987
4987
|
/**
|
|
4988
|
+
* Per-node detection-engine runtime-provisioning transition. Emitted by
|
|
4989
|
+
* the detection-pipeline provider on every state change of its lazy
|
|
4990
|
+
* engine-provisioning machine (idle → installing → verifying → ready,
|
|
4991
|
+
* or → failed with a `nextRetryAt`). Payload is the
|
|
4992
|
+
* `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
|
|
4993
|
+
* node. The Pipeline page subscribes to drive a live "installing
|
|
4994
|
+
* OpenVINO… / ready" indicator per node without polling
|
|
4995
|
+
* `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
|
|
4996
|
+
* also reads the cap snapshot on mount / reconnect. Phase 2.
|
|
4997
|
+
*/
|
|
4998
|
+
EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
|
|
4999
|
+
/**
|
|
4988
5000
|
* Cluster topology snapshot. Carries the same payload returned by
|
|
4989
5001
|
* `nodes.topology` (every reachable node + addons + processes).
|
|
4990
5002
|
* Emitted by the hub on any agent / addon lifecycle change
|
|
@@ -9566,6 +9578,24 @@ var DetectorOutputSchema = object({
|
|
|
9566
9578
|
inferenceMs: number(),
|
|
9567
9579
|
modelId: string()
|
|
9568
9580
|
});
|
|
9581
|
+
var EngineProvisioningSchema = object({
|
|
9582
|
+
runtimeId: _enum([
|
|
9583
|
+
"onnx",
|
|
9584
|
+
"openvino",
|
|
9585
|
+
"coreml"
|
|
9586
|
+
]).nullable(),
|
|
9587
|
+
device: string().nullable(),
|
|
9588
|
+
state: _enum([
|
|
9589
|
+
"idle",
|
|
9590
|
+
"installing",
|
|
9591
|
+
"verifying",
|
|
9592
|
+
"ready",
|
|
9593
|
+
"failed"
|
|
9594
|
+
]),
|
|
9595
|
+
progress: number().optional(),
|
|
9596
|
+
error: string().optional(),
|
|
9597
|
+
nextRetryAt: number().optional()
|
|
9598
|
+
});
|
|
9569
9599
|
var PipelineStepInputSchema = lazy(() => object({
|
|
9570
9600
|
addonId: string(),
|
|
9571
9601
|
modelId: string(),
|
|
@@ -9665,6 +9695,15 @@ var pipelineExecutorCapability = {
|
|
|
9665
9695
|
kind: "mutation",
|
|
9666
9696
|
auth: "admin"
|
|
9667
9697
|
}),
|
|
9698
|
+
/**
|
|
9699
|
+
* Per-node detection-engine provisioning snapshot. Returns the live
|
|
9700
|
+
* state of the lazy runtime-provisioning machine on `nodeId`
|
|
9701
|
+
* (idle / installing / verifying / ready / failed). The UI pairs this
|
|
9702
|
+
* one-shot query with the `pipeline.engine-provisioning` live event
|
|
9703
|
+
* (emitted on every transition) to drive a per-node "engine ready?"
|
|
9704
|
+
* indicator without polling. Phase 2.
|
|
9705
|
+
*/
|
|
9706
|
+
getEngineProvisioning: method(object({ nodeId: string() }), EngineProvisioningSchema),
|
|
9668
9707
|
getVideoPipelineSteps: method(_void(), record(string(), object({
|
|
9669
9708
|
modelId: string(),
|
|
9670
9709
|
settings: record(string(), unknown()).readonly()
|
|
@@ -13541,7 +13580,10 @@ var AgentAddonConfigSchema = object({
|
|
|
13541
13580
|
modelId: string(),
|
|
13542
13581
|
settings: record(string(), unknown()).readonly()
|
|
13543
13582
|
});
|
|
13544
|
-
var AgentPipelineSettingsSchema = object({
|
|
13583
|
+
var AgentPipelineSettingsSchema = object({
|
|
13584
|
+
addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
|
|
13585
|
+
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
13586
|
+
});
|
|
13545
13587
|
var CameraPipelineForAgentSchema = object({
|
|
13546
13588
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
13547
13589
|
audio: object({
|
|
@@ -13643,6 +13685,133 @@ var GlobalMetricsSchema = object({
|
|
|
13643
13685
|
* capability providers.
|
|
13644
13686
|
*/
|
|
13645
13687
|
var CapabilityBindingsSchema = record(string(), string());
|
|
13688
|
+
/** Source block — always present; derives from the stream catalog. */
|
|
13689
|
+
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
13690
|
+
camStreamId: string(),
|
|
13691
|
+
codec: string(),
|
|
13692
|
+
width: number(),
|
|
13693
|
+
height: number(),
|
|
13694
|
+
fps: number(),
|
|
13695
|
+
kind: string()
|
|
13696
|
+
})).readonly() });
|
|
13697
|
+
/** Assignment block — always present (orchestrator-local, no remote call). */
|
|
13698
|
+
var CameraAssignmentStatusSchema = object({
|
|
13699
|
+
detectionNodeId: string().nullable(),
|
|
13700
|
+
decoderNodeId: string().nullable(),
|
|
13701
|
+
audioNodeId: string().nullable(),
|
|
13702
|
+
pinned: object({
|
|
13703
|
+
detection: boolean(),
|
|
13704
|
+
decoder: boolean(),
|
|
13705
|
+
audio: boolean()
|
|
13706
|
+
}),
|
|
13707
|
+
reasons: object({
|
|
13708
|
+
detection: string().optional(),
|
|
13709
|
+
decoder: string().optional(),
|
|
13710
|
+
audio: string().optional()
|
|
13711
|
+
})
|
|
13712
|
+
});
|
|
13713
|
+
/** Broker block — null when the broker stage is unreachable or inactive. */
|
|
13714
|
+
var CameraBrokerStatusSchema = object({
|
|
13715
|
+
profiles: array(object({
|
|
13716
|
+
profile: string(),
|
|
13717
|
+
status: string(),
|
|
13718
|
+
codec: string(),
|
|
13719
|
+
width: number(),
|
|
13720
|
+
height: number(),
|
|
13721
|
+
subscribers: number(),
|
|
13722
|
+
inFps: number(),
|
|
13723
|
+
outFps: number()
|
|
13724
|
+
})).readonly(),
|
|
13725
|
+
webrtcSessions: number(),
|
|
13726
|
+
rtspRestream: boolean()
|
|
13727
|
+
});
|
|
13728
|
+
/** Shared-memory ring statistics within the decoder block. */
|
|
13729
|
+
var CameraDecoderShmSchema = object({
|
|
13730
|
+
framesWritten: number(),
|
|
13731
|
+
getFrameHits: number(),
|
|
13732
|
+
getFrameMisses: number(),
|
|
13733
|
+
budgetMb: number()
|
|
13734
|
+
});
|
|
13735
|
+
/** Decoder block — null when the decoder stage is unreachable or inactive. */
|
|
13736
|
+
var CameraDecoderStatusSchema = object({
|
|
13737
|
+
nodeId: string(),
|
|
13738
|
+
formats: array(string()).readonly(),
|
|
13739
|
+
sessionCount: number(),
|
|
13740
|
+
shm: CameraDecoderShmSchema
|
|
13741
|
+
});
|
|
13742
|
+
/** Motion block — null when motion detection is not active for this device. */
|
|
13743
|
+
var CameraMotionStatusSchema = object({
|
|
13744
|
+
enabled: boolean(),
|
|
13745
|
+
fps: number()
|
|
13746
|
+
});
|
|
13747
|
+
/** Detection provisioning sub-block. */
|
|
13748
|
+
var CameraDetectionProvisioningSchema = object({
|
|
13749
|
+
state: _enum([
|
|
13750
|
+
"idle",
|
|
13751
|
+
"installing",
|
|
13752
|
+
"verifying",
|
|
13753
|
+
"ready",
|
|
13754
|
+
"failed"
|
|
13755
|
+
]),
|
|
13756
|
+
error: string().optional()
|
|
13757
|
+
});
|
|
13758
|
+
/** Detection phase — derived from the runner's engine phase. */
|
|
13759
|
+
var CameraDetectionPhaseSchema = _enum([
|
|
13760
|
+
"idle",
|
|
13761
|
+
"watching",
|
|
13762
|
+
"active"
|
|
13763
|
+
]);
|
|
13764
|
+
/** Detection block — null when no detection node is assigned or reachable. */
|
|
13765
|
+
var CameraDetectionStatusSchema = object({
|
|
13766
|
+
nodeId: string(),
|
|
13767
|
+
engine: object({
|
|
13768
|
+
backend: string(),
|
|
13769
|
+
device: string()
|
|
13770
|
+
}),
|
|
13771
|
+
phase: CameraDetectionPhaseSchema,
|
|
13772
|
+
configuredFps: number(),
|
|
13773
|
+
actualFps: number(),
|
|
13774
|
+
queueDepth: number(),
|
|
13775
|
+
avgInferenceMs: number(),
|
|
13776
|
+
provisioning: CameraDetectionProvisioningSchema
|
|
13777
|
+
});
|
|
13778
|
+
/** Audio block — null when no audio node is assigned or reachable. */
|
|
13779
|
+
var CameraAudioStatusSchema = object({
|
|
13780
|
+
nodeId: string(),
|
|
13781
|
+
enabled: boolean()
|
|
13782
|
+
});
|
|
13783
|
+
/** Recording block — null when no recording cap is active for this device. */
|
|
13784
|
+
var CameraRecordingStatusSchema = object({
|
|
13785
|
+
mode: _enum([
|
|
13786
|
+
"off",
|
|
13787
|
+
"continuous",
|
|
13788
|
+
"events"
|
|
13789
|
+
]),
|
|
13790
|
+
active: boolean(),
|
|
13791
|
+
storageBytes: number()
|
|
13792
|
+
});
|
|
13793
|
+
/**
|
|
13794
|
+
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
13795
|
+
*
|
|
13796
|
+
* The `assignment` and `source` blocks are always present.
|
|
13797
|
+
* Every other block is `null` when the stage is inactive or unreachable
|
|
13798
|
+
* during the bounded parallel fan-out in the orchestrator implementation.
|
|
13799
|
+
*
|
|
13800
|
+
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
13801
|
+
*/
|
|
13802
|
+
var CameraStatusSchema = object({
|
|
13803
|
+
deviceId: number(),
|
|
13804
|
+
assignment: CameraAssignmentStatusSchema,
|
|
13805
|
+
source: CameraSourceStatusSchema,
|
|
13806
|
+
broker: CameraBrokerStatusSchema.nullable(),
|
|
13807
|
+
decoder: CameraDecoderStatusSchema.nullable(),
|
|
13808
|
+
motion: CameraMotionStatusSchema.nullable(),
|
|
13809
|
+
detection: CameraDetectionStatusSchema.nullable(),
|
|
13810
|
+
audio: CameraAudioStatusSchema.nullable(),
|
|
13811
|
+
recording: CameraRecordingStatusSchema.nullable(),
|
|
13812
|
+
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
13813
|
+
fetchedAt: number()
|
|
13814
|
+
});
|
|
13646
13815
|
/**
|
|
13647
13816
|
* Pipeline Orchestrator capability — global load balancer + camera dispatcher.
|
|
13648
13817
|
*
|
|
@@ -13819,6 +13988,25 @@ var pipelineOrchestratorCapability = {
|
|
|
13819
13988
|
kind: "mutation",
|
|
13820
13989
|
auth: "admin"
|
|
13821
13990
|
}),
|
|
13991
|
+
/**
|
|
13992
|
+
* Set the per-node camera cap for one agent.
|
|
13993
|
+
*
|
|
13994
|
+
* `maxCameras: 0` and `maxCameras: null` are both treated as unlimited
|
|
13995
|
+
* by the load balancer (0 is stored as-is; the balancer treats ≤0 as
|
|
13996
|
+
* unlimited alongside null). Pass `null` to clear an existing cap.
|
|
13997
|
+
* Note: the admin UI normalises 0→null before calling this method, so
|
|
13998
|
+
* `maxCameras: 0` only reaches the store via direct SDK or CLI calls.
|
|
13999
|
+
* Changes take effect immediately on the next dispatch cycle — cameras
|
|
14000
|
+
* currently assigned over-cap are left in place (no forced eviction),
|
|
14001
|
+
* but new assignments obey the updated cap.
|
|
14002
|
+
*/
|
|
14003
|
+
setAgentMaxCameras: method(object({
|
|
14004
|
+
agentNodeId: string(),
|
|
14005
|
+
maxCameras: number().int().nonnegative().nullable()
|
|
14006
|
+
}), object({ success: literal(true) }), {
|
|
14007
|
+
kind: "mutation",
|
|
14008
|
+
auth: "admin"
|
|
14009
|
+
}),
|
|
13822
14010
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
13823
14011
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
13824
14012
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -13859,6 +14047,29 @@ var pipelineOrchestratorCapability = {
|
|
|
13859
14047
|
deviceId: number(),
|
|
13860
14048
|
agentNodeId: string().optional()
|
|
13861
14049
|
}), CameraPipelineConfigSchema),
|
|
14050
|
+
/**
|
|
14051
|
+
* Server-composed aggregated status for a single camera.
|
|
14052
|
+
*
|
|
14053
|
+
* Fans out in parallel (bounded, per-stage graceful degradation) to
|
|
14054
|
+
* broker / decoder / motion / detection / audio / recording source caps
|
|
14055
|
+
* via `ctx.api`. A stage whose source errors or times out becomes `null`
|
|
14056
|
+
* in the returned payload — one slow agent never breaks the whole call.
|
|
14057
|
+
*
|
|
14058
|
+
* Intended for "on open / on camera select" snapshots. Live deltas
|
|
14059
|
+
* keep arriving from the existing ~1Hz events the UI already subscribes to.
|
|
14060
|
+
*/
|
|
14061
|
+
getCameraStatus: method(object({ deviceId: number() }), CameraStatusSchema),
|
|
14062
|
+
/**
|
|
14063
|
+
* Server-composed aggregated status for multiple cameras in one call.
|
|
14064
|
+
*
|
|
14065
|
+
* `deviceIds` defaults to all cameras currently tracked by the
|
|
14066
|
+
* orchestrator's assignment map when omitted.
|
|
14067
|
+
*
|
|
14068
|
+
* Runs per-device composition in parallel (bounded). Use this to
|
|
14069
|
+
* populate the cluster assignments table and the pipeline flow overview
|
|
14070
|
+
* rail without issuing N parallel browser round-trips.
|
|
14071
|
+
*/
|
|
14072
|
+
getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
|
|
13862
14073
|
/** List every template the operator has saved. */
|
|
13863
14074
|
listTemplates: method(_void(), array(PipelineTemplateSchema).readonly()),
|
|
13864
14075
|
/** Create a new named preset from a given CameraPipelineConfig. */
|
|
@@ -19698,6 +19909,12 @@ Object.freeze({
|
|
|
19698
19909
|
addonId: null,
|
|
19699
19910
|
access: "view"
|
|
19700
19911
|
},
|
|
19912
|
+
"pipelineExecutor.getEngineProvisioning": {
|
|
19913
|
+
capName: "pipeline-executor",
|
|
19914
|
+
capScope: "system",
|
|
19915
|
+
addonId: null,
|
|
19916
|
+
access: "view"
|
|
19917
|
+
},
|
|
19701
19918
|
"pipelineExecutor.getGlobalPipelineConfig": {
|
|
19702
19919
|
capName: "pipeline-executor",
|
|
19703
19920
|
capScope: "system",
|
|
@@ -19902,6 +20119,18 @@ Object.freeze({
|
|
|
19902
20119
|
addonId: null,
|
|
19903
20120
|
access: "view"
|
|
19904
20121
|
},
|
|
20122
|
+
"pipelineOrchestrator.getCameraStatus": {
|
|
20123
|
+
capName: "pipeline-orchestrator",
|
|
20124
|
+
capScope: "system",
|
|
20125
|
+
addonId: null,
|
|
20126
|
+
access: "view"
|
|
20127
|
+
},
|
|
20128
|
+
"pipelineOrchestrator.getCameraStatuses": {
|
|
20129
|
+
capName: "pipeline-orchestrator",
|
|
20130
|
+
capScope: "system",
|
|
20131
|
+
addonId: null,
|
|
20132
|
+
access: "view"
|
|
20133
|
+
},
|
|
19905
20134
|
"pipelineOrchestrator.getCameraStepOverrides": {
|
|
19906
20135
|
capName: "pipeline-orchestrator",
|
|
19907
20136
|
capScope: "system",
|
|
@@ -19986,6 +20215,12 @@ Object.freeze({
|
|
|
19986
20215
|
addonId: null,
|
|
19987
20216
|
access: "create"
|
|
19988
20217
|
},
|
|
20218
|
+
"pipelineOrchestrator.setAgentMaxCameras": {
|
|
20219
|
+
capName: "pipeline-orchestrator",
|
|
20220
|
+
capScope: "system",
|
|
20221
|
+
addonId: null,
|
|
20222
|
+
access: "create"
|
|
20223
|
+
},
|
|
19989
20224
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
19990
20225
|
capName: "pipeline-orchestrator",
|
|
19991
20226
|
capScope: "system",
|
|
@@ -22016,7 +22251,10 @@ var StoredAgentAddonConfigSchema = object({
|
|
|
22016
22251
|
modelId: string(),
|
|
22017
22252
|
settings: record(string(), unknown()).readonly()
|
|
22018
22253
|
});
|
|
22019
|
-
var StoredAgentPipelineSettingsSchema = object({
|
|
22254
|
+
var StoredAgentPipelineSettingsSchema = object({
|
|
22255
|
+
addonDefaults: record(string(), StoredAgentAddonConfigSchema).readonly(),
|
|
22256
|
+
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
22257
|
+
});
|
|
22020
22258
|
var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
|
|
22021
22259
|
var StoredCameraStepOverridePatchSchema = object({
|
|
22022
22260
|
enabled: boolean().optional(),
|
|
@@ -22061,10 +22299,26 @@ function computeCapacityScore(load) {
|
|
|
22061
22299
|
return load.attachedCameras * Math.max(load.avgInferenceFps, 0) + Math.max(load.queueDepthTotal, 0);
|
|
22062
22300
|
}
|
|
22063
22301
|
/**
|
|
22302
|
+
* Returns true when the node has remaining capacity.
|
|
22303
|
+
* A node is eligible iff `cap` is unlimited (null/absent/<=0) OR
|
|
22304
|
+
* its `attachedCameras` count is strictly less than the cap.
|
|
22305
|
+
* Pins count toward the cap via `attachedCameras`.
|
|
22306
|
+
*/
|
|
22307
|
+
function isEligible(node, caps) {
|
|
22308
|
+
const cap = caps?.[node.nodeId];
|
|
22309
|
+
if (cap === null || cap === void 0 || cap <= 0) return true;
|
|
22310
|
+
return node.attachedCameras < cap;
|
|
22311
|
+
}
|
|
22312
|
+
/**
|
|
22064
22313
|
* Run the two-level camera balancer.
|
|
22065
22314
|
*
|
|
22066
|
-
* L1 (manual affinity): if `preferredAgent` names an online node
|
|
22067
|
-
*
|
|
22315
|
+
* L1 (manual affinity): if `preferredAgent` names an online node AND that
|
|
22316
|
+
* node is under its `maxCameras` cap, return it. If the node is online but at
|
|
22317
|
+
* or over cap, return `{kind:'pending'}` — a pinned camera is never silently
|
|
22318
|
+
* over-assigned.
|
|
22319
|
+
*
|
|
22320
|
+
* L2 (capacity): filter to eligible nodes and pick the lowest capacity score.
|
|
22321
|
+
* If all nodes are at/over cap, return `{kind:'pending'}`.
|
|
22068
22322
|
*
|
|
22069
22323
|
* Returns `null` when no runners are online. The orchestrator decides how to
|
|
22070
22324
|
* react — typically by logging and deferring the assignment until a runner
|
|
@@ -22073,19 +22327,32 @@ function computeCapacityScore(load) {
|
|
|
22073
22327
|
function balance(input) {
|
|
22074
22328
|
const online = input.nodes.filter((n) => n.nodeId.length > 0);
|
|
22075
22329
|
if (online.length === 0) return null;
|
|
22330
|
+
const eligible = online.filter((n) => isEligible(n, input.nodeCaps));
|
|
22076
22331
|
if (input.preferredAgent) {
|
|
22077
|
-
const
|
|
22078
|
-
if (
|
|
22079
|
-
|
|
22080
|
-
|
|
22081
|
-
|
|
22082
|
-
|
|
22332
|
+
const pinnedOnline = online.find((n) => n.nodeId === input.preferredAgent);
|
|
22333
|
+
if (pinnedOnline) {
|
|
22334
|
+
if (eligible.some((n) => n.nodeId === pinnedOnline.nodeId)) return {
|
|
22335
|
+
kind: "assigned",
|
|
22336
|
+
agentNodeId: pinnedOnline.nodeId,
|
|
22337
|
+
reason: "manual",
|
|
22338
|
+
score: computeCapacityScore(pinnedOnline)
|
|
22339
|
+
};
|
|
22340
|
+
return {
|
|
22341
|
+
kind: "pending",
|
|
22342
|
+
reason: "over-cap"
|
|
22343
|
+
};
|
|
22344
|
+
}
|
|
22083
22345
|
}
|
|
22084
|
-
|
|
22346
|
+
if (eligible.length === 0) return {
|
|
22347
|
+
kind: "pending",
|
|
22348
|
+
reason: "over-cap"
|
|
22349
|
+
};
|
|
22350
|
+
const best = eligible.map((node) => ({
|
|
22085
22351
|
node,
|
|
22086
22352
|
score: computeCapacityScore(node)
|
|
22087
22353
|
})).toSorted((a, b) => a.score - b.score)[0];
|
|
22088
22354
|
return {
|
|
22355
|
+
kind: "assigned",
|
|
22089
22356
|
agentNodeId: best.node.nodeId,
|
|
22090
22357
|
reason: "capacity",
|
|
22091
22358
|
score: best.score
|
|
@@ -22504,6 +22771,139 @@ var PipelineWatchdog = class {
|
|
|
22504
22771
|
}
|
|
22505
22772
|
};
|
|
22506
22773
|
//#endregion
|
|
22774
|
+
//#region src/camera-status/compose-camera-status.ts
|
|
22775
|
+
function mapAssignment(input) {
|
|
22776
|
+
return {
|
|
22777
|
+
detectionNodeId: input.detectionNodeId,
|
|
22778
|
+
decoderNodeId: input.decoderNodeId,
|
|
22779
|
+
audioNodeId: input.audioNodeId,
|
|
22780
|
+
pinned: {
|
|
22781
|
+
detection: input.pinned.detection,
|
|
22782
|
+
decoder: input.pinned.decoder,
|
|
22783
|
+
audio: input.pinned.audio
|
|
22784
|
+
},
|
|
22785
|
+
reasons: {
|
|
22786
|
+
detection: input.reasons.detection,
|
|
22787
|
+
decoder: input.reasons.decoder,
|
|
22788
|
+
audio: input.reasons.audio
|
|
22789
|
+
}
|
|
22790
|
+
};
|
|
22791
|
+
}
|
|
22792
|
+
function mapSource(sourceResult) {
|
|
22793
|
+
if (sourceResult === null) return { streams: [] };
|
|
22794
|
+
return { streams: sourceResult.streams.map((s) => ({
|
|
22795
|
+
camStreamId: s.camStreamId,
|
|
22796
|
+
codec: s.codec,
|
|
22797
|
+
width: s.width,
|
|
22798
|
+
height: s.height,
|
|
22799
|
+
fps: s.fps,
|
|
22800
|
+
kind: s.kind
|
|
22801
|
+
})) };
|
|
22802
|
+
}
|
|
22803
|
+
function mapBroker(brokerResult) {
|
|
22804
|
+
if (brokerResult === null) return null;
|
|
22805
|
+
return {
|
|
22806
|
+
profiles: brokerResult.profiles.map((p) => ({
|
|
22807
|
+
profile: p.profile,
|
|
22808
|
+
status: p.status,
|
|
22809
|
+
codec: p.codec,
|
|
22810
|
+
width: p.width,
|
|
22811
|
+
height: p.height,
|
|
22812
|
+
subscribers: p.subscribers,
|
|
22813
|
+
inFps: p.inFps,
|
|
22814
|
+
outFps: p.outFps
|
|
22815
|
+
})),
|
|
22816
|
+
webrtcSessions: brokerResult.webrtcSessions,
|
|
22817
|
+
rtspRestream: brokerResult.rtspRestream
|
|
22818
|
+
};
|
|
22819
|
+
}
|
|
22820
|
+
function mapDecoderShm(shm) {
|
|
22821
|
+
return {
|
|
22822
|
+
framesWritten: shm.framesWritten,
|
|
22823
|
+
getFrameHits: shm.getFrameHits,
|
|
22824
|
+
getFrameMisses: shm.getFrameMisses,
|
|
22825
|
+
budgetMb: shm.budgetMb
|
|
22826
|
+
};
|
|
22827
|
+
}
|
|
22828
|
+
function mapDecoder(decoderResult) {
|
|
22829
|
+
if (decoderResult === null) return null;
|
|
22830
|
+
return {
|
|
22831
|
+
nodeId: decoderResult.nodeId,
|
|
22832
|
+
formats: [...decoderResult.formats],
|
|
22833
|
+
sessionCount: decoderResult.sessionCount,
|
|
22834
|
+
shm: mapDecoderShm(decoderResult.shm)
|
|
22835
|
+
};
|
|
22836
|
+
}
|
|
22837
|
+
function mapMotion(motionResult) {
|
|
22838
|
+
if (motionResult === null) return null;
|
|
22839
|
+
return {
|
|
22840
|
+
enabled: motionResult.enabled,
|
|
22841
|
+
fps: motionResult.fps
|
|
22842
|
+
};
|
|
22843
|
+
}
|
|
22844
|
+
function mapProvisioning(p) {
|
|
22845
|
+
if (p.error !== void 0) return {
|
|
22846
|
+
state: p.state,
|
|
22847
|
+
error: p.error
|
|
22848
|
+
};
|
|
22849
|
+
return { state: p.state };
|
|
22850
|
+
}
|
|
22851
|
+
function mapDetection(detectionResult) {
|
|
22852
|
+
if (detectionResult === null) return null;
|
|
22853
|
+
const phase = detectionResult.phase;
|
|
22854
|
+
return {
|
|
22855
|
+
nodeId: detectionResult.nodeId,
|
|
22856
|
+
engine: {
|
|
22857
|
+
backend: detectionResult.engine.backend,
|
|
22858
|
+
device: detectionResult.engine.device
|
|
22859
|
+
},
|
|
22860
|
+
phase,
|
|
22861
|
+
configuredFps: detectionResult.configuredFps,
|
|
22862
|
+
actualFps: detectionResult.actualFps,
|
|
22863
|
+
queueDepth: detectionResult.queueDepth,
|
|
22864
|
+
avgInferenceMs: detectionResult.avgInferenceMs,
|
|
22865
|
+
provisioning: mapProvisioning(detectionResult.provisioning)
|
|
22866
|
+
};
|
|
22867
|
+
}
|
|
22868
|
+
function mapAudio(audioResult) {
|
|
22869
|
+
if (audioResult === null) return null;
|
|
22870
|
+
return {
|
|
22871
|
+
nodeId: audioResult.nodeId,
|
|
22872
|
+
enabled: audioResult.enabled
|
|
22873
|
+
};
|
|
22874
|
+
}
|
|
22875
|
+
function mapRecording(recordingResult) {
|
|
22876
|
+
if (recordingResult === null) return null;
|
|
22877
|
+
return {
|
|
22878
|
+
mode: recordingResult.mode,
|
|
22879
|
+
active: recordingResult.active,
|
|
22880
|
+
storageBytes: recordingResult.storageBytes
|
|
22881
|
+
};
|
|
22882
|
+
}
|
|
22883
|
+
/**
|
|
22884
|
+
* Pure function that composes a `CameraStatus` from per-stage fetch results.
|
|
22885
|
+
*
|
|
22886
|
+
* - `assignment` is always built from orchestrator-local data (never null).
|
|
22887
|
+
* - `source` always present: defaults to `{ streams: [] }` when sourceResult is null.
|
|
22888
|
+
* - Every other block is null when its stage result is null (graceful degradation).
|
|
22889
|
+
* - `fetchedAt` is stamped exactly as provided — never calls `Date.now()`.
|
|
22890
|
+
* - No mutation of the input.
|
|
22891
|
+
*/
|
|
22892
|
+
function composeCameraStatus(input) {
|
|
22893
|
+
return {
|
|
22894
|
+
deviceId: input.deviceId,
|
|
22895
|
+
assignment: mapAssignment(input),
|
|
22896
|
+
source: mapSource(input.sourceResult),
|
|
22897
|
+
broker: mapBroker(input.brokerResult),
|
|
22898
|
+
decoder: mapDecoder(input.decoderResult),
|
|
22899
|
+
motion: mapMotion(input.motionResult),
|
|
22900
|
+
detection: mapDetection(input.detectionResult),
|
|
22901
|
+
audio: mapAudio(input.audioResult),
|
|
22902
|
+
recording: mapRecording(input.recordingResult),
|
|
22903
|
+
fetchedAt: input.fetchedAt
|
|
22904
|
+
};
|
|
22905
|
+
}
|
|
22906
|
+
//#endregion
|
|
22507
22907
|
//#region src/index.ts
|
|
22508
22908
|
var PHASE_MODE_VALUES = new Set([
|
|
22509
22909
|
"disabled",
|
|
@@ -23261,8 +23661,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23261
23661
|
});
|
|
23262
23662
|
return {
|
|
23263
23663
|
success: true,
|
|
23264
|
-
|
|
23265
|
-
reason: "capacity"
|
|
23664
|
+
kind: "pending"
|
|
23266
23665
|
};
|
|
23267
23666
|
}
|
|
23268
23667
|
}
|
|
@@ -23272,14 +23671,22 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23272
23671
|
const preferredAgent = typeof pipelinePin === "string" && pipelinePin !== "auto" ? pipelinePin : legacyPreferred;
|
|
23273
23672
|
const decision = balance({
|
|
23274
23673
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
23275
|
-
preferredAgent
|
|
23674
|
+
preferredAgent,
|
|
23675
|
+
nodeCaps: this.buildNodeCaps()
|
|
23276
23676
|
});
|
|
23277
|
-
|
|
23278
|
-
if (
|
|
23677
|
+
if (!decision) throw new Error(`dispatchCamera: no runner available for ${runnerConfig.deviceId}`);
|
|
23678
|
+
if (decision.kind === "pending") {
|
|
23679
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId: runnerConfig.deviceId } });
|
|
23680
|
+
return {
|
|
23681
|
+
success: true,
|
|
23682
|
+
kind: "pending"
|
|
23683
|
+
};
|
|
23684
|
+
}
|
|
23685
|
+
const targetNodeId = decision.agentNodeId;
|
|
23279
23686
|
await this.attachOn(targetNodeId, runnerConfig);
|
|
23280
23687
|
if (targetNodeId === this.localNodeId) this.pipelineWatchdog?.register(this.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId)));
|
|
23281
|
-
const reason = decision
|
|
23282
|
-
const pinned = decision
|
|
23688
|
+
const reason = decision.reason;
|
|
23689
|
+
const pinned = decision.reason === "manual";
|
|
23283
23690
|
this.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, pinned);
|
|
23284
23691
|
const decoderNodeId = await this.resolveDecoderNode(runnerConfig.deviceId, targetNodeId);
|
|
23285
23692
|
this.ctx.logger.info("dispatchCamera", {
|
|
@@ -23289,12 +23696,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23289
23696
|
},
|
|
23290
23697
|
meta: {
|
|
23291
23698
|
reason,
|
|
23292
|
-
score: decision
|
|
23699
|
+
score: decision.score,
|
|
23293
23700
|
decoderNodeId
|
|
23294
23701
|
}
|
|
23295
23702
|
});
|
|
23296
23703
|
return {
|
|
23297
23704
|
success: true,
|
|
23705
|
+
kind: "assigned",
|
|
23298
23706
|
agentNodeId: targetNodeId,
|
|
23299
23707
|
reason
|
|
23300
23708
|
};
|
|
@@ -23368,10 +23776,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23368
23776
|
if (cached) {
|
|
23369
23777
|
const decision = balance({
|
|
23370
23778
|
nodes: await this.collectAgentLoad({ onlyEnabled: true }),
|
|
23371
|
-
preferredAgent: null
|
|
23779
|
+
preferredAgent: null,
|
|
23780
|
+
nodeCaps: this.buildNodeCaps()
|
|
23372
23781
|
});
|
|
23373
|
-
|
|
23374
|
-
if (
|
|
23782
|
+
if (!decision) this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
|
|
23783
|
+
else if (decision.kind === "pending") this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId: input.deviceId } });
|
|
23784
|
+
else {
|
|
23785
|
+
const targetNodeId = decision.agentNodeId;
|
|
23375
23786
|
if (current && current.agentNodeId !== targetNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
23376
23787
|
const msg = errMsg(err);
|
|
23377
23788
|
this.ctx.logger.debug("unassignPipeline detach-old failed", {
|
|
@@ -23380,7 +23791,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23380
23791
|
});
|
|
23381
23792
|
});
|
|
23382
23793
|
if (!current || current.agentNodeId !== targetNodeId) await this.attachOn(targetNodeId, cached);
|
|
23383
|
-
const reason = decision
|
|
23794
|
+
const reason = decision.reason === "manual" ? "capacity" : decision.reason;
|
|
23384
23795
|
this.recordAssignment(input.deviceId, targetNodeId, reason, false);
|
|
23385
23796
|
this.ctx.logger.info("unassignPipeline: re-dispatched via auto", {
|
|
23386
23797
|
tags: {
|
|
@@ -23391,7 +23802,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23391
23802
|
});
|
|
23392
23803
|
return { success: true };
|
|
23393
23804
|
}
|
|
23394
|
-
this.ctx.logger.warn("unassignPipeline: no runner available, leaving unassigned", { tags: { deviceId: input.deviceId } });
|
|
23395
23805
|
}
|
|
23396
23806
|
if (current) {
|
|
23397
23807
|
await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
@@ -23409,15 +23819,21 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23409
23819
|
async rebalance() {
|
|
23410
23820
|
if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
|
|
23411
23821
|
const loads = await this.collectAgentLoad({ onlyEnabled: true });
|
|
23822
|
+
const nodeCaps = this.buildNodeCaps();
|
|
23412
23823
|
let migrated = 0;
|
|
23413
23824
|
for (const [deviceId, config] of this.cameraConfigs) {
|
|
23414
23825
|
const current = this.assignments.get(deviceId);
|
|
23415
23826
|
if (current?.pinned) continue;
|
|
23416
23827
|
const decision = balance({
|
|
23417
23828
|
nodes: loads,
|
|
23418
|
-
preferredAgent: await this.readPreferredAgent(deviceId)
|
|
23829
|
+
preferredAgent: await this.readPreferredAgent(deviceId),
|
|
23830
|
+
nodeCaps
|
|
23419
23831
|
});
|
|
23420
23832
|
if (!decision) continue;
|
|
23833
|
+
if (decision.kind === "pending") {
|
|
23834
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId } });
|
|
23835
|
+
continue;
|
|
23836
|
+
}
|
|
23421
23837
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
23422
23838
|
if (current) await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
|
|
23423
23839
|
const msg = errMsg(err);
|
|
@@ -23555,15 +23971,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23555
23971
|
nodeId
|
|
23556
23972
|
});
|
|
23557
23973
|
}
|
|
23558
|
-
async localRunnerNodeId() {
|
|
23559
|
-
const api = this.ctx.api;
|
|
23560
|
-
if (!api) return null;
|
|
23561
|
-
try {
|
|
23562
|
-
return (await api.pipelineRunner.getLocalLoad.query({ nodeId: this.localNodeId }))?.nodeId ?? this.localNodeId;
|
|
23563
|
-
} catch {
|
|
23564
|
-
return this.localNodeId;
|
|
23565
|
-
}
|
|
23566
|
-
}
|
|
23567
23974
|
/**
|
|
23568
23975
|
* Enumerate every runner-capable node currently known (populated via
|
|
23569
23976
|
* AgentOnline/AgentOffline events). Used to populate the `enabledNodes`
|
|
@@ -23718,6 +24125,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23718
24125
|
return null;
|
|
23719
24126
|
}
|
|
23720
24127
|
}
|
|
24128
|
+
/**
|
|
24129
|
+
* Build a per-node camera cap map from the persisted agent settings.
|
|
24130
|
+
* Returns `null` for nodes where `maxCameras` is null (unlimited).
|
|
24131
|
+
* Used by every `balance()` call so the balancer can honour operator-set
|
|
24132
|
+
* per-node maximums without polling the store on each decision.
|
|
24133
|
+
*/
|
|
24134
|
+
buildNodeCaps() {
|
|
24135
|
+
const blob = this.agentSettingsState.get();
|
|
24136
|
+
const caps = {};
|
|
24137
|
+
for (const [nodeId, settings] of Object.entries(blob)) caps[nodeId] = settings.maxCameras ?? null;
|
|
24138
|
+
return caps;
|
|
24139
|
+
}
|
|
23721
24140
|
async readAudioNodePin(deviceId) {
|
|
23722
24141
|
if (!this.ctx?.settings) return null;
|
|
23723
24142
|
try {
|
|
@@ -23860,13 +24279,19 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
23860
24279
|
for (const { deviceId, config } of affected) {
|
|
23861
24280
|
const decision = balance({
|
|
23862
24281
|
nodes: loads,
|
|
23863
|
-
preferredAgent: null
|
|
24282
|
+
preferredAgent: null,
|
|
24283
|
+
nodeCaps: this.buildNodeCaps()
|
|
23864
24284
|
});
|
|
23865
24285
|
if (!decision) {
|
|
23866
24286
|
this.ctx.logger.error("Failover: no online runner", { tags: { deviceId } });
|
|
23867
24287
|
this.assignments.delete(deviceId);
|
|
23868
24288
|
continue;
|
|
23869
24289
|
}
|
|
24290
|
+
if (decision.kind === "pending") {
|
|
24291
|
+
this.ctx.logger.warn("camera left pending — all nodes at maxCameras", { tags: { deviceId } });
|
|
24292
|
+
this.assignments.delete(deviceId);
|
|
24293
|
+
continue;
|
|
24294
|
+
}
|
|
23870
24295
|
try {
|
|
23871
24296
|
await this.attachOn(decision.agentNodeId, config);
|
|
23872
24297
|
this.recordAssignment(deviceId, decision.agentNodeId, "failover", false);
|
|
@@ -24346,7 +24771,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24346
24771
|
}
|
|
24347
24772
|
async setAgentAddonDefaults(input) {
|
|
24348
24773
|
let existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24349
|
-
if (!existing)
|
|
24774
|
+
if (!existing) {
|
|
24775
|
+
await this.seedAgentSettingsFromCatalog(input.agentNodeId);
|
|
24776
|
+
existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24777
|
+
}
|
|
24350
24778
|
if (!existing) await this.writeAgentSettings(input.agentNodeId, { addonDefaults: { ...input.defaults } });
|
|
24351
24779
|
else await this.writeAgentSettings(input.agentNodeId, {
|
|
24352
24780
|
...existing,
|
|
@@ -24384,6 +24812,22 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24384
24812
|
removed: true
|
|
24385
24813
|
};
|
|
24386
24814
|
}
|
|
24815
|
+
async setAgentMaxCameras(input) {
|
|
24816
|
+
const existing = (await this.readAgentSettingsMap())[input.agentNodeId];
|
|
24817
|
+
const next = existing ? {
|
|
24818
|
+
...existing,
|
|
24819
|
+
maxCameras: input.maxCameras
|
|
24820
|
+
} : {
|
|
24821
|
+
addonDefaults: {},
|
|
24822
|
+
maxCameras: input.maxCameras
|
|
24823
|
+
};
|
|
24824
|
+
await this.writeAgentSettings(input.agentNodeId, next);
|
|
24825
|
+
this.ctx.logger.info("agentSettings.maxCameras updated", {
|
|
24826
|
+
tags: { nodeId: input.agentNodeId },
|
|
24827
|
+
meta: { maxCameras: input.maxCameras }
|
|
24828
|
+
});
|
|
24829
|
+
return { success: true };
|
|
24830
|
+
}
|
|
24387
24831
|
async getCameraSettings(input) {
|
|
24388
24832
|
return (await this.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
24389
24833
|
}
|
|
@@ -24513,6 +24957,204 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24513
24957
|
this.ctx.logger.info("template deleted", { meta: { templateId: input.id } });
|
|
24514
24958
|
return { success: true };
|
|
24515
24959
|
}
|
|
24960
|
+
/**
|
|
24961
|
+
* Races a promise against a timeout. Returns `null` on timeout OR rejection.
|
|
24962
|
+
* Never throws — individual stage failures become `null` in the aggregate.
|
|
24963
|
+
*
|
|
24964
|
+
* @param p The stage fetch promise.
|
|
24965
|
+
* @param ms Timeout in milliseconds.
|
|
24966
|
+
*/
|
|
24967
|
+
boundedStage(p, ms) {
|
|
24968
|
+
let timer;
|
|
24969
|
+
const timeout = new Promise((resolve) => {
|
|
24970
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
24971
|
+
});
|
|
24972
|
+
return Promise.race([p, timeout]).catch(() => null).finally(() => {
|
|
24973
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
24974
|
+
});
|
|
24975
|
+
}
|
|
24976
|
+
/**
|
|
24977
|
+
* Server-composed aggregated status for a single camera.
|
|
24978
|
+
*
|
|
24979
|
+
* Fans out in parallel (bounded, per-stage graceful degradation) to
|
|
24980
|
+
* broker / decoder / motion / detection / audio / recording source caps
|
|
24981
|
+
* via `ctx.api`. A stage whose source errors or times out becomes `null`
|
|
24982
|
+
* in the returned payload — one slow agent never breaks the whole call.
|
|
24983
|
+
*/
|
|
24984
|
+
async getCameraStatus(input) {
|
|
24985
|
+
const { deviceId } = input;
|
|
24986
|
+
const api = this.ctx.api;
|
|
24987
|
+
const STAGE_TIMEOUT_MS = 3e3;
|
|
24988
|
+
const pipelineAssignment = this.assignments.get(deviceId) ?? null;
|
|
24989
|
+
const detectionNodeId = pipelineAssignment?.agentNodeId ?? null;
|
|
24990
|
+
const decoderPinRaw = (api ? await this.ctx.settings?.readDeviceStore(deviceId).catch(() => ({})) ?? {} : {})["decoderNodeId"];
|
|
24991
|
+
const decoderPinned = typeof decoderPinRaw === "string" && decoderPinRaw !== "auto";
|
|
24992
|
+
const decoderNodeId = detectionNodeId ? await this.resolveDecoderNode(deviceId, detectionNodeId).catch(() => null) : null;
|
|
24993
|
+
const audioAssignment = this.audioAssignments.get(deviceId) ?? null;
|
|
24994
|
+
const audioNodeId = audioAssignment?.nodeId ?? null;
|
|
24995
|
+
const audioPinned = audioAssignment?.pinned ?? false;
|
|
24996
|
+
const pinned = {
|
|
24997
|
+
detection: pipelineAssignment?.pinned ?? false,
|
|
24998
|
+
decoder: decoderPinned,
|
|
24999
|
+
audio: audioPinned
|
|
25000
|
+
};
|
|
25001
|
+
const reasons = {
|
|
25002
|
+
detection: pipelineAssignment?.reason,
|
|
25003
|
+
decoder: decoderPinned ? "manual" : "co-located",
|
|
25004
|
+
audio: audioPinned ? "manual" : void 0
|
|
25005
|
+
};
|
|
25006
|
+
const allSlotsFetch = api ? api.streamBroker.listAllProfileSlots.query() : null;
|
|
25007
|
+
const sourceFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then((slots) => {
|
|
25008
|
+
return { streams: slots.filter((s) => s.deviceId === deviceId).map((s) => ({
|
|
25009
|
+
camStreamId: s.sourceCamStreamId ?? s.brokerId,
|
|
25010
|
+
codec: s.codec ?? "",
|
|
25011
|
+
width: s.resolution?.width ?? 0,
|
|
25012
|
+
height: s.resolution?.height ?? 0,
|
|
25013
|
+
fps: 0,
|
|
25014
|
+
kind: s.profile
|
|
25015
|
+
})) };
|
|
25016
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25017
|
+
const WEBRTC_KINDS = new Set([
|
|
25018
|
+
"webrtc-browser",
|
|
25019
|
+
"webrtc-mobile",
|
|
25020
|
+
"webrtc-whep"
|
|
25021
|
+
]);
|
|
25022
|
+
const brokerFetch = api && allSlotsFetch ? this.boundedStage(allSlotsFetch.then(async (slots) => {
|
|
25023
|
+
const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
|
|
25024
|
+
if (deviceSlots.length === 0) return {
|
|
25025
|
+
profiles: [],
|
|
25026
|
+
webrtcSessions: 0,
|
|
25027
|
+
rtspRestream: false
|
|
25028
|
+
};
|
|
25029
|
+
const [statsAndClients, rtspEntry] = await Promise.all([Promise.all(deviceSlots.map(async (slot) => {
|
|
25030
|
+
return {
|
|
25031
|
+
slot,
|
|
25032
|
+
stats: await api.streamBroker.getBrokerStats.query({ brokerId: slot.brokerId }).catch(() => null),
|
|
25033
|
+
clients: await api.streamBroker.listClients.query({ brokerId: slot.brokerId }).catch(() => null)
|
|
25034
|
+
};
|
|
25035
|
+
})), api.streamBroker.getAllRtspEntries.query({}).catch(() => null)]);
|
|
25036
|
+
return {
|
|
25037
|
+
profiles: statsAndClients.map(({ slot, stats, clients }) => ({
|
|
25038
|
+
profile: slot.profile,
|
|
25039
|
+
status: slot.status,
|
|
25040
|
+
codec: stats?.codec ?? slot.codec ?? "",
|
|
25041
|
+
width: slot.resolution?.width ?? 0,
|
|
25042
|
+
height: slot.resolution?.height ?? 0,
|
|
25043
|
+
subscribers: clients?.encodedSubscribers ?? 0,
|
|
25044
|
+
inFps: stats?.inputFps ?? 0,
|
|
25045
|
+
outFps: stats?.decodeFps ?? 0
|
|
25046
|
+
})),
|
|
25047
|
+
webrtcSessions: statsAndClients.reduce((total, { clients }) => {
|
|
25048
|
+
if (!clients) return total;
|
|
25049
|
+
return total + clients.encoded.filter((c) => WEBRTC_KINDS.has(c.attribution.kind)).length;
|
|
25050
|
+
}, 0),
|
|
25051
|
+
rtspRestream: rtspEntry?.some((e) => {
|
|
25052
|
+
return e.brokerId.split("/")[0] === String(deviceId) && e.enabled;
|
|
25053
|
+
}) ?? false
|
|
25054
|
+
};
|
|
25055
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25056
|
+
const decoderFetch = decoderNodeId ? Promise.resolve({
|
|
25057
|
+
nodeId: decoderNodeId,
|
|
25058
|
+
formats: [],
|
|
25059
|
+
sessionCount: 0,
|
|
25060
|
+
shm: {
|
|
25061
|
+
framesWritten: 0,
|
|
25062
|
+
getFrameHits: 0,
|
|
25063
|
+
getFrameMisses: 0,
|
|
25064
|
+
budgetMb: 0
|
|
25065
|
+
}
|
|
25066
|
+
}) : Promise.resolve(null);
|
|
25067
|
+
const motionResult = (() => {
|
|
25068
|
+
const config = this.cameraConfigs.get(deviceId);
|
|
25069
|
+
if (!config) return null;
|
|
25070
|
+
return {
|
|
25071
|
+
enabled: config.motionSources.includes("analyzer") || config.motionSources.includes("onboard"),
|
|
25072
|
+
fps: config.motionFps
|
|
25073
|
+
};
|
|
25074
|
+
})();
|
|
25075
|
+
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]) => {
|
|
25076
|
+
const metrics = await api.pipelineRunner.getCameraMetrics.query({
|
|
25077
|
+
deviceId,
|
|
25078
|
+
nodeId: detectionNodeId
|
|
25079
|
+
}).catch(() => null);
|
|
25080
|
+
const phase = (() => {
|
|
25081
|
+
const p = metrics?.phase;
|
|
25082
|
+
if (p === "active") return "active";
|
|
25083
|
+
if (p === "idle") return "idle";
|
|
25084
|
+
return "watching";
|
|
25085
|
+
})();
|
|
25086
|
+
return {
|
|
25087
|
+
nodeId: detectionNodeId,
|
|
25088
|
+
engine: {
|
|
25089
|
+
backend: engine?.backend ?? "",
|
|
25090
|
+
device: engine?.device ?? ""
|
|
25091
|
+
},
|
|
25092
|
+
phase,
|
|
25093
|
+
configuredFps: metrics?.configuredFps ?? 0,
|
|
25094
|
+
actualFps: metrics?.actualFps ?? 0,
|
|
25095
|
+
queueDepth: metrics?.queueDepth ?? 0,
|
|
25096
|
+
avgInferenceMs: metrics?.avgInferenceTimeMs ?? 0,
|
|
25097
|
+
provisioning: {
|
|
25098
|
+
state: provisioning?.state ?? "idle",
|
|
25099
|
+
...provisioning?.error !== void 0 ? { error: provisioning.error } : {}
|
|
25100
|
+
}
|
|
25101
|
+
};
|
|
25102
|
+
}), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25103
|
+
const audioResult = audioNodeId ? {
|
|
25104
|
+
nodeId: audioNodeId,
|
|
25105
|
+
enabled: true
|
|
25106
|
+
} : null;
|
|
25107
|
+
function isRecordingStatus(v) {
|
|
25108
|
+
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";
|
|
25109
|
+
}
|
|
25110
|
+
const recordingFetch = api ? this.boundedStage(api.recording.getStatus.query({ deviceId }).then((rawStatus) => {
|
|
25111
|
+
if (!isRecordingStatus(rawStatus)) return null;
|
|
25112
|
+
return {
|
|
25113
|
+
mode: rawStatus.activeMode,
|
|
25114
|
+
active: rawStatus.enabled && rawStatus.activeMode !== "off",
|
|
25115
|
+
storageBytes: rawStatus.storageBytes
|
|
25116
|
+
};
|
|
25117
|
+
}).catch(() => null), STAGE_TIMEOUT_MS) : Promise.resolve(null);
|
|
25118
|
+
const [sourceResult, brokerResult, decoderResult, detectionResult, recordingResult] = await Promise.all([
|
|
25119
|
+
sourceFetch,
|
|
25120
|
+
brokerFetch,
|
|
25121
|
+
decoderFetch,
|
|
25122
|
+
detectionFetch,
|
|
25123
|
+
recordingFetch
|
|
25124
|
+
]);
|
|
25125
|
+
return composeCameraStatus({
|
|
25126
|
+
deviceId,
|
|
25127
|
+
fetchedAt: Date.now(),
|
|
25128
|
+
detectionNodeId,
|
|
25129
|
+
decoderNodeId,
|
|
25130
|
+
audioNodeId,
|
|
25131
|
+
pinned,
|
|
25132
|
+
reasons,
|
|
25133
|
+
sourceResult,
|
|
25134
|
+
brokerResult,
|
|
25135
|
+
decoderResult,
|
|
25136
|
+
motionResult,
|
|
25137
|
+
detectionResult,
|
|
25138
|
+
audioResult,
|
|
25139
|
+
recordingResult
|
|
25140
|
+
});
|
|
25141
|
+
}
|
|
25142
|
+
/**
|
|
25143
|
+
* Server-composed aggregated status for multiple cameras in one call.
|
|
25144
|
+
*
|
|
25145
|
+
* `deviceIds` defaults to all cameras currently tracked by the
|
|
25146
|
+
* orchestrator's assignment map when omitted.
|
|
25147
|
+
*
|
|
25148
|
+
* v1: `Promise.all` over per-device composition (no concurrency cap).
|
|
25149
|
+
* Note: for large fleets (hundreds of cameras) this may fan out many
|
|
25150
|
+
* parallel calls. A concurrency limiter (p-limit / semaphore) should be
|
|
25151
|
+
* added if latency measurements show it's necessary — deliberately
|
|
25152
|
+
* deferred per the YAGNI constraint in the spec.
|
|
25153
|
+
*/
|
|
25154
|
+
async getCameraStatuses(input) {
|
|
25155
|
+
const ids = input.deviceIds !== void 0 && input.deviceIds.length > 0 ? input.deviceIds : [...this.assignments.keys()];
|
|
25156
|
+
return Promise.all(ids.map((deviceId) => this.getCameraStatus({ deviceId })));
|
|
25157
|
+
}
|
|
24516
25158
|
/** Read the templates map from the addon store via the durable handle. */
|
|
24517
25159
|
async readTemplatesMap() {
|
|
24518
25160
|
const raw = await this.templatesState.get();
|
|
@@ -24543,7 +25185,11 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24543
25185
|
return;
|
|
24544
25186
|
}
|
|
24545
25187
|
const all = await this.readAgentSettingsMap();
|
|
24546
|
-
all[nodeId]
|
|
25188
|
+
const existing = all[nodeId];
|
|
25189
|
+
all[nodeId] = {
|
|
25190
|
+
addonDefaults: settings.addonDefaults,
|
|
25191
|
+
maxCameras: settings.maxCameras !== void 0 ? settings.maxCameras ?? null : existing?.maxCameras ?? null
|
|
25192
|
+
};
|
|
24547
25193
|
await this.agentSettingsState.set(all);
|
|
24548
25194
|
}
|
|
24549
25195
|
/** Read the `cameraSettings` map (keyed on `deviceId` as string) via the durable handle. */
|
|
@@ -24753,12 +25399,15 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
24753
25399
|
}
|
|
24754
25400
|
let agent = (await this.readAgentSettingsMap())[nodeId];
|
|
24755
25401
|
if (!agent || Object.keys(agent.addonDefaults ?? {}).length === 0) {
|
|
24756
|
-
|
|
24757
|
-
if (!seeded) {
|
|
25402
|
+
if (!await this.seedAgentSettingsFromCatalog(nodeId)) {
|
|
24758
25403
|
await sleep$1(2e3);
|
|
24759
25404
|
continue;
|
|
24760
25405
|
}
|
|
24761
|
-
agent =
|
|
25406
|
+
agent = (await this.readAgentSettingsMap())[nodeId];
|
|
25407
|
+
}
|
|
25408
|
+
if (!agent) {
|
|
25409
|
+
await sleep$1(2e3);
|
|
25410
|
+
continue;
|
|
24762
25411
|
}
|
|
24763
25412
|
const engine = await this.readDetectionPipelineEngine(nodeId) ?? catalog.selectedEngine;
|
|
24764
25413
|
return {
|
|
@@ -25665,7 +26314,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
25665
26314
|
};
|
|
25666
26315
|
let dispatchedNodeId = null;
|
|
25667
26316
|
try {
|
|
25668
|
-
|
|
26317
|
+
const result = await this.dispatchCamera(runnerConfig);
|
|
26318
|
+
if (result.kind === "assigned") dispatchedNodeId = result.agentNodeId;
|
|
25669
26319
|
} catch (err) {
|
|
25670
26320
|
const msg = errMsg(err);
|
|
25671
26321
|
log.error("dispatchCamera failed", { meta: { error: msg } });
|