@camstack/addon-pipeline 1.1.56 → 1.1.57
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +71 -74
- package/dist/detection-pipeline/index.mjs +70 -73
- package/dist/{dist-ClE6_i0p.js → dist-CK8Ur-OS.js} +104 -6
- package/dist/{dist-CrjLGrkj.mjs → dist-CL531uVA.mjs} +104 -6
- package/dist/motion-wasm/index.js +9 -41
- package/dist/motion-wasm/index.mjs +9 -41
- package/dist/pipeline-runner/index.js +222 -656
- package/dist/pipeline-runner/index.mjs +211 -647
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/remote-restream-BeHi78PZ.mjs +25 -0
- package/dist/remote-restream-CO36Sr30.js +36 -0
- package/dist/{step-definitions-dCghpdRo.mjs → step-definitions-1d3_vQ6S.mjs} +44 -2
- package/dist/{step-definitions-C9kUXgC7.js → step-definitions-DHAxruAQ.js} +44 -2
- package/dist/stream-broker/_stub.js +282 -222
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D4xH4Tm-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cm_B-hQT.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D2z_vRMH.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ba7Eyd47.mjs} +1 -1
- package/dist/stream-broker/{hostInit-Di9WUNlQ.mjs → hostInit-DEaqJpGA.mjs} +1 -1
- package/dist/stream-broker/index.js +877 -37
- package/dist/stream-broker/index.mjs +874 -34
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Ca93F1js.js → MaskShapeCanvas-DI4BY7W2-CZUzu3MF.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CDqgb-WA.js → MotionZonesSettings-NcxxQN8r-CmZbzLQi.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BlxPhY_w.js → PrivacyMaskSettings-APgPLF7p-BOKDrH0N.js} +1 -1
- package/embed-dist/assets/{index-B23riUw8.js → index-DOsK8CZb.js} +10 -10
- package/embed-dist/index.html +1 -1
- package/package.json +7 -5
- package/python/inference_pool.py +172 -28
- package/python/postprocessors/__init__.py +2 -0
- package/python/postprocessors/ssd.py +128 -0
- package/python/requirements-edgetpu.txt +23 -0
- package/python/test_inference_pool_layout.py +16 -1
- package/python/test_inference_pool_preprocess.py +65 -0
- package/dist/remote-source-plane-B28iGi7h.mjs +0 -1001
- package/dist/remote-source-plane-BAqEHSgm.js +0 -1030
- package/dist/{model-download-service-C-IHWnXx-BPy6aoAx.mjs → model-download-service-C-IHWnXx-3Mmeob3l.mjs} +1 -1
- package/dist/{model-download-service-C-IHWnXx-BLXzxpRB.js → model-download-service-C-IHWnXx-D326YNnt.js} +1 -1
|
@@ -2,15 +2,16 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
+
const require_dist = require("../dist-CK8Ur-OS.js");
|
|
7
|
+
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
7
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
8
9
|
const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
|
|
9
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
-
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
10
|
+
const require_step_definitions = require("../step-definitions-DHAxruAQ.js");
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
|
-
let
|
|
13
|
+
let sharp = require("sharp");
|
|
14
|
+
sharp = require_chunk.__toESM(sharp);
|
|
14
15
|
//#region src/pipeline-runner/bench-actions.ts
|
|
15
16
|
/**
|
|
16
17
|
* Synthetic-bench custom actions for pipeline-runner.
|
|
@@ -105,329 +106,6 @@ var pipelineRunnerBenchActions = require_dist.defineCustomActions({
|
|
|
105
106
|
runSyntheticBench: require_dist.customAction(RunSyntheticBenchInputSchema, RunSyntheticBenchResultSchema, { kind: "mutation" })
|
|
106
107
|
});
|
|
107
108
|
//#endregion
|
|
108
|
-
//#region src/pipeline-runner/foreign-handle-throttle.ts
|
|
109
|
-
var ForeignHandleThrottle = class {
|
|
110
|
-
minFetchIntervalMs;
|
|
111
|
-
warnIntervalMs;
|
|
112
|
-
lastFetchStartedAt = Number.NEGATIVE_INFINITY;
|
|
113
|
-
inFlight = false;
|
|
114
|
-
lastWarnAt;
|
|
115
|
-
_foreignHandles = 0;
|
|
116
|
-
_remoteFetches = 0;
|
|
117
|
-
_remoteFailures = 0;
|
|
118
|
-
_dropped = 0;
|
|
119
|
-
constructor(options = {}) {
|
|
120
|
-
const maxRemoteFps = options.maxRemoteFps ?? 1;
|
|
121
|
-
this.minFetchIntervalMs = maxRemoteFps > 0 ? 1e3 / maxRemoteFps : Number.POSITIVE_INFINITY;
|
|
122
|
-
this.warnIntervalMs = options.warnIntervalMs ?? 6e4;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Decide whether the current foreign handle should trigger a remote fetch.
|
|
126
|
-
* `'fetch'` only when no fetch is in flight AND at least `1000/maxRemoteFps`
|
|
127
|
-
* ms have elapsed since the last fetch started; otherwise `'drop'`.
|
|
128
|
-
*/
|
|
129
|
-
plan(now) {
|
|
130
|
-
this._foreignHandles += 1;
|
|
131
|
-
const rateOpen = now - this.lastFetchStartedAt >= this.minFetchIntervalMs;
|
|
132
|
-
if (this.inFlight || !rateOpen) {
|
|
133
|
-
this._dropped += 1;
|
|
134
|
-
return "drop";
|
|
135
|
-
}
|
|
136
|
-
return "fetch";
|
|
137
|
-
}
|
|
138
|
-
/** Mark a remote fetch as begun (arms single-flight + the rate window). */
|
|
139
|
-
noteFetchStart(now) {
|
|
140
|
-
this.inFlight = true;
|
|
141
|
-
this.lastFetchStartedAt = now;
|
|
142
|
-
this._remoteFetches += 1;
|
|
143
|
-
}
|
|
144
|
-
/** Mark the in-flight remote fetch as finished; records a failure outcome. */
|
|
145
|
-
noteFetchEnd(result = "ok") {
|
|
146
|
-
this.inFlight = false;
|
|
147
|
-
if (result === "failed") this._remoteFailures += 1;
|
|
148
|
-
}
|
|
149
|
-
/** `true` at most once per `warnIntervalMs`; stamps the window when it fires. */
|
|
150
|
-
shouldWarn(now) {
|
|
151
|
-
if (this.lastWarnAt !== void 0 && now - this.lastWarnAt < this.warnIntervalMs) return false;
|
|
152
|
-
this.lastWarnAt = now;
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
get counters() {
|
|
156
|
-
return {
|
|
157
|
-
foreignHandles: this._foreignHandles,
|
|
158
|
-
remoteFetches: this._remoteFetches,
|
|
159
|
-
remoteFailures: this._remoteFailures,
|
|
160
|
-
dropped: this._dropped
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region src/pipeline-runner/resolve-polled-frame.ts
|
|
166
|
-
async function resolvePolledFrame(handle, deps) {
|
|
167
|
-
const { ownNodeId, readers, throttle, getRemoteFrame, logger, tag } = deps;
|
|
168
|
-
if (handle.nodeId === ownNodeId) return readers.read(handle);
|
|
169
|
-
const now = Date.now();
|
|
170
|
-
const decision = throttle.plan(now);
|
|
171
|
-
if (throttle.shouldWarn(now)) logger.warn("frame poller: foreign handle — decoder session is on another node; using bounded decoder.getFrame fallback", { meta: {
|
|
172
|
-
tag,
|
|
173
|
-
producerNode: handle.nodeId,
|
|
174
|
-
ownNodeId,
|
|
175
|
-
...throttle.counters
|
|
176
|
-
} });
|
|
177
|
-
if (decision === "drop") return null;
|
|
178
|
-
throttle.noteFetchStart(now);
|
|
179
|
-
try {
|
|
180
|
-
const remote = await getRemoteFrame(handle);
|
|
181
|
-
throttle.noteFetchEnd("ok");
|
|
182
|
-
return remote;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
throttle.noteFetchEnd("failed");
|
|
185
|
-
logger.debug("frame poller: remote getFrame failed", { meta: {
|
|
186
|
-
tag,
|
|
187
|
-
producerNode: handle.nodeId,
|
|
188
|
-
error: require_dist.errMsg(err)
|
|
189
|
-
} });
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
//#endregion
|
|
194
|
-
//#region src/pipeline-runner/frame-handle-poller.ts
|
|
195
|
-
/**
|
|
196
|
-
* `FrameHandlePoller` — the consumer-side poll loop of the shared-memory frame
|
|
197
|
-
* plane (Phase 5 / D9).
|
|
198
|
-
*
|
|
199
|
-
* Replaces the live-object `IStreamBroker.onDecodedFrame(cb)` callback path.
|
|
200
|
-
* A consumer (motion, detection):
|
|
201
|
-
*
|
|
202
|
-
* 1. `subscribeFrames({ brokerId, format, maxFps, tag })` over tRPC — the
|
|
203
|
-
* broker spins up a `frameSink: 'shm'` decoder session for that format
|
|
204
|
-
* and returns a `subscriptionId`;
|
|
205
|
-
* 2. polls `pullFrameHandles({ subscriptionId, maxCount })` at the cadence
|
|
206
|
-
* `maxFps` implies, draining zero-pixel `FrameHandle`s;
|
|
207
|
-
* 3. feeds each handle to a `FrameRingReaderCache` which opens the named shm
|
|
208
|
-
* segment and reads the pixels back zero-copy;
|
|
209
|
-
* 4. on teardown, `unsubscribeFrames` + closes the cached readers.
|
|
210
|
-
*
|
|
211
|
-
* The loop tolerates an initially-empty stream — `pullFrameHandles` returns
|
|
212
|
-
* `[]` until the broker has a source, and a recycled ring slot yields a `null`
|
|
213
|
-
* pixel read (a dropped frame, the correct latest-wins behaviour).
|
|
214
|
-
*
|
|
215
|
-
* Foreign-handle path: a `FrameHandle` carries the `nodeId` of the node whose
|
|
216
|
-
* shared memory holds its segment. A LOCAL handle is read back zero-copy; a
|
|
217
|
-
* FOREIGN handle (produced on another node — e.g. a camera decoded on the hub
|
|
218
|
-
* whose detection was placed on an agent) cannot be `shm_open`ed here. Reading
|
|
219
|
-
* it unconditionally floods `openSegment failed shm_open ENOENT` at frame rate,
|
|
220
|
-
* so the poll delegates every handle to `resolvePolledFrame`, which gates on
|
|
221
|
-
* `handle.nodeId`: local → shm read; foreign → a BOUNDED, single-flight
|
|
222
|
-
* `getRemoteFrame` (`decoder.getFrame`) fallback (≤ `maxRemoteFps`) plus a
|
|
223
|
-
* rate-limited WARN naming the topology mismatch. `ownNodeId` + `getRemoteFrame`
|
|
224
|
-
* are REQUIRED options — every in-repo call site supplies them, so there is no
|
|
225
|
-
* silent default that re-creates the unconditional read.
|
|
226
|
-
*
|
|
227
|
-
* Boot-race tolerance: the broker for a given camStream may not be registered
|
|
228
|
-
* yet when a consumer attempts to subscribe (the pipeline-runner spawns before
|
|
229
|
-
* provider addons publish their cameraStreams). `subscribeFrames` retries with
|
|
230
|
-
* exponential backoff (capped at `MAX_SUBSCRIBE_RETRY_BACKOFF_MS`) until it
|
|
231
|
-
* succeeds or the returned teardown closure is invoked. The closure always
|
|
232
|
-
* resolves to a function, even when the broker is missing; the function
|
|
233
|
-
* cancels pending retries and releases any subscription acquired in flight.
|
|
234
|
-
*/
|
|
235
|
-
/** How many handles to drain per poll — a small burst absorbs jitter. */
|
|
236
|
-
var PULL_MAX_COUNT = 4;
|
|
237
|
-
/** Floor on the poll period so a 0/absurd `maxFps` can't busy-spin. */
|
|
238
|
-
var MIN_POLL_INTERVAL_MS = 20;
|
|
239
|
-
/** Poll period when the broker reports a non-positive cadence hint. */
|
|
240
|
-
var FALLBACK_POLL_INTERVAL_MS = 200;
|
|
241
|
-
/**
|
|
242
|
-
* Ceiling on the poll period — we never poll SLOWER than this even when the
|
|
243
|
-
* subscriber's `maxFps` implies a longer interval (e.g. detection at 10 fps →
|
|
244
|
-
* 100 ms). A decoded frame otherwise sits in the ring up to a full poll period
|
|
245
|
-
* before it is picked, and that wait lands directly in `frameAge` (the analyzed
|
|
246
|
-
* frame's staleness). Polling faster grabs the freshest frame sooner; because
|
|
247
|
-
* the loop reads ONLY the newest handle per tick and the decoder emits at its
|
|
248
|
-
* own rate, the extra ticks that find no new frame are near-free (an empty
|
|
249
|
-
* `pullHandles` + no pixel read), so this lowers latency without a proportional
|
|
250
|
-
* read-cost increase. `maxFps` still bounds the DELIVERED rate downstream via
|
|
251
|
-
* the latest-wins queue — this only decouples poll cadence from it.
|
|
252
|
-
*/
|
|
253
|
-
var MAX_POLL_INTERVAL_MS = 50;
|
|
254
|
-
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
255
|
-
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
256
|
-
/**
|
|
257
|
-
* Subscribe-retry backoff ceiling. 5 s is fast enough to recover within a
|
|
258
|
-
* single roster refresh of the consuming UI and slow enough that a
|
|
259
|
-
* misconfigured camStream costs ~12 op/min, not 50/s.
|
|
260
|
-
*/
|
|
261
|
-
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
262
|
-
/**
|
|
263
|
-
* The default source: the broker's shm plane over tRPC — today's path. The
|
|
264
|
-
* methods return the underlying client promises DIRECTLY (no async wrapper)
|
|
265
|
-
* so the poller's scheduling is microtask-identical to the pre-seam code.
|
|
266
|
-
*/
|
|
267
|
-
function brokerFrameHandleSource(api, brokerId) {
|
|
268
|
-
return {
|
|
269
|
-
subscribe: (input) => api.streamBroker.subscribeFrames.mutate({
|
|
270
|
-
brokerId,
|
|
271
|
-
format: input.format,
|
|
272
|
-
maxFps: input.maxFps,
|
|
273
|
-
tag: input.tag
|
|
274
|
-
}),
|
|
275
|
-
pullHandles: (input) => api.streamBroker.pullFrameHandles.query({
|
|
276
|
-
subscriptionId: input.subscriptionId,
|
|
277
|
-
maxCount: input.maxCount
|
|
278
|
-
}),
|
|
279
|
-
unsubscribe: (input) => api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: input.subscriptionId }).then(() => void 0)
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Subscribe to a broker's shm frame stream and start the poll loop. Always
|
|
284
|
-
* resolves to a teardown closure — when the broker is not yet registered the
|
|
285
|
-
* closure cancels the ongoing retry loop; when polling is active it stops the
|
|
286
|
-
* loop, releases the broker subscription, and closes every shm segment the
|
|
287
|
-
* reader cache opened.
|
|
288
|
-
*/
|
|
289
|
-
async function startFrameHandlePoller(options) {
|
|
290
|
-
const lifecycle = {
|
|
291
|
-
stopped: false,
|
|
292
|
-
retryTimer: void 0,
|
|
293
|
-
activeTeardown: null
|
|
294
|
-
};
|
|
295
|
-
const teardown = () => {
|
|
296
|
-
if (lifecycle.stopped) return;
|
|
297
|
-
lifecycle.stopped = true;
|
|
298
|
-
if (lifecycle.retryTimer) {
|
|
299
|
-
clearTimeout(lifecycle.retryTimer);
|
|
300
|
-
lifecycle.retryTimer = void 0;
|
|
301
|
-
}
|
|
302
|
-
lifecycle.activeTeardown?.();
|
|
303
|
-
};
|
|
304
|
-
subscribeWithRetry(options, lifecycle);
|
|
305
|
-
return teardown;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Run the subscribe → poll handshake with exponential backoff on subscribe
|
|
309
|
-
* failures. Resolves once the subscription is acquired (and the poll loop has
|
|
310
|
-
* been started) or once `lifecycle.stopped` flips, whichever comes first.
|
|
311
|
-
*/
|
|
312
|
-
async function subscribeWithRetry(options, lifecycle) {
|
|
313
|
-
const { api, brokerId, format, maxFps, tag, logger } = options;
|
|
314
|
-
const source = options.source ?? brokerFrameHandleSource(api, brokerId);
|
|
315
|
-
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
316
|
-
let attempt = 0;
|
|
317
|
-
while (!lifecycle.stopped) {
|
|
318
|
-
attempt += 1;
|
|
319
|
-
try {
|
|
320
|
-
const result = await source.subscribe({
|
|
321
|
-
format,
|
|
322
|
-
maxFps,
|
|
323
|
-
tag
|
|
324
|
-
});
|
|
325
|
-
if (lifecycle.stopped) {
|
|
326
|
-
await source.unsubscribe({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
327
|
-
logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
|
|
328
|
-
brokerId,
|
|
329
|
-
subscriptionId: result.subscriptionId,
|
|
330
|
-
error: require_dist.errMsg(err)
|
|
331
|
-
} });
|
|
332
|
-
});
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
lifecycle.activeTeardown = startPolling(options, source, result.subscriptionId, result.maxFps, lifecycle);
|
|
336
|
-
return;
|
|
337
|
-
} catch (err) {
|
|
338
|
-
if (lifecycle.stopped) return;
|
|
339
|
-
if (attempt === 1) logger.warn("frame-handle poller: subscribeFrames failed, retrying", { meta: {
|
|
340
|
-
brokerId,
|
|
341
|
-
format,
|
|
342
|
-
tag,
|
|
343
|
-
error: require_dist.errMsg(err),
|
|
344
|
-
nextRetryInMs: backoffMs
|
|
345
|
-
} });
|
|
346
|
-
else logger.debug("frame-handle poller: subscribeFrames still failing", { meta: {
|
|
347
|
-
brokerId,
|
|
348
|
-
format,
|
|
349
|
-
tag,
|
|
350
|
-
attempt,
|
|
351
|
-
error: require_dist.errMsg(err),
|
|
352
|
-
nextRetryInMs: backoffMs
|
|
353
|
-
} });
|
|
354
|
-
await sleep(backoffMs, lifecycle);
|
|
355
|
-
backoffMs = Math.min(MAX_SUBSCRIBE_RETRY_BACKOFF_MS, backoffMs * 2);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Sleep `ms` milliseconds, but resolve early when `lifecycle.stopped` flips so
|
|
361
|
-
* a teardown during a retry pause doesn't have to wait out the full backoff.
|
|
362
|
-
*/
|
|
363
|
-
function sleep(ms, lifecycle) {
|
|
364
|
-
return new Promise((resolve) => {
|
|
365
|
-
lifecycle.retryTimer = setTimeout(() => {
|
|
366
|
-
lifecycle.retryTimer = void 0;
|
|
367
|
-
resolve();
|
|
368
|
-
}, ms);
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Run the steady-state poll loop for a successfully-acquired subscription.
|
|
373
|
-
* Returns the teardown closure that stops the loop and releases the
|
|
374
|
-
* subscription.
|
|
375
|
-
*/
|
|
376
|
-
function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
377
|
-
const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
|
|
378
|
-
const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
|
|
379
|
-
const throttle = new ForeignHandleThrottle();
|
|
380
|
-
const pollIntervalMs = Math.min(MAX_POLL_INTERVAL_MS, resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS);
|
|
381
|
-
let timer;
|
|
382
|
-
const tick = async () => {
|
|
383
|
-
if (lifecycle.stopped) return;
|
|
384
|
-
try {
|
|
385
|
-
const handles = await source.pullHandles({
|
|
386
|
-
subscriptionId,
|
|
387
|
-
maxCount: PULL_MAX_COUNT
|
|
388
|
-
});
|
|
389
|
-
for (let i = handles.length - 1; i >= 0; i -= 1) {
|
|
390
|
-
if (lifecycle.stopped) break;
|
|
391
|
-
const handle = handles[i];
|
|
392
|
-
const frame = await resolvePolledFrame(handle, {
|
|
393
|
-
ownNodeId,
|
|
394
|
-
readers,
|
|
395
|
-
throttle,
|
|
396
|
-
getRemoteFrame,
|
|
397
|
-
logger,
|
|
398
|
-
tag
|
|
399
|
-
});
|
|
400
|
-
if (frame) {
|
|
401
|
-
onFrame(frame, handle);
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
} catch (err) {
|
|
406
|
-
logger.warn("frame-handle poller: pullFrameHandles failed", { meta: {
|
|
407
|
-
brokerId,
|
|
408
|
-
subscriptionId,
|
|
409
|
-
error: require_dist.errMsg(err)
|
|
410
|
-
} });
|
|
411
|
-
}
|
|
412
|
-
if (!lifecycle.stopped) timer = setTimeout(() => void tick(), pollIntervalMs);
|
|
413
|
-
};
|
|
414
|
-
tick();
|
|
415
|
-
return () => {
|
|
416
|
-
if (timer) {
|
|
417
|
-
clearTimeout(timer);
|
|
418
|
-
timer = void 0;
|
|
419
|
-
}
|
|
420
|
-
readers.close();
|
|
421
|
-
source.unsubscribe({ subscriptionId }).catch((err) => {
|
|
422
|
-
logger.warn("frame-handle poller: unsubscribe failed", { meta: {
|
|
423
|
-
brokerId,
|
|
424
|
-
subscriptionId,
|
|
425
|
-
error: require_dist.errMsg(err)
|
|
426
|
-
} });
|
|
427
|
-
});
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
//#endregion
|
|
431
109
|
//#region src/pipeline-runner/frame-governor.ts
|
|
432
110
|
var FrameGovernor = class {
|
|
433
111
|
config;
|
|
@@ -1464,137 +1142,141 @@ function startGuardedSessionDecode(deps) {
|
|
|
1464
1142
|
if (release) release();
|
|
1465
1143
|
};
|
|
1466
1144
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
/**
|
|
1470
|
-
* Session-decode frame retention ring (Shape-2, Slice 1).
|
|
1471
|
-
*
|
|
1472
|
-
* The default detection path is **session-decode** (`useSessionDecode` true):
|
|
1473
|
-
* a per-session forked decode worker hands the runner decoded rgb pixels over
|
|
1474
|
-
* IPC — the frames never touch shared memory, so the emitted
|
|
1475
|
-
* `PipelineInferenceResult` carried NO `FrameHandle`, and hub-side post-analysis
|
|
1476
|
-
* (which gates ALL event/face/track crop media on a present handle) produced no
|
|
1477
|
-
* media. This ring restores that: every decoded detection frame is copied into
|
|
1478
|
-
* a short shared-memory retention ring on the runner's node, and the resulting
|
|
1479
|
-
* `FrameHandle` rides the inference-result event so post-analysis can resolve
|
|
1480
|
-
* the exact frame zero-copy and cut crops from it.
|
|
1481
|
-
*
|
|
1482
|
-
* ## Why a distinct prefix (`csr.`)
|
|
1483
|
-
*
|
|
1484
|
-
* The decoder addon runs `purgeOrphanSegments('csf.')` at startup and unlinks
|
|
1485
|
-
* EVERY `csf.*` segment on the node. A retention ring under the `csf.` prefix
|
|
1486
|
-
* would be reclaimed out from under a live session on every decoder redeploy.
|
|
1487
|
-
* The retention ring therefore uses its own prefix `csr.` and its own startup
|
|
1488
|
-
* orphan purge (`shm-retention-purge.ts`).
|
|
1489
|
-
*
|
|
1490
|
-
* ## Sizing
|
|
1491
|
-
*
|
|
1492
|
-
* Retention is ring depth: `slots ≈ detectionFps × retainSeconds`. At the
|
|
1493
|
-
* Slice-1 ≤640-wide detection geometry a slot is ~0.69 MB, so the 32 MB default
|
|
1494
|
-
* budget yields ~46 latest-wins slots (~4.6 s @10 fps). Budget is per active
|
|
1495
|
-
* session; the sink is armed lazily on the first rgb frame and destroyed on
|
|
1496
|
-
* session teardown, so idle cameras hold no shared memory.
|
|
1497
|
-
*/
|
|
1498
|
-
/** Segment name prefix for session-decode retention rings — isolated from the
|
|
1499
|
-
* decoder's `csf.` orphan purge so a decoder redeploy never reclaims a live
|
|
1500
|
-
* retention ring. */
|
|
1501
|
-
var RETENTION_SEGMENT_PREFIX = "csr.";
|
|
1502
|
-
/** {@link RETAIN_BUDGET_MB} in bytes. */
|
|
1503
|
-
var RETAIN_BUDGET_BYTES = (() => {
|
|
1145
|
+
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
1146
|
+
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
1504
1147
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
1505
1148
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 32;
|
|
1506
1149
|
})() * 1024 * 1024;
|
|
1507
|
-
/**
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
const { deviceId, nodeId, logger } = options;
|
|
1513
|
-
const sink = new _camstack_shm_ring.DecoderFrameRingSink({
|
|
1514
|
-
seed: `retain:${deviceId}`,
|
|
1515
|
-
logger,
|
|
1516
|
-
nodeId,
|
|
1517
|
-
segmentPrefix: RETENTION_SEGMENT_PREFIX,
|
|
1518
|
-
budgetBytes: RETAIN_BUDGET_BYTES
|
|
1519
|
-
});
|
|
1520
|
-
return {
|
|
1521
|
-
write(frame) {
|
|
1522
|
-
if (frame.format !== "rgb") return null;
|
|
1523
|
-
return sink.writeFrame(frame.data, {
|
|
1524
|
-
width: frame.width,
|
|
1525
|
-
height: frame.height,
|
|
1526
|
-
format: frame.format,
|
|
1527
|
-
pts: performance.now(),
|
|
1528
|
-
byteLength: frame.data.byteLength
|
|
1529
|
-
});
|
|
1530
|
-
},
|
|
1531
|
-
destroy() {
|
|
1532
|
-
sink.destroy();
|
|
1533
|
-
},
|
|
1534
|
-
get currentSegmentName() {
|
|
1535
|
-
return sink.currentSegmentName;
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1150
|
+
/** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
|
|
1151
|
+
var RETAINED_FRAME_KEY_PREFIX = "ram:";
|
|
1152
|
+
/** A retention frame is uniquely identified by its key + slot + committed seq. */
|
|
1153
|
+
function keyOf(handle) {
|
|
1154
|
+
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
1538
1155
|
}
|
|
1539
|
-
//#endregion
|
|
1540
|
-
//#region src/pipeline-runner/shm-retention-purge.ts
|
|
1541
|
-
/**
|
|
1542
|
-
* Startup reclamation of orphaned session-decode retention segments.
|
|
1543
|
-
*
|
|
1544
|
-
* The retention ring (`retention-sink.ts`) writes decoded detection frames into
|
|
1545
|
-
* named `/dev/shm` segments under the `csr.` prefix and unlinks each on graceful
|
|
1546
|
-
* session teardown. When the runner process dies ungracefully (SIGBUS, OOM-kill,
|
|
1547
|
-
* SIGKILL during redeploy) that teardown never runs and the segment is orphaned.
|
|
1548
|
-
* Across crashes/redeploys these accumulate until `/dev/shm` fills, at which
|
|
1549
|
-
* point the next `mmap` write faults with an uncatchable SIGBUS.
|
|
1550
|
-
*
|
|
1551
|
-
* This is the retention-ring analogue of the decoder addon's
|
|
1552
|
-
* `purgeOrphanSegments('csf.')`: a fresh pipeline-runner owns no live sessions,
|
|
1553
|
-
* so every pre-existing `csr.*` segment is by definition an orphan from a dead
|
|
1554
|
-
* instance. It MUST use the `csr.` prefix (never `csf.`): the decoder purge owns
|
|
1555
|
-
* `csf.` and unlinking a live retention ring would break media mid-session.
|
|
1556
|
-
*
|
|
1557
|
-
* `shm_unlink` only removes the name — a consumer still holding a mapping keeps
|
|
1558
|
-
* reading valid memory until it closes (POSIX deferred reclaim + the ring
|
|
1559
|
-
* seqlock), so unlinking is safe even if a stale reader is momentarily attached.
|
|
1560
|
-
*
|
|
1561
|
-
* POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
|
|
1562
|
-
* without that directory (Windows, macOS) the scan finds nothing — no-op.
|
|
1563
|
-
*/
|
|
1564
|
-
/** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
|
|
1565
|
-
var DEFAULT_SHM_DIR = "/dev/shm";
|
|
1566
1156
|
/**
|
|
1567
|
-
*
|
|
1568
|
-
* `
|
|
1569
|
-
* is created, to reclaim segments orphaned by a previously-crashed instance. A
|
|
1570
|
-
* per-file unlink failure is swallowed so one stuck segment cannot block
|
|
1571
|
-
* reclaiming the rest.
|
|
1157
|
+
* Node-level RAM store of retained rgb detection frames, keyed by
|
|
1158
|
+
* `FrameHandle` identity. Latest-wins with a byte budget.
|
|
1572
1159
|
*/
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1160
|
+
var RetainedFrameStore = class RetainedFrameStore {
|
|
1161
|
+
nodeId;
|
|
1162
|
+
budgetBytes;
|
|
1163
|
+
/** Insertion-ordered (Map) → oldest is `keys().next()`. */
|
|
1164
|
+
entries = /* @__PURE__ */ new Map();
|
|
1165
|
+
bytes = 0;
|
|
1166
|
+
seq = 0;
|
|
1167
|
+
/** Ring width used only to spread the `slot` field; the map key is authoritative. */
|
|
1168
|
+
static SLOT_SPREAD = 1024;
|
|
1169
|
+
constructor(options) {
|
|
1170
|
+
this.nodeId = options.nodeId;
|
|
1171
|
+
this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
|
|
1172
|
+
}
|
|
1173
|
+
/** Number of retained frames (tests / metrics). */
|
|
1174
|
+
get size() {
|
|
1175
|
+
return this.entries.size;
|
|
1176
|
+
}
|
|
1177
|
+
/** Total retained bytes (tests / metrics). */
|
|
1178
|
+
get retainedBytes() {
|
|
1179
|
+
return this.bytes;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Retain one decoded rgb frame and return its `FrameHandle`. Returns `null`
|
|
1183
|
+
* for a non-rgb frame (motion gray is never retained — post-analysis crops
|
|
1184
|
+
* from the rgb frame). The frame buffer is retained by reference: the
|
|
1185
|
+
* decode-worker IPC delivers a fresh Buffer per frame, so no defensive copy
|
|
1186
|
+
* is needed.
|
|
1187
|
+
*/
|
|
1188
|
+
write(deviceId, frame) {
|
|
1189
|
+
if (frame.format !== "rgb") return null;
|
|
1190
|
+
const seq = ++this.seq;
|
|
1191
|
+
const byteLength = frame.data.byteLength;
|
|
1192
|
+
const handle = {
|
|
1193
|
+
shmId: `${RETAINED_FRAME_KEY_PREFIX}${this.nodeId}:${deviceId}`,
|
|
1194
|
+
slot: seq % RetainedFrameStore.SLOT_SPREAD,
|
|
1195
|
+
seq,
|
|
1196
|
+
width: frame.width,
|
|
1197
|
+
height: frame.height,
|
|
1198
|
+
format: "rgb",
|
|
1199
|
+
pts: performance.now(),
|
|
1200
|
+
byteLength,
|
|
1201
|
+
nodeId: this.nodeId,
|
|
1202
|
+
slotCount: RetainedFrameStore.SLOT_SPREAD
|
|
1584
1203
|
};
|
|
1204
|
+
this.entries.set(keyOf(handle), frame);
|
|
1205
|
+
this.bytes += byteLength;
|
|
1206
|
+
this.evictToBudget();
|
|
1207
|
+
return handle;
|
|
1585
1208
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1209
|
+
/** The retained frame for `handle`, or `null` if never retained / already evicted. */
|
|
1210
|
+
getFrame(handle) {
|
|
1211
|
+
return this.entries.get(keyOf(handle)) ?? null;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Crop (and optionally downscale) the retained rgb frame for `handle` to the
|
|
1215
|
+
* normalized `bbox`. A full-frame `bbox` ({x:0,y:0,w:1,h:1}) with no
|
|
1216
|
+
* `maxWidth` returns the whole frame. Returns `null` when the frame is no
|
|
1217
|
+
* longer retained. Nearest-neighbour scale — this is the best-effort
|
|
1218
|
+
* post-analysis fallback path; native-resolution ROI crops come from the
|
|
1219
|
+
* decode worker's surface (`getNativeCrop`), not here.
|
|
1220
|
+
*/
|
|
1221
|
+
crop(handle, bbox, maxWidth) {
|
|
1222
|
+
const frame = this.entries.get(keyOf(handle));
|
|
1223
|
+
if (!frame || frame.format !== "rgb") return null;
|
|
1224
|
+
return cropRgb(frame, bbox, maxWidth);
|
|
1225
|
+
}
|
|
1226
|
+
/** Drop every retained frame (node/addon shutdown). */
|
|
1227
|
+
clear() {
|
|
1228
|
+
this.entries.clear();
|
|
1229
|
+
this.bytes = 0;
|
|
1230
|
+
}
|
|
1231
|
+
/** Evict oldest frames until the retained bytes fit the budget (keep ≥1). */
|
|
1232
|
+
evictToBudget() {
|
|
1233
|
+
while (this.bytes > this.budgetBytes && this.entries.size > 1) {
|
|
1234
|
+
const oldest = this.entries.keys().next().value;
|
|
1235
|
+
if (oldest === void 0) break;
|
|
1236
|
+
const evicted = this.entries.get(oldest);
|
|
1237
|
+
this.entries.delete(oldest);
|
|
1238
|
+
if (evicted) this.bytes -= evicted.data.byteLength;
|
|
1239
|
+
}
|
|
1240
|
+
if (this.bytes < 0) this.bytes = 0;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
/**
|
|
1244
|
+
* Nearest-neighbour crop + optional downscale of a packed RGB24 frame to a
|
|
1245
|
+
* normalized `bbox`. Exported for unit coverage. Clamps the bbox into the frame
|
|
1246
|
+
* and never allocates more than the output size.
|
|
1247
|
+
*/
|
|
1248
|
+
function cropRgb(frame, bbox, maxWidth) {
|
|
1249
|
+
const srcW = frame.width;
|
|
1250
|
+
const srcH = frame.height;
|
|
1251
|
+
const nx = Math.min(Math.max(bbox.x, 0), 1);
|
|
1252
|
+
const ny = Math.min(Math.max(bbox.y, 0), 1);
|
|
1253
|
+
const nw = Math.min(Math.max(bbox.w, 0), 1 - nx);
|
|
1254
|
+
const nh = Math.min(Math.max(bbox.h, 0), 1 - ny);
|
|
1255
|
+
const cropX = Math.floor(nx * srcW);
|
|
1256
|
+
const cropY = Math.floor(ny * srcH);
|
|
1257
|
+
const cropW = Math.max(1, Math.round(nw * srcW));
|
|
1258
|
+
const cropH = Math.max(1, Math.round(nh * srcH));
|
|
1259
|
+
const clampedW = Math.min(cropW, srcW - cropX);
|
|
1260
|
+
const clampedH = Math.min(cropH, srcH - cropY);
|
|
1261
|
+
const outW = maxWidth && maxWidth > 0 && maxWidth < clampedW ? maxWidth : clampedW;
|
|
1262
|
+
const outH = outW === clampedW ? clampedH : Math.max(1, Math.round(outW * clampedH / clampedW));
|
|
1263
|
+
const src = frame.data;
|
|
1264
|
+
const out = Buffer.allocUnsafe(outW * outH * 3);
|
|
1265
|
+
for (let oy = 0; oy < outH; oy += 1) {
|
|
1266
|
+
const srcRow = (cropY + Math.min(clampedH - 1, Math.floor(oy * clampedH / outH))) * srcW * 3;
|
|
1267
|
+
const outRow = oy * outW * 3;
|
|
1268
|
+
for (let ox = 0; ox < outW; ox += 1) {
|
|
1269
|
+
const s = srcRow + (cropX + Math.min(clampedW - 1, Math.floor(ox * clampedW / outW))) * 3;
|
|
1270
|
+
const o = outRow + ox * 3;
|
|
1271
|
+
out[o] = src[s];
|
|
1272
|
+
out[o + 1] = src[s + 1];
|
|
1273
|
+
out[o + 2] = src[s + 2];
|
|
1274
|
+
}
|
|
1593
1275
|
}
|
|
1594
1276
|
return {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1277
|
+
bytes: out,
|
|
1278
|
+
width: outW,
|
|
1279
|
+
height: outH
|
|
1598
1280
|
};
|
|
1599
1281
|
}
|
|
1600
1282
|
//#endregion
|
|
@@ -1984,19 +1666,6 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1984
1666
|
//#endregion
|
|
1985
1667
|
//#region src/session-decode/session-decode-pump.ts
|
|
1986
1668
|
/**
|
|
1987
|
-
* Pure gate: does this runner route camera decode through a per-session
|
|
1988
|
-
* decode worker (Epic C) instead of the shm frame-handle poller? This is now
|
|
1989
|
-
* a RUNNER-LEVEL setting (the pipeline-runner addon's own config,
|
|
1990
|
-
* `this.config.useSessionDecode`) rather than a per-camera one — every
|
|
1991
|
-
* camera attached to this node shares the same decode path. DEFAULT TRUE:
|
|
1992
|
-
* the caller passes the runner's own `useSessionDecode` config value;
|
|
1993
|
-
* an operator unchecks it (node-wide) to fall back to the shared shm
|
|
1994
|
-
* frame-handle poller for every camera on this node.
|
|
1995
|
-
*/
|
|
1996
|
-
function shouldUseSessionDecode(enabled) {
|
|
1997
|
-
return enabled === true;
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
1669
|
* Detection working width cap — mirrors the shm decoder's
|
|
2001
1670
|
* `initScaler` (`nodeav-decoder-session.ts`: `maxW = floor(640 / scale)`,
|
|
2002
1671
|
* `scale = 1` default). The worker decodes full source resolution (e.g. a 4K
|
|
@@ -2311,8 +1980,8 @@ async function resolveCrop(deps, input) {
|
|
|
2311
1980
|
}
|
|
2312
1981
|
if (input.cropJpeg) {
|
|
2313
1982
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
2314
|
-
const sharp = (await import("sharp")).default;
|
|
2315
|
-
const meta = await sharp(jpeg).metadata();
|
|
1983
|
+
const sharp$4 = (await import("sharp")).default;
|
|
1984
|
+
const meta = await sharp$4(jpeg).metadata();
|
|
2316
1985
|
const width = meta.width ?? 0;
|
|
2317
1986
|
const height = meta.height ?? 0;
|
|
2318
1987
|
if (width <= 0 || height <= 0) return null;
|
|
@@ -2363,7 +2032,6 @@ var DEFAULT_CONFIG = {
|
|
|
2363
2032
|
maxConcurrentInferences: 16,
|
|
2364
2033
|
targetLoadPercent: 80,
|
|
2365
2034
|
minThrottledFps: 1,
|
|
2366
|
-
useSessionDecode: true,
|
|
2367
2035
|
maxConcurrentDecodeSpawns: 3
|
|
2368
2036
|
};
|
|
2369
2037
|
/**
|
|
@@ -2379,6 +2047,11 @@ var DEFAULT_CONFIG = {
|
|
|
2379
2047
|
* cameras produced when every gate open/close forked and killed a worker.
|
|
2380
2048
|
* Mirrors Scrypted's decode-fork ~15s idle-timeout.
|
|
2381
2049
|
*/
|
|
2050
|
+
/** JPEG quality for the runner→executor detection-frame hop. q82 keeps
|
|
2051
|
+
* detector-relevant detail (source is h264 anyway) at ~10-15× less payload
|
|
2052
|
+
* than raw RGB — the hop transits the hub main process (UDS child↔child
|
|
2053
|
+
* routing), which OOM'd under raw frames. */
|
|
2054
|
+
var DETECTION_HOP_JPEG_QUALITY = 82;
|
|
2382
2055
|
var DETECTION_KEEPWARM_MS = 15e3;
|
|
2383
2056
|
/**
|
|
2384
2057
|
* Occupancy-burst safety timeout (both the dial path and the analyzer-tap
|
|
@@ -2546,7 +2219,7 @@ function resolveMotionFrameSource(config, sharedWithDetection) {
|
|
|
2546
2219
|
*/
|
|
2547
2220
|
function resolveSessionDecodeHostname(config, hubUrl) {
|
|
2548
2221
|
const frameSource = config.frameSource;
|
|
2549
|
-
if (!
|
|
2222
|
+
if (!require_remote_restream.isRemoteRestream(frameSource)) return void 0;
|
|
2550
2223
|
return resolveOwnerRestreamHost(frameSource, hubUrl);
|
|
2551
2224
|
}
|
|
2552
2225
|
/**
|
|
@@ -2557,7 +2230,7 @@ function resolveSessionDecodeHostname(config, hubUrl) {
|
|
|
2557
2230
|
*/
|
|
2558
2231
|
function resolveSessionDecodeOwnerNodeId(config) {
|
|
2559
2232
|
const frameSource = config.frameSource;
|
|
2560
|
-
if (!
|
|
2233
|
+
if (!require_remote_restream.isRemoteRestream(frameSource)) return void 0;
|
|
2561
2234
|
return frameSource.ownerNodeId;
|
|
2562
2235
|
}
|
|
2563
2236
|
/**
|
|
@@ -2593,7 +2266,7 @@ function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
|
2593
2266
|
async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
2594
2267
|
const { deviceId, streamId, tag, hostname, ownerNodeId } = params;
|
|
2595
2268
|
const pin = ownerNodeId !== void 0 ? require_dist.nodePin(ownerNodeId) : void 0;
|
|
2596
|
-
const profile =
|
|
2269
|
+
const profile = require_remote_restream.profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, pin), deviceId, streamId);
|
|
2597
2270
|
if (profile === null) {
|
|
2598
2271
|
logger.error("session-decode: no broker profile feeds camStream — cannot acquire restream", {
|
|
2599
2272
|
tags: { deviceId },
|
|
@@ -2668,6 +2341,15 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2668
2341
|
* to the detection-frame crop). See {@link NativeCropRegistry}.
|
|
2669
2342
|
*/
|
|
2670
2343
|
nativeCropRegistry = new NativeCropRegistry();
|
|
2344
|
+
/**
|
|
2345
|
+
* Node-level RAM store of retained rgb detection frames, keyed by the
|
|
2346
|
+
* `FrameHandle` that rides the inference-result event (shm-ring removal
|
|
2347
|
+
* 2026-07-16 — replaces the `csr.` shm retention ring). Backs the
|
|
2348
|
+
* FULL-FRAME branch of `getNativeCrop` so hub-side post-analysis resolves a
|
|
2349
|
+
* frame's pixels over tRPC (routed by `handle.nodeId`) with no `/dev/shm`.
|
|
2350
|
+
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
2351
|
+
*/
|
|
2352
|
+
retainedFrames = null;
|
|
2671
2353
|
attached = /* @__PURE__ */ new Map();
|
|
2672
2354
|
nodeId = "unknown";
|
|
2673
2355
|
metricsSnapshotTimer = null;
|
|
@@ -2704,7 +2386,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2704
2386
|
* payloads (`local-broker` / absent) — the map stays empty and every code
|
|
2705
2387
|
* path is identical to pre-Phase-2. Torn down on detach / shutdown.
|
|
2706
2388
|
*/
|
|
2707
|
-
remotePlanes = /* @__PURE__ */ new Map();
|
|
2708
2389
|
/**
|
|
2709
2390
|
* Devices whose motion currently rides the detection session-decode worker
|
|
2710
2391
|
* (P4 co-active sharing) instead of a dedicated motion worker. Membership →
|
|
@@ -2764,8 +2445,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2764
2445
|
occupancyMaxConcurrent = 3;
|
|
2765
2446
|
/**
|
|
2766
2447
|
* TAP-based occupancy bursts in flight, keyed by deviceId (Task 10). See
|
|
2767
|
-
* {@link OccupancyTapState}. A device has an entry here XOR is dialing
|
|
2768
|
-
*
|
|
2448
|
+
* {@link OccupancyTapState}. A device has an entry here XOR is dialing a
|
|
2449
|
+
* session-decode burst in `runOccupancyBurst` — never both; the choice
|
|
2769
2450
|
* is made once at burst start based on `attachment.motionUnsubscribe`.
|
|
2770
2451
|
*/
|
|
2771
2452
|
occupancyTaps = /* @__PURE__ */ new Map();
|
|
@@ -2785,16 +2466,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2785
2466
|
async onInitialize() {
|
|
2786
2467
|
const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
2787
2468
|
this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
scanned: purged.scanned,
|
|
2793
|
-
prefix: RETENTION_SEGMENT_PREFIX
|
|
2794
|
-
} });
|
|
2795
|
-
} catch (err) {
|
|
2796
|
-
this.ctx.logger.warn("session-decode retention: orphan purge failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
2797
|
-
}
|
|
2469
|
+
this.retainedFrames = new RetainedFrameStore({
|
|
2470
|
+
nodeId: this.nodeId,
|
|
2471
|
+
logger: this.ctx.logger.child("retained-frames")
|
|
2472
|
+
});
|
|
2798
2473
|
this.runner = new PipelineRunner({
|
|
2799
2474
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2800
2475
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -2867,9 +2542,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2867
2542
|
}
|
|
2868
2543
|
this.lastAnalyzerDetected.clear();
|
|
2869
2544
|
for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
|
|
2870
|
-
const remotePlanes = [...this.remotePlanes.values()];
|
|
2871
|
-
this.remotePlanes.clear();
|
|
2872
|
-
await Promise.all(remotePlanes.map((plane) => plane.dispose().catch(() => {})));
|
|
2873
2545
|
if (this.runner) {
|
|
2874
2546
|
this.runner.stop();
|
|
2875
2547
|
this.runner = null;
|
|
@@ -2884,10 +2556,13 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2884
2556
|
}
|
|
2885
2557
|
this.attached.clear();
|
|
2886
2558
|
this.lastMotionFrameAt.clear();
|
|
2559
|
+
this.nativeCropRegistry.clear();
|
|
2560
|
+
this.retainedFrames?.clear();
|
|
2561
|
+
this.retainedFrames = null;
|
|
2887
2562
|
}
|
|
2888
2563
|
async cacheBenchFrame(input) {
|
|
2889
|
-
const sharp = (await import("sharp")).default;
|
|
2890
|
-
const { data, info } = await sharp(Buffer.from(input.imageBase64, "base64")).raw().toBuffer({ resolveWithObject: true });
|
|
2564
|
+
const sharp$2 = (await import("sharp")).default;
|
|
2565
|
+
const { data, info } = await sharp$2(Buffer.from(input.imageBase64, "base64")).raw().toBuffer({ resolveWithObject: true });
|
|
2891
2566
|
if (info.channels !== 3) throw new Error(`cacheBenchFrame: expected 3 channels (rgb), got ${info.channels}`);
|
|
2892
2567
|
const rgb = new Uint8Array(data);
|
|
2893
2568
|
const ttlMs = Math.max(6e4, (input.ttlSeconds ?? 600) * 1e3);
|
|
@@ -3274,13 +2949,23 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3274
2949
|
* worker downloads ONLY the ROI — a full native frame never travels the wire.
|
|
3275
2950
|
*/
|
|
3276
2951
|
async getNativeCrop(input) {
|
|
3277
|
-
const
|
|
3278
|
-
if (!
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
2952
|
+
const b = input.bbox;
|
|
2953
|
+
if (!(b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999)) {
|
|
2954
|
+
const requester = this.nativeCropRegistry.get(input.handle);
|
|
2955
|
+
if (requester) {
|
|
2956
|
+
const result = await requester(input.bbox, input.maxWidth);
|
|
2957
|
+
if (result) return {
|
|
2958
|
+
bytes: result.bytes,
|
|
2959
|
+
width: result.width,
|
|
2960
|
+
height: result.height
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
|
|
2965
|
+
return crop ? {
|
|
2966
|
+
bytes: crop.bytes,
|
|
2967
|
+
width: crop.width,
|
|
2968
|
+
height: crop.height
|
|
3284
2969
|
} : null;
|
|
3285
2970
|
}
|
|
3286
2971
|
/**
|
|
@@ -3324,9 +3009,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3324
3009
|
bbox: paddedBbox
|
|
3325
3010
|
});
|
|
3326
3011
|
if (!native) return null;
|
|
3327
|
-
const sharp = (await import("sharp")).default;
|
|
3012
|
+
const sharp$3 = (await import("sharp")).default;
|
|
3328
3013
|
return {
|
|
3329
|
-
jpeg: await sharp(Buffer.from(native.bytes), { raw: {
|
|
3014
|
+
jpeg: await sharp$3(Buffer.from(native.bytes), { raw: {
|
|
3330
3015
|
width: native.width,
|
|
3331
3016
|
height: native.height,
|
|
3332
3017
|
channels: 3
|
|
@@ -3413,16 +3098,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3413
3098
|
attachment.detectionUnsubscribe?.();
|
|
3414
3099
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3415
3100
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3416
|
-
const remotePlane = this.remotePlanes.get(deviceId);
|
|
3417
|
-
if (remotePlane) {
|
|
3418
|
-
this.remotePlanes.delete(deviceId);
|
|
3419
|
-
remotePlane.dispose().catch((err) => {
|
|
3420
|
-
this.ctx?.logger.debug("remote-source plane dispose failed on detach", {
|
|
3421
|
-
tags: { deviceId },
|
|
3422
|
-
meta: { error: require_dist.errMsg(err) }
|
|
3423
|
-
});
|
|
3424
|
-
});
|
|
3425
|
-
}
|
|
3426
3101
|
this.attached.delete(deviceId);
|
|
3427
3102
|
this.lastMotionAt.delete(deviceId);
|
|
3428
3103
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -3540,65 +3215,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3540
3215
|
return [...this.attached.keys()];
|
|
3541
3216
|
}
|
|
3542
3217
|
/**
|
|
3543
|
-
* Cross-node frame fetch for FOREIGN handles (segment on another node). Wired
|
|
3544
|
-
* into every `startFrameHandlePoller` call so `resolvePolledFrame` can take
|
|
3545
|
-
* the bounded `decoder.getFrame` fallback instead of an unmappable local shm
|
|
3546
|
-
* read. The `nodeId` selector routes the call to the producing node (the
|
|
3547
|
-
* cap's `nodeIdMode: 'routing'` default; agent→hub via `$hub-cap-fwd`). The
|
|
3548
|
-
* wire schema returns `data: Uint8Array`; downstream inference expects a Node
|
|
3549
|
-
* `Buffer`, so we adapt it here exactly as post-analysis does.
|
|
3550
|
-
*/
|
|
3551
|
-
async getRemoteFrame(handle) {
|
|
3552
|
-
const api = this.ctx.api;
|
|
3553
|
-
if (!api) return null;
|
|
3554
|
-
const remote = await api.decoder.getFrame.query({
|
|
3555
|
-
handle,
|
|
3556
|
-
nodeId: handle.nodeId
|
|
3557
|
-
});
|
|
3558
|
-
if (!remote) return null;
|
|
3559
|
-
return {
|
|
3560
|
-
data: Buffer.from(remote.data),
|
|
3561
|
-
width: remote.width,
|
|
3562
|
-
height: remote.height,
|
|
3563
|
-
format: remote.format,
|
|
3564
|
-
timestamp: remote.timestamp
|
|
3565
|
-
};
|
|
3566
|
-
}
|
|
3567
|
-
/**
|
|
3568
|
-
* The remote-source leg for a camera, or `null` for today's co-located
|
|
3569
|
-
* broker path (P2d). Created lazily on the FIRST subscription of a
|
|
3570
|
-
* `remote-restream` camera and shared by its motion + detection pollers;
|
|
3571
|
-
* `detachInternal` / `onShutdown` dispose it. With rollout OFF every
|
|
3572
|
-
* attach payload is `local-broker` (or absent) → always `null` → the
|
|
3573
|
-
* subscribe paths below are byte-identical to pre-Phase-2.
|
|
3574
|
-
*/
|
|
3575
|
-
remoteSourceFor(config) {
|
|
3576
|
-
const frameSource = config.frameSource;
|
|
3577
|
-
if (!require_remote_source_plane.isRemoteRestream(frameSource)) return null;
|
|
3578
|
-
const existing = this.remotePlanes.get(config.deviceId);
|
|
3579
|
-
if (existing) return existing;
|
|
3580
|
-
const api = this.ctx.api;
|
|
3581
|
-
if (!api) return null;
|
|
3582
|
-
const hostname = resolveOwnerRestreamHost(frameSource, process.env["CAMSTACK_HUB_URL"]);
|
|
3583
|
-
const plane = new require_remote_source_plane.RemoteSourcePlane({
|
|
3584
|
-
api,
|
|
3585
|
-
logger: this.ctx.logger,
|
|
3586
|
-
deviceId: config.deviceId,
|
|
3587
|
-
ownNodeId: this.nodeId,
|
|
3588
|
-
ownerNodeId: frameSource.ownerNodeId,
|
|
3589
|
-
hostname
|
|
3590
|
-
});
|
|
3591
|
-
this.ctx.logger.info("remote-source leg armed for camera", {
|
|
3592
|
-
tags: { deviceId: config.deviceId },
|
|
3593
|
-
meta: {
|
|
3594
|
-
ownerNodeId: frameSource.ownerNodeId,
|
|
3595
|
-
hostname: hostname ?? "(unresolved — acquires will fail until set)"
|
|
3596
|
-
}
|
|
3597
|
-
});
|
|
3598
|
-
this.remotePlanes.set(config.deviceId, plane);
|
|
3599
|
-
return plane;
|
|
3600
|
-
}
|
|
3601
|
-
/**
|
|
3602
3218
|
* Thin delegator to the module-level {@link acquireSessionDecodeRestreamFrom}
|
|
3603
3219
|
* (see its doc for the acquire behavior) — supplies this addon's `ctx.logger`.
|
|
3604
3220
|
* Kept as a method so the two call sites need no change.
|
|
@@ -3700,11 +3316,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3700
3316
|
const runner = this.runner;
|
|
3701
3317
|
const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
|
|
3702
3318
|
if (!runner) return () => {};
|
|
3703
|
-
const
|
|
3704
|
-
deviceId: config.deviceId,
|
|
3705
|
-
nodeId: this.nodeId,
|
|
3706
|
-
logger: log
|
|
3707
|
-
});
|
|
3319
|
+
const retainedFrames = this.retainedFrames;
|
|
3708
3320
|
const teardown = startGuardedSessionDecode({
|
|
3709
3321
|
logger: log,
|
|
3710
3322
|
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
@@ -3724,7 +3336,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3724
3336
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
3725
3337
|
onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
|
|
3726
3338
|
onDecodedFrame: (frame, nativeCrop) => {
|
|
3727
|
-
const handle =
|
|
3339
|
+
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3728
3340
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
3729
3341
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
3730
3342
|
}
|
|
@@ -3733,7 +3345,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3733
3345
|
});
|
|
3734
3346
|
return () => {
|
|
3735
3347
|
teardown();
|
|
3736
|
-
retentionSink.destroy();
|
|
3737
3348
|
};
|
|
3738
3349
|
}
|
|
3739
3350
|
/**
|
|
@@ -3799,30 +3410,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3799
3410
|
log.warn("subscribeMotionFrames: this.ctx.api not available");
|
|
3800
3411
|
return null;
|
|
3801
3412
|
}
|
|
3802
|
-
if (
|
|
3803
|
-
|
|
3804
|
-
return this.startMotionSessionDecode(config, api);
|
|
3805
|
-
}
|
|
3806
|
-
const src = resolveMotionFrameSource(config, opts?.sharedWithDetection === true);
|
|
3807
|
-
const shared = src.shared;
|
|
3808
|
-
const streamId = src.streamId;
|
|
3809
|
-
const remote = this.remoteSourceFor(config);
|
|
3810
|
-
return startFrameHandlePoller({
|
|
3811
|
-
api,
|
|
3812
|
-
brokerId: require_dist.makeSourceBrokerId(config.deviceId, streamId),
|
|
3813
|
-
format: src.format,
|
|
3814
|
-
maxFps: config.motionFps,
|
|
3815
|
-
tag: shared ? "motion-shared" : "motion",
|
|
3816
|
-
logger: log,
|
|
3817
|
-
ownNodeId: this.nodeId,
|
|
3818
|
-
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
3819
|
-
...remote ? { source: remote.makeSource(streamId) } : {},
|
|
3820
|
-
onFrame: shared ? (frame) => {
|
|
3821
|
-
runner.enqueueMotionFrame(config.deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
3822
|
-
} : (frame, handle) => {
|
|
3823
|
-
runner.enqueueMotionFrame(config.deviceId, frame, handle);
|
|
3824
|
-
}
|
|
3825
|
-
});
|
|
3413
|
+
if (resolveMotionFrameSource(config, opts?.sharedWithDetection === true).shared) return this.registerSharedMotionSink(config.deviceId, config.motionFps);
|
|
3414
|
+
return this.startMotionSessionDecode(config, api);
|
|
3826
3415
|
}
|
|
3827
3416
|
handleDetectionStreamChange(deviceId, action) {
|
|
3828
3417
|
const attachment = this.attached.get(deviceId);
|
|
@@ -3910,24 +3499,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3910
3499
|
log.warn("subscribeDetectionFrames: this.ctx.api not available");
|
|
3911
3500
|
return null;
|
|
3912
3501
|
}
|
|
3913
|
-
|
|
3914
|
-
const remote = this.remoteSourceFor(config);
|
|
3915
|
-
return startFrameHandlePoller({
|
|
3916
|
-
api,
|
|
3917
|
-
brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.detectionStreamId),
|
|
3918
|
-
format: "rgb",
|
|
3919
|
-
maxFps: config.detectionFps,
|
|
3920
|
-
tag: "detection",
|
|
3921
|
-
logger: log,
|
|
3922
|
-
ownNodeId: this.nodeId,
|
|
3923
|
-
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
3924
|
-
...remote ? { source: remote.makeSource(config.detectionStreamId) } : {},
|
|
3925
|
-
onFrame: (frame, handle) => {
|
|
3926
|
-
runner.enqueueDetectionFrame(config.deviceId, frame, handle);
|
|
3927
|
-
}
|
|
3928
|
-
});
|
|
3502
|
+
return this.startDetectionSessionDecode(config, api);
|
|
3929
3503
|
}
|
|
3930
|
-
async runInference(deviceId, frame,
|
|
3504
|
+
async runInference(deviceId, frame, _handle) {
|
|
3931
3505
|
if (!this.ctx) return null;
|
|
3932
3506
|
const log = this.ctx.logger.withTags({ deviceId });
|
|
3933
3507
|
const api = this.ctx.api;
|
|
@@ -3941,11 +3515,20 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3941
3515
|
return null;
|
|
3942
3516
|
}
|
|
3943
3517
|
if (steps.length === 0) return null;
|
|
3944
|
-
|
|
3518
|
+
let image;
|
|
3519
|
+
if (frame.format === "rgb") try {
|
|
3520
|
+
image = await (0, sharp.default)(frame.data, { raw: {
|
|
3521
|
+
width: frame.width,
|
|
3522
|
+
height: frame.height,
|
|
3523
|
+
channels: 3
|
|
3524
|
+
} }).jpeg({ quality: DETECTION_HOP_JPEG_QUALITY }).toBuffer();
|
|
3525
|
+
} catch (err) {
|
|
3526
|
+
log.debug("runInference: frame JPEG encode failed — falling back to raw pixels", { meta: { error: require_dist.errMsg(err) } });
|
|
3527
|
+
}
|
|
3945
3528
|
try {
|
|
3946
3529
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
3947
3530
|
steps: [...steps],
|
|
3948
|
-
...
|
|
3531
|
+
...image !== void 0 ? { image } : { frame },
|
|
3949
3532
|
deviceId,
|
|
3950
3533
|
plane: "frame"
|
|
3951
3534
|
});
|
|
@@ -3969,10 +3552,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3969
3552
|
log.warn("runMotionAnalysis: this.ctx.api not available");
|
|
3970
3553
|
return;
|
|
3971
3554
|
}
|
|
3972
|
-
const useHandle = handle !== void 0 && handle.nodeId === this.nodeId;
|
|
3973
3555
|
const result = await api.motionDetection.analyze.mutate({
|
|
3974
3556
|
deviceId,
|
|
3975
|
-
|
|
3557
|
+
frame: toFrameInput(frame)
|
|
3976
3558
|
});
|
|
3977
3559
|
if (!result) return;
|
|
3978
3560
|
const detected = result.regions.length > 0;
|
|
@@ -4190,9 +3772,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4190
3772
|
await this.runOccupancyDial(deviceId, frames, attachment, runner, api, log, false);
|
|
4191
3773
|
}
|
|
4192
3774
|
/**
|
|
4193
|
-
* Dial-path occupancy burst:
|
|
4194
|
-
*
|
|
4195
|
-
* at {@link OCCUPANCY_BURST_SAFETY_TIMEOUT_MS}). Called either directly
|
|
3775
|
+
* Dial-path occupancy burst: spins up a fresh per-session decode worker
|
|
3776
|
+
* (session-decode) over the main stream and collects `frames` decoded frames
|
|
3777
|
+
* (or times out at {@link OCCUPANCY_BURST_SAFETY_TIMEOUT_MS}). Called either directly
|
|
4196
3778
|
* from `runOccupancyBurst` (no live/fresh motion tap available,
|
|
4197
3779
|
* `alreadyInFlight: false`) or as the Task 10 hardening #2 fallback after
|
|
4198
3780
|
* a zero-collected tap (`alreadyInFlight: true` — the tap already claimed
|
|
@@ -4201,6 +3783,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4201
3783
|
*/
|
|
4202
3784
|
async runOccupancyDial(deviceId, frames, attachment, runner, api, log, alreadyInFlight) {
|
|
4203
3785
|
const config = attachment.config;
|
|
3786
|
+
const retainedFrames = this.retainedFrames;
|
|
4204
3787
|
if (!alreadyInFlight) {
|
|
4205
3788
|
this.occupancyBurstInFlight.add(deviceId);
|
|
4206
3789
|
this.occupancyBurstFound.delete(deviceId);
|
|
@@ -4210,37 +3793,35 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4210
3793
|
fallback: alreadyInFlight
|
|
4211
3794
|
} });
|
|
4212
3795
|
let count = 0;
|
|
4213
|
-
let unsubscribe = null;
|
|
4214
|
-
let unsubbed = false;
|
|
4215
3796
|
let settled = false;
|
|
4216
|
-
|
|
4217
|
-
if (!unsubbed && unsubscribe) {
|
|
4218
|
-
unsubbed = true;
|
|
4219
|
-
unsubscribe();
|
|
4220
|
-
}
|
|
4221
|
-
};
|
|
3797
|
+
let teardown = null;
|
|
4222
3798
|
const cleanup = () => {
|
|
4223
3799
|
if (settled) return;
|
|
4224
3800
|
settled = true;
|
|
4225
|
-
|
|
3801
|
+
teardown?.();
|
|
4226
3802
|
this.finishOccupancyBurst(deviceId, log, count);
|
|
4227
3803
|
};
|
|
4228
3804
|
const safetyTimer = setTimeout(() => {
|
|
4229
3805
|
log.debug("occupancy re-check: safety timeout reached", { meta: { collected: count } });
|
|
4230
3806
|
cleanup();
|
|
4231
3807
|
}, OCCUPANCY_BURST_SAFETY_TIMEOUT_MS);
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
maxFps: config.detectionFps,
|
|
4238
|
-
tag: "occupancy",
|
|
3808
|
+
teardown = startGuardedSessionDecode({
|
|
3809
|
+
logger: log,
|
|
3810
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
3811
|
+
startPump: (source) => startSessionDecodePump({
|
|
3812
|
+
fork: forkDecodeWorker,
|
|
4239
3813
|
logger: log,
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
3814
|
+
source,
|
|
3815
|
+
opts: {
|
|
3816
|
+
fps: config.detectionFps,
|
|
3817
|
+
format: "rgb"
|
|
3818
|
+
},
|
|
3819
|
+
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "occupancy"),
|
|
3820
|
+
onEnded: () => cleanup(),
|
|
3821
|
+
onDecodedFrame: (frame, nativeCrop) => {
|
|
4243
3822
|
if (settled) return;
|
|
3823
|
+
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3824
|
+
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
4244
3825
|
runner.enqueueOccupancyFrame(deviceId, frame, handle);
|
|
4245
3826
|
count++;
|
|
4246
3827
|
if (count >= frames) {
|
|
@@ -4248,17 +3829,11 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4248
3829
|
cleanup();
|
|
4249
3830
|
}
|
|
4250
3831
|
}
|
|
4251
|
-
})
|
|
4252
|
-
}
|
|
4253
|
-
clearTimeout(safetyTimer);
|
|
4254
|
-
this.occupancyBurstInFlight.delete(deviceId);
|
|
4255
|
-
this.occupancyBurstFound.delete(deviceId);
|
|
4256
|
-
log.debug("occupancy re-check: poller start failed", { meta: { error: require_dist.errMsg(err) } });
|
|
4257
|
-
return;
|
|
4258
|
-
}
|
|
3832
|
+
})
|
|
3833
|
+
});
|
|
4259
3834
|
if (settled) {
|
|
4260
3835
|
clearTimeout(safetyTimer);
|
|
4261
|
-
|
|
3836
|
+
teardown();
|
|
4262
3837
|
}
|
|
4263
3838
|
}
|
|
4264
3839
|
emitInferenceResult(deviceId, frame, result, handle) {
|
|
@@ -4392,14 +3967,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4392
3967
|
default: DEFAULT_CONFIG.minThrottledFps,
|
|
4393
3968
|
showValue: true
|
|
4394
3969
|
},
|
|
4395
|
-
{
|
|
4396
|
-
type: "boolean",
|
|
4397
|
-
key: "useSessionDecode",
|
|
4398
|
-
style: "checkbox",
|
|
4399
|
-
label: "Use per-session decode workers",
|
|
4400
|
-
description: "Default decode path for every camera on this runner/node: each attached camera decodes in a disposable per-session worker process instead of the shared shm frame-handle poller, freeing the VAAPI/GPU decode pool (and any /dev/shm ring segments) as soon as it goes idle. Uncheck to fall this node back to the shared shm frame-handle poller for ALL its cameras — no per-camera override.",
|
|
4401
|
-
default: DEFAULT_CONFIG.useSessionDecode
|
|
4402
|
-
},
|
|
4403
3970
|
{
|
|
4404
3971
|
type: "slider",
|
|
4405
3972
|
key: "maxConcurrentDecodeSpawns",
|
|
@@ -4422,7 +3989,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4422
3989
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
4423
3990
|
targetLoadPercent: this.config.targetLoadPercent,
|
|
4424
3991
|
minThrottledFps: this.config.minThrottledFps,
|
|
4425
|
-
useSessionDecode: this.config.useSessionDecode,
|
|
4426
3992
|
maxConcurrentDecodeSpawns: this.config.maxConcurrentDecodeSpawns
|
|
4427
3993
|
} });
|
|
4428
3994
|
}
|