@camstack/addon-pipeline 1.1.24 → 1.1.26
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/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/decoder-ffmpeg/index.js +84 -12
- package/dist/decoder-ffmpeg/index.mjs +84 -12
- package/dist/detection-pipeline/index.js +149 -27
- package/dist/detection-pipeline/index.mjs +149 -27
- package/dist/{dist-pd_-3C0T.mjs → dist-CgEP_0OL.mjs} +794 -16
- package/dist/{dist-CE3a05qT.js → dist-DAIlCdAx.js} +799 -15
- package/dist/frame-handle-plane-Dq20KtKL.mjs +636 -0
- package/dist/frame-handle-plane-DtTRX_0n.js +647 -0
- package/dist/hub-hostname-DAJXlOgV.js +54 -0
- package/dist/hub-hostname-cCknRYKj.mjs +49 -0
- package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +341 -17
- package/dist/pipeline-runner/index.mjs +341 -17
- package/dist/recorder/index.js +267 -65
- package/dist/recorder/index.mjs +267 -65
- package/dist/stream-broker/_stub.js +39 -39
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BJK0-svt.mjs +26 -0
- package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-DyLqyJaS.mjs} +2 -2
- package/dist/stream-broker/index.js +7 -639
- package/dist/stream-broker/index.mjs +5 -637
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-CoLjNiUN.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js} +1 -1
- package/embed-dist/assets/index-C-pL8ETk.js +81 -0
- package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/python/inference_pool.py +522 -27
- package/python/test_inference_pool_backpressure.py +121 -0
- package/python/test_inference_pool_coreml_cache.py +416 -0
- package/python/test_inference_pool_device_selection.py +256 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
- package/embed-dist/assets/index-C7SSikPl.css +0 -2
- package/embed-dist/assets/index-DhdVxWXL.js +0 -80
- package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as errMsg, C as defineCustomActions, G as EventCategory, J as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Z as makeSourceBrokerId, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, rt as array } from "../dist-CgEP_0OL.mjs";
|
|
2
|
+
import { t as FrameHandlePlane } from "../frame-handle-plane-Dq20KtKL.mjs";
|
|
3
|
+
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
2
4
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
3
5
|
//#region src/pipeline-runner/bench-actions.ts
|
|
4
6
|
/**
|
|
@@ -236,6 +238,26 @@ var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
|
236
238
|
*/
|
|
237
239
|
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
238
240
|
/**
|
|
241
|
+
* The default source: the broker's shm plane over tRPC — today's path. The
|
|
242
|
+
* methods return the underlying client promises DIRECTLY (no async wrapper)
|
|
243
|
+
* so the poller's scheduling is microtask-identical to the pre-seam code.
|
|
244
|
+
*/
|
|
245
|
+
function brokerFrameHandleSource(api, brokerId) {
|
|
246
|
+
return {
|
|
247
|
+
subscribe: (input) => api.streamBroker.subscribeFrames.mutate({
|
|
248
|
+
brokerId,
|
|
249
|
+
format: input.format,
|
|
250
|
+
maxFps: input.maxFps,
|
|
251
|
+
tag: input.tag
|
|
252
|
+
}),
|
|
253
|
+
pullHandles: (input) => api.streamBroker.pullFrameHandles.query({
|
|
254
|
+
subscriptionId: input.subscriptionId,
|
|
255
|
+
maxCount: input.maxCount
|
|
256
|
+
}),
|
|
257
|
+
unsubscribe: (input) => api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: input.subscriptionId }).then(() => void 0)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
239
261
|
* Subscribe to a broker's shm frame stream and start the poll loop. Always
|
|
240
262
|
* resolves to a teardown closure — when the broker is not yet registered the
|
|
241
263
|
* closure cancels the ongoing retry loop; when polling is active it stops the
|
|
@@ -267,19 +289,19 @@ async function startFrameHandlePoller(options) {
|
|
|
267
289
|
*/
|
|
268
290
|
async function subscribeWithRetry(options, lifecycle) {
|
|
269
291
|
const { api, brokerId, format, maxFps, tag, logger } = options;
|
|
292
|
+
const source = options.source ?? brokerFrameHandleSource(api, brokerId);
|
|
270
293
|
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
271
294
|
let attempt = 0;
|
|
272
295
|
while (!lifecycle.stopped) {
|
|
273
296
|
attempt += 1;
|
|
274
297
|
try {
|
|
275
|
-
const result = await
|
|
276
|
-
brokerId,
|
|
298
|
+
const result = await source.subscribe({
|
|
277
299
|
format,
|
|
278
300
|
maxFps,
|
|
279
301
|
tag
|
|
280
302
|
});
|
|
281
303
|
if (lifecycle.stopped) {
|
|
282
|
-
await
|
|
304
|
+
await source.unsubscribe({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
283
305
|
logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
|
|
284
306
|
brokerId,
|
|
285
307
|
subscriptionId: result.subscriptionId,
|
|
@@ -288,7 +310,7 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
288
310
|
});
|
|
289
311
|
return;
|
|
290
312
|
}
|
|
291
|
-
lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
|
|
313
|
+
lifecycle.activeTeardown = startPolling(options, source, result.subscriptionId, result.maxFps, lifecycle);
|
|
292
314
|
return;
|
|
293
315
|
} catch (err) {
|
|
294
316
|
if (lifecycle.stopped) return;
|
|
@@ -329,8 +351,8 @@ function sleep(ms, lifecycle) {
|
|
|
329
351
|
* Returns the teardown closure that stops the loop and releases the
|
|
330
352
|
* subscription.
|
|
331
353
|
*/
|
|
332
|
-
function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
333
|
-
const {
|
|
354
|
+
function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
355
|
+
const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
|
|
334
356
|
const readers = new FrameRingReaderCache(logger);
|
|
335
357
|
const throttle = new ForeignHandleThrottle();
|
|
336
358
|
const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
|
|
@@ -338,7 +360,7 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
338
360
|
const tick = async () => {
|
|
339
361
|
if (lifecycle.stopped) return;
|
|
340
362
|
try {
|
|
341
|
-
const handles = await
|
|
363
|
+
const handles = await source.pullHandles({
|
|
342
364
|
subscriptionId,
|
|
343
365
|
maxCount: PULL_MAX_COUNT
|
|
344
366
|
});
|
|
@@ -370,8 +392,8 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
370
392
|
timer = void 0;
|
|
371
393
|
}
|
|
372
394
|
readers.close();
|
|
373
|
-
|
|
374
|
-
logger.warn("frame-handle poller:
|
|
395
|
+
source.unsubscribe({ subscriptionId }).catch((err) => {
|
|
396
|
+
logger.warn("frame-handle poller: unsubscribe failed", { meta: {
|
|
375
397
|
brokerId,
|
|
376
398
|
subscriptionId,
|
|
377
399
|
error: errMsg(err)
|
|
@@ -380,6 +402,221 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
380
402
|
};
|
|
381
403
|
}
|
|
382
404
|
//#endregion
|
|
405
|
+
//#region src/pipeline-runner/remote-source-plane.ts
|
|
406
|
+
/**
|
|
407
|
+
* Runner-side mode selection (pure): whether an attach payload routes this
|
|
408
|
+
* camera through the remote-source leg. Absent `frameSource` (a pre-P2c
|
|
409
|
+
* payload) and `local-broker` both take today's co-located broker path —
|
|
410
|
+
* the rollout-OFF safety invariant.
|
|
411
|
+
*/
|
|
412
|
+
function isRemoteRestream(frameSource) {
|
|
413
|
+
return frameSource !== void 0 && frameSource.kind === "remote-restream";
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Resolve WHICH broker profile a camStream id feeds for a device (pure).
|
|
417
|
+
*
|
|
418
|
+
* `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
|
|
419
|
+
* runner is dispatched with camStream ids (`motionStreamId` /
|
|
420
|
+
* `detectionStreamId`). The broker's profile slots carry the mapping
|
|
421
|
+
* (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
|
|
422
|
+
* fails fast and the poller's backoff retries (the assignment may still be
|
|
423
|
+
* propagating).
|
|
424
|
+
*/
|
|
425
|
+
function profileForStreamId(slots, deviceId, streamId) {
|
|
426
|
+
for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
var RemoteSourcePlane = class {
|
|
430
|
+
api;
|
|
431
|
+
logger;
|
|
432
|
+
deviceId;
|
|
433
|
+
ownNodeId;
|
|
434
|
+
hostname;
|
|
435
|
+
/** Single-flight entry creation per camStream id. */
|
|
436
|
+
entries = /* @__PURE__ */ new Map();
|
|
437
|
+
disposed = false;
|
|
438
|
+
constructor(options) {
|
|
439
|
+
this.api = options.api;
|
|
440
|
+
this.logger = options.logger.withTags({ deviceId: options.deviceId });
|
|
441
|
+
this.deviceId = options.deviceId;
|
|
442
|
+
this.ownNodeId = options.ownNodeId;
|
|
443
|
+
this.hostname = options.hostname;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* A {@link FrameHandleSource} for one camStream id, pluggable into
|
|
447
|
+
* `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
|
|
448
|
+
* subscriptions of the same stream share ONE acquired restream and ONE
|
|
449
|
+
* satellite plane (per-format decoder sessions inside it — hub parity).
|
|
450
|
+
*/
|
|
451
|
+
makeSource(streamId) {
|
|
452
|
+
return {
|
|
453
|
+
subscribe: async (input) => {
|
|
454
|
+
if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
|
|
455
|
+
const entry = await this.ensureEntry(streamId);
|
|
456
|
+
const result = await entry.plane.subscribe({
|
|
457
|
+
format: input.format,
|
|
458
|
+
maxFps: input.maxFps,
|
|
459
|
+
tag: input.tag
|
|
460
|
+
});
|
|
461
|
+
entry.subscriptionIds.add(result.subscriptionId);
|
|
462
|
+
return result;
|
|
463
|
+
},
|
|
464
|
+
pullHandles: async (input) => {
|
|
465
|
+
const entry = await this.peekEntry(streamId);
|
|
466
|
+
if (!entry) return [];
|
|
467
|
+
return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
|
|
468
|
+
},
|
|
469
|
+
unsubscribe: async (input) => {
|
|
470
|
+
const entry = await this.peekEntry(streamId);
|
|
471
|
+
if (!entry) return;
|
|
472
|
+
entry.subscriptionIds.delete(input.subscriptionId);
|
|
473
|
+
await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
|
|
474
|
+
if (entry.subscriptionIds.size === 0) {
|
|
475
|
+
this.entries.delete(streamId);
|
|
476
|
+
await this.teardownEntry(entry, streamId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
|
|
482
|
+
async dispose() {
|
|
483
|
+
if (this.disposed) return;
|
|
484
|
+
this.disposed = true;
|
|
485
|
+
const pending = [...this.entries.entries()];
|
|
486
|
+
this.entries.clear();
|
|
487
|
+
for (const [streamId, entryPromise] of pending) try {
|
|
488
|
+
const entry = await entryPromise;
|
|
489
|
+
await this.teardownEntry(entry, streamId);
|
|
490
|
+
} catch {}
|
|
491
|
+
}
|
|
492
|
+
/** The live entry for a stream, or `null` (never creates one). */
|
|
493
|
+
async peekEntry(streamId) {
|
|
494
|
+
const pending = this.entries.get(streamId);
|
|
495
|
+
if (!pending) return null;
|
|
496
|
+
try {
|
|
497
|
+
return await pending;
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Single-flight: acquire the restream + build the satellite plane once per stream. */
|
|
503
|
+
ensureEntry(streamId) {
|
|
504
|
+
const existing = this.entries.get(streamId);
|
|
505
|
+
if (existing) return existing;
|
|
506
|
+
const creation = this.createEntry(streamId);
|
|
507
|
+
this.entries.set(streamId, creation);
|
|
508
|
+
creation.catch(() => {
|
|
509
|
+
if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
|
|
510
|
+
});
|
|
511
|
+
return creation;
|
|
512
|
+
}
|
|
513
|
+
async createEntry(streamId) {
|
|
514
|
+
const { api, deviceId, ownNodeId } = this;
|
|
515
|
+
if (this.hostname === void 0) throw new Error("RemoteSourcePlane: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
|
|
516
|
+
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
|
|
517
|
+
if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
|
|
518
|
+
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
519
|
+
deviceId,
|
|
520
|
+
video: "copy",
|
|
521
|
+
audio: "none",
|
|
522
|
+
profile,
|
|
523
|
+
hostname: this.hostname,
|
|
524
|
+
tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
|
|
525
|
+
});
|
|
526
|
+
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
527
|
+
const plane = new FrameHandlePlane({
|
|
528
|
+
decoderApi: this.buildPinnedDecoderCapApi(),
|
|
529
|
+
logger: this.logger,
|
|
530
|
+
localNodeId: ownNodeId,
|
|
531
|
+
resolveStreamInfo: () => ({
|
|
532
|
+
codec,
|
|
533
|
+
numericDeviceId: deviceId,
|
|
534
|
+
tag: `runner-remote:${deviceId}/${streamId}`,
|
|
535
|
+
pullModeUrl: rtpSource.url
|
|
536
|
+
})
|
|
537
|
+
});
|
|
538
|
+
this.logger.info("remote-source plane: owner restream acquired", { meta: {
|
|
539
|
+
streamId,
|
|
540
|
+
profile,
|
|
541
|
+
codec,
|
|
542
|
+
pipelineKey: rtpSource.pipelineKey,
|
|
543
|
+
hostname: this.hostname
|
|
544
|
+
} });
|
|
545
|
+
return {
|
|
546
|
+
plane,
|
|
547
|
+
pipelineKey: rtpSource.pipelineKey,
|
|
548
|
+
subscriptionIds: /* @__PURE__ */ new Set(),
|
|
549
|
+
released: false
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/** Destroy an entry's satellite plane and release its restream refcount once. */
|
|
553
|
+
async teardownEntry(entry, streamId) {
|
|
554
|
+
await entry.plane.dispose().catch((err) => {
|
|
555
|
+
this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
|
|
556
|
+
streamId,
|
|
557
|
+
error: errMsg(err)
|
|
558
|
+
} });
|
|
559
|
+
});
|
|
560
|
+
if (entry.released) return;
|
|
561
|
+
entry.released = true;
|
|
562
|
+
await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }).catch((err) => {
|
|
563
|
+
this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
|
|
564
|
+
streamId,
|
|
565
|
+
pipelineKey: entry.pipelineKey,
|
|
566
|
+
error: errMsg(err)
|
|
567
|
+
} });
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Every decoder call pinned to THIS runner's node — mirror of the hub
|
|
572
|
+
* broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
|
|
573
|
+
* `decoder` cap is a singleton whose UNPINNED calls resolve to the
|
|
574
|
+
* cluster-elected node, which is generally NOT this agent. Pinning create
|
|
575
|
+
* AND every session op to the same local node keeps the session + its shm
|
|
576
|
+
* ring here, where this plane's readers live.
|
|
577
|
+
*/
|
|
578
|
+
buildPinnedDecoderCapApi() {
|
|
579
|
+
const api = this.api;
|
|
580
|
+
const pin = nodePin(this.ownNodeId);
|
|
581
|
+
const toWireBytes = (bytes) => {
|
|
582
|
+
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
583
|
+
copy.set(bytes);
|
|
584
|
+
return copy;
|
|
585
|
+
};
|
|
586
|
+
return {
|
|
587
|
+
supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
|
|
588
|
+
getInfo: async () => api.decoder.getInfo.query(void 0, pin),
|
|
589
|
+
createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
|
|
590
|
+
pushPacket: async (input) => {
|
|
591
|
+
await api.decoder.pushPacket.query({
|
|
592
|
+
sessionId: input.sessionId,
|
|
593
|
+
packet: {
|
|
594
|
+
...input.packet,
|
|
595
|
+
data: toWireBytes(input.packet.data)
|
|
596
|
+
}
|
|
597
|
+
}, pin);
|
|
598
|
+
},
|
|
599
|
+
pullFrames: async (input) => {
|
|
600
|
+
return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
|
|
601
|
+
...frame,
|
|
602
|
+
data: Buffer.from(frame.data)
|
|
603
|
+
}));
|
|
604
|
+
},
|
|
605
|
+
pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
|
|
606
|
+
destroySession: async (input) => {
|
|
607
|
+
await api.decoder.destroySession.query({ ...input }, pin);
|
|
608
|
+
},
|
|
609
|
+
openStream: async (input) => {
|
|
610
|
+
await api.decoder.openStream.query({ ...input }, pin);
|
|
611
|
+
},
|
|
612
|
+
updateConfig: async (input) => {
|
|
613
|
+
await api.decoder.updateConfig.query({ ...input }, pin);
|
|
614
|
+
},
|
|
615
|
+
getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
383
620
|
//#region src/pipeline-runner/frame-queue.ts
|
|
384
621
|
/**
|
|
385
622
|
* Latest-frame-only buffer. Keeps only the most recent item, dropping all
|
|
@@ -537,7 +774,10 @@ var PipelineTimingSampler = class {
|
|
|
537
774
|
detSamples = /* @__PURE__ */ new Map();
|
|
538
775
|
motSamples = /* @__PURE__ */ new Map();
|
|
539
776
|
audioSamples = /* @__PURE__ */ new Map();
|
|
540
|
-
|
|
777
|
+
/** Per-device cumulative dropped-frames readers (see {@link DroppedFramesSource}). */
|
|
778
|
+
droppedSources = /* @__PURE__ */ new Map();
|
|
779
|
+
/** Cumulative value seen at the previous report — the delta base. */
|
|
780
|
+
lastDroppedCumulative = /* @__PURE__ */ new Map();
|
|
541
781
|
reportTimer = null;
|
|
542
782
|
log = null;
|
|
543
783
|
runtimeInfo = {};
|
|
@@ -569,13 +809,34 @@ var PipelineTimingSampler = class {
|
|
|
569
809
|
if (!this.audioSamples.has(deviceId)) this.audioSamples.set(deviceId, []);
|
|
570
810
|
this.audioSamples.get(deviceId).push(s);
|
|
571
811
|
}
|
|
572
|
-
|
|
573
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Register the cumulative dropped-frames reader for a device. The
|
|
814
|
+
* baseline snapshot is taken immediately so the first report after
|
|
815
|
+
* registration only covers drops that happened AFTER registration.
|
|
816
|
+
*/
|
|
817
|
+
setDroppedFramesSource(deviceId, source) {
|
|
818
|
+
this.droppedSources.set(deviceId, source);
|
|
819
|
+
this.lastDroppedCumulative.set(deviceId, source());
|
|
820
|
+
}
|
|
821
|
+
clearDroppedFramesSource(deviceId) {
|
|
822
|
+
this.droppedSources.delete(deviceId);
|
|
823
|
+
this.lastDroppedCumulative.delete(deviceId);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Snapshot the device's cumulative dropped counter and return the
|
|
827
|
+
* delta since the previous snapshot. Guarded to never go negative
|
|
828
|
+
* (a replaced/reset queue restarts its counter at 0).
|
|
829
|
+
*/
|
|
830
|
+
sampleDroppedDelta(deviceId) {
|
|
831
|
+
const source = this.droppedSources.get(deviceId);
|
|
832
|
+
if (!source) return 0;
|
|
833
|
+
const cumulative = source();
|
|
834
|
+
const previous = this.lastDroppedCumulative.get(deviceId) ?? 0;
|
|
835
|
+
this.lastDroppedCumulative.set(deviceId, cumulative);
|
|
836
|
+
return Math.max(0, cumulative - previous);
|
|
574
837
|
}
|
|
575
838
|
report() {
|
|
576
839
|
if (!this.log) return;
|
|
577
|
-
const dropped = this.droppedFrames;
|
|
578
|
-
this.droppedFrames = 0;
|
|
579
840
|
const avg = (arr) => arr.length > 0 ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;
|
|
580
841
|
const max = (arr) => arr.length > 0 ? Math.round(Math.max(...arr)) : 0;
|
|
581
842
|
const p95 = (arr) => {
|
|
@@ -584,8 +845,11 @@ var PipelineTimingSampler = class {
|
|
|
584
845
|
return Math.round(sorted[Math.floor(sorted.length * .95)] ?? sorted[sorted.length - 1]);
|
|
585
846
|
};
|
|
586
847
|
const rt = this.runtimeInfo;
|
|
587
|
-
|
|
588
|
-
|
|
848
|
+
const statDeviceIds = new Set([...this.detSamples.keys(), ...this.droppedSources.keys()]);
|
|
849
|
+
for (const deviceId of statDeviceIds) {
|
|
850
|
+
const det = this.detSamples.get(deviceId) ?? [];
|
|
851
|
+
const dropped = this.sampleDroppedDelta(deviceId);
|
|
852
|
+
if (det.length === 0 && dropped === 0) continue;
|
|
589
853
|
const e2e = det.map((s) => s.endToEnd);
|
|
590
854
|
const inf = det.map((s) => s.inference);
|
|
591
855
|
const frameAge = det.map((s) => s.frameAge).filter((v) => v >= 0);
|
|
@@ -781,6 +1045,7 @@ var PipelineRunner = class {
|
|
|
781
1045
|
occupancyTimer: null
|
|
782
1046
|
};
|
|
783
1047
|
this.cameras.set(deviceId, state);
|
|
1048
|
+
this.timingSampler.setDroppedFramesSource(deviceId, () => detectionQueue.droppedFrames);
|
|
784
1049
|
if (registration.detectionMode === "on-motion") this.defaultRoundRobinKeys.push(deviceId);
|
|
785
1050
|
if (registration.detectionMode === "on-motion" && (registration.occupancyRecheckSec ?? 0) > 0) state.occupancyTimer = setInterval(() => {
|
|
786
1051
|
if (state.phase === "watching") this.config.onOccupancyRecheck?.(deviceId, registration.occupancyRecheckFrames ?? 4);
|
|
@@ -810,6 +1075,7 @@ var PipelineRunner = class {
|
|
|
810
1075
|
if (state.phase === "active") this.detectionStreamHandler?.(deviceId, "unsubscribe");
|
|
811
1076
|
state.motionQueue.clear();
|
|
812
1077
|
state.detectionQueue.clear();
|
|
1078
|
+
this.timingSampler.clearDroppedFramesSource(deviceId);
|
|
813
1079
|
this.cameras.delete(deviceId);
|
|
814
1080
|
const idx = this.defaultRoundRobinKeys.indexOf(deviceId);
|
|
815
1081
|
if (idx !== -1) {
|
|
@@ -1153,6 +1419,14 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1153
1419
|
*/
|
|
1154
1420
|
onboardAnalyzerTeardownTimers = /* @__PURE__ */ new Map();
|
|
1155
1421
|
/**
|
|
1422
|
+
* REMOTE-SOURCE legs (cross-node Phase 2, P2d) — one per attached camera
|
|
1423
|
+
* whose `frameSource.kind === 'remote-restream'`: the owner's compressed
|
|
1424
|
+
* restream + a local satellite frame plane. DORMANT with today's default
|
|
1425
|
+
* payloads (`local-broker` / absent) — the map stays empty and every code
|
|
1426
|
+
* path is identical to pre-Phase-2. Torn down on detach / shutdown.
|
|
1427
|
+
*/
|
|
1428
|
+
remotePlanes = /* @__PURE__ */ new Map();
|
|
1429
|
+
/**
|
|
1156
1430
|
* Snapshot-equality cache for metrics-snapshot defer. The runner
|
|
1157
1431
|
* fires per-camera metrics every `METRICS_SNAPSHOT_INTERVAL_MS`;
|
|
1158
1432
|
* for an idle camera (no inference, queue empty, fps=0) every tick
|
|
@@ -1248,6 +1522,9 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1248
1522
|
}
|
|
1249
1523
|
this.lastAnalyzerDetected.clear();
|
|
1250
1524
|
for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
|
|
1525
|
+
const remotePlanes = [...this.remotePlanes.values()];
|
|
1526
|
+
this.remotePlanes.clear();
|
|
1527
|
+
await Promise.all(remotePlanes.map((plane) => plane.dispose().catch(() => {})));
|
|
1251
1528
|
if (this.runner) {
|
|
1252
1529
|
this.runner.stop();
|
|
1253
1530
|
this.runner = null;
|
|
@@ -1643,6 +1920,16 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1643
1920
|
this.clearOnboardAnalyzer(deviceId);
|
|
1644
1921
|
attachment.motionUnsubscribe?.();
|
|
1645
1922
|
attachment.detectionUnsubscribe?.();
|
|
1923
|
+
const remotePlane = this.remotePlanes.get(deviceId);
|
|
1924
|
+
if (remotePlane) {
|
|
1925
|
+
this.remotePlanes.delete(deviceId);
|
|
1926
|
+
remotePlane.dispose().catch((err) => {
|
|
1927
|
+
this.ctx?.logger.debug("remote-source plane dispose failed on detach", {
|
|
1928
|
+
tags: { deviceId },
|
|
1929
|
+
meta: { error: errMsg(err) }
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1646
1933
|
this.attached.delete(deviceId);
|
|
1647
1934
|
this.lastMotionAt.delete(deviceId);
|
|
1648
1935
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -1784,6 +2071,39 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1784
2071
|
timestamp: remote.timestamp
|
|
1785
2072
|
};
|
|
1786
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* The remote-source leg for a camera, or `null` for today's co-located
|
|
2076
|
+
* broker path (P2d). Created lazily on the FIRST subscription of a
|
|
2077
|
+
* `remote-restream` camera and shared by its motion + detection pollers;
|
|
2078
|
+
* `detachInternal` / `onShutdown` dispose it. With rollout OFF every
|
|
2079
|
+
* attach payload is `local-broker` (or absent) → always `null` → the
|
|
2080
|
+
* subscribe paths below are byte-identical to pre-Phase-2.
|
|
2081
|
+
*/
|
|
2082
|
+
remoteSourceFor(config) {
|
|
2083
|
+
const frameSource = config.frameSource;
|
|
2084
|
+
if (!isRemoteRestream(frameSource)) return null;
|
|
2085
|
+
const existing = this.remotePlanes.get(config.deviceId);
|
|
2086
|
+
if (existing) return existing;
|
|
2087
|
+
const api = this.ctx.api;
|
|
2088
|
+
if (!api) return null;
|
|
2089
|
+
const hostname = resolveHubHostname(frameSource.hubHostnameOverride, process.env["CAMSTACK_HUB_URL"]);
|
|
2090
|
+
const plane = new RemoteSourcePlane({
|
|
2091
|
+
api,
|
|
2092
|
+
logger: this.ctx.logger,
|
|
2093
|
+
deviceId: config.deviceId,
|
|
2094
|
+
ownNodeId: this.nodeId,
|
|
2095
|
+
hostname
|
|
2096
|
+
});
|
|
2097
|
+
this.ctx.logger.info("remote-source leg armed for camera", {
|
|
2098
|
+
tags: { deviceId: config.deviceId },
|
|
2099
|
+
meta: {
|
|
2100
|
+
ownerNodeId: frameSource.ownerNodeId,
|
|
2101
|
+
hostname: hostname ?? "(unresolved — acquires will fail until set)"
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
this.remotePlanes.set(config.deviceId, plane);
|
|
2105
|
+
return plane;
|
|
2106
|
+
}
|
|
1787
2107
|
async subscribeMotionFrames(config) {
|
|
1788
2108
|
const ctx = this.ctx;
|
|
1789
2109
|
const runner = this.runner;
|
|
@@ -1794,6 +2114,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1794
2114
|
log.warn("subscribeMotionFrames: this.ctx.api not available");
|
|
1795
2115
|
return null;
|
|
1796
2116
|
}
|
|
2117
|
+
const remote = this.remoteSourceFor(config);
|
|
1797
2118
|
return startFrameHandlePoller({
|
|
1798
2119
|
api,
|
|
1799
2120
|
brokerId: makeSourceBrokerId(config.deviceId, config.motionStreamId),
|
|
@@ -1803,6 +2124,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1803
2124
|
logger: log,
|
|
1804
2125
|
ownNodeId: this.nodeId,
|
|
1805
2126
|
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
2127
|
+
...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
|
|
1806
2128
|
onFrame: (frame, _handle) => {
|
|
1807
2129
|
runner.enqueueMotionFrame(config.deviceId, frame);
|
|
1808
2130
|
}
|
|
@@ -1879,6 +2201,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1879
2201
|
log.warn("subscribeDetectionFrames: this.ctx.api not available");
|
|
1880
2202
|
return null;
|
|
1881
2203
|
}
|
|
2204
|
+
const remote = this.remoteSourceFor(config);
|
|
1882
2205
|
return startFrameHandlePoller({
|
|
1883
2206
|
api,
|
|
1884
2207
|
brokerId: makeSourceBrokerId(config.deviceId, config.detectionStreamId),
|
|
@@ -1888,6 +2211,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
1888
2211
|
logger: log,
|
|
1889
2212
|
ownNodeId: this.nodeId,
|
|
1890
2213
|
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
2214
|
+
...remote ? { source: remote.makeSource(config.detectionStreamId) } : {},
|
|
1891
2215
|
onFrame: (frame, handle) => {
|
|
1892
2216
|
runner.enqueueDetectionFrame(config.deviceId, frame, handle);
|
|
1893
2217
|
}
|