@camstack/addon-pipeline-orchestrator 0.1.1
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/@mf-types/compiled-types/widgets/PipelineQuickStats.d.ts +3 -0
- package/dist/@mf-types/compiled-types/widgets/PipelineQuickStats.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/ZoneEditor.d.ts +17 -0
- package/dist/@mf-types/compiled-types/widgets/ZoneEditor.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/ZonesTab.d.ts +7 -0
- package/dist/@mf-types/compiled-types/widgets/ZonesTab.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/index.d.ts +13 -0
- package/dist/@mf-types/compiled-types/widgets/index.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts +60 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts +23 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts +15 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneOverlay.d.ts +19 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneOverlay.d.ts.map +1 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneRulesEditor.d.ts +9 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneRulesEditor.d.ts.map +1 -0
- package/dist/@mf-types/widgets.d.ts +2 -0
- package/dist/@mf-types.d.ts +3 -0
- package/dist/@mf-types.zip +0 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-CCBTZBOa.mjs +12 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CvusB3VY.mjs +17 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BrYYwKk_.mjs +34 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-DoWbefqS.mjs +104 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-D4eEXltm.mjs +85 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-CVrnrGED.mjs +62 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs-B848Fc_m.mjs +88 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-B-q1guKT.mjs +29 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-Cg6QsnjR.mjs +36 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-Dp8hqYOB.mjs +45 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CeGb2_QF.mjs +6 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-BBmNf5hf.mjs +34 -0
- package/dist/_stub.js +16268 -0
- package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C5Dbnljh.mjs +157 -0
- package/dist/client-BkQItW6e.mjs +9836 -0
- package/dist/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +211 -0
- package/dist/hostInit-xzBPWUz0.mjs +168 -0
- package/dist/index-BI-_eQhe.mjs +185 -0
- package/dist/index-BJzn4K_R.mjs +2603 -0
- package/dist/index-BZ6YICSw.mjs +17936 -0
- package/dist/index-Bj470a3A.mjs +725 -0
- package/dist/index-C3iAUQqS.mjs +533 -0
- package/dist/index-CWkKuNLr.mjs +232 -0
- package/dist/index-Cj-UePAd.mjs +435 -0
- package/dist/index-D0dNM7_R.mjs +2892 -0
- package/dist/index-DNWfP1gi.mjs +2464 -0
- package/dist/index-DnFVXz0U.mjs +14162 -0
- package/dist/index-xncRG7-x.mjs +2713 -0
- package/dist/index.d.mts +907 -0
- package/dist/index.d.ts +907 -0
- package/dist/index.js +18670 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +18668 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsx-runtime-CJ4xYF4l.mjs +55 -0
- package/dist/remoteEntry.js +85 -0
- package/dist/virtualExposes-8FzWTdq3.mjs +42 -0
- package/package.json +95 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import * as _camstack_types from '@camstack/types';
|
|
2
|
+
import { RunnerLocalLoad, PipelineAssignment, IPipelineOrchestratorProvider, RunnerCameraConfig, BaseAddon, AddonInitResult, CameraMetrics, AgentLoadSummary, PipelineOrchestratorGlobalMetrics, DecoderAssignment, AgentPipelineSettings, AgentAddonConfig, CameraPipelineSettings, CameraStepOverridePatch, CameraPipelineConfig, CameraPipelineTemplate, ConfigUISchemaWithValues, CameraDetectionConfig } from '@camstack/types';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Input to the load balancer: one entry per online runner node.
|
|
7
|
+
*/
|
|
8
|
+
interface BalancerInput {
|
|
9
|
+
/** Per-node load snapshots fetched from each runner's `getLocalLoad()` cap. */
|
|
10
|
+
readonly nodes: readonly RunnerLocalLoad[];
|
|
11
|
+
/** Optional L1 affinity — if set and the node is online, it wins unconditionally. */
|
|
12
|
+
readonly preferredAgent?: string | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Output of the load balancer: the picked agent nodeId plus the reason the
|
|
16
|
+
* balancer chose it. `null` when no online runner is available.
|
|
17
|
+
*/
|
|
18
|
+
interface BalancerDecision {
|
|
19
|
+
readonly agentNodeId: string;
|
|
20
|
+
readonly reason: 'manual' | 'capacity';
|
|
21
|
+
readonly score: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute the L2 capacity score for a runner node. Lower is better.
|
|
25
|
+
* The score is a weighted sum of the runner's active workload so the balancer
|
|
26
|
+
* prefers agents that are serving fewer cameras OR draining queues quickly.
|
|
27
|
+
*
|
|
28
|
+
* Rationale:
|
|
29
|
+
* - `attachedCameras * avgInferenceFps` approximates the total inference rate
|
|
30
|
+
* the agent is currently sustaining (not just how many cameras are assigned).
|
|
31
|
+
* - `queueDepthTotal` penalises agents that are falling behind the frame feed.
|
|
32
|
+
*/
|
|
33
|
+
declare function computeCapacityScore(load: RunnerLocalLoad): number;
|
|
34
|
+
/**
|
|
35
|
+
* Run the two-level camera balancer.
|
|
36
|
+
*
|
|
37
|
+
* L1 (manual affinity): if `preferredAgent` names an online node, return it.
|
|
38
|
+
* L2 (capacity): compute capacity scores and pick the lowest.
|
|
39
|
+
*
|
|
40
|
+
* Returns `null` when no runners are online. The orchestrator decides how to
|
|
41
|
+
* react — typically by logging and deferring the assignment until a runner
|
|
42
|
+
* comes online.
|
|
43
|
+
*/
|
|
44
|
+
declare function balance(input: BalancerInput): BalancerDecision | null;
|
|
45
|
+
interface DecoderBalancerInput {
|
|
46
|
+
readonly decoderNodes: readonly RunnerLocalLoad[];
|
|
47
|
+
readonly pipelineNodeId: string;
|
|
48
|
+
readonly preferredDecoderNode?: string | null;
|
|
49
|
+
}
|
|
50
|
+
interface DecoderBalancerDecision {
|
|
51
|
+
readonly decoderNodeId: string;
|
|
52
|
+
readonly reason: 'manual' | 'capacity' | 'co-located';
|
|
53
|
+
readonly score: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Choose decoder node. Priority: manual pin → co-located with pipeline → capacity.
|
|
57
|
+
*/
|
|
58
|
+
declare function balanceDecoder(input: DecoderBalancerInput): DecoderBalancerDecision | null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Custom-action catalog exposed through `api.addons.custom` (Task 9.1 PoC).
|
|
62
|
+
*
|
|
63
|
+
* The orchestrator's cap surface is the contract for all runtime traffic
|
|
64
|
+
* (assignCamera / unassignCamera / rebalance / getGlobalMetrics etc). This
|
|
65
|
+
* catalog is reserved for read-only diagnostics that are intentionally
|
|
66
|
+
* outside the cap — they expose internal state (balancer caches, enabledNodes
|
|
67
|
+
* set, active detection count) that is useful for admin tooling but does not
|
|
68
|
+
* belong on the capability contract.
|
|
69
|
+
*/
|
|
70
|
+
declare const OrchestratorDiagnosticsSchema: z.ZodObject<{
|
|
71
|
+
localNodeId: z.ZodString;
|
|
72
|
+
knownRunnerNodes: z.ZodArray<z.ZodString>;
|
|
73
|
+
cachedAgentLoadNodeIds: z.ZodArray<z.ZodString>;
|
|
74
|
+
enabledNodes: z.ZodArray<z.ZodString>;
|
|
75
|
+
assignedDeviceCount: z.ZodNumber;
|
|
76
|
+
cameraConfigCount: z.ZodNumber;
|
|
77
|
+
activeDetectionCount: z.ZodNumber;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
type OrchestratorDiagnostics = z.infer<typeof OrchestratorDiagnosticsSchema>;
|
|
80
|
+
declare const pipelineOrchestratorActions: {
|
|
81
|
+
dumpState: _camstack_types.CustomActionSpec<z.ZodVoid, z.ZodObject<{
|
|
82
|
+
localNodeId: z.ZodString;
|
|
83
|
+
knownRunnerNodes: z.ZodArray<z.ZodString>;
|
|
84
|
+
cachedAgentLoadNodeIds: z.ZodArray<z.ZodString>;
|
|
85
|
+
enabledNodes: z.ZodArray<z.ZodString>;
|
|
86
|
+
assignedDeviceCount: z.ZodNumber;
|
|
87
|
+
cameraConfigCount: z.ZodNumber;
|
|
88
|
+
activeDetectionCount: z.ZodNumber;
|
|
89
|
+
}, z.core.$strip>, "query", "protected", {
|
|
90
|
+
readonly kind: "system";
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
93
|
+
type PipelineOrchestratorActions = typeof pipelineOrchestratorActions;
|
|
94
|
+
/**
|
|
95
|
+
* Result of a successful dispatch. Returned by `dispatchCamera` so the
|
|
96
|
+
* caller (DetectionWiringService) can log / emit follow-up events with
|
|
97
|
+
* the same rationale the balancer used.
|
|
98
|
+
*/
|
|
99
|
+
interface DispatchResult {
|
|
100
|
+
readonly success: true;
|
|
101
|
+
readonly agentNodeId: string;
|
|
102
|
+
readonly reason: PipelineAssignment["reason"];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Local-only provider surface resolved via the kernel capability registry.
|
|
106
|
+
* Extends the cap interface with the in-process dispatch entrypoints that
|
|
107
|
+
* can't cleanly round-trip through the tRPC serializer because they carry
|
|
108
|
+
* a full `PipelineConfig` payload attached to the `RunnerCameraConfig`.
|
|
109
|
+
*/
|
|
110
|
+
interface IPipelineOrchestratorLocalProvider extends IPipelineOrchestratorProvider {
|
|
111
|
+
/**
|
|
112
|
+
* In-process dispatch entrypoint. Caches the config, runs the load
|
|
113
|
+
* balancer, dispatches the attach call to the chosen runner.
|
|
114
|
+
*/
|
|
115
|
+
dispatchCamera(runnerConfig: RunnerCameraConfig): Promise<DispatchResult>;
|
|
116
|
+
/** In-process release entrypoint — detach + clear cached state. */
|
|
117
|
+
releaseCamera(input: {
|
|
118
|
+
deviceId: number;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
success: true;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
123
|
+
declare class PipelineOrchestratorAddon extends BaseAddon implements IPipelineOrchestratorLocalProvider {
|
|
124
|
+
/** This node's Moleculer nodeId (from this.ctx.kernel.localNodeId). */
|
|
125
|
+
private localNodeId;
|
|
126
|
+
/** Set of nodeIds known to be running pipeline-runner; maintained via AgentOnline/AgentOffline events. */
|
|
127
|
+
private readonly knownRunnerNodes;
|
|
128
|
+
/** EventBus unsubscribes for agent online/offline events. */
|
|
129
|
+
private unsubAgentOnline;
|
|
130
|
+
private unsubAgentOffline;
|
|
131
|
+
/**
|
|
132
|
+
* Readiness tracker — drives `detection-pipeline` seed on every
|
|
133
|
+
* provider life cycle. Prefers the kernel-shared singleton via
|
|
134
|
+
* `ctx.kernel.readinessRegistry`; falls back to a local instance for
|
|
135
|
+
* test contexts without a kernel.
|
|
136
|
+
*/
|
|
137
|
+
private readinessRegistry;
|
|
138
|
+
/** True when this addon owns the registry (local fallback) and should `close()` it on shutdown. */
|
|
139
|
+
private ownsReadinessRegistry;
|
|
140
|
+
/** Per-node readiness subscription unsubs — torn down on AgentOffline / shutdown. */
|
|
141
|
+
private readonly readinessSubsByNode;
|
|
142
|
+
/** Per-node idempotency guard — re-seed only when the producer's epoch advances. */
|
|
143
|
+
private readonly lastSeenReadinessEpochByNode;
|
|
144
|
+
/** Most recent RunnerCameraConfig per device — used by rebalance + re-dispatch. */
|
|
145
|
+
private readonly cameraConfigs;
|
|
146
|
+
/**
|
|
147
|
+
* Per-camera zones CRUD provider. Constructed lazily in `onInitialize`
|
|
148
|
+
* because it captures `this.ctx` for settings + api access; cleared
|
|
149
|
+
* to null in `onShutdown` so a soft-restart is observable.
|
|
150
|
+
*/
|
|
151
|
+
private zonesProvider;
|
|
152
|
+
/**
|
|
153
|
+
* Per-camera zone-rules CRUD provider — bulk-replace per stage,
|
|
154
|
+
* mirrors to dev.state slices `motion-zone-rules` /
|
|
155
|
+
* `detection-zone-rules` so consumer addons subscribe per stage.
|
|
156
|
+
*/
|
|
157
|
+
private zoneRulesProvider;
|
|
158
|
+
/** In-memory assignment map — mirrored into events + per-device settings for the pin. */
|
|
159
|
+
private readonly assignments;
|
|
160
|
+
/** Per-device audio node assignment — nodeId of the audio-analyzer handling this device's chunks. */
|
|
161
|
+
private readonly audioNodeByDevice;
|
|
162
|
+
/** Assignments with metadata — replaces plain audioNodeByDevice values as the source of truth. */
|
|
163
|
+
private readonly audioAssignments;
|
|
164
|
+
/** Nodes known to have a ready audio-analyzer. */
|
|
165
|
+
private readonly readyAudioNodes;
|
|
166
|
+
/** Last seen readiness epoch per node for audio-analyzer. */
|
|
167
|
+
private readonly lastSeenAudioEpochByNode;
|
|
168
|
+
/** Last observed agent-load snapshot per runner, populated during each dispatch. */
|
|
169
|
+
private cachedAgentLoad;
|
|
170
|
+
/**
|
|
171
|
+
* Cached `PipelineSchema` per engine key (`runtime/backend/format`).
|
|
172
|
+
* Populated on demand by {@link getCatalogForEngine} via
|
|
173
|
+
* `pipelineExecutor.getSchema({nodeId})` and invalidated on every
|
|
174
|
+
* `$node.connected` / `$node.disconnected` — node changes can mean
|
|
175
|
+
* model downloads arrived, addons enabled, etc. TTL is a soft
|
|
176
|
+
* guard only (invalidation is the main signal).
|
|
177
|
+
*/
|
|
178
|
+
private catalogCache;
|
|
179
|
+
private static readonly CATALOG_CACHE_TTL_MS;
|
|
180
|
+
/** Runtime failover policy, driven by `getConfigSchema` fields + `onConfigChange`. */
|
|
181
|
+
private failoverPolicy;
|
|
182
|
+
/**
|
|
183
|
+
* Hub-wide allow-list of node ids eligible to run the detection
|
|
184
|
+
* pipeline. Driven by the `enabledNodes` multiselect in the addon
|
|
185
|
+
* schema. Hydrated from the schema default (`['hub']`) on first
|
|
186
|
+
* boot; every other node must be explicitly added by the operator.
|
|
187
|
+
* The list is a strict whitelist — disabled nodes stay connected
|
|
188
|
+
* for other capabilities (metrics, logs, cluster-wide events) but
|
|
189
|
+
* never receive camera assignments.
|
|
190
|
+
*/
|
|
191
|
+
private enabledNodes;
|
|
192
|
+
/**
|
|
193
|
+
* Hub-wide allow-list of node ids eligible to run decoder sessions.
|
|
194
|
+
* Driven by the `enabledDecoderNodes` multiselect in the addon schema.
|
|
195
|
+
* Defaults to `['hub']` — the hub always has a decoder available.
|
|
196
|
+
*/
|
|
197
|
+
private enabledDecoderNodes;
|
|
198
|
+
/**
|
|
199
|
+
* Hub-wide allow-list of node ids eligible to run audio-analyzer sessions.
|
|
200
|
+
* Driven by the `enabledAudioNodes` multiselect in the addon schema.
|
|
201
|
+
* Defaults to `['hub']`.
|
|
202
|
+
*/
|
|
203
|
+
private enabledAudioNodes;
|
|
204
|
+
/**
|
|
205
|
+
* Full global settings snapshot kept in memory so synchronous cap methods
|
|
206
|
+
* (e.g. `getGlobalOrchestrationSettings`) can resolve without awaiting the
|
|
207
|
+
* backing store. Refreshed in `initialize` and `onConfigChange`.
|
|
208
|
+
*/
|
|
209
|
+
private globalSettings;
|
|
210
|
+
/** Per-device active detection config — set on startDetection, cleared on stopDetection. */
|
|
211
|
+
private readonly activeDetections;
|
|
212
|
+
/** Per-device audio broker unsubscribe callbacks. */
|
|
213
|
+
private readonly audioSubscriptions;
|
|
214
|
+
/** Per-device pending lazy-audio teardown timer (closes audio sub
|
|
215
|
+
* after motion cooldown when audioMode === 'on-motion'). */
|
|
216
|
+
private readonly lazyAudioTeardownTimers;
|
|
217
|
+
/** Event bus subscription lifetimes. */
|
|
218
|
+
private unsubDeviceRegistered;
|
|
219
|
+
/** Pending CameraStreams.profileSlotsChanged debounce timers, cancelled on shutdown. */
|
|
220
|
+
private profileSlotTimers;
|
|
221
|
+
private unsubDeviceUnregistered;
|
|
222
|
+
private unsubSettingsChanged;
|
|
223
|
+
private unsubInferenceResult;
|
|
224
|
+
private unsubProviderDetection;
|
|
225
|
+
private unsubCameraUpdated;
|
|
226
|
+
private unsubBindingsChanged;
|
|
227
|
+
private unsubCameraMetrics;
|
|
228
|
+
private unsubLazyAudioMotion;
|
|
229
|
+
/** Per-camera last-seen actualFps for load management enforcement. */
|
|
230
|
+
private readonly cameraFpsMap;
|
|
231
|
+
/**
|
|
232
|
+
* Load-shed state per camera:
|
|
233
|
+
* - `lowSinceTs`: timestamp of the first metrics snapshot that
|
|
234
|
+
* dipped below `fpsMinThreshold`, or `null` when fps is healthy.
|
|
235
|
+
* Camera pauses when `now - lowSinceTs >= fpsLowWindowMs`.
|
|
236
|
+
* Resets to `null` the moment fps recovers above the threshold,
|
|
237
|
+
* so transient dips (GC, model swap) don't accumulate.
|
|
238
|
+
* - `pausedAt`: timestamp when camera was paused (null = not paused)
|
|
239
|
+
* - `resumeAttempts`: number of auto-resume attempts (drives backoff)
|
|
240
|
+
*/
|
|
241
|
+
private readonly loadShedState;
|
|
242
|
+
/** Timer for the auto-resume sweep. */
|
|
243
|
+
private loadShedResumeTimer;
|
|
244
|
+
private initTimestamp;
|
|
245
|
+
constructor();
|
|
246
|
+
protected onInitialize(): Promise<AddonInitResult<PipelineOrchestratorActions>>;
|
|
247
|
+
/**
|
|
248
|
+
* One-shot migration from the legacy per-device flags
|
|
249
|
+
* (`audioEnabled:false` / `pipelineEnabled:false` in the orchestrator's
|
|
250
|
+
* device-store, `disableInference:true` in the cameraSettings map) to
|
|
251
|
+
* the new binding system. For each match we call
|
|
252
|
+
* `device-manager.setWrapperActive(false)` so the equivalent state
|
|
253
|
+
* sticks after the flags are gone. Guarded by an addon-store flag so
|
|
254
|
+
* it runs at most once per cluster install.
|
|
255
|
+
*
|
|
256
|
+
* Runs in the background after onInitialize — the `ctx.api` usually
|
|
257
|
+
* lights up after a short delay once the device-manager + capability
|
|
258
|
+
* registry finish wiring, so we poll briefly and give up with a warn
|
|
259
|
+
* when the window closes. Failure re-arms on next boot since we only
|
|
260
|
+
* persist the flag after a successful pass.
|
|
261
|
+
*/
|
|
262
|
+
private migrateLegacyFlagsToBindings;
|
|
263
|
+
/**
|
|
264
|
+
* Build the diagnostic snapshot exposed via the `dumpState` custom action.
|
|
265
|
+
* Pure read — never mutates state. Extracted from `handleCustomAction` so
|
|
266
|
+
* tests can assert the snapshot shape directly without going through the
|
|
267
|
+
* dispatcher.
|
|
268
|
+
*/
|
|
269
|
+
private dumpDiagnostics;
|
|
270
|
+
protected onShutdown(): Promise<void>;
|
|
271
|
+
dispatchCamera(runnerConfig: RunnerCameraConfig): Promise<DispatchResult>;
|
|
272
|
+
releaseCamera(input: {
|
|
273
|
+
deviceId: number;
|
|
274
|
+
}): Promise<{
|
|
275
|
+
success: true;
|
|
276
|
+
}>;
|
|
277
|
+
assignPipeline(input: {
|
|
278
|
+
deviceId: number;
|
|
279
|
+
agentNodeId: string;
|
|
280
|
+
}): Promise<{
|
|
281
|
+
success: true;
|
|
282
|
+
}>;
|
|
283
|
+
unassignPipeline(input: {
|
|
284
|
+
deviceId: number;
|
|
285
|
+
}): Promise<{
|
|
286
|
+
success: true;
|
|
287
|
+
}>;
|
|
288
|
+
rebalance(): Promise<{
|
|
289
|
+
migrated: number;
|
|
290
|
+
}>;
|
|
291
|
+
getPipelineAssignments(): readonly PipelineAssignment[];
|
|
292
|
+
getPipelineAssignment(input: {
|
|
293
|
+
deviceId: number;
|
|
294
|
+
}): PipelineAssignment | null;
|
|
295
|
+
/**
|
|
296
|
+
* Per-camera live metrics. Centralised entry point for the UI: the
|
|
297
|
+
* orchestrator owns the device→agent assignment map, so it knows
|
|
298
|
+
* which runner holds the camera and can route the cap call to the
|
|
299
|
+
* exact node. Calling `pipelineRunner.getCameraMetrics` directly
|
|
300
|
+
* from the UI breaks in multi-agent setups because the hub-side cap
|
|
301
|
+
* router resolves the runner singleton against the registry's first-
|
|
302
|
+
* registered provider, which races between agents and may not be the
|
|
303
|
+
* one the orchestrator actually dispatched the camera to.
|
|
304
|
+
*/
|
|
305
|
+
getCameraMetrics(input: {
|
|
306
|
+
deviceId: number;
|
|
307
|
+
}): Promise<CameraMetrics | null>;
|
|
308
|
+
/**
|
|
309
|
+
* Live snapshot of every runner's load. Triggers a fresh
|
|
310
|
+
* `collectAgentLoad()` on each call so the UI's "Live load" panel
|
|
311
|
+
* always reflects reality — the in-memory cache is only populated
|
|
312
|
+
* during dispatch / rebalance, so without a refresh here the panel
|
|
313
|
+
* would stay empty until the next dispatch event (problematic after
|
|
314
|
+
* a server restart that rehydrates assignments from disk without
|
|
315
|
+
* re-dispatching).
|
|
316
|
+
*/
|
|
317
|
+
getAgentLoad(): Promise<readonly AgentLoadSummary[]>;
|
|
318
|
+
getGlobalMetrics(): Promise<PipelineOrchestratorGlobalMetrics>;
|
|
319
|
+
/**
|
|
320
|
+
* Call `attachCamera` on the target runner via this.ctx.api. The generated cap
|
|
321
|
+
* router extracts `nodeId` from the input and routes to local or remote
|
|
322
|
+
* runner transparently — no broker reference needed.
|
|
323
|
+
*/
|
|
324
|
+
private attachOn;
|
|
325
|
+
private detachOn;
|
|
326
|
+
private localRunnerNodeId;
|
|
327
|
+
/**
|
|
328
|
+
* Enumerate every runner-capable node currently known (populated via
|
|
329
|
+
* AgentOnline/AgentOffline events). Used to populate the `enabledNodes`
|
|
330
|
+
* multiselect options at schema-build time.
|
|
331
|
+
*
|
|
332
|
+
* Forked child nodeIds (`hub/classifier`, `agent-1/motion-wasm`) are
|
|
333
|
+
* filtered out — they name isolated-process services, never dispatchable
|
|
334
|
+
* pipeline runners. The on-write guards in AgentOnline + applyRuntimeSettings
|
|
335
|
+
* already exclude them, but we filter here too so any legacy seed that
|
|
336
|
+
* slipped in before those guards existed doesn't leak into the UI picker.
|
|
337
|
+
*/
|
|
338
|
+
private collectRunnerNodeIds;
|
|
339
|
+
/**
|
|
340
|
+
* Enumerate every node currently known as decoder-capable. Reuses
|
|
341
|
+
* `knownRunnerNodes` since any runner node can potentially host a
|
|
342
|
+
* decoder session. `hub` is always included. Forked child nodeIds are
|
|
343
|
+
* filtered out for the same reason as `collectRunnerNodeIds`.
|
|
344
|
+
*/
|
|
345
|
+
private collectDecoderNodeIds;
|
|
346
|
+
/**
|
|
347
|
+
* Enumerate every node currently known as audio-analyzer-capable.
|
|
348
|
+
* Uses `knownRunnerNodes` as the universe and filters out child
|
|
349
|
+
* nodeIds (containing '/') the same way the other collectors do.
|
|
350
|
+
*/
|
|
351
|
+
private collectAudioNodeIds;
|
|
352
|
+
/**
|
|
353
|
+
* Resolve which node should run the decoder for a given device.
|
|
354
|
+
* Reads the per-device `decoderNodeId` pin first (manual override),
|
|
355
|
+
* then falls back to the `balanceDecoder` algorithm which prefers
|
|
356
|
+
* co-location with the pipeline node, then capacity.
|
|
357
|
+
*/
|
|
358
|
+
private resolveDecoderNode;
|
|
359
|
+
/**
|
|
360
|
+
* Return `true` when `nodeId` is in the `enabledNodes` whitelist.
|
|
361
|
+
* A strict list — an empty whitelist means "no node allowed", not
|
|
362
|
+
* "all nodes allowed". The schema default (`['hub']`) guarantees
|
|
363
|
+
* fresh installs always have at least the hub enabled.
|
|
364
|
+
*/
|
|
365
|
+
private isNodeEnabled;
|
|
366
|
+
/**
|
|
367
|
+
* Query every online runner in the cluster for its current load, plus the
|
|
368
|
+
* local runner if co-located. Refreshes the `cachedAgentLoad` snapshot so
|
|
369
|
+
* `getAgentLoad()` / `getGlobalMetrics()` can return fresh data.
|
|
370
|
+
*
|
|
371
|
+
* `{onlyEnabled: true}` applies the `enabledNodes` whitelist filter —
|
|
372
|
+
* used when the caller is the dispatcher / balancer and must only
|
|
373
|
+
* see runners eligible for new camera assignments. The default
|
|
374
|
+
* (unfiltered) collects every known runner, which is what the UI
|
|
375
|
+
* wants: operators need visibility into every connected node, even
|
|
376
|
+
* ones currently disabled from the dispatch pool.
|
|
377
|
+
*/
|
|
378
|
+
private collectAgentLoad;
|
|
379
|
+
private refreshAgentLoadCache;
|
|
380
|
+
private readPreferredAgent;
|
|
381
|
+
private readAudioNodePin;
|
|
382
|
+
private collectAudioNodeLoad;
|
|
383
|
+
private recordAudioAssignment;
|
|
384
|
+
private dispatchAudio;
|
|
385
|
+
private recordAssignment;
|
|
386
|
+
private emitUnassigned;
|
|
387
|
+
/**
|
|
388
|
+
* Failover handler. When a runner node goes offline, the behaviour is
|
|
389
|
+
* driven by the persisted failover policy (`failoverPolicy`):
|
|
390
|
+
* - `onDisconnect: 'migrate'` → move non-pinned cameras to another runner
|
|
391
|
+
* - `onDisconnect: 'leave-unassigned'` → drop all assignments for the node
|
|
392
|
+
* - `pinnedOnDisconnect: 'leave-pinned'` → pinned cameras stay unassigned
|
|
393
|
+
* - `pinnedOnDisconnect: 'unpin-and-migrate'` → clear the pin + migrate
|
|
394
|
+
*/
|
|
395
|
+
private handleNodeDisconnect;
|
|
396
|
+
/**
|
|
397
|
+
* Reconnect handler. When a runner node comes back online, the behaviour
|
|
398
|
+
* is driven by the `onReconnect` policy:
|
|
399
|
+
* - `restore` → re-attach cameras previously assigned to that node
|
|
400
|
+
* (only the ones still known in `cameraConfigs`)
|
|
401
|
+
* - `rebalance` → run a full `rebalance()` pass so non-pinned cameras
|
|
402
|
+
* can migrate back onto the newly-available runner
|
|
403
|
+
*/
|
|
404
|
+
private handleNodeConnect;
|
|
405
|
+
/**
|
|
406
|
+
* Subscribe to `detection-pipeline` readiness transitions for `nodeId`.
|
|
407
|
+
* Idempotent: re-subscribing the same node is a no-op. The handler
|
|
408
|
+
* skips same-epoch replays so repeated `ready` emits don't force
|
|
409
|
+
* redundant reseeds.
|
|
410
|
+
*/
|
|
411
|
+
private subscribeToDetectionPipelineReadiness;
|
|
412
|
+
private unsubscribeDetectionPipelineReadiness;
|
|
413
|
+
/**
|
|
414
|
+
* Subscribe to `audio-analyzer` readiness transitions for `nodeId`.
|
|
415
|
+
* Idempotent — re-subscribing the same node is a no-op (uses its own
|
|
416
|
+
* key space separate from detection readiness subscriptions).
|
|
417
|
+
*/
|
|
418
|
+
private subscribeToAudioAnalyzerReadiness;
|
|
419
|
+
private handleAudioAnalyzerReadiness;
|
|
420
|
+
/**
|
|
421
|
+
* Act on a `detection-pipeline` transition: on `ready` with a new
|
|
422
|
+
* epoch (= producer restart or first-time ready), invalidate the
|
|
423
|
+
* catalog cache, re-seed that node's `agentAddonDefaults`, and
|
|
424
|
+
* redispatch every camera currently assigned to it. Everything else
|
|
425
|
+
* (down, starting, same-epoch keepalive) is skipped.
|
|
426
|
+
*/
|
|
427
|
+
private handleDetectionPipelineReadiness;
|
|
428
|
+
/**
|
|
429
|
+
* Walk every cached RunnerCameraConfig and re-attach any whose stored
|
|
430
|
+
* `steps` are empty AND whose latest resolve produces a non-empty
|
|
431
|
+
* pipeline. Catches the "camera dispatched mid-readiness" race where
|
|
432
|
+
* the initial attach landed BEFORE the catalog was cached but the
|
|
433
|
+
* subsequent readiness sweep already finished iterating.
|
|
434
|
+
*/
|
|
435
|
+
private reattachStaleEmpties;
|
|
436
|
+
getCapabilityBindings(input: {
|
|
437
|
+
nodeId: string;
|
|
438
|
+
}): Promise<Readonly<Record<string, string>>>;
|
|
439
|
+
setCapabilityBinding(input: {
|
|
440
|
+
nodeId: string;
|
|
441
|
+
capName: string;
|
|
442
|
+
addonId: string;
|
|
443
|
+
}): Promise<{
|
|
444
|
+
success: true;
|
|
445
|
+
}>;
|
|
446
|
+
assignDecoder(input: {
|
|
447
|
+
readonly deviceId: number;
|
|
448
|
+
readonly nodeId: string;
|
|
449
|
+
}): Promise<void>;
|
|
450
|
+
unassignDecoder(input: {
|
|
451
|
+
readonly deviceId: number;
|
|
452
|
+
}): Promise<void>;
|
|
453
|
+
/**
|
|
454
|
+
* Per-camera decoder node accessor (Phase 8). Exposes the existing
|
|
455
|
+
* private `resolveDecoderNode` logic as a cap method so
|
|
456
|
+
* `stream-broker.createBroker` can filter its decoder provider
|
|
457
|
+
* selection deterministically. Caller hint: pass `pipelineNodeId` to
|
|
458
|
+
* bias the balancer toward co-location with the runner; omitted → the
|
|
459
|
+
* last-known assignment, else 'hub'.
|
|
460
|
+
*/
|
|
461
|
+
getDecoderAssignment(input: {
|
|
462
|
+
readonly deviceId: number;
|
|
463
|
+
readonly pipelineNodeId?: string;
|
|
464
|
+
}): Promise<DecoderAssignment>;
|
|
465
|
+
getDecoderAssignments(): Promise<readonly DecoderAssignment[]>;
|
|
466
|
+
assignAudio(input: {
|
|
467
|
+
readonly deviceId: number;
|
|
468
|
+
readonly nodeId: string;
|
|
469
|
+
}): Promise<{
|
|
470
|
+
success: true;
|
|
471
|
+
}>;
|
|
472
|
+
unassignAudio(input: {
|
|
473
|
+
readonly deviceId: number;
|
|
474
|
+
}): Promise<{
|
|
475
|
+
success: true;
|
|
476
|
+
}>;
|
|
477
|
+
getAudioAssignment(input: {
|
|
478
|
+
readonly deviceId: number;
|
|
479
|
+
}): Promise<{
|
|
480
|
+
nodeId: string;
|
|
481
|
+
pinned: boolean;
|
|
482
|
+
assignedAt: number;
|
|
483
|
+
} | null>;
|
|
484
|
+
getAudioNodeLoad(): Promise<Array<{
|
|
485
|
+
nodeId: string;
|
|
486
|
+
deviceCount: number;
|
|
487
|
+
}>>;
|
|
488
|
+
getAudioAssignments(): Promise<Array<{
|
|
489
|
+
deviceId: number;
|
|
490
|
+
nodeId: string;
|
|
491
|
+
pinned: boolean;
|
|
492
|
+
assignedAt: number;
|
|
493
|
+
}>>;
|
|
494
|
+
getAgentSettings(input: {
|
|
495
|
+
readonly agentNodeId: string;
|
|
496
|
+
}): Promise<AgentPipelineSettings | null>;
|
|
497
|
+
listAgentSettings(): Promise<readonly {
|
|
498
|
+
readonly nodeId: string;
|
|
499
|
+
readonly settings: AgentPipelineSettings;
|
|
500
|
+
}[]>;
|
|
501
|
+
setAgentAddonDefaults(input: {
|
|
502
|
+
readonly agentNodeId: string;
|
|
503
|
+
readonly defaults: Record<string, AgentAddonConfig>;
|
|
504
|
+
}): Promise<{
|
|
505
|
+
success: true;
|
|
506
|
+
}>;
|
|
507
|
+
removeAgentSettings(input: {
|
|
508
|
+
readonly agentNodeId: string;
|
|
509
|
+
}): Promise<{
|
|
510
|
+
success: boolean;
|
|
511
|
+
removed: boolean;
|
|
512
|
+
}>;
|
|
513
|
+
getCameraSettings(input: {
|
|
514
|
+
readonly deviceId: number;
|
|
515
|
+
}): Promise<CameraPipelineSettings | null>;
|
|
516
|
+
setCameraStepToggle(input: {
|
|
517
|
+
readonly deviceId: number;
|
|
518
|
+
readonly addonId: string;
|
|
519
|
+
readonly enabled: boolean | null;
|
|
520
|
+
}): Promise<{
|
|
521
|
+
success: true;
|
|
522
|
+
}>;
|
|
523
|
+
getCameraStepOverrides(input: {
|
|
524
|
+
readonly deviceId: number;
|
|
525
|
+
}): Promise<Record<string, Record<string, CameraStepOverridePatch>> | null>;
|
|
526
|
+
setCameraStepOverride(input: {
|
|
527
|
+
readonly deviceId: number;
|
|
528
|
+
readonly agentNodeId: string;
|
|
529
|
+
readonly addonId: string;
|
|
530
|
+
readonly patch: CameraStepOverridePatch | null;
|
|
531
|
+
}): Promise<{
|
|
532
|
+
success: true;
|
|
533
|
+
}>;
|
|
534
|
+
setCameraPipelineForAgent(input: {
|
|
535
|
+
readonly deviceId: number;
|
|
536
|
+
readonly agentNodeId: string;
|
|
537
|
+
readonly pipeline: {
|
|
538
|
+
steps: CameraPipelineConfig["steps"];
|
|
539
|
+
audio: {
|
|
540
|
+
modelId: string;
|
|
541
|
+
enabled: boolean;
|
|
542
|
+
} | null;
|
|
543
|
+
} | null;
|
|
544
|
+
}): Promise<{
|
|
545
|
+
success: true;
|
|
546
|
+
}>;
|
|
547
|
+
resolvePipeline(input: {
|
|
548
|
+
readonly deviceId: number;
|
|
549
|
+
readonly agentNodeId?: string;
|
|
550
|
+
}): Promise<CameraPipelineConfig>;
|
|
551
|
+
/** Emit `pipeline.camera-updated` carrying the freshly-resolved config. */
|
|
552
|
+
private emitResolvedCameraUpdated;
|
|
553
|
+
/** Re-dispatch every camera currently assigned to `nodeId` — used when agent defaults change. */
|
|
554
|
+
private redispatchCamerasOnAgent;
|
|
555
|
+
listTemplates(): Promise<readonly CameraPipelineTemplate[]>;
|
|
556
|
+
saveTemplate(input: {
|
|
557
|
+
readonly name: string;
|
|
558
|
+
readonly description?: string;
|
|
559
|
+
readonly config: CameraPipelineConfig;
|
|
560
|
+
}): Promise<CameraPipelineTemplate>;
|
|
561
|
+
updateTemplate(input: {
|
|
562
|
+
readonly id: string;
|
|
563
|
+
readonly name?: string;
|
|
564
|
+
readonly description?: string;
|
|
565
|
+
readonly config?: CameraPipelineConfig;
|
|
566
|
+
}): Promise<CameraPipelineTemplate>;
|
|
567
|
+
deleteTemplate(input: {
|
|
568
|
+
readonly id: string;
|
|
569
|
+
}): Promise<{
|
|
570
|
+
success: true;
|
|
571
|
+
}>;
|
|
572
|
+
/** Read the templates map from the addon store. */
|
|
573
|
+
private readTemplatesMap;
|
|
574
|
+
/** Emit the `pipeline.camera-updated` event so the internal subscriber hot-reloads the runner. */
|
|
575
|
+
private emitCameraUpdated;
|
|
576
|
+
/** Read the `agentSettings` map (keyed on `nodeId`) from the addon store. */
|
|
577
|
+
private readAgentSettingsMap;
|
|
578
|
+
/** Overwrite one agent's settings. Does NOT touch other agents' entries. */
|
|
579
|
+
private writeAgentSettings;
|
|
580
|
+
/** Read the `cameraSettings` map (keyed on `deviceId` as string) from the addon store. */
|
|
581
|
+
private readCameraSettingsMap;
|
|
582
|
+
/** Overwrite one camera's settings. */
|
|
583
|
+
private writeCameraSettings;
|
|
584
|
+
/**
|
|
585
|
+
* Fetch `pipelineExecutor.getSchema({nodeId})` through the standard cap
|
|
586
|
+
* router, cached per `(nodeId, engineKey)` with a short TTL. Returns
|
|
587
|
+
* `null` when the executor can't be reached (no runner yet attached)
|
|
588
|
+
* — callers fall back to an empty pipeline in that case.
|
|
589
|
+
*/
|
|
590
|
+
private getCatalogForAgent;
|
|
591
|
+
/**
|
|
592
|
+
* Ensure `agentSettings[nodeId]` exists and is up-to-date with the
|
|
593
|
+
* current catalog. Triggered on orchestrator init (for the hub) and on
|
|
594
|
+
* every `$node.connected` for remote runners.
|
|
595
|
+
*
|
|
596
|
+
* Behaviour:
|
|
597
|
+
* - Fetch catalog for the node.
|
|
598
|
+
* - Call `seedAgentAddonDefaults(current.addonDefaults, engine, catalog)` — preserves
|
|
599
|
+
* operator customisations, adds missing addons, drops orphans.
|
|
600
|
+
* - Persist if anything changed.
|
|
601
|
+
*
|
|
602
|
+
* Idempotent: a second call with the same catalog is a no-op.
|
|
603
|
+
*/
|
|
604
|
+
private seedAgentSettingsFromCatalog;
|
|
605
|
+
/**
|
|
606
|
+
* Fetch the engine triple (`runtime/backend/device`) from the
|
|
607
|
+
* `detection-pipeline` addon's global settings on the target node.
|
|
608
|
+
* This is the authoritative engine source since phase 2f. Returns
|
|
609
|
+
* `null` when the addon hasn't responded yet or doesn't expose the
|
|
610
|
+
* fields — callers fall back to `catalog.selectedEngine`.
|
|
611
|
+
*/
|
|
612
|
+
private readDetectionPipelineEngine;
|
|
613
|
+
/** Sleep helper used inside readiness retry loops. */
|
|
614
|
+
private static sleep;
|
|
615
|
+
/**
|
|
616
|
+
* Resolve the pipeline for a device. NEVER returns an empty/fallback
|
|
617
|
+
* config: if the catalog or agent settings aren't available yet, this
|
|
618
|
+
* blocks (with backoff) until the detection-pipeline cap is registered
|
|
619
|
+
* and a non-null catalog comes back. Cameras must never be dispatched
|
|
620
|
+
* with `engine=node/cpu, steps=[]` because the runner then attaches
|
|
621
|
+
* the camera with no steps and silently never recovers.
|
|
622
|
+
*
|
|
623
|
+
* Two legitimate exceptions to "never empty":
|
|
624
|
+
* 1. The device's `detection-pipeline` binding is INACTIVE — the
|
|
625
|
+
* operator explicitly turned inference off for this camera.
|
|
626
|
+
* 2. `cam.pipelineByAgent[agentNodeId]` carries a saved wholesale
|
|
627
|
+
* override with empty steps — also operator intent.
|
|
628
|
+
*
|
|
629
|
+
* Everything else (boot ordering, catalog cold-start, transient cap
|
|
630
|
+
* registration delay) loops with backoff until the catalog responds.
|
|
631
|
+
*/
|
|
632
|
+
private resolvePipelineForDevice;
|
|
633
|
+
/**
|
|
634
|
+
* Block until `pipeline-executor` is ready on `nodeId` AND
|
|
635
|
+
* `getCatalogForAgent` returns a non-null catalog AND `agentSettings`
|
|
636
|
+
* for that node has populated `addonDefaults`.
|
|
637
|
+
*
|
|
638
|
+
* `acquireCapability` defaults to infinite wait now — a single call
|
|
639
|
+
* carries the readiness gate. The thin retry below only handles
|
|
640
|
+
* the secondary case where the cap is "ready" but a derived call
|
|
641
|
+
* (getSchema) still returns null due to Moleculer service-registry
|
|
642
|
+
* propagation lag.
|
|
643
|
+
*/
|
|
644
|
+
private waitForAgentAndCatalog;
|
|
645
|
+
/**
|
|
646
|
+
* Block until a real engine choice is available. `acquireCapability`
|
|
647
|
+
* defaults to infinite wait; the post-acquire loop only handles the
|
|
648
|
+
* propagation gap where the cap is `ready` but the derived calls
|
|
649
|
+
* still return null for a tick.
|
|
650
|
+
*/
|
|
651
|
+
private waitForEngine;
|
|
652
|
+
/**
|
|
653
|
+
* Structural runtime check for `CameraPipelineConfig`. Persisted JSON
|
|
654
|
+
* may be stale or partial across upgrades, so we gate reads on shape
|
|
655
|
+
* rather than trusting the raw object. False = silently ignore that
|
|
656
|
+
* entry and fall back to defaults — the orchestrator's job is to
|
|
657
|
+
* never crash on corrupt store state.
|
|
658
|
+
*/
|
|
659
|
+
private isCameraPipelineConfig;
|
|
660
|
+
private isPipelineTemplate;
|
|
661
|
+
/**
|
|
662
|
+
* Generate a short template id. Collision risk at N≤1000 templates is
|
|
663
|
+
* effectively zero (12 hex chars = 48 bits of entropy). Not a UUID
|
|
664
|
+
* because the persisted shape is a plain map keyed by id and shorter
|
|
665
|
+
* keys keep the JSON compact.
|
|
666
|
+
*/
|
|
667
|
+
private generateTemplateId;
|
|
668
|
+
/** Build the addon-level schema (cluster-wide tunables: balancer + failover). */
|
|
669
|
+
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
670
|
+
/** Build the device-level schema (per-camera detection wiring). */
|
|
671
|
+
protected deviceSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
672
|
+
updateGlobalSettings(patch: Record<string, unknown>): Promise<void>;
|
|
673
|
+
private writeDeviceOrchestratorSettings;
|
|
674
|
+
getDeviceSettingsContribution(input: {
|
|
675
|
+
deviceId: number;
|
|
676
|
+
}): Promise<ConfigUISchemaWithValues | null>;
|
|
677
|
+
getDeviceLiveContribution(input: {
|
|
678
|
+
deviceId: number;
|
|
679
|
+
}): Promise<ConfigUISchemaWithValues | null>;
|
|
680
|
+
/**
|
|
681
|
+
* Best-effort camera-type check used to gate the settings/live
|
|
682
|
+
* contributions on non-camera devices (Lights, Switches, Sensors,
|
|
683
|
+
* Buttons). Returns `true` on lookup failure so a transient
|
|
684
|
+
* device-manager hiccup never silently hides legitimate camera
|
|
685
|
+
* sections.
|
|
686
|
+
*/
|
|
687
|
+
private isCameraDevice;
|
|
688
|
+
applyDeviceSettingsPatch(input: {
|
|
689
|
+
deviceId: number;
|
|
690
|
+
patch: Record<string, unknown>;
|
|
691
|
+
}): Promise<{
|
|
692
|
+
success: true;
|
|
693
|
+
}>;
|
|
694
|
+
/**
|
|
695
|
+
* Peel the pipeline-config keys out of an inbound patch. The remaining
|
|
696
|
+
* patch is forwarded to the existing device-level store writer. JSON
|
|
697
|
+
* textarea values are parsed here — `FormBuilder` serialises JSON
|
|
698
|
+
* editors as strings on submit, so the cap boundary has to reverse it
|
|
699
|
+
* before the content lands in the typed pipelines map.
|
|
700
|
+
*/
|
|
701
|
+
private splitPipelinePatch;
|
|
702
|
+
/**
|
|
703
|
+
* Apply a `cameraPipeline` patch emitted by the `pipeline-editor`
|
|
704
|
+
* ConfigField. Routes the full `CameraPipelineConfig` into the
|
|
705
|
+
* 3-level `cameraSettings[deviceId].pipelineByAgent[agent]` store.
|
|
706
|
+
* The target agent is the camera's current assignment (or the local
|
|
707
|
+
* hub when unassigned). Emits `pipeline.camera-updated` so the
|
|
708
|
+
* internal subscriber hot-reloads the runner.
|
|
709
|
+
*/
|
|
710
|
+
private applyPipelinePatch;
|
|
711
|
+
/** Tolerant JSON parser used to round-trip textarea-isJson fields. */
|
|
712
|
+
private tryParseJson;
|
|
713
|
+
/**
|
|
714
|
+
* Apply the subset of global settings that affect runtime behaviour
|
|
715
|
+
* (balancer thresholds, broker timeout, failover policy) into the
|
|
716
|
+
* in-memory state so subsequent `balance()` / `handleNodeDisconnect`
|
|
717
|
+
* calls use the new values immediately.
|
|
718
|
+
*/
|
|
719
|
+
private applyRuntimeSettings;
|
|
720
|
+
private get api();
|
|
721
|
+
/**
|
|
722
|
+
* Resolve this addon's own per-device detection settings — combines the
|
|
723
|
+
* raw device store with the device schema defaults via `hydrateSchema()`,
|
|
724
|
+
* then narrows to a typed DTO. Replaces the old call to
|
|
725
|
+
* `this.ctx.settings!.getDevice` which used the deleted multi-level resolver
|
|
726
|
+
* (the new model has no overlap between addon-level and device-level
|
|
727
|
+
* keys, so there's no merge across levels — only schema defaults are
|
|
728
|
+
* applied to fill in keys missing from the device store).
|
|
729
|
+
*/
|
|
730
|
+
/**
|
|
731
|
+
* True when `capName` is bound (`kind:'wrapped'`) for `deviceId` via
|
|
732
|
+
* `device-manager.getBindings`. Single source of truth for the
|
|
733
|
+
* "binding-gated cap" checks sprinkled around this addon (detection
|
|
734
|
+
* pipeline, audio analysis, …). Failures log-and-return `true` — the
|
|
735
|
+
* safe default is "cap on" unless the operator has explicitly toggled
|
|
736
|
+
* the wrapper off via `device-manager.setWrapperActive`.
|
|
737
|
+
*/
|
|
738
|
+
private isCapActiveForDevice;
|
|
739
|
+
private isDetectionPipelineActive;
|
|
740
|
+
private isAudioAnalysisActive;
|
|
741
|
+
private isMotionDetectionActive;
|
|
742
|
+
/**
|
|
743
|
+
* Resolve the device's feature array. Used by the resolver to pick
|
|
744
|
+
* the right `DeviceProfile` when computing per-camera scheduling
|
|
745
|
+
* defaults. Reads via the `device-manager.getDevice` cap because the
|
|
746
|
+
* orchestrator may run on a different node from the live IDevice;
|
|
747
|
+
* the live registry isn't available cross-process.
|
|
748
|
+
*/
|
|
749
|
+
private lookupDeviceFeatures;
|
|
750
|
+
/**
|
|
751
|
+
* True when the device has a native `motion` or `native-object-detection`
|
|
752
|
+
* cap registered by its driver — i.e. the camera does motion detection
|
|
753
|
+
* itself (Reolink Baichuan onboard PIR, ONVIF event-driven, etc.).
|
|
754
|
+
* Drives the dynamic `motionSources` default in
|
|
755
|
+
* `resolveDeviceDetectionSettings`: when the operator hasn't pinned a
|
|
756
|
+
* value, `['onboard']` is preferred over `['analyzer']` so we don't
|
|
757
|
+
* burn CPU running a redundant motion pipeline.
|
|
758
|
+
*
|
|
759
|
+
* Detection is via `device-manager.getBindings`: any binding entry of
|
|
760
|
+
* kind `'native'` whose capName is `motion` or
|
|
761
|
+
* `native-object-detection` qualifies. Failures default to `false` so
|
|
762
|
+
* we err toward the analyzer (never lose detection coverage when the
|
|
763
|
+
* binding lookup hiccups).
|
|
764
|
+
*/
|
|
765
|
+
private deviceHasOnboardMotionCap;
|
|
766
|
+
private resolveDeviceDetectionSettings;
|
|
767
|
+
/**
|
|
768
|
+
* True when the device has registered a native `motion` or
|
|
769
|
+
* `native-object-detection` cap provider — i.e. the camera's driver
|
|
770
|
+
* addon does motion detection on the device itself. Drives the
|
|
771
|
+
* dynamic motionSource default in `resolveDeviceDetectionSettings`.
|
|
772
|
+
* Each lookup is isolated: a throw for one cap (legitimate "no
|
|
773
|
+
* provider registered" from the registry) must not mask the presence
|
|
774
|
+
* of the other cap.
|
|
775
|
+
*/
|
|
776
|
+
/**
|
|
777
|
+
* Resolve the requested profile against the set of currently-assigned
|
|
778
|
+
* profile slots. Returns the profile id itself if the slot is live;
|
|
779
|
+
* otherwise walks the quality chain so a device missing the requested
|
|
780
|
+
* profile still gets a detection subscription on the closest assigned
|
|
781
|
+
* slot. Returns `null` only when nothing is assigned.
|
|
782
|
+
*/
|
|
783
|
+
private resolveAssignedProfile;
|
|
784
|
+
/**
|
|
785
|
+
* Fetch the current profile-slot map for a device from the system
|
|
786
|
+
* stream-broker cap (`listAllProfileSlots`). Returns a map of
|
|
787
|
+
* `CamProfile → sourceCamStreamId` so callers can both check which
|
|
788
|
+
* profiles are live AND resolve a profile to the cam-stream-keyed
|
|
789
|
+
* brokerId (`${deviceId}/${camStreamId}`) used by the broker.
|
|
790
|
+
*/
|
|
791
|
+
private fetchAssignedProfiles;
|
|
792
|
+
/**
|
|
793
|
+
* Build a `CameraDetectionConfig` from the resolved per-device
|
|
794
|
+
* orchestrator settings + the device's currently-assigned profile
|
|
795
|
+
* slots. Returns `null` when the device has no assigned slot or when
|
|
796
|
+
* settings cannot be resolved.
|
|
797
|
+
*
|
|
798
|
+
* `motionStreamId` / `detectionStreamId` hold a `camStreamId` value
|
|
799
|
+
* (e.g. `'native:main'`, `'rtsp:sub'`). The pipeline-runner
|
|
800
|
+
* concatenates `${deviceId}/${motionStreamId}` to build the broker
|
|
801
|
+
* id, matching the cam-stream-keyed brokers published by the
|
|
802
|
+
* stream-broker. The profile chosen by `motionStreamProfile` /
|
|
803
|
+
* `detectionStreamProfile` is resolved to the cam-stream backing it
|
|
804
|
+
* via the slot map fetched from `listAllProfileSlots`.
|
|
805
|
+
*/
|
|
806
|
+
buildDetectionConfig(deviceId: number): Promise<CameraDetectionConfig | null>;
|
|
807
|
+
/**
|
|
808
|
+
* Start detection for a camera: dispatch to runner via the orchestrator
|
|
809
|
+
* local path and subscribe to the audio broker if audio is enabled.
|
|
810
|
+
*/
|
|
811
|
+
startDetection(deviceId: number, config: CameraDetectionConfig): Promise<void>;
|
|
812
|
+
/** Stop detection for a camera: release via releaseCamera + audio cleanup. */
|
|
813
|
+
stopDetection(deviceId: number): Promise<void>;
|
|
814
|
+
/** Stop detection and purge all persisted config for a removed device. */
|
|
815
|
+
private handleDeviceUnregistered;
|
|
816
|
+
/**
|
|
817
|
+
* Handle a profile-slot change: build a fresh detection config from
|
|
818
|
+
* per-device settings and start detection if motion is enabled.
|
|
819
|
+
* Fires on `camera-streams.onProfileSlotsChanged` — any publish,
|
|
820
|
+
* retract, or assignment mutation in the stream-broker lands here.
|
|
821
|
+
*/
|
|
822
|
+
private handleDeviceRegistered;
|
|
823
|
+
/**
|
|
824
|
+
* Field-by-field comparison of two `CameraDetectionConfig` records.
|
|
825
|
+
* `motionSources` is an unordered set; everything else is a primitive.
|
|
826
|
+
*/
|
|
827
|
+
private static detectionConfigEquals;
|
|
828
|
+
/**
|
|
829
|
+
* Handle an orchestration settings update: rebuild the config and restart
|
|
830
|
+
* detection with the new values.
|
|
831
|
+
*/
|
|
832
|
+
private handleSettingsChanged;
|
|
833
|
+
/**
|
|
834
|
+
* Hot-reload the runner currently hosting `deviceId` with a fresh
|
|
835
|
+
* pipeline config. Called from the `PipelineCameraUpdated` subscriber —
|
|
836
|
+
* any mutation to `pipelines[deviceId]` flows through this path so the
|
|
837
|
+
* operator's edit reaches the next frame without a rebalance or a full
|
|
838
|
+
* detection restart.
|
|
839
|
+
*
|
|
840
|
+
* No-ops (with a debug log) when the camera has no cached
|
|
841
|
+
* RunnerCameraConfig — means the camera either isn't registered yet or
|
|
842
|
+
* isn't assigned to a runner. The next DeviceStreamsRegistered cycle
|
|
843
|
+
* will read the fresh config from the store and dispatch normally.
|
|
844
|
+
*/
|
|
845
|
+
private handlePipelineCameraUpdated;
|
|
846
|
+
/**
|
|
847
|
+
* Re-emit `PipelineInferenceResult` as `DetectionResult` when the
|
|
848
|
+
* frame carries detections. Kept deliberately thin: refinement
|
|
849
|
+
* (tracking, zone/state analysis, per-kind event persistence) is now
|
|
850
|
+
* owned end-to-end by `addon-pipeline-analytics`, which subscribes to
|
|
851
|
+
* `PipelineInferenceResult` directly. Consumers that want
|
|
852
|
+
* enriched-event-driven notifications subscribe to
|
|
853
|
+
* `pipeline-analytics.detection-event` + drive `notificationOutput`
|
|
854
|
+
* themselves.
|
|
855
|
+
*/
|
|
856
|
+
/**
|
|
857
|
+
* Load management enforcement — called on every camera metrics snapshot.
|
|
858
|
+
*
|
|
859
|
+
* Time-based window: a camera must stay below `fpsMinThreshold` for
|
|
860
|
+
* at least `fpsLowWindowMs` (default 60s) before it is paused. The
|
|
861
|
+
* window resets the moment fps recovers above threshold, so a
|
|
862
|
+
* transient dip (GC, model swap, brief stall) doesn't accumulate
|
|
863
|
+
* across recoveries.
|
|
864
|
+
*/
|
|
865
|
+
private static readonly DEFAULT_FPS_LOW_WINDOW_MS;
|
|
866
|
+
private enforceLoadManagement;
|
|
867
|
+
/** Base cooldown before first auto-resume attempt. */
|
|
868
|
+
private static readonly LOAD_SHED_BASE_COOLDOWN_MS;
|
|
869
|
+
/** Maximum backoff cap. */
|
|
870
|
+
private static readonly LOAD_SHED_MAX_COOLDOWN_MS;
|
|
871
|
+
/** How often the sweep runs. */
|
|
872
|
+
private static readonly LOAD_SHED_SWEEP_INTERVAL_MS;
|
|
873
|
+
private ensureLoadShedResumeTimer;
|
|
874
|
+
private loadShedResumeSweep;
|
|
875
|
+
/**
|
|
876
|
+
* Re-dispatch a single camera through the normal assignment pipeline.
|
|
877
|
+
* Used by auto-resume and settings-change recovery.
|
|
878
|
+
*/
|
|
879
|
+
private redispatchSingleCamera;
|
|
880
|
+
private handleInferenceResult;
|
|
881
|
+
/**
|
|
882
|
+
* Lazy audio gate handler. Fires on every `MotionOnMotionChanged`
|
|
883
|
+
* event for a device whose `audioMode === 'on-motion'`.
|
|
884
|
+
*
|
|
885
|
+
* - `detected:true` → cancel any pending teardown, open audio sub
|
|
886
|
+
* if not already open. Idempotent: re-fires of the same `true`
|
|
887
|
+
* while subscribed are no-ops aside from cancelling the timer.
|
|
888
|
+
* - `detected:false` → schedule teardown after the device's
|
|
889
|
+
* motion cooldown window. Mirrors the runner's own `'on-motion'`
|
|
890
|
+
* detection-mode behaviour where the camera stays in `active`
|
|
891
|
+
* for the cooldown window after the last motion ping.
|
|
892
|
+
*
|
|
893
|
+
* Idempotency is critical because `MotionOnMotionChanged` can fire
|
|
894
|
+
* frequently (every 1-2s while motion is active on Reolink); each
|
|
895
|
+
* rearm just keeps the audio stream open without restarting it.
|
|
896
|
+
*/
|
|
897
|
+
private handleLazyAudioMotion;
|
|
898
|
+
/**
|
|
899
|
+
* Subscribe to decoded audio chunks for a camera and feed them into the
|
|
900
|
+
* audio-analyzer. Reads the analyzer's settings via its own
|
|
901
|
+
* `resolveDeviceSettings(deviceId)` method so the orchestrator does not
|
|
902
|
+
* touch the audio-analyzer schema field names directly.
|
|
903
|
+
*/
|
|
904
|
+
private subscribeAudioStream;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export { type BalancerDecision, type BalancerInput, type DecoderBalancerDecision, type DecoderBalancerInput, type DispatchResult, type IPipelineOrchestratorLocalProvider, type OrchestratorDiagnostics, type PipelineOrchestratorActions, balance, balanceDecoder, computeCapacityScore, PipelineOrchestratorAddon as default, pipelineOrchestratorActions };
|