@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,4 +1,4 @@
1
- import { B as BaseAddon, J as hydrateSchema, U as DeviceType, k as motionDetectionCapability, w as evaluateZoneRules } from "../dist-tzhTTRq4.mjs";
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-BiP1gPeY.js");
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);