@camstack/addon-pipeline 1.1.21 → 1.1.23
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/decoder-ffmpeg/index.js +1 -1
- package/dist/decoder-ffmpeg/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/{dist-BiP1gPeY.js → dist-CE3a05qT.js} +228 -16
- package/dist/{dist-tzhTTRq4.mjs → dist-pd_-3C0T.mjs} +228 -16
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +412 -247
- package/dist/pipeline-runner/index.mjs +412 -247
- package/dist/recorder/index.js +260 -234
- package/dist/recorder/index.mjs +260 -234
- package/dist/stream-broker/_stub.js +378 -324
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D08qJTw_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C0QtG7oR.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs} +1 -1
- package/dist/stream-broker/{hostInit-CGnMNtPl.mjs → hostInit-DHej3Ep9.mjs} +2 -2
- package/dist/stream-broker/index.js +13788 -12378
- package/dist/stream-broker/index.mjs +13907 -12497
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CW4Qu4V6.js → MaskShapeCanvas-DI4BY7W2-BqXU75it.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CHJ39tfi.js → MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-dInJ7kcz.js → PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js} +1 -1
- package/embed-dist/assets/index-DhdVxWXL.js +80 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BSWrW78G.mjs +0 -26
- package/embed-dist/assets/index-CKpVWKSG.js +0 -80
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as BaseAddon, J as hydrateSchema, U as DeviceType, k as motionDetectionCapability, w as evaluateZoneRules } from "../dist-
|
|
1
|
+
import { B as BaseAddon, J as hydrateSchema, U as DeviceType, k as motionDetectionCapability, w as evaluateZoneRules } from "../dist-pd_-3C0T.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
@@ -2,8 +2,388 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CE3a05qT.js");
|
|
6
6
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
7
|
+
//#region src/pipeline-runner/bench-actions.ts
|
|
8
|
+
/**
|
|
9
|
+
* Synthetic-bench custom actions for pipeline-runner.
|
|
10
|
+
*
|
|
11
|
+
* Lets the admin UI drive a production-realistic detection benchmark:
|
|
12
|
+
* the runner — already co-located with detection-pipeline in the
|
|
13
|
+
* `pipeline` group — invokes `api.pipelineExecutor.runPipeline` directly
|
|
14
|
+
* (in-process via localProviderLink, no Moleculer hop), N workers
|
|
15
|
+
* concurrent × M iterations. Returns aggregate stats.
|
|
16
|
+
*
|
|
17
|
+
* Why this exists: the older bench path (UI → benchmark addon → pipeline
|
|
18
|
+
* addon) crosses TWO Moleculer process boundaries that the production
|
|
19
|
+
* camera path does NOT have (broker / runner / detection-pipeline are
|
|
20
|
+
* all in the same group). Running the loop from inside the runner gives
|
|
21
|
+
* fps numbers that match what real cameras achieve.
|
|
22
|
+
*/
|
|
23
|
+
var BenchEngineChoiceSchema = require_dist.object({
|
|
24
|
+
runtime: require_dist._enum(["node", "python"]),
|
|
25
|
+
backend: require_dist.string(),
|
|
26
|
+
format: require_dist._enum([
|
|
27
|
+
"onnx",
|
|
28
|
+
"coreml",
|
|
29
|
+
"openvino",
|
|
30
|
+
"tflite",
|
|
31
|
+
"pt"
|
|
32
|
+
]),
|
|
33
|
+
device: require_dist.string().optional()
|
|
34
|
+
});
|
|
35
|
+
var BenchStepSchema = require_dist.lazy(() => require_dist.object({
|
|
36
|
+
addonId: require_dist.string(),
|
|
37
|
+
modelId: require_dist.string(),
|
|
38
|
+
enabled: require_dist.boolean(),
|
|
39
|
+
children: require_dist.array(BenchStepSchema).optional()
|
|
40
|
+
}));
|
|
41
|
+
var CacheBenchFrameInputSchema = require_dist.object({
|
|
42
|
+
imageBase64: require_dist.string(),
|
|
43
|
+
ttlSeconds: require_dist.number().int().positive().optional()
|
|
44
|
+
});
|
|
45
|
+
var CacheBenchFrameResultSchema = require_dist.object({
|
|
46
|
+
frameId: require_dist.string(),
|
|
47
|
+
width: require_dist.number(),
|
|
48
|
+
height: require_dist.number(),
|
|
49
|
+
expiresAt: require_dist.number()
|
|
50
|
+
});
|
|
51
|
+
var ReleaseBenchFrameInputSchema = require_dist.object({ frameId: require_dist.string() });
|
|
52
|
+
var ReleaseBenchFrameResultSchema = require_dist.object({ released: require_dist.boolean() });
|
|
53
|
+
var RunSyntheticBenchInputSchema = require_dist.object({
|
|
54
|
+
frameId: require_dist.string(),
|
|
55
|
+
steps: require_dist.array(BenchStepSchema).min(1),
|
|
56
|
+
parallel: require_dist.number().int().min(1).max(32),
|
|
57
|
+
iterations: require_dist.number().int().min(1).max(1e4),
|
|
58
|
+
warmup: require_dist.number().int().min(0).max(100).optional(),
|
|
59
|
+
sessionId: require_dist.string().optional(),
|
|
60
|
+
simulatePipeline: require_dist.boolean().optional(),
|
|
61
|
+
nodeId: require_dist.string().optional(),
|
|
62
|
+
engine: BenchEngineChoiceSchema.optional()
|
|
63
|
+
});
|
|
64
|
+
var TimingSplitSchema = require_dist.object({
|
|
65
|
+
mean: require_dist.number(),
|
|
66
|
+
p50: require_dist.number(),
|
|
67
|
+
p95: require_dist.number(),
|
|
68
|
+
p99: require_dist.number()
|
|
69
|
+
});
|
|
70
|
+
var RunSyntheticBenchResultSchema = require_dist.object({
|
|
71
|
+
runs: require_dist.number(),
|
|
72
|
+
wallSec: require_dist.number(),
|
|
73
|
+
fps: require_dist.number(),
|
|
74
|
+
detectionsPerSec: require_dist.number(),
|
|
75
|
+
avgDetections: require_dist.number(),
|
|
76
|
+
callMs: TimingSplitSchema,
|
|
77
|
+
inferMs: require_dist.number(),
|
|
78
|
+
preprocessMs: require_dist.number(),
|
|
79
|
+
predictMs: require_dist.number(),
|
|
80
|
+
batchSizeMean: require_dist.number(),
|
|
81
|
+
batchSizeMax: require_dist.number(),
|
|
82
|
+
engine: require_dist.object({
|
|
83
|
+
runtime: require_dist.string(),
|
|
84
|
+
backend: require_dist.string(),
|
|
85
|
+
device: require_dist.string().optional()
|
|
86
|
+
}).optional(),
|
|
87
|
+
tuning: require_dist.object({
|
|
88
|
+
batchMode: require_dist.string(),
|
|
89
|
+
windowMs: require_dist.number(),
|
|
90
|
+
maxBatchSize: require_dist.number(),
|
|
91
|
+
concurrency: require_dist.number()
|
|
92
|
+
}).optional(),
|
|
93
|
+
path: require_dist.string().optional()
|
|
94
|
+
});
|
|
95
|
+
var pipelineRunnerBenchActions = require_dist.defineCustomActions({
|
|
96
|
+
cacheBenchFrame: require_dist.customAction(CacheBenchFrameInputSchema, CacheBenchFrameResultSchema, { kind: "mutation" }),
|
|
97
|
+
releaseBenchFrame: require_dist.customAction(ReleaseBenchFrameInputSchema, ReleaseBenchFrameResultSchema, { kind: "mutation" }),
|
|
98
|
+
runSyntheticBench: require_dist.customAction(RunSyntheticBenchInputSchema, RunSyntheticBenchResultSchema, { kind: "mutation" })
|
|
99
|
+
});
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/pipeline-runner/foreign-handle-throttle.ts
|
|
102
|
+
var ForeignHandleThrottle = class {
|
|
103
|
+
minFetchIntervalMs;
|
|
104
|
+
warnIntervalMs;
|
|
105
|
+
lastFetchStartedAt = Number.NEGATIVE_INFINITY;
|
|
106
|
+
inFlight = false;
|
|
107
|
+
lastWarnAt;
|
|
108
|
+
_foreignHandles = 0;
|
|
109
|
+
_remoteFetches = 0;
|
|
110
|
+
_remoteFailures = 0;
|
|
111
|
+
_dropped = 0;
|
|
112
|
+
constructor(options = {}) {
|
|
113
|
+
const maxRemoteFps = options.maxRemoteFps ?? 1;
|
|
114
|
+
this.minFetchIntervalMs = maxRemoteFps > 0 ? 1e3 / maxRemoteFps : Number.POSITIVE_INFINITY;
|
|
115
|
+
this.warnIntervalMs = options.warnIntervalMs ?? 6e4;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decide whether the current foreign handle should trigger a remote fetch.
|
|
119
|
+
* `'fetch'` only when no fetch is in flight AND at least `1000/maxRemoteFps`
|
|
120
|
+
* ms have elapsed since the last fetch started; otherwise `'drop'`.
|
|
121
|
+
*/
|
|
122
|
+
plan(now) {
|
|
123
|
+
this._foreignHandles += 1;
|
|
124
|
+
const rateOpen = now - this.lastFetchStartedAt >= this.minFetchIntervalMs;
|
|
125
|
+
if (this.inFlight || !rateOpen) {
|
|
126
|
+
this._dropped += 1;
|
|
127
|
+
return "drop";
|
|
128
|
+
}
|
|
129
|
+
return "fetch";
|
|
130
|
+
}
|
|
131
|
+
/** Mark a remote fetch as begun (arms single-flight + the rate window). */
|
|
132
|
+
noteFetchStart(now) {
|
|
133
|
+
this.inFlight = true;
|
|
134
|
+
this.lastFetchStartedAt = now;
|
|
135
|
+
this._remoteFetches += 1;
|
|
136
|
+
}
|
|
137
|
+
/** Mark the in-flight remote fetch as finished; records a failure outcome. */
|
|
138
|
+
noteFetchEnd(result = "ok") {
|
|
139
|
+
this.inFlight = false;
|
|
140
|
+
if (result === "failed") this._remoteFailures += 1;
|
|
141
|
+
}
|
|
142
|
+
/** `true` at most once per `warnIntervalMs`; stamps the window when it fires. */
|
|
143
|
+
shouldWarn(now) {
|
|
144
|
+
if (this.lastWarnAt !== void 0 && now - this.lastWarnAt < this.warnIntervalMs) return false;
|
|
145
|
+
this.lastWarnAt = now;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
get counters() {
|
|
149
|
+
return {
|
|
150
|
+
foreignHandles: this._foreignHandles,
|
|
151
|
+
remoteFetches: this._remoteFetches,
|
|
152
|
+
remoteFailures: this._remoteFailures,
|
|
153
|
+
dropped: this._dropped
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/pipeline-runner/resolve-polled-frame.ts
|
|
159
|
+
async function resolvePolledFrame(handle, deps) {
|
|
160
|
+
const { ownNodeId, readers, throttle, getRemoteFrame, logger, tag } = deps;
|
|
161
|
+
if (handle.nodeId === ownNodeId) return readers.read(handle);
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
const decision = throttle.plan(now);
|
|
164
|
+
if (throttle.shouldWarn(now)) logger.warn("frame poller: foreign handle — decoder session is on another node; using bounded decoder.getFrame fallback", { meta: {
|
|
165
|
+
tag,
|
|
166
|
+
producerNode: handle.nodeId,
|
|
167
|
+
ownNodeId,
|
|
168
|
+
...throttle.counters
|
|
169
|
+
} });
|
|
170
|
+
if (decision === "drop") return null;
|
|
171
|
+
throttle.noteFetchStart(now);
|
|
172
|
+
try {
|
|
173
|
+
const remote = await getRemoteFrame(handle);
|
|
174
|
+
throttle.noteFetchEnd("ok");
|
|
175
|
+
return remote;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throttle.noteFetchEnd("failed");
|
|
178
|
+
logger.debug("frame poller: remote getFrame failed", { meta: {
|
|
179
|
+
tag,
|
|
180
|
+
producerNode: handle.nodeId,
|
|
181
|
+
error: require_dist.errMsg(err)
|
|
182
|
+
} });
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/pipeline-runner/frame-handle-poller.ts
|
|
188
|
+
/**
|
|
189
|
+
* `FrameHandlePoller` — the consumer-side poll loop of the shared-memory frame
|
|
190
|
+
* plane (Phase 5 / D9).
|
|
191
|
+
*
|
|
192
|
+
* Replaces the live-object `IStreamBroker.onDecodedFrame(cb)` callback path.
|
|
193
|
+
* A consumer (motion, detection):
|
|
194
|
+
*
|
|
195
|
+
* 1. `subscribeFrames({ brokerId, format, maxFps, tag })` over tRPC — the
|
|
196
|
+
* broker spins up a `frameSink: 'shm'` decoder session for that format
|
|
197
|
+
* and returns a `subscriptionId`;
|
|
198
|
+
* 2. polls `pullFrameHandles({ subscriptionId, maxCount })` at the cadence
|
|
199
|
+
* `maxFps` implies, draining zero-pixel `FrameHandle`s;
|
|
200
|
+
* 3. feeds each handle to a `FrameRingReaderCache` which opens the named shm
|
|
201
|
+
* segment and reads the pixels back zero-copy;
|
|
202
|
+
* 4. on teardown, `unsubscribeFrames` + closes the cached readers.
|
|
203
|
+
*
|
|
204
|
+
* The loop tolerates an initially-empty stream — `pullFrameHandles` returns
|
|
205
|
+
* `[]` until the broker has a source, and a recycled ring slot yields a `null`
|
|
206
|
+
* pixel read (a dropped frame, the correct latest-wins behaviour).
|
|
207
|
+
*
|
|
208
|
+
* Foreign-handle path: a `FrameHandle` carries the `nodeId` of the node whose
|
|
209
|
+
* shared memory holds its segment. A LOCAL handle is read back zero-copy; a
|
|
210
|
+
* FOREIGN handle (produced on another node — e.g. a camera decoded on the hub
|
|
211
|
+
* whose detection was placed on an agent) cannot be `shm_open`ed here. Reading
|
|
212
|
+
* it unconditionally floods `openSegment failed shm_open ENOENT` at frame rate,
|
|
213
|
+
* so the poll delegates every handle to `resolvePolledFrame`, which gates on
|
|
214
|
+
* `handle.nodeId`: local → shm read; foreign → a BOUNDED, single-flight
|
|
215
|
+
* `getRemoteFrame` (`decoder.getFrame`) fallback (≤ `maxRemoteFps`) plus a
|
|
216
|
+
* rate-limited WARN naming the topology mismatch. `ownNodeId` + `getRemoteFrame`
|
|
217
|
+
* are REQUIRED options — every in-repo call site supplies them, so there is no
|
|
218
|
+
* silent default that re-creates the unconditional read.
|
|
219
|
+
*
|
|
220
|
+
* Boot-race tolerance: the broker for a given camStream may not be registered
|
|
221
|
+
* yet when a consumer attempts to subscribe (the pipeline-runner spawns before
|
|
222
|
+
* provider addons publish their cameraStreams). `subscribeFrames` retries with
|
|
223
|
+
* exponential backoff (capped at `MAX_SUBSCRIBE_RETRY_BACKOFF_MS`) until it
|
|
224
|
+
* succeeds or the returned teardown closure is invoked. The closure always
|
|
225
|
+
* resolves to a function, even when the broker is missing; the function
|
|
226
|
+
* cancels pending retries and releases any subscription acquired in flight.
|
|
227
|
+
*/
|
|
228
|
+
/** How many handles to drain per poll — a small burst absorbs jitter. */
|
|
229
|
+
var PULL_MAX_COUNT = 4;
|
|
230
|
+
/** Floor on the poll period so a 0/absurd `maxFps` can't busy-spin. */
|
|
231
|
+
var MIN_POLL_INTERVAL_MS = 20;
|
|
232
|
+
/** Poll period when the broker reports a non-positive cadence hint. */
|
|
233
|
+
var FALLBACK_POLL_INTERVAL_MS = 200;
|
|
234
|
+
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
235
|
+
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
236
|
+
/**
|
|
237
|
+
* Subscribe-retry backoff ceiling. 5 s is fast enough to recover within a
|
|
238
|
+
* single roster refresh of the consuming UI and slow enough that a
|
|
239
|
+
* misconfigured camStream costs ~12 op/min, not 50/s.
|
|
240
|
+
*/
|
|
241
|
+
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
242
|
+
/**
|
|
243
|
+
* Subscribe to a broker's shm frame stream and start the poll loop. Always
|
|
244
|
+
* resolves to a teardown closure — when the broker is not yet registered the
|
|
245
|
+
* closure cancels the ongoing retry loop; when polling is active it stops the
|
|
246
|
+
* loop, releases the broker subscription, and closes every shm segment the
|
|
247
|
+
* reader cache opened.
|
|
248
|
+
*/
|
|
249
|
+
async function startFrameHandlePoller(options) {
|
|
250
|
+
const lifecycle = {
|
|
251
|
+
stopped: false,
|
|
252
|
+
retryTimer: void 0,
|
|
253
|
+
activeTeardown: null
|
|
254
|
+
};
|
|
255
|
+
const teardown = () => {
|
|
256
|
+
if (lifecycle.stopped) return;
|
|
257
|
+
lifecycle.stopped = true;
|
|
258
|
+
if (lifecycle.retryTimer) {
|
|
259
|
+
clearTimeout(lifecycle.retryTimer);
|
|
260
|
+
lifecycle.retryTimer = void 0;
|
|
261
|
+
}
|
|
262
|
+
lifecycle.activeTeardown?.();
|
|
263
|
+
};
|
|
264
|
+
subscribeWithRetry(options, lifecycle);
|
|
265
|
+
return teardown;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Run the subscribe → poll handshake with exponential backoff on subscribe
|
|
269
|
+
* failures. Resolves once the subscription is acquired (and the poll loop has
|
|
270
|
+
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
271
|
+
*/
|
|
272
|
+
async function subscribeWithRetry(options, lifecycle) {
|
|
273
|
+
const { api, brokerId, format, maxFps, tag, logger } = options;
|
|
274
|
+
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
275
|
+
let attempt = 0;
|
|
276
|
+
while (!lifecycle.stopped) {
|
|
277
|
+
attempt += 1;
|
|
278
|
+
try {
|
|
279
|
+
const result = await api.streamBroker.subscribeFrames.mutate({
|
|
280
|
+
brokerId,
|
|
281
|
+
format,
|
|
282
|
+
maxFps,
|
|
283
|
+
tag
|
|
284
|
+
});
|
|
285
|
+
if (lifecycle.stopped) {
|
|
286
|
+
await api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
287
|
+
logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
|
|
288
|
+
brokerId,
|
|
289
|
+
subscriptionId: result.subscriptionId,
|
|
290
|
+
error: require_dist.errMsg(err)
|
|
291
|
+
} });
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
|
|
296
|
+
return;
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (lifecycle.stopped) return;
|
|
299
|
+
if (attempt === 1) logger.warn("frame-handle poller: subscribeFrames failed, retrying", { meta: {
|
|
300
|
+
brokerId,
|
|
301
|
+
format,
|
|
302
|
+
tag,
|
|
303
|
+
error: require_dist.errMsg(err),
|
|
304
|
+
nextRetryInMs: backoffMs
|
|
305
|
+
} });
|
|
306
|
+
else logger.debug("frame-handle poller: subscribeFrames still failing", { meta: {
|
|
307
|
+
brokerId,
|
|
308
|
+
format,
|
|
309
|
+
tag,
|
|
310
|
+
attempt,
|
|
311
|
+
error: require_dist.errMsg(err),
|
|
312
|
+
nextRetryInMs: backoffMs
|
|
313
|
+
} });
|
|
314
|
+
await sleep(backoffMs, lifecycle);
|
|
315
|
+
backoffMs = Math.min(MAX_SUBSCRIBE_RETRY_BACKOFF_MS, backoffMs * 2);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Sleep `ms` milliseconds, but resolve early when `lifecycle.stopped` flips so
|
|
321
|
+
* a teardown during a retry pause doesn't have to wait out the full backoff.
|
|
322
|
+
*/
|
|
323
|
+
function sleep(ms, lifecycle) {
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
lifecycle.retryTimer = setTimeout(() => {
|
|
326
|
+
lifecycle.retryTimer = void 0;
|
|
327
|
+
resolve();
|
|
328
|
+
}, ms);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run the steady-state poll loop for a successfully-acquired subscription.
|
|
333
|
+
* Returns the teardown closure that stops the loop and releases the
|
|
334
|
+
* subscription.
|
|
335
|
+
*/
|
|
336
|
+
function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
337
|
+
const { api, brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
|
|
338
|
+
const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
|
|
339
|
+
const throttle = new ForeignHandleThrottle();
|
|
340
|
+
const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
|
|
341
|
+
let timer;
|
|
342
|
+
const tick = async () => {
|
|
343
|
+
if (lifecycle.stopped) return;
|
|
344
|
+
try {
|
|
345
|
+
const handles = await api.streamBroker.pullFrameHandles.query({
|
|
346
|
+
subscriptionId,
|
|
347
|
+
maxCount: PULL_MAX_COUNT
|
|
348
|
+
});
|
|
349
|
+
for (const handle of handles) {
|
|
350
|
+
if (lifecycle.stopped) break;
|
|
351
|
+
const frame = await resolvePolledFrame(handle, {
|
|
352
|
+
ownNodeId,
|
|
353
|
+
readers,
|
|
354
|
+
throttle,
|
|
355
|
+
getRemoteFrame,
|
|
356
|
+
logger,
|
|
357
|
+
tag
|
|
358
|
+
});
|
|
359
|
+
if (frame) onFrame(frame, handle);
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.warn("frame-handle poller: pullFrameHandles failed", { meta: {
|
|
363
|
+
brokerId,
|
|
364
|
+
subscriptionId,
|
|
365
|
+
error: require_dist.errMsg(err)
|
|
366
|
+
} });
|
|
367
|
+
}
|
|
368
|
+
if (!lifecycle.stopped) timer = setTimeout(() => void tick(), pollIntervalMs);
|
|
369
|
+
};
|
|
370
|
+
tick();
|
|
371
|
+
return () => {
|
|
372
|
+
if (timer) {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
timer = void 0;
|
|
375
|
+
}
|
|
376
|
+
readers.close();
|
|
377
|
+
api.streamBroker.unsubscribeFrames.mutate({ subscriptionId }).catch((err) => {
|
|
378
|
+
logger.warn("frame-handle poller: unsubscribeFrames failed", { meta: {
|
|
379
|
+
brokerId,
|
|
380
|
+
subscriptionId,
|
|
381
|
+
error: require_dist.errMsg(err)
|
|
382
|
+
} });
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
7
387
|
//#region src/pipeline-runner/frame-queue.ts
|
|
8
388
|
/**
|
|
9
389
|
* Latest-frame-only buffer. Keeps only the most recent item, dropping all
|
|
@@ -697,252 +1077,6 @@ var PipelineRunner = class {
|
|
|
697
1077
|
}
|
|
698
1078
|
};
|
|
699
1079
|
//#endregion
|
|
700
|
-
//#region src/pipeline-runner/frame-handle-poller.ts
|
|
701
|
-
/** How many handles to drain per poll — a small burst absorbs jitter. */
|
|
702
|
-
var PULL_MAX_COUNT = 4;
|
|
703
|
-
/** Floor on the poll period so a 0/absurd `maxFps` can't busy-spin. */
|
|
704
|
-
var MIN_POLL_INTERVAL_MS = 20;
|
|
705
|
-
/** Poll period when the broker reports a non-positive cadence hint. */
|
|
706
|
-
var FALLBACK_POLL_INTERVAL_MS = 200;
|
|
707
|
-
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
708
|
-
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
709
|
-
/**
|
|
710
|
-
* Subscribe-retry backoff ceiling. 5 s is fast enough to recover within a
|
|
711
|
-
* single roster refresh of the consuming UI and slow enough that a
|
|
712
|
-
* misconfigured camStream costs ~12 op/min, not 50/s.
|
|
713
|
-
*/
|
|
714
|
-
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
715
|
-
/**
|
|
716
|
-
* Subscribe to a broker's shm frame stream and start the poll loop. Always
|
|
717
|
-
* resolves to a teardown closure — when the broker is not yet registered the
|
|
718
|
-
* closure cancels the ongoing retry loop; when polling is active it stops the
|
|
719
|
-
* loop, releases the broker subscription, and closes every shm segment the
|
|
720
|
-
* reader cache opened.
|
|
721
|
-
*/
|
|
722
|
-
async function startFrameHandlePoller(options) {
|
|
723
|
-
const lifecycle = {
|
|
724
|
-
stopped: false,
|
|
725
|
-
retryTimer: void 0,
|
|
726
|
-
activeTeardown: null
|
|
727
|
-
};
|
|
728
|
-
const teardown = () => {
|
|
729
|
-
if (lifecycle.stopped) return;
|
|
730
|
-
lifecycle.stopped = true;
|
|
731
|
-
if (lifecycle.retryTimer) {
|
|
732
|
-
clearTimeout(lifecycle.retryTimer);
|
|
733
|
-
lifecycle.retryTimer = void 0;
|
|
734
|
-
}
|
|
735
|
-
lifecycle.activeTeardown?.();
|
|
736
|
-
};
|
|
737
|
-
subscribeWithRetry(options, lifecycle);
|
|
738
|
-
return teardown;
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Run the subscribe → poll handshake with exponential backoff on subscribe
|
|
742
|
-
* failures. Resolves once the subscription is acquired (and the poll loop has
|
|
743
|
-
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
744
|
-
*/
|
|
745
|
-
async function subscribeWithRetry(options, lifecycle) {
|
|
746
|
-
const { api, brokerId, format, maxFps, tag, logger } = options;
|
|
747
|
-
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
748
|
-
let attempt = 0;
|
|
749
|
-
while (!lifecycle.stopped) {
|
|
750
|
-
attempt += 1;
|
|
751
|
-
try {
|
|
752
|
-
const result = await api.streamBroker.subscribeFrames.mutate({
|
|
753
|
-
brokerId,
|
|
754
|
-
format,
|
|
755
|
-
maxFps,
|
|
756
|
-
tag
|
|
757
|
-
});
|
|
758
|
-
if (lifecycle.stopped) {
|
|
759
|
-
await api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
760
|
-
logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
|
|
761
|
-
brokerId,
|
|
762
|
-
subscriptionId: result.subscriptionId,
|
|
763
|
-
error: require_dist.errMsg(err)
|
|
764
|
-
} });
|
|
765
|
-
});
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
|
|
769
|
-
return;
|
|
770
|
-
} catch (err) {
|
|
771
|
-
if (lifecycle.stopped) return;
|
|
772
|
-
if (attempt === 1) logger.warn("frame-handle poller: subscribeFrames failed, retrying", { meta: {
|
|
773
|
-
brokerId,
|
|
774
|
-
format,
|
|
775
|
-
tag,
|
|
776
|
-
error: require_dist.errMsg(err),
|
|
777
|
-
nextRetryInMs: backoffMs
|
|
778
|
-
} });
|
|
779
|
-
else logger.debug("frame-handle poller: subscribeFrames still failing", { meta: {
|
|
780
|
-
brokerId,
|
|
781
|
-
format,
|
|
782
|
-
tag,
|
|
783
|
-
attempt,
|
|
784
|
-
error: require_dist.errMsg(err),
|
|
785
|
-
nextRetryInMs: backoffMs
|
|
786
|
-
} });
|
|
787
|
-
await sleep(backoffMs, lifecycle);
|
|
788
|
-
backoffMs = Math.min(MAX_SUBSCRIBE_RETRY_BACKOFF_MS, backoffMs * 2);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Sleep `ms` milliseconds, but resolve early when `lifecycle.stopped` flips so
|
|
794
|
-
* a teardown during a retry pause doesn't have to wait out the full backoff.
|
|
795
|
-
*/
|
|
796
|
-
function sleep(ms, lifecycle) {
|
|
797
|
-
return new Promise((resolve) => {
|
|
798
|
-
lifecycle.retryTimer = setTimeout(() => {
|
|
799
|
-
lifecycle.retryTimer = void 0;
|
|
800
|
-
resolve();
|
|
801
|
-
}, ms);
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Run the steady-state poll loop for a successfully-acquired subscription.
|
|
806
|
-
* Returns the teardown closure that stops the loop and releases the
|
|
807
|
-
* subscription.
|
|
808
|
-
*/
|
|
809
|
-
function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
810
|
-
const { api, brokerId, onFrame, logger } = options;
|
|
811
|
-
const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
|
|
812
|
-
const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
|
|
813
|
-
let timer;
|
|
814
|
-
const tick = async () => {
|
|
815
|
-
if (lifecycle.stopped) return;
|
|
816
|
-
try {
|
|
817
|
-
const handles = await api.streamBroker.pullFrameHandles.query({
|
|
818
|
-
subscriptionId,
|
|
819
|
-
maxCount: PULL_MAX_COUNT
|
|
820
|
-
});
|
|
821
|
-
for (const handle of handles) {
|
|
822
|
-
if (lifecycle.stopped) break;
|
|
823
|
-
const frame = readers.read(handle);
|
|
824
|
-
if (frame) onFrame(frame, handle);
|
|
825
|
-
}
|
|
826
|
-
} catch (err) {
|
|
827
|
-
logger.warn("frame-handle poller: pullFrameHandles failed", { meta: {
|
|
828
|
-
brokerId,
|
|
829
|
-
subscriptionId,
|
|
830
|
-
error: require_dist.errMsg(err)
|
|
831
|
-
} });
|
|
832
|
-
}
|
|
833
|
-
if (!lifecycle.stopped) timer = setTimeout(() => void tick(), pollIntervalMs);
|
|
834
|
-
};
|
|
835
|
-
tick();
|
|
836
|
-
return () => {
|
|
837
|
-
if (timer) {
|
|
838
|
-
clearTimeout(timer);
|
|
839
|
-
timer = void 0;
|
|
840
|
-
}
|
|
841
|
-
readers.close();
|
|
842
|
-
api.streamBroker.unsubscribeFrames.mutate({ subscriptionId }).catch((err) => {
|
|
843
|
-
logger.warn("frame-handle poller: unsubscribeFrames failed", { meta: {
|
|
844
|
-
brokerId,
|
|
845
|
-
subscriptionId,
|
|
846
|
-
error: require_dist.errMsg(err)
|
|
847
|
-
} });
|
|
848
|
-
});
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
//#endregion
|
|
852
|
-
//#region src/pipeline-runner/bench-actions.ts
|
|
853
|
-
/**
|
|
854
|
-
* Synthetic-bench custom actions for pipeline-runner.
|
|
855
|
-
*
|
|
856
|
-
* Lets the admin UI drive a production-realistic detection benchmark:
|
|
857
|
-
* the runner — already co-located with detection-pipeline in the
|
|
858
|
-
* `pipeline` group — invokes `api.pipelineExecutor.runPipeline` directly
|
|
859
|
-
* (in-process via localProviderLink, no Moleculer hop), N workers
|
|
860
|
-
* concurrent × M iterations. Returns aggregate stats.
|
|
861
|
-
*
|
|
862
|
-
* Why this exists: the older bench path (UI → benchmark addon → pipeline
|
|
863
|
-
* addon) crosses TWO Moleculer process boundaries that the production
|
|
864
|
-
* camera path does NOT have (broker / runner / detection-pipeline are
|
|
865
|
-
* all in the same group). Running the loop from inside the runner gives
|
|
866
|
-
* fps numbers that match what real cameras achieve.
|
|
867
|
-
*/
|
|
868
|
-
var BenchEngineChoiceSchema = require_dist.object({
|
|
869
|
-
runtime: require_dist._enum(["node", "python"]),
|
|
870
|
-
backend: require_dist.string(),
|
|
871
|
-
format: require_dist._enum([
|
|
872
|
-
"onnx",
|
|
873
|
-
"coreml",
|
|
874
|
-
"openvino",
|
|
875
|
-
"tflite",
|
|
876
|
-
"pt"
|
|
877
|
-
]),
|
|
878
|
-
device: require_dist.string().optional()
|
|
879
|
-
});
|
|
880
|
-
var BenchStepSchema = require_dist.lazy(() => require_dist.object({
|
|
881
|
-
addonId: require_dist.string(),
|
|
882
|
-
modelId: require_dist.string(),
|
|
883
|
-
enabled: require_dist.boolean(),
|
|
884
|
-
children: require_dist.array(BenchStepSchema).optional()
|
|
885
|
-
}));
|
|
886
|
-
var CacheBenchFrameInputSchema = require_dist.object({
|
|
887
|
-
imageBase64: require_dist.string(),
|
|
888
|
-
ttlSeconds: require_dist.number().int().positive().optional()
|
|
889
|
-
});
|
|
890
|
-
var CacheBenchFrameResultSchema = require_dist.object({
|
|
891
|
-
frameId: require_dist.string(),
|
|
892
|
-
width: require_dist.number(),
|
|
893
|
-
height: require_dist.number(),
|
|
894
|
-
expiresAt: require_dist.number()
|
|
895
|
-
});
|
|
896
|
-
var ReleaseBenchFrameInputSchema = require_dist.object({ frameId: require_dist.string() });
|
|
897
|
-
var ReleaseBenchFrameResultSchema = require_dist.object({ released: require_dist.boolean() });
|
|
898
|
-
var RunSyntheticBenchInputSchema = require_dist.object({
|
|
899
|
-
frameId: require_dist.string(),
|
|
900
|
-
steps: require_dist.array(BenchStepSchema).min(1),
|
|
901
|
-
parallel: require_dist.number().int().min(1).max(32),
|
|
902
|
-
iterations: require_dist.number().int().min(1).max(1e4),
|
|
903
|
-
warmup: require_dist.number().int().min(0).max(100).optional(),
|
|
904
|
-
sessionId: require_dist.string().optional(),
|
|
905
|
-
simulatePipeline: require_dist.boolean().optional(),
|
|
906
|
-
nodeId: require_dist.string().optional(),
|
|
907
|
-
engine: BenchEngineChoiceSchema.optional()
|
|
908
|
-
});
|
|
909
|
-
var TimingSplitSchema = require_dist.object({
|
|
910
|
-
mean: require_dist.number(),
|
|
911
|
-
p50: require_dist.number(),
|
|
912
|
-
p95: require_dist.number(),
|
|
913
|
-
p99: require_dist.number()
|
|
914
|
-
});
|
|
915
|
-
var RunSyntheticBenchResultSchema = require_dist.object({
|
|
916
|
-
runs: require_dist.number(),
|
|
917
|
-
wallSec: require_dist.number(),
|
|
918
|
-
fps: require_dist.number(),
|
|
919
|
-
detectionsPerSec: require_dist.number(),
|
|
920
|
-
avgDetections: require_dist.number(),
|
|
921
|
-
callMs: TimingSplitSchema,
|
|
922
|
-
inferMs: require_dist.number(),
|
|
923
|
-
preprocessMs: require_dist.number(),
|
|
924
|
-
predictMs: require_dist.number(),
|
|
925
|
-
batchSizeMean: require_dist.number(),
|
|
926
|
-
batchSizeMax: require_dist.number(),
|
|
927
|
-
engine: require_dist.object({
|
|
928
|
-
runtime: require_dist.string(),
|
|
929
|
-
backend: require_dist.string(),
|
|
930
|
-
device: require_dist.string().optional()
|
|
931
|
-
}).optional(),
|
|
932
|
-
tuning: require_dist.object({
|
|
933
|
-
batchMode: require_dist.string(),
|
|
934
|
-
windowMs: require_dist.number(),
|
|
935
|
-
maxBatchSize: require_dist.number(),
|
|
936
|
-
concurrency: require_dist.number()
|
|
937
|
-
}).optional(),
|
|
938
|
-
path: require_dist.string().optional()
|
|
939
|
-
});
|
|
940
|
-
var pipelineRunnerBenchActions = require_dist.defineCustomActions({
|
|
941
|
-
cacheBenchFrame: require_dist.customAction(CacheBenchFrameInputSchema, CacheBenchFrameResultSchema, { kind: "mutation" }),
|
|
942
|
-
releaseBenchFrame: require_dist.customAction(ReleaseBenchFrameInputSchema, ReleaseBenchFrameResultSchema, { kind: "mutation" }),
|
|
943
|
-
runSyntheticBench: require_dist.customAction(RunSyntheticBenchInputSchema, RunSyntheticBenchResultSchema, { kind: "mutation" })
|
|
944
|
-
});
|
|
945
|
-
//#endregion
|
|
946
1080
|
//#region src/pipeline-runner/index.ts
|
|
947
1081
|
var DEFAULT_CONFIG = {
|
|
948
1082
|
maxQueueDepth: 30,
|
|
@@ -1629,6 +1763,31 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1629
1763
|
getLocalCameras() {
|
|
1630
1764
|
return [...this.attached.keys()];
|
|
1631
1765
|
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Cross-node frame fetch for FOREIGN handles (segment on another node). Wired
|
|
1768
|
+
* into every `startFrameHandlePoller` call so `resolvePolledFrame` can take
|
|
1769
|
+
* the bounded `decoder.getFrame` fallback instead of an unmappable local shm
|
|
1770
|
+
* read. The `nodeId` selector routes the call to the producing node (the
|
|
1771
|
+
* cap's `nodeIdMode: 'routing'` default; agent→hub via `$hub-cap-fwd`). The
|
|
1772
|
+
* wire schema returns `data: Uint8Array`; downstream inference expects a Node
|
|
1773
|
+
* `Buffer`, so we adapt it here exactly as post-analysis does.
|
|
1774
|
+
*/
|
|
1775
|
+
async getRemoteFrame(handle) {
|
|
1776
|
+
const api = this.ctx.api;
|
|
1777
|
+
if (!api) return null;
|
|
1778
|
+
const remote = await api.decoder.getFrame.query({
|
|
1779
|
+
handle,
|
|
1780
|
+
nodeId: handle.nodeId
|
|
1781
|
+
});
|
|
1782
|
+
if (!remote) return null;
|
|
1783
|
+
return {
|
|
1784
|
+
data: Buffer.from(remote.data),
|
|
1785
|
+
width: remote.width,
|
|
1786
|
+
height: remote.height,
|
|
1787
|
+
format: remote.format,
|
|
1788
|
+
timestamp: remote.timestamp
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1632
1791
|
async subscribeMotionFrames(config) {
|
|
1633
1792
|
const ctx = this.ctx;
|
|
1634
1793
|
const runner = this.runner;
|
|
@@ -1646,6 +1805,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1646
1805
|
maxFps: config.motionFps,
|
|
1647
1806
|
tag: "motion",
|
|
1648
1807
|
logger: log,
|
|
1808
|
+
ownNodeId: this.nodeId,
|
|
1809
|
+
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
1649
1810
|
onFrame: (frame, _handle) => {
|
|
1650
1811
|
runner.enqueueMotionFrame(config.deviceId, frame);
|
|
1651
1812
|
}
|
|
@@ -1729,6 +1890,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1729
1890
|
maxFps: config.detectionFps,
|
|
1730
1891
|
tag: "detection",
|
|
1731
1892
|
logger: log,
|
|
1893
|
+
ownNodeId: this.nodeId,
|
|
1894
|
+
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
1732
1895
|
onFrame: (frame, handle) => {
|
|
1733
1896
|
runner.enqueueDetectionFrame(config.deviceId, frame, handle);
|
|
1734
1897
|
}
|
|
@@ -1895,6 +2058,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1895
2058
|
maxFps: config.detectionFps,
|
|
1896
2059
|
tag: "occupancy",
|
|
1897
2060
|
logger: log,
|
|
2061
|
+
ownNodeId: this.nodeId,
|
|
2062
|
+
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
1898
2063
|
onFrame: (frame, handle) => {
|
|
1899
2064
|
if (settled) return;
|
|
1900
2065
|
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|