@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/shared/hub-hostname.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the host a REMOTE node should use to dial the hub-only
|
|
4
|
+
* stream-broker's restream port.
|
|
5
|
+
*
|
|
6
|
+
* The broker mints `127.0.0.1` restream URLs (hub-local). A cross-node
|
|
7
|
+
* consumer — the recorder on a designated remote recording node, or the
|
|
8
|
+
* pipeline-runner's remote-source leg (cross-node Phase 2) — must pull from
|
|
9
|
+
* the hub, so it needs the hub's cluster-resolvable host. Resolution order:
|
|
10
|
+
*
|
|
11
|
+
* 1. `override` — an explicit operator setting (the recorder's
|
|
12
|
+
* `recordingHubHostname` global, or the runner attach payload's
|
|
13
|
+
* `frameSource.hubHostnameOverride`), when the auto-derived host isn't
|
|
14
|
+
* reachable for RTSP on the consuming interface.
|
|
15
|
+
* 2. `hubUrl` — derived from the node's `CAMSTACK_HUB_URL` (the address it
|
|
16
|
+
* already uses to reach the hub). The RTSP restream port differs from
|
|
17
|
+
* the hub API port, but the HOST is the same — only the host is
|
|
18
|
+
* extracted.
|
|
19
|
+
*
|
|
20
|
+
* Returns `undefined` when neither yields a host (the caller logs / fails the
|
|
21
|
+
* acquire; a `127.0.0.1` URL on a remote node would dial the wrong box).
|
|
22
|
+
*
|
|
23
|
+
* Pure: the env string is passed in (not read here) so it can be unit-tested.
|
|
24
|
+
* Promoted from `recorder/addon/recording-hub-host.ts` in P2d so both
|
|
25
|
+
* cross-node consumers share ONE resolver.
|
|
26
|
+
*/
|
|
27
|
+
function resolveHubHostname(override, hubUrl) {
|
|
28
|
+
const trimmedOverride = override?.trim();
|
|
29
|
+
if (trimmedOverride !== void 0 && trimmedOverride.length > 0) return trimmedOverride;
|
|
30
|
+
return extractHost(hubUrl);
|
|
31
|
+
}
|
|
32
|
+
/** Extract the bare host from a URL/authority string, scheme + port optional. */
|
|
33
|
+
function extractHost(raw) {
|
|
34
|
+
const value = raw?.trim();
|
|
35
|
+
if (value === void 0 || value.length === 0) return void 0;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(value);
|
|
38
|
+
if (parsed.hostname.length > 0) return parsed.hostname;
|
|
39
|
+
} catch {}
|
|
40
|
+
const authority = value.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").split("/")[0] ?? "";
|
|
41
|
+
if (authority.startsWith("[")) {
|
|
42
|
+
const end = authority.indexOf("]");
|
|
43
|
+
if (end > 0) return authority.slice(0, end + 1);
|
|
44
|
+
}
|
|
45
|
+
const host = authority.split(":")[0] ?? "";
|
|
46
|
+
return host.length > 0 ? host : void 0;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { resolveHubHostname as t };
|
|
@@ -19,12 +19,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
19
19
|
value: mod,
|
|
20
20
|
enumerable: true
|
|
21
21
|
}) : target, mod));
|
|
22
|
-
|
|
22
|
+
require("node:crypto");
|
|
23
23
|
let node_fs = require("node:fs");
|
|
24
24
|
node_fs = __toESM(node_fs, 1);
|
|
25
25
|
let node_path = require("node:path");
|
|
26
26
|
node_path = __toESM(node_path, 1);
|
|
27
|
-
require("node:crypto");
|
|
28
27
|
//#region ../system/dist/model-download-service-C-IHWnXx.mjs
|
|
29
28
|
/**
|
|
30
29
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as motionDetectionCapability, T as evaluateZoneRules, V as BaseAddon, W as DeviceType, Y as hydrateSchema } from "../dist-CgEP_0OL.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,7 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
6
|
+
const require_frame_handle_plane = require("../frame-handle-plane-DtTRX_0n.js");
|
|
7
|
+
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
6
8
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
7
9
|
//#region src/pipeline-runner/bench-actions.ts
|
|
8
10
|
/**
|
|
@@ -240,6 +242,26 @@ var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
|
240
242
|
*/
|
|
241
243
|
var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
|
|
242
244
|
/**
|
|
245
|
+
* The default source: the broker's shm plane over tRPC — today's path. The
|
|
246
|
+
* methods return the underlying client promises DIRECTLY (no async wrapper)
|
|
247
|
+
* so the poller's scheduling is microtask-identical to the pre-seam code.
|
|
248
|
+
*/
|
|
249
|
+
function brokerFrameHandleSource(api, brokerId) {
|
|
250
|
+
return {
|
|
251
|
+
subscribe: (input) => api.streamBroker.subscribeFrames.mutate({
|
|
252
|
+
brokerId,
|
|
253
|
+
format: input.format,
|
|
254
|
+
maxFps: input.maxFps,
|
|
255
|
+
tag: input.tag
|
|
256
|
+
}),
|
|
257
|
+
pullHandles: (input) => api.streamBroker.pullFrameHandles.query({
|
|
258
|
+
subscriptionId: input.subscriptionId,
|
|
259
|
+
maxCount: input.maxCount
|
|
260
|
+
}),
|
|
261
|
+
unsubscribe: (input) => api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: input.subscriptionId }).then(() => void 0)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
243
265
|
* Subscribe to a broker's shm frame stream and start the poll loop. Always
|
|
244
266
|
* resolves to a teardown closure — when the broker is not yet registered the
|
|
245
267
|
* closure cancels the ongoing retry loop; when polling is active it stops the
|
|
@@ -271,19 +293,19 @@ async function startFrameHandlePoller(options) {
|
|
|
271
293
|
*/
|
|
272
294
|
async function subscribeWithRetry(options, lifecycle) {
|
|
273
295
|
const { api, brokerId, format, maxFps, tag, logger } = options;
|
|
296
|
+
const source = options.source ?? brokerFrameHandleSource(api, brokerId);
|
|
274
297
|
let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
|
|
275
298
|
let attempt = 0;
|
|
276
299
|
while (!lifecycle.stopped) {
|
|
277
300
|
attempt += 1;
|
|
278
301
|
try {
|
|
279
|
-
const result = await
|
|
280
|
-
brokerId,
|
|
302
|
+
const result = await source.subscribe({
|
|
281
303
|
format,
|
|
282
304
|
maxFps,
|
|
283
305
|
tag
|
|
284
306
|
});
|
|
285
307
|
if (lifecycle.stopped) {
|
|
286
|
-
await
|
|
308
|
+
await source.unsubscribe({ subscriptionId: result.subscriptionId }).catch((err) => {
|
|
287
309
|
logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
|
|
288
310
|
brokerId,
|
|
289
311
|
subscriptionId: result.subscriptionId,
|
|
@@ -292,7 +314,7 @@ async function subscribeWithRetry(options, lifecycle) {
|
|
|
292
314
|
});
|
|
293
315
|
return;
|
|
294
316
|
}
|
|
295
|
-
lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
|
|
317
|
+
lifecycle.activeTeardown = startPolling(options, source, result.subscriptionId, result.maxFps, lifecycle);
|
|
296
318
|
return;
|
|
297
319
|
} catch (err) {
|
|
298
320
|
if (lifecycle.stopped) return;
|
|
@@ -333,8 +355,8 @@ function sleep(ms, lifecycle) {
|
|
|
333
355
|
* Returns the teardown closure that stops the loop and releases the
|
|
334
356
|
* subscription.
|
|
335
357
|
*/
|
|
336
|
-
function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
337
|
-
const {
|
|
358
|
+
function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
359
|
+
const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
|
|
338
360
|
const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
|
|
339
361
|
const throttle = new ForeignHandleThrottle();
|
|
340
362
|
const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
|
|
@@ -342,7 +364,7 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
342
364
|
const tick = async () => {
|
|
343
365
|
if (lifecycle.stopped) return;
|
|
344
366
|
try {
|
|
345
|
-
const handles = await
|
|
367
|
+
const handles = await source.pullHandles({
|
|
346
368
|
subscriptionId,
|
|
347
369
|
maxCount: PULL_MAX_COUNT
|
|
348
370
|
});
|
|
@@ -374,8 +396,8 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
374
396
|
timer = void 0;
|
|
375
397
|
}
|
|
376
398
|
readers.close();
|
|
377
|
-
|
|
378
|
-
logger.warn("frame-handle poller:
|
|
399
|
+
source.unsubscribe({ subscriptionId }).catch((err) => {
|
|
400
|
+
logger.warn("frame-handle poller: unsubscribe failed", { meta: {
|
|
379
401
|
brokerId,
|
|
380
402
|
subscriptionId,
|
|
381
403
|
error: require_dist.errMsg(err)
|
|
@@ -384,6 +406,221 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
|
|
|
384
406
|
};
|
|
385
407
|
}
|
|
386
408
|
//#endregion
|
|
409
|
+
//#region src/pipeline-runner/remote-source-plane.ts
|
|
410
|
+
/**
|
|
411
|
+
* Runner-side mode selection (pure): whether an attach payload routes this
|
|
412
|
+
* camera through the remote-source leg. Absent `frameSource` (a pre-P2c
|
|
413
|
+
* payload) and `local-broker` both take today's co-located broker path —
|
|
414
|
+
* the rollout-OFF safety invariant.
|
|
415
|
+
*/
|
|
416
|
+
function isRemoteRestream(frameSource) {
|
|
417
|
+
return frameSource !== void 0 && frameSource.kind === "remote-restream";
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Resolve WHICH broker profile a camStream id feeds for a device (pure).
|
|
421
|
+
*
|
|
422
|
+
* `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
|
|
423
|
+
* runner is dispatched with camStream ids (`motionStreamId` /
|
|
424
|
+
* `detectionStreamId`). The broker's profile slots carry the mapping
|
|
425
|
+
* (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
|
|
426
|
+
* fails fast and the poller's backoff retries (the assignment may still be
|
|
427
|
+
* propagating).
|
|
428
|
+
*/
|
|
429
|
+
function profileForStreamId(slots, deviceId, streamId) {
|
|
430
|
+
for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
var RemoteSourcePlane = class {
|
|
434
|
+
api;
|
|
435
|
+
logger;
|
|
436
|
+
deviceId;
|
|
437
|
+
ownNodeId;
|
|
438
|
+
hostname;
|
|
439
|
+
/** Single-flight entry creation per camStream id. */
|
|
440
|
+
entries = /* @__PURE__ */ new Map();
|
|
441
|
+
disposed = false;
|
|
442
|
+
constructor(options) {
|
|
443
|
+
this.api = options.api;
|
|
444
|
+
this.logger = options.logger.withTags({ deviceId: options.deviceId });
|
|
445
|
+
this.deviceId = options.deviceId;
|
|
446
|
+
this.ownNodeId = options.ownNodeId;
|
|
447
|
+
this.hostname = options.hostname;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* A {@link FrameHandleSource} for one camStream id, pluggable into
|
|
451
|
+
* `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
|
|
452
|
+
* subscriptions of the same stream share ONE acquired restream and ONE
|
|
453
|
+
* satellite plane (per-format decoder sessions inside it — hub parity).
|
|
454
|
+
*/
|
|
455
|
+
makeSource(streamId) {
|
|
456
|
+
return {
|
|
457
|
+
subscribe: async (input) => {
|
|
458
|
+
if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
|
|
459
|
+
const entry = await this.ensureEntry(streamId);
|
|
460
|
+
const result = await entry.plane.subscribe({
|
|
461
|
+
format: input.format,
|
|
462
|
+
maxFps: input.maxFps,
|
|
463
|
+
tag: input.tag
|
|
464
|
+
});
|
|
465
|
+
entry.subscriptionIds.add(result.subscriptionId);
|
|
466
|
+
return result;
|
|
467
|
+
},
|
|
468
|
+
pullHandles: async (input) => {
|
|
469
|
+
const entry = await this.peekEntry(streamId);
|
|
470
|
+
if (!entry) return [];
|
|
471
|
+
return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
|
|
472
|
+
},
|
|
473
|
+
unsubscribe: async (input) => {
|
|
474
|
+
const entry = await this.peekEntry(streamId);
|
|
475
|
+
if (!entry) return;
|
|
476
|
+
entry.subscriptionIds.delete(input.subscriptionId);
|
|
477
|
+
await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
|
|
478
|
+
if (entry.subscriptionIds.size === 0) {
|
|
479
|
+
this.entries.delete(streamId);
|
|
480
|
+
await this.teardownEntry(entry, streamId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
|
|
486
|
+
async dispose() {
|
|
487
|
+
if (this.disposed) return;
|
|
488
|
+
this.disposed = true;
|
|
489
|
+
const pending = [...this.entries.entries()];
|
|
490
|
+
this.entries.clear();
|
|
491
|
+
for (const [streamId, entryPromise] of pending) try {
|
|
492
|
+
const entry = await entryPromise;
|
|
493
|
+
await this.teardownEntry(entry, streamId);
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
496
|
+
/** The live entry for a stream, or `null` (never creates one). */
|
|
497
|
+
async peekEntry(streamId) {
|
|
498
|
+
const pending = this.entries.get(streamId);
|
|
499
|
+
if (!pending) return null;
|
|
500
|
+
try {
|
|
501
|
+
return await pending;
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** Single-flight: acquire the restream + build the satellite plane once per stream. */
|
|
507
|
+
ensureEntry(streamId) {
|
|
508
|
+
const existing = this.entries.get(streamId);
|
|
509
|
+
if (existing) return existing;
|
|
510
|
+
const creation = this.createEntry(streamId);
|
|
511
|
+
this.entries.set(streamId, creation);
|
|
512
|
+
creation.catch(() => {
|
|
513
|
+
if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
|
|
514
|
+
});
|
|
515
|
+
return creation;
|
|
516
|
+
}
|
|
517
|
+
async createEntry(streamId) {
|
|
518
|
+
const { api, deviceId, ownNodeId } = this;
|
|
519
|
+
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)");
|
|
520
|
+
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
|
|
521
|
+
if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
|
|
522
|
+
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
523
|
+
deviceId,
|
|
524
|
+
video: "copy",
|
|
525
|
+
audio: "none",
|
|
526
|
+
profile,
|
|
527
|
+
hostname: this.hostname,
|
|
528
|
+
tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
|
|
529
|
+
});
|
|
530
|
+
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
531
|
+
const plane = new require_frame_handle_plane.FrameHandlePlane({
|
|
532
|
+
decoderApi: this.buildPinnedDecoderCapApi(),
|
|
533
|
+
logger: this.logger,
|
|
534
|
+
localNodeId: ownNodeId,
|
|
535
|
+
resolveStreamInfo: () => ({
|
|
536
|
+
codec,
|
|
537
|
+
numericDeviceId: deviceId,
|
|
538
|
+
tag: `runner-remote:${deviceId}/${streamId}`,
|
|
539
|
+
pullModeUrl: rtpSource.url
|
|
540
|
+
})
|
|
541
|
+
});
|
|
542
|
+
this.logger.info("remote-source plane: owner restream acquired", { meta: {
|
|
543
|
+
streamId,
|
|
544
|
+
profile,
|
|
545
|
+
codec,
|
|
546
|
+
pipelineKey: rtpSource.pipelineKey,
|
|
547
|
+
hostname: this.hostname
|
|
548
|
+
} });
|
|
549
|
+
return {
|
|
550
|
+
plane,
|
|
551
|
+
pipelineKey: rtpSource.pipelineKey,
|
|
552
|
+
subscriptionIds: /* @__PURE__ */ new Set(),
|
|
553
|
+
released: false
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/** Destroy an entry's satellite plane and release its restream refcount once. */
|
|
557
|
+
async teardownEntry(entry, streamId) {
|
|
558
|
+
await entry.plane.dispose().catch((err) => {
|
|
559
|
+
this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
|
|
560
|
+
streamId,
|
|
561
|
+
error: require_dist.errMsg(err)
|
|
562
|
+
} });
|
|
563
|
+
});
|
|
564
|
+
if (entry.released) return;
|
|
565
|
+
entry.released = true;
|
|
566
|
+
await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }).catch((err) => {
|
|
567
|
+
this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
|
|
568
|
+
streamId,
|
|
569
|
+
pipelineKey: entry.pipelineKey,
|
|
570
|
+
error: require_dist.errMsg(err)
|
|
571
|
+
} });
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Every decoder call pinned to THIS runner's node — mirror of the hub
|
|
576
|
+
* broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
|
|
577
|
+
* `decoder` cap is a singleton whose UNPINNED calls resolve to the
|
|
578
|
+
* cluster-elected node, which is generally NOT this agent. Pinning create
|
|
579
|
+
* AND every session op to the same local node keeps the session + its shm
|
|
580
|
+
* ring here, where this plane's readers live.
|
|
581
|
+
*/
|
|
582
|
+
buildPinnedDecoderCapApi() {
|
|
583
|
+
const api = this.api;
|
|
584
|
+
const pin = require_dist.nodePin(this.ownNodeId);
|
|
585
|
+
const toWireBytes = (bytes) => {
|
|
586
|
+
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
587
|
+
copy.set(bytes);
|
|
588
|
+
return copy;
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
|
|
592
|
+
getInfo: async () => api.decoder.getInfo.query(void 0, pin),
|
|
593
|
+
createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
|
|
594
|
+
pushPacket: async (input) => {
|
|
595
|
+
await api.decoder.pushPacket.query({
|
|
596
|
+
sessionId: input.sessionId,
|
|
597
|
+
packet: {
|
|
598
|
+
...input.packet,
|
|
599
|
+
data: toWireBytes(input.packet.data)
|
|
600
|
+
}
|
|
601
|
+
}, pin);
|
|
602
|
+
},
|
|
603
|
+
pullFrames: async (input) => {
|
|
604
|
+
return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
|
|
605
|
+
...frame,
|
|
606
|
+
data: Buffer.from(frame.data)
|
|
607
|
+
}));
|
|
608
|
+
},
|
|
609
|
+
pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
|
|
610
|
+
destroySession: async (input) => {
|
|
611
|
+
await api.decoder.destroySession.query({ ...input }, pin);
|
|
612
|
+
},
|
|
613
|
+
openStream: async (input) => {
|
|
614
|
+
await api.decoder.openStream.query({ ...input }, pin);
|
|
615
|
+
},
|
|
616
|
+
updateConfig: async (input) => {
|
|
617
|
+
await api.decoder.updateConfig.query({ ...input }, pin);
|
|
618
|
+
},
|
|
619
|
+
getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
//#endregion
|
|
387
624
|
//#region src/pipeline-runner/frame-queue.ts
|
|
388
625
|
/**
|
|
389
626
|
* Latest-frame-only buffer. Keeps only the most recent item, dropping all
|
|
@@ -541,7 +778,10 @@ var PipelineTimingSampler = class {
|
|
|
541
778
|
detSamples = /* @__PURE__ */ new Map();
|
|
542
779
|
motSamples = /* @__PURE__ */ new Map();
|
|
543
780
|
audioSamples = /* @__PURE__ */ new Map();
|
|
544
|
-
|
|
781
|
+
/** Per-device cumulative dropped-frames readers (see {@link DroppedFramesSource}). */
|
|
782
|
+
droppedSources = /* @__PURE__ */ new Map();
|
|
783
|
+
/** Cumulative value seen at the previous report — the delta base. */
|
|
784
|
+
lastDroppedCumulative = /* @__PURE__ */ new Map();
|
|
545
785
|
reportTimer = null;
|
|
546
786
|
log = null;
|
|
547
787
|
runtimeInfo = {};
|
|
@@ -573,13 +813,34 @@ var PipelineTimingSampler = class {
|
|
|
573
813
|
if (!this.audioSamples.has(deviceId)) this.audioSamples.set(deviceId, []);
|
|
574
814
|
this.audioSamples.get(deviceId).push(s);
|
|
575
815
|
}
|
|
576
|
-
|
|
577
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Register the cumulative dropped-frames reader for a device. The
|
|
818
|
+
* baseline snapshot is taken immediately so the first report after
|
|
819
|
+
* registration only covers drops that happened AFTER registration.
|
|
820
|
+
*/
|
|
821
|
+
setDroppedFramesSource(deviceId, source) {
|
|
822
|
+
this.droppedSources.set(deviceId, source);
|
|
823
|
+
this.lastDroppedCumulative.set(deviceId, source());
|
|
824
|
+
}
|
|
825
|
+
clearDroppedFramesSource(deviceId) {
|
|
826
|
+
this.droppedSources.delete(deviceId);
|
|
827
|
+
this.lastDroppedCumulative.delete(deviceId);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Snapshot the device's cumulative dropped counter and return the
|
|
831
|
+
* delta since the previous snapshot. Guarded to never go negative
|
|
832
|
+
* (a replaced/reset queue restarts its counter at 0).
|
|
833
|
+
*/
|
|
834
|
+
sampleDroppedDelta(deviceId) {
|
|
835
|
+
const source = this.droppedSources.get(deviceId);
|
|
836
|
+
if (!source) return 0;
|
|
837
|
+
const cumulative = source();
|
|
838
|
+
const previous = this.lastDroppedCumulative.get(deviceId) ?? 0;
|
|
839
|
+
this.lastDroppedCumulative.set(deviceId, cumulative);
|
|
840
|
+
return Math.max(0, cumulative - previous);
|
|
578
841
|
}
|
|
579
842
|
report() {
|
|
580
843
|
if (!this.log) return;
|
|
581
|
-
const dropped = this.droppedFrames;
|
|
582
|
-
this.droppedFrames = 0;
|
|
583
844
|
const avg = (arr) => arr.length > 0 ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;
|
|
584
845
|
const max = (arr) => arr.length > 0 ? Math.round(Math.max(...arr)) : 0;
|
|
585
846
|
const p95 = (arr) => {
|
|
@@ -588,8 +849,11 @@ var PipelineTimingSampler = class {
|
|
|
588
849
|
return Math.round(sorted[Math.floor(sorted.length * .95)] ?? sorted[sorted.length - 1]);
|
|
589
850
|
};
|
|
590
851
|
const rt = this.runtimeInfo;
|
|
591
|
-
|
|
592
|
-
|
|
852
|
+
const statDeviceIds = new Set([...this.detSamples.keys(), ...this.droppedSources.keys()]);
|
|
853
|
+
for (const deviceId of statDeviceIds) {
|
|
854
|
+
const det = this.detSamples.get(deviceId) ?? [];
|
|
855
|
+
const dropped = this.sampleDroppedDelta(deviceId);
|
|
856
|
+
if (det.length === 0 && dropped === 0) continue;
|
|
593
857
|
const e2e = det.map((s) => s.endToEnd);
|
|
594
858
|
const inf = det.map((s) => s.inference);
|
|
595
859
|
const frameAge = det.map((s) => s.frameAge).filter((v) => v >= 0);
|
|
@@ -785,6 +1049,7 @@ var PipelineRunner = class {
|
|
|
785
1049
|
occupancyTimer: null
|
|
786
1050
|
};
|
|
787
1051
|
this.cameras.set(deviceId, state);
|
|
1052
|
+
this.timingSampler.setDroppedFramesSource(deviceId, () => detectionQueue.droppedFrames);
|
|
788
1053
|
if (registration.detectionMode === "on-motion") this.defaultRoundRobinKeys.push(deviceId);
|
|
789
1054
|
if (registration.detectionMode === "on-motion" && (registration.occupancyRecheckSec ?? 0) > 0) state.occupancyTimer = setInterval(() => {
|
|
790
1055
|
if (state.phase === "watching") this.config.onOccupancyRecheck?.(deviceId, registration.occupancyRecheckFrames ?? 4);
|
|
@@ -814,6 +1079,7 @@ var PipelineRunner = class {
|
|
|
814
1079
|
if (state.phase === "active") this.detectionStreamHandler?.(deviceId, "unsubscribe");
|
|
815
1080
|
state.motionQueue.clear();
|
|
816
1081
|
state.detectionQueue.clear();
|
|
1082
|
+
this.timingSampler.clearDroppedFramesSource(deviceId);
|
|
817
1083
|
this.cameras.delete(deviceId);
|
|
818
1084
|
const idx = this.defaultRoundRobinKeys.indexOf(deviceId);
|
|
819
1085
|
if (idx !== -1) {
|
|
@@ -1157,6 +1423,14 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1157
1423
|
*/
|
|
1158
1424
|
onboardAnalyzerTeardownTimers = /* @__PURE__ */ new Map();
|
|
1159
1425
|
/**
|
|
1426
|
+
* REMOTE-SOURCE legs (cross-node Phase 2, P2d) — one per attached camera
|
|
1427
|
+
* whose `frameSource.kind === 'remote-restream'`: the owner's compressed
|
|
1428
|
+
* restream + a local satellite frame plane. DORMANT with today's default
|
|
1429
|
+
* payloads (`local-broker` / absent) — the map stays empty and every code
|
|
1430
|
+
* path is identical to pre-Phase-2. Torn down on detach / shutdown.
|
|
1431
|
+
*/
|
|
1432
|
+
remotePlanes = /* @__PURE__ */ new Map();
|
|
1433
|
+
/**
|
|
1160
1434
|
* Snapshot-equality cache for metrics-snapshot defer. The runner
|
|
1161
1435
|
* fires per-camera metrics every `METRICS_SNAPSHOT_INTERVAL_MS`;
|
|
1162
1436
|
* for an idle camera (no inference, queue empty, fps=0) every tick
|
|
@@ -1252,6 +1526,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1252
1526
|
}
|
|
1253
1527
|
this.lastAnalyzerDetected.clear();
|
|
1254
1528
|
for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
|
|
1529
|
+
const remotePlanes = [...this.remotePlanes.values()];
|
|
1530
|
+
this.remotePlanes.clear();
|
|
1531
|
+
await Promise.all(remotePlanes.map((plane) => plane.dispose().catch(() => {})));
|
|
1255
1532
|
if (this.runner) {
|
|
1256
1533
|
this.runner.stop();
|
|
1257
1534
|
this.runner = null;
|
|
@@ -1647,6 +1924,16 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1647
1924
|
this.clearOnboardAnalyzer(deviceId);
|
|
1648
1925
|
attachment.motionUnsubscribe?.();
|
|
1649
1926
|
attachment.detectionUnsubscribe?.();
|
|
1927
|
+
const remotePlane = this.remotePlanes.get(deviceId);
|
|
1928
|
+
if (remotePlane) {
|
|
1929
|
+
this.remotePlanes.delete(deviceId);
|
|
1930
|
+
remotePlane.dispose().catch((err) => {
|
|
1931
|
+
this.ctx?.logger.debug("remote-source plane dispose failed on detach", {
|
|
1932
|
+
tags: { deviceId },
|
|
1933
|
+
meta: { error: require_dist.errMsg(err) }
|
|
1934
|
+
});
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1650
1937
|
this.attached.delete(deviceId);
|
|
1651
1938
|
this.lastMotionAt.delete(deviceId);
|
|
1652
1939
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -1788,6 +2075,39 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1788
2075
|
timestamp: remote.timestamp
|
|
1789
2076
|
};
|
|
1790
2077
|
}
|
|
2078
|
+
/**
|
|
2079
|
+
* The remote-source leg for a camera, or `null` for today's co-located
|
|
2080
|
+
* broker path (P2d). Created lazily on the FIRST subscription of a
|
|
2081
|
+
* `remote-restream` camera and shared by its motion + detection pollers;
|
|
2082
|
+
* `detachInternal` / `onShutdown` dispose it. With rollout OFF every
|
|
2083
|
+
* attach payload is `local-broker` (or absent) → always `null` → the
|
|
2084
|
+
* subscribe paths below are byte-identical to pre-Phase-2.
|
|
2085
|
+
*/
|
|
2086
|
+
remoteSourceFor(config) {
|
|
2087
|
+
const frameSource = config.frameSource;
|
|
2088
|
+
if (!isRemoteRestream(frameSource)) return null;
|
|
2089
|
+
const existing = this.remotePlanes.get(config.deviceId);
|
|
2090
|
+
if (existing) return existing;
|
|
2091
|
+
const api = this.ctx.api;
|
|
2092
|
+
if (!api) return null;
|
|
2093
|
+
const hostname = require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, process.env["CAMSTACK_HUB_URL"]);
|
|
2094
|
+
const plane = new RemoteSourcePlane({
|
|
2095
|
+
api,
|
|
2096
|
+
logger: this.ctx.logger,
|
|
2097
|
+
deviceId: config.deviceId,
|
|
2098
|
+
ownNodeId: this.nodeId,
|
|
2099
|
+
hostname
|
|
2100
|
+
});
|
|
2101
|
+
this.ctx.logger.info("remote-source leg armed for camera", {
|
|
2102
|
+
tags: { deviceId: config.deviceId },
|
|
2103
|
+
meta: {
|
|
2104
|
+
ownerNodeId: frameSource.ownerNodeId,
|
|
2105
|
+
hostname: hostname ?? "(unresolved — acquires will fail until set)"
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
this.remotePlanes.set(config.deviceId, plane);
|
|
2109
|
+
return plane;
|
|
2110
|
+
}
|
|
1791
2111
|
async subscribeMotionFrames(config) {
|
|
1792
2112
|
const ctx = this.ctx;
|
|
1793
2113
|
const runner = this.runner;
|
|
@@ -1798,6 +2118,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1798
2118
|
log.warn("subscribeMotionFrames: this.ctx.api not available");
|
|
1799
2119
|
return null;
|
|
1800
2120
|
}
|
|
2121
|
+
const remote = this.remoteSourceFor(config);
|
|
1801
2122
|
return startFrameHandlePoller({
|
|
1802
2123
|
api,
|
|
1803
2124
|
brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.motionStreamId),
|
|
@@ -1807,6 +2128,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1807
2128
|
logger: log,
|
|
1808
2129
|
ownNodeId: this.nodeId,
|
|
1809
2130
|
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
2131
|
+
...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
|
|
1810
2132
|
onFrame: (frame, _handle) => {
|
|
1811
2133
|
runner.enqueueMotionFrame(config.deviceId, frame);
|
|
1812
2134
|
}
|
|
@@ -1883,6 +2205,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1883
2205
|
log.warn("subscribeDetectionFrames: this.ctx.api not available");
|
|
1884
2206
|
return null;
|
|
1885
2207
|
}
|
|
2208
|
+
const remote = this.remoteSourceFor(config);
|
|
1886
2209
|
return startFrameHandlePoller({
|
|
1887
2210
|
api,
|
|
1888
2211
|
brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.detectionStreamId),
|
|
@@ -1892,6 +2215,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1892
2215
|
logger: log,
|
|
1893
2216
|
ownNodeId: this.nodeId,
|
|
1894
2217
|
getRemoteFrame: (handle) => this.getRemoteFrame(handle),
|
|
2218
|
+
...remote ? { source: remote.makeSource(config.detectionStreamId) } : {},
|
|
1895
2219
|
onFrame: (frame, handle) => {
|
|
1896
2220
|
runner.enqueueDetectionFrame(config.deviceId, frame, handle);
|
|
1897
2221
|
}
|