@camstack/addon-pipeline 1.1.21 → 1.1.22

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