@camstack/addon-pipeline 1.1.39 → 1.1.41
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 +168 -61
- package/dist/audio-analyzer/index.mjs +168 -61
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
- package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
- package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
- package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +893 -12
- package/dist/pipeline-runner/index.mjs +892 -13
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +544 -0
- package/dist/session-decode/decode-worker-child.mjs +543 -0
- package/dist/stream-broker/_stub.js +44 -44
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2l0kmD4.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.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-dGka8yZY.mjs +26 -0
- package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-DHaCRMjM.mjs} +3 -3
- package/dist/stream-broker/index.js +268 -38
- package/dist/stream-broker/index.mjs +268 -38
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-BCfO8gUF.js +67 -0
- package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-B6fza7ic.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-Cy-4iTog.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-CYX33lJb.js} +1 -1
- package/embed-dist/assets/index-BPkayx9u.js +81 -0
- package/embed-dist/assets/index-CSFtK41z.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +6 -3
- package/python/inference_pool.py +3 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.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-BsrrdOBU.mjs +0 -26
- package/embed-dist/assets/index-C5UpuPr8.css +0 -2
- package/embed-dist/assets/index-CR367rYR.js +0 -81
|
@@ -2,10 +2,13 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_frame_handle_plane = require("../frame-handle-plane-
|
|
5
|
+
const require_dist = require("../dist-BJTPkJFw.js");
|
|
6
|
+
const require_frame_handle_plane = require("../frame-handle-plane-BP8YV4sF.js");
|
|
7
7
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-BCfO8gUF.js");
|
|
8
9
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
10
|
+
let node_child_process = require("node:child_process");
|
|
11
|
+
let node_url = require("node:url");
|
|
9
12
|
//#region src/pipeline-runner/bench-actions.ts
|
|
10
13
|
/**
|
|
11
14
|
* Synthetic-bench custom actions for pipeline-runner.
|
|
@@ -233,6 +236,19 @@ var PULL_MAX_COUNT = 4;
|
|
|
233
236
|
var MIN_POLL_INTERVAL_MS = 20;
|
|
234
237
|
/** Poll period when the broker reports a non-positive cadence hint. */
|
|
235
238
|
var FALLBACK_POLL_INTERVAL_MS = 200;
|
|
239
|
+
/**
|
|
240
|
+
* Ceiling on the poll period — we never poll SLOWER than this even when the
|
|
241
|
+
* subscriber's `maxFps` implies a longer interval (e.g. detection at 10 fps →
|
|
242
|
+
* 100 ms). A decoded frame otherwise sits in the ring up to a full poll period
|
|
243
|
+
* before it is picked, and that wait lands directly in `frameAge` (the analyzed
|
|
244
|
+
* frame's staleness). Polling faster grabs the freshest frame sooner; because
|
|
245
|
+
* the loop reads ONLY the newest handle per tick and the decoder emits at its
|
|
246
|
+
* own rate, the extra ticks that find no new frame are near-free (an empty
|
|
247
|
+
* `pullHandles` + no pixel read), so this lowers latency without a proportional
|
|
248
|
+
* read-cost increase. `maxFps` still bounds the DELIVERED rate downstream via
|
|
249
|
+
* the latest-wins queue — this only decouples poll cadence from it.
|
|
250
|
+
*/
|
|
251
|
+
var MAX_POLL_INTERVAL_MS = 50;
|
|
236
252
|
/** First subscribe-retry delay, doubled on every subsequent failure. */
|
|
237
253
|
var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
|
|
238
254
|
/**
|
|
@@ -359,7 +375,7 @@ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle
|
|
|
359
375
|
const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
|
|
360
376
|
const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
|
|
361
377
|
const throttle = new ForeignHandleThrottle();
|
|
362
|
-
const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
|
|
378
|
+
const pollIntervalMs = Math.min(MAX_POLL_INTERVAL_MS, resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS);
|
|
363
379
|
let timer;
|
|
364
380
|
const tick = async () => {
|
|
365
381
|
if (lifecycle.stopped) return;
|
|
@@ -368,8 +384,9 @@ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle
|
|
|
368
384
|
subscriptionId,
|
|
369
385
|
maxCount: PULL_MAX_COUNT
|
|
370
386
|
});
|
|
371
|
-
for (
|
|
387
|
+
for (let i = handles.length - 1; i >= 0; i -= 1) {
|
|
372
388
|
if (lifecycle.stopped) break;
|
|
389
|
+
const handle = handles[i];
|
|
373
390
|
const frame = await resolvePolledFrame(handle, {
|
|
374
391
|
ownNodeId,
|
|
375
392
|
readers,
|
|
@@ -378,7 +395,10 @@ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle
|
|
|
378
395
|
logger,
|
|
379
396
|
tag
|
|
380
397
|
});
|
|
381
|
-
if (frame)
|
|
398
|
+
if (frame) {
|
|
399
|
+
onFrame(frame, handle);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
382
402
|
}
|
|
383
403
|
} catch (err) {
|
|
384
404
|
logger.warn("frame-handle poller: pullFrameHandles failed", { meta: {
|
|
@@ -1074,12 +1094,13 @@ var PipelineRunner = class {
|
|
|
1074
1094
|
motionCooldownTimer: null,
|
|
1075
1095
|
lastArmedSource: null,
|
|
1076
1096
|
lastArmedRegions: void 0,
|
|
1077
|
-
occupancyTimer: null
|
|
1097
|
+
occupancyTimer: null,
|
|
1098
|
+
motionInFlight: false
|
|
1078
1099
|
};
|
|
1079
1100
|
this.cameras.set(deviceId, state);
|
|
1080
1101
|
this.timingSampler.setDroppedFramesSource(deviceId, () => detectionQueue.droppedFrames);
|
|
1081
1102
|
if (registration.detectionMode === "on-motion") this.defaultRoundRobinKeys.push(deviceId);
|
|
1082
|
-
if (registration.detectionMode === "on-motion" && (registration.occupancyRecheckSec ?? 0) > 0) state.occupancyTimer = setInterval(() => {
|
|
1103
|
+
if (registration.detectionMode === "on-motion" && registration.occupancyRecheckEnabled === true && (registration.occupancyRecheckSec ?? 0) > 0) state.occupancyTimer = setInterval(() => {
|
|
1083
1104
|
if (state.phase === "watching") this.config.onOccupancyRecheck?.(deviceId, registration.occupancyRecheckFrames ?? 4);
|
|
1084
1105
|
}, (registration.occupancyRecheckSec ?? 0) * 1e3);
|
|
1085
1106
|
if (initialPhase === "active") {
|
|
@@ -1304,9 +1325,15 @@ var PipelineRunner = class {
|
|
|
1304
1325
|
this.processWithSemaphore(deviceId, entry, frameInput, state, "detection");
|
|
1305
1326
|
}
|
|
1306
1327
|
drainMotionQueues() {
|
|
1307
|
-
for (const [deviceId, state] of this.cameras)
|
|
1328
|
+
for (const [deviceId, state] of this.cameras) {
|
|
1329
|
+
if (state.motionInFlight) continue;
|
|
1330
|
+
if (state.motionQueue.size === 0) continue;
|
|
1308
1331
|
const entry = state.motionQueue.dequeue();
|
|
1309
|
-
if (entry)
|
|
1332
|
+
if (!entry) continue;
|
|
1333
|
+
state.motionInFlight = true;
|
|
1334
|
+
Promise.resolve(this.config.analyzeMotion(deviceId, entry.frame, entry.handle)).catch(() => {}).finally(() => {
|
|
1335
|
+
state.motionInFlight = false;
|
|
1336
|
+
});
|
|
1310
1337
|
}
|
|
1311
1338
|
}
|
|
1312
1339
|
async processWithSemaphore(deviceId, entry, frameInput, state, streamType) {
|
|
@@ -1375,12 +1402,568 @@ var PipelineRunner = class {
|
|
|
1375
1402
|
}
|
|
1376
1403
|
};
|
|
1377
1404
|
//#endregion
|
|
1405
|
+
//#region src/session-decode/fork-decode-worker.ts
|
|
1406
|
+
/**
|
|
1407
|
+
* Real `fork` implementation for the session-decode coordinator — Epic C P1,
|
|
1408
|
+
* Task 4.
|
|
1409
|
+
*
|
|
1410
|
+
* Spawns the decode-worker child (`decode-worker-child.ts`, Task 2) as a
|
|
1411
|
+
* dedicated `child_process.fork` and adapts the returned `ChildProcess` to the
|
|
1412
|
+
* coordinator's minimal {@link ForkedChildLike} contract WITHOUT any cast —
|
|
1413
|
+
* an explicit forwarding wrapper, so a wider Node `ChildProcess` signature
|
|
1414
|
+
* never has to structurally satisfy the narrower interface.
|
|
1415
|
+
*
|
|
1416
|
+
* `serialization: 'advanced'` is MANDATORY: Node's default 'json' IPC
|
|
1417
|
+
* base64-serialises the `buffer` reply's `Uint8Array`, breaking the "raw
|
|
1418
|
+
* bytes, not base64" contract the coordinator + `worker-protocol.ts`'s
|
|
1419
|
+
* `value.bytes instanceof Uint8Array` guard both depend on. Advanced (V8
|
|
1420
|
+
* structured-clone) preserves the typed array on the wire.
|
|
1421
|
+
*
|
|
1422
|
+
* The child entry is resolved relative to THIS module's built location. Both
|
|
1423
|
+
* this module and `pipeline-runner/index.ts` bundle into the SAME entry
|
|
1424
|
+
* (`dist/pipeline-runner/index.js`, `manualChunks: () => null`), so
|
|
1425
|
+
* `import.meta.url` here is that file's URL and the sibling child lands at
|
|
1426
|
+
* `dist/session-decode/decode-worker-child.js` (its own vite build entry).
|
|
1427
|
+
*/
|
|
1428
|
+
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
1429
|
+
var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
|
|
1430
|
+
/**
|
|
1431
|
+
* Known kill signals the coordinator may pass, mapped to their `NodeJS.Signals`
|
|
1432
|
+
* literal so `ChildProcess.kill` receives a correctly-typed signal without a
|
|
1433
|
+
* cast. The coordinator only ever sends the default (undefined → SIGTERM) and
|
|
1434
|
+
* the SIGKILL fallback; anything unrecognised degrades to a plain `kill()`.
|
|
1435
|
+
*/
|
|
1436
|
+
var KILL_SIGNALS = {
|
|
1437
|
+
SIGKILL: "SIGKILL",
|
|
1438
|
+
SIGTERM: "SIGTERM"
|
|
1439
|
+
};
|
|
1440
|
+
/** Adapt a Node {@link ChildProcess} to the coordinator's {@link ForkedChildLike}. */
|
|
1441
|
+
function wrapChild(child) {
|
|
1442
|
+
return {
|
|
1443
|
+
send: (msg) => {
|
|
1444
|
+
child.send(msg);
|
|
1445
|
+
},
|
|
1446
|
+
on: (ev, cb) => {
|
|
1447
|
+
if (ev === "exit") child.on("exit", () => cb(void 0));
|
|
1448
|
+
else child.on("message", (m) => cb(m));
|
|
1449
|
+
},
|
|
1450
|
+
kill: (signal) => {
|
|
1451
|
+
if (signal === void 0) {
|
|
1452
|
+
child.kill();
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const mapped = KILL_SIGNALS[signal];
|
|
1456
|
+
if (mapped) child.kill(mapped);
|
|
1457
|
+
else child.kill();
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Fork one decode-worker child. `source` is part of the coordinator's fork
|
|
1463
|
+
* signature (it forwards the same source in the subsequent `start` message);
|
|
1464
|
+
* the child entry itself is source-independent, so it is not consumed here.
|
|
1465
|
+
*/
|
|
1466
|
+
function forkDecodeWorker(_source) {
|
|
1467
|
+
return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], { serialization: "advanced" }));
|
|
1468
|
+
}
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/session-decode/guarded-session-decode.ts
|
|
1471
|
+
/**
|
|
1472
|
+
* Guards the in-flight restream-acquire against a detach/unsubscribe race.
|
|
1473
|
+
*
|
|
1474
|
+
* `acquireSessionDecodeRestream` + forking the decode worker both take real
|
|
1475
|
+
* time (a broker round-trip, then a process fork). If the caller's teardown
|
|
1476
|
+
* runs while that acquire is still in flight, the closure that would release
|
|
1477
|
+
* the restream and stop the pump does not exist yet by the time the caller
|
|
1478
|
+
* stores it — a naive `await acquire(); return () => {...}` construction
|
|
1479
|
+
* leaks the acquired restream refcount and, once the acquire resolves, forks
|
|
1480
|
+
* an orphan decode worker nobody will ever stop.
|
|
1481
|
+
*
|
|
1482
|
+
* This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
|
|
1483
|
+
* run in the background; an `aborted` flag recorded by the (possibly
|
|
1484
|
+
* already-called) teardown is checked right after the acquire resolves —
|
|
1485
|
+
* if set, the restream is released immediately and the pump is NEVER
|
|
1486
|
+
* started (no orphan worker). Otherwise the pump starts and the teardown,
|
|
1487
|
+
* once called, stops the pump then releases the restream.
|
|
1488
|
+
*/
|
|
1489
|
+
function startGuardedSessionDecode(deps) {
|
|
1490
|
+
let aborted = false;
|
|
1491
|
+
let stopPump = null;
|
|
1492
|
+
let release = null;
|
|
1493
|
+
(async () => {
|
|
1494
|
+
const acquired = await deps.acquire();
|
|
1495
|
+
if (!acquired) return;
|
|
1496
|
+
if (aborted) {
|
|
1497
|
+
acquired.release();
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
release = acquired.release;
|
|
1501
|
+
stopPump = deps.startPump(acquired.source);
|
|
1502
|
+
})().catch((err) => {
|
|
1503
|
+
deps.logger.warn("session-decode: guarded start failed", { meta: { error: require_dist.errMsg(err) } });
|
|
1504
|
+
});
|
|
1505
|
+
return () => {
|
|
1506
|
+
aborted = true;
|
|
1507
|
+
stopPump?.();
|
|
1508
|
+
if (release) release();
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
//#endregion
|
|
1512
|
+
//#region src/session-decode/session-decode-coordinator.ts
|
|
1513
|
+
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
1514
|
+
var SIGKILL_FALLBACK_MS = 1e3;
|
|
1515
|
+
/**
|
|
1516
|
+
* Default timeout for the `pull` round-trip preceding the FIRST delivered
|
|
1517
|
+
* frame. Task 0 live-measured cold-start (restream dial + hwaccel context +
|
|
1518
|
+
* keyframe wait) at 5-8s, so this must tolerate a slow-but-healthy start.
|
|
1519
|
+
*/
|
|
1520
|
+
var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
|
|
1521
|
+
/** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
|
|
1522
|
+
var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
|
|
1523
|
+
function toError(err) {
|
|
1524
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Per-session mutable state + the message-handling logic that adapts the
|
|
1528
|
+
* child's `WorkerReply` stream into resolved/rejected waiter promises. One
|
|
1529
|
+
* instance is created per `generateVideoFrames()` call (i.e. per forked
|
|
1530
|
+
* child) — never shared across sessions.
|
|
1531
|
+
*/
|
|
1532
|
+
var SessionDecodeSession = class {
|
|
1533
|
+
child;
|
|
1534
|
+
logger;
|
|
1535
|
+
firstFrameTimeoutMs;
|
|
1536
|
+
frameTimeoutMs;
|
|
1537
|
+
pendingPull = null;
|
|
1538
|
+
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
1539
|
+
exited = false;
|
|
1540
|
+
sigkillTimer = null;
|
|
1541
|
+
pullTimeoutTimer = null;
|
|
1542
|
+
firstFrameReceived = false;
|
|
1543
|
+
constructor(child, logger, firstFrameTimeoutMs, frameTimeoutMs) {
|
|
1544
|
+
this.child = child;
|
|
1545
|
+
this.logger = logger;
|
|
1546
|
+
this.firstFrameTimeoutMs = firstFrameTimeoutMs;
|
|
1547
|
+
this.frameTimeoutMs = frameTimeoutMs;
|
|
1548
|
+
this.child.on("message", (m) => this.handleMessage(m));
|
|
1549
|
+
this.child.on("exit", () => this.handleExit());
|
|
1550
|
+
}
|
|
1551
|
+
start(source, opts) {
|
|
1552
|
+
this.trySend({
|
|
1553
|
+
kind: "start",
|
|
1554
|
+
source,
|
|
1555
|
+
opts
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Send `pull` and resolve with the next `frame` or `ended` reply. Arms a
|
|
1560
|
+
* bounded liveness timeout for the round-trip: if the child stops
|
|
1561
|
+
* producing frames WITHOUT exiting, the timeout settles this pull as
|
|
1562
|
+
* `ended` so the caller's async generator resumes, runs its `finally`,
|
|
1563
|
+
* and tears the child down — see {@link handlePullTimeout}.
|
|
1564
|
+
*/
|
|
1565
|
+
pullNext() {
|
|
1566
|
+
if (this.exited) return Promise.resolve({ kind: "ended" });
|
|
1567
|
+
return new Promise((resolve, reject) => {
|
|
1568
|
+
this.pendingPull = {
|
|
1569
|
+
resolve,
|
|
1570
|
+
reject
|
|
1571
|
+
};
|
|
1572
|
+
this.armPullTimeout();
|
|
1573
|
+
try {
|
|
1574
|
+
this.child.send({ kind: "pull" });
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
this.clearPullTimeout();
|
|
1577
|
+
this.pendingPull = null;
|
|
1578
|
+
reject(toError(err));
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
/** Send `toBuffer` for `frameId` and resolve with the matching `buffer` reply's bytes. */
|
|
1583
|
+
requestBuffer(frameId, opts) {
|
|
1584
|
+
if (this.exited) return Promise.reject(/* @__PURE__ */ new Error(`session-decode-coordinator: worker already exited (frameId=${frameId})`));
|
|
1585
|
+
return new Promise((resolve, reject) => {
|
|
1586
|
+
const waiter = {
|
|
1587
|
+
resolve,
|
|
1588
|
+
reject
|
|
1589
|
+
};
|
|
1590
|
+
this.queueToBufferWaiter(frameId, waiter);
|
|
1591
|
+
try {
|
|
1592
|
+
this.child.send({
|
|
1593
|
+
kind: "toBuffer",
|
|
1594
|
+
frameId,
|
|
1595
|
+
opts
|
|
1596
|
+
});
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
this.removeToBufferWaiter(frameId, waiter);
|
|
1599
|
+
reject(toError(err));
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
/** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
|
|
1604
|
+
teardown() {
|
|
1605
|
+
this.clearPullTimeout();
|
|
1606
|
+
this.trySend({ kind: "stop" });
|
|
1607
|
+
this.tryKill(void 0);
|
|
1608
|
+
if (this.exited) return;
|
|
1609
|
+
this.sigkillTimer = setTimeout(() => {
|
|
1610
|
+
this.sigkillTimer = null;
|
|
1611
|
+
if (this.exited) return;
|
|
1612
|
+
this.logger.warn("session-decode-coordinator: child did not exit within SIGKILL fallback window — forcing", { meta: { fallbackMs: SIGKILL_FALLBACK_MS } });
|
|
1613
|
+
this.tryKill("SIGKILL");
|
|
1614
|
+
}, SIGKILL_FALLBACK_MS);
|
|
1615
|
+
this.sigkillTimer.unref?.();
|
|
1616
|
+
}
|
|
1617
|
+
handleMessage(raw) {
|
|
1618
|
+
if (!require_worker_protocol.isWorkerReply(raw)) {
|
|
1619
|
+
this.logger.warn("session-decode-coordinator: dropping malformed worker reply", { meta: { raw } });
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
switch (raw.kind) {
|
|
1623
|
+
case "frame":
|
|
1624
|
+
this.settlePendingPull({
|
|
1625
|
+
kind: "frame",
|
|
1626
|
+
reply: raw
|
|
1627
|
+
});
|
|
1628
|
+
return;
|
|
1629
|
+
case "ended":
|
|
1630
|
+
this.settlePendingPull({ kind: "ended" });
|
|
1631
|
+
return;
|
|
1632
|
+
case "buffer":
|
|
1633
|
+
this.settleToBuffer(raw.frameId, raw.bytes);
|
|
1634
|
+
return;
|
|
1635
|
+
case "error":
|
|
1636
|
+
this.settleError(raw.message, raw.frameId);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
handleExit() {
|
|
1641
|
+
this.exited = true;
|
|
1642
|
+
if (this.sigkillTimer) {
|
|
1643
|
+
clearTimeout(this.sigkillTimer);
|
|
1644
|
+
this.sigkillTimer = null;
|
|
1645
|
+
}
|
|
1646
|
+
this.clearPullTimeout();
|
|
1647
|
+
if (this.pendingPull) {
|
|
1648
|
+
const waiter = this.pendingPull;
|
|
1649
|
+
this.pendingPull = null;
|
|
1650
|
+
waiter.resolve({ kind: "ended" });
|
|
1651
|
+
}
|
|
1652
|
+
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
|
|
1653
|
+
this.pendingToBuffers.clear();
|
|
1654
|
+
}
|
|
1655
|
+
settlePendingPull(outcome) {
|
|
1656
|
+
const waiter = this.pendingPull;
|
|
1657
|
+
if (!waiter) {
|
|
1658
|
+
this.logger.debug("session-decode-coordinator: dropping unsolicited pull-shaped reply", { meta: { outcomeKind: outcome.kind } });
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
this.pendingPull = null;
|
|
1662
|
+
this.clearPullTimeout();
|
|
1663
|
+
if (outcome.kind === "frame") this.firstFrameReceived = true;
|
|
1664
|
+
waiter.resolve(outcome);
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Fires when a `pull` round-trip outlives its liveness timeout — the child
|
|
1668
|
+
* stopped producing frames WITHOUT exiting (a hung decode). Settles the
|
|
1669
|
+
* outstanding pull as `ended` (not a throw) so the consumer's async
|
|
1670
|
+
* generator resumes cleanly, runs its `finally`, and tears the child down
|
|
1671
|
+
* via {@link teardown} — which sends `stop`, `kill()`s, and arms the 1s
|
|
1672
|
+
* SIGKILL fallback.
|
|
1673
|
+
*/
|
|
1674
|
+
handlePullTimeout(timeoutMs) {
|
|
1675
|
+
this.pullTimeoutTimer = null;
|
|
1676
|
+
const waiter = this.pendingPull;
|
|
1677
|
+
if (!waiter) return;
|
|
1678
|
+
this.pendingPull = null;
|
|
1679
|
+
this.logger.warn(`session-decode-coordinator: decode worker produced no frame within ${timeoutMs}ms — ending session`, { meta: {
|
|
1680
|
+
timeoutMs,
|
|
1681
|
+
firstFrameReceived: this.firstFrameReceived
|
|
1682
|
+
} });
|
|
1683
|
+
waiter.resolve({ kind: "ended" });
|
|
1684
|
+
}
|
|
1685
|
+
armPullTimeout() {
|
|
1686
|
+
const timeoutMs = this.firstFrameReceived ? this.frameTimeoutMs : this.firstFrameTimeoutMs;
|
|
1687
|
+
this.pullTimeoutTimer = setTimeout(() => this.handlePullTimeout(timeoutMs), timeoutMs);
|
|
1688
|
+
this.pullTimeoutTimer.unref?.();
|
|
1689
|
+
}
|
|
1690
|
+
clearPullTimeout() {
|
|
1691
|
+
if (this.pullTimeoutTimer) {
|
|
1692
|
+
clearTimeout(this.pullTimeoutTimer);
|
|
1693
|
+
this.pullTimeoutTimer = null;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Correlate by `frameId`: a `buffer` reply only ever settles waiters queued
|
|
1698
|
+
* under the SAME frameId — a late reply for a stale/superseded frame that
|
|
1699
|
+
* nobody is (still) awaiting is dropped, never mistakenly resolves a
|
|
1700
|
+
* different, newer frame's waiter.
|
|
1701
|
+
*/
|
|
1702
|
+
settleToBuffer(frameId, bytes) {
|
|
1703
|
+
const queue = this.pendingToBuffers.get(frameId);
|
|
1704
|
+
if (!queue || queue.length === 0) {
|
|
1705
|
+
this.logger.debug("session-decode-coordinator: dropping stale/unknown buffer reply", { meta: { frameId } });
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const waiter = queue.shift();
|
|
1709
|
+
if (queue.length === 0) this.pendingToBuffers.delete(frameId);
|
|
1710
|
+
waiter?.resolve(bytes);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* `error` replies MAY carry a `frameId` (a `toBuffer`-scoped miss, e.g. a
|
|
1714
|
+
* stale/unknown frameId or a scale failure in `decode-worker-child.ts`) —
|
|
1715
|
+
* those fail ONLY that frame's `toBuffer` waiter(s) so the pull stays alive
|
|
1716
|
+
* and the generator keeps running (P2/I1: a single dropped frame must not
|
|
1717
|
+
* tear the whole session down). An error WITHOUT a `frameId` is a
|
|
1718
|
+
* session-level failure (decode-loop crash, dial error) and keeps the
|
|
1719
|
+
* original fatal behavior: fail the outstanding `pull` first (the more
|
|
1720
|
+
* common source), else fail every outstanding `toBuffer` waiter rather than
|
|
1721
|
+
* silently dropping the error and leaving a caller hanging forever.
|
|
1722
|
+
*/
|
|
1723
|
+
settleError(message, frameId) {
|
|
1724
|
+
const err = /* @__PURE__ */ new Error(`session-decode-coordinator: worker error — ${message}`);
|
|
1725
|
+
if (frameId !== void 0) {
|
|
1726
|
+
const queue = this.pendingToBuffers.get(frameId);
|
|
1727
|
+
if (queue && queue.length > 0) {
|
|
1728
|
+
for (const waiter of queue) waiter.reject(err);
|
|
1729
|
+
this.pendingToBuffers.delete(frameId);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
this.logger.debug("session-decode-coordinator: dropping toBuffer error for unknown frameId", { meta: { frameId } });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (this.pendingPull) {
|
|
1736
|
+
const waiter = this.pendingPull;
|
|
1737
|
+
this.pendingPull = null;
|
|
1738
|
+
this.clearPullTimeout();
|
|
1739
|
+
waiter.reject(err);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(err);
|
|
1743
|
+
this.pendingToBuffers.clear();
|
|
1744
|
+
}
|
|
1745
|
+
queueToBufferWaiter(frameId, waiter) {
|
|
1746
|
+
const queue = this.pendingToBuffers.get(frameId);
|
|
1747
|
+
if (queue) queue.push(waiter);
|
|
1748
|
+
else this.pendingToBuffers.set(frameId, [waiter]);
|
|
1749
|
+
}
|
|
1750
|
+
removeToBufferWaiter(frameId, waiter) {
|
|
1751
|
+
const queue = this.pendingToBuffers.get(frameId);
|
|
1752
|
+
if (!queue) return;
|
|
1753
|
+
const idx = queue.indexOf(waiter);
|
|
1754
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
1755
|
+
if (queue.length === 0) this.pendingToBuffers.delete(frameId);
|
|
1756
|
+
}
|
|
1757
|
+
trySend(msg) {
|
|
1758
|
+
try {
|
|
1759
|
+
this.child.send(msg);
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
this.logger.warn("session-decode-coordinator: send to child failed", { meta: {
|
|
1762
|
+
requestKind: msg.kind,
|
|
1763
|
+
err: toError(err).message
|
|
1764
|
+
} });
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
tryKill(signal) {
|
|
1768
|
+
try {
|
|
1769
|
+
this.child.kill(signal);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
this.logger.warn("session-decode-coordinator: kill() on child failed", { meta: {
|
|
1772
|
+
signal,
|
|
1773
|
+
err: toError(err).message
|
|
1774
|
+
} });
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
/** Build the {@link FrameImage} adapter for one `frame` reply. */
|
|
1779
|
+
function buildFrameImage(session, reply) {
|
|
1780
|
+
let closed = false;
|
|
1781
|
+
return {
|
|
1782
|
+
width: reply.width,
|
|
1783
|
+
height: reply.height,
|
|
1784
|
+
format: reply.format,
|
|
1785
|
+
toBuffer: async (opts) => {
|
|
1786
|
+
if (closed) throw new Error(`session-decode-coordinator: toBuffer() called on a closed frame (frameId=${reply.frameId})`);
|
|
1787
|
+
return session.requestBuffer(reply.frameId, opts);
|
|
1788
|
+
},
|
|
1789
|
+
close: () => {
|
|
1790
|
+
closed = true;
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Create the `VideoFrameGenerator` adapter: forks one decode-worker child per
|
|
1796
|
+
* `generateVideoFrames()` call, drives it with `pull`/`toBuffer` requests
|
|
1797
|
+
* one at a time (no pre-fetch — back-pressure is the async generator's
|
|
1798
|
+
* natural suspend-on-yield), and guarantees the child is torn down
|
|
1799
|
+
* (`stop` + `kill()` + SIGKILL fallback) in every exit path via `finally`.
|
|
1800
|
+
*/
|
|
1801
|
+
function createSessionDecodeCoordinator(deps) {
|
|
1802
|
+
const firstFrameTimeoutMs = deps.firstFrameTimeoutMs ?? DEFAULT_FIRST_FRAME_TIMEOUT_MS;
|
|
1803
|
+
const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
|
|
1804
|
+
return { async *generateVideoFrames(source, opts) {
|
|
1805
|
+
const session = new SessionDecodeSession(deps.fork(source), deps.logger, firstFrameTimeoutMs, frameTimeoutMs);
|
|
1806
|
+
session.start(source, opts);
|
|
1807
|
+
try {
|
|
1808
|
+
while (true) {
|
|
1809
|
+
const outcome = await session.pullNext();
|
|
1810
|
+
if (outcome.kind === "ended") return;
|
|
1811
|
+
const { reply } = outcome;
|
|
1812
|
+
yield {
|
|
1813
|
+
timestamp: reply.timestamp,
|
|
1814
|
+
image: buildFrameImage(session, reply)
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
} finally {
|
|
1818
|
+
session.teardown();
|
|
1819
|
+
}
|
|
1820
|
+
} };
|
|
1821
|
+
}
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/session-decode/session-decode-pump.ts
|
|
1824
|
+
/**
|
|
1825
|
+
* Pure gate: does this runner route camera decode through a per-session
|
|
1826
|
+
* decode worker (Epic C) instead of the shm frame-handle poller? This is now
|
|
1827
|
+
* a RUNNER-LEVEL setting (the pipeline-runner addon's own config,
|
|
1828
|
+
* `this.config.useSessionDecode`) rather than a per-camera one — every
|
|
1829
|
+
* camera attached to this node shares the same decode path. DEFAULT TRUE:
|
|
1830
|
+
* the caller passes the runner's own `useSessionDecode` config value;
|
|
1831
|
+
* an operator unchecks it (node-wide) to fall back to the shared shm
|
|
1832
|
+
* frame-handle poller for every camera on this node.
|
|
1833
|
+
*/
|
|
1834
|
+
function shouldUseSessionDecode(enabled) {
|
|
1835
|
+
return enabled === true;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Detection working width cap — mirrors the shm decoder's
|
|
1839
|
+
* `initScaler` (`nodeav-decoder-session.ts`: `maxW = floor(640 / scale)`,
|
|
1840
|
+
* `scale = 1` default). The worker decodes full source resolution (e.g. a 4K
|
|
1841
|
+
* `native:main` stream); without this downscale `toBuffer` would ship a full
|
|
1842
|
+
* 3840×2160 rgb frame (~24 MB) over IPC per pull, which the detection path
|
|
1843
|
+
* cannot keep up with. Aspect ratio is preserved. A source already ≤ this
|
|
1844
|
+
* width (e.g. the small `native:sub` motion stream) is passed through
|
|
1845
|
+
* unscaled, exactly as the shm scaler's `min(srcW, maxW)` no-ops.
|
|
1846
|
+
*/
|
|
1847
|
+
var WORKING_MAX_WIDTH = 640;
|
|
1848
|
+
/** Aspect-preserving cap to {@link WORKING_MAX_WIDTH}; `undefined` when no downscale is needed. */
|
|
1849
|
+
function resolveWorkingResize(width, height) {
|
|
1850
|
+
if (width <= WORKING_MAX_WIDTH || width <= 0) return void 0;
|
|
1851
|
+
const outWidth = WORKING_MAX_WIDTH;
|
|
1852
|
+
return {
|
|
1853
|
+
width: outWidth,
|
|
1854
|
+
height: Math.round(outWidth * height / width)
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
|
|
1859
|
+
* the detection working width (see {@link resolveWorkingResize}) so the ROI
|
|
1860
|
+
* scaler ships a small frame instead of the full source resolution.
|
|
1861
|
+
* `capturedAt` is set to `Date.now()` so downstream frameAge metrics diff
|
|
1862
|
+
* against the wall clock (mirrors the shm-ring source, which stamps commit
|
|
1863
|
+
* time) — this is what lets Step 5 MEASURE session-decode frameAge vs the
|
|
1864
|
+
* shm-ring cameras.
|
|
1865
|
+
*/
|
|
1866
|
+
async function toDecodedFrame(frame, format) {
|
|
1867
|
+
const resize = resolveWorkingResize(frame.image.width, frame.image.height);
|
|
1868
|
+
const bytes = await frame.image.toBuffer({
|
|
1869
|
+
format,
|
|
1870
|
+
...resize ? { resize } : {}
|
|
1871
|
+
});
|
|
1872
|
+
return {
|
|
1873
|
+
data: Buffer.from(bytes),
|
|
1874
|
+
width: resize?.width ?? frame.image.width,
|
|
1875
|
+
height: resize?.height ?? frame.image.height,
|
|
1876
|
+
format,
|
|
1877
|
+
timestamp: frame.timestamp,
|
|
1878
|
+
capturedAt: Date.now()
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Start pulling frames from a per-session decode worker and feed each decoded
|
|
1883
|
+
* frame to `onDecodedFrame`. Returns an idempotent teardown closure that stops
|
|
1884
|
+
* the async generator (`it.return()`) — which triggers the coordinator's
|
|
1885
|
+
* `finally` → `stop` + `kill()` + 1s SIGKILL fallback → worker exit → the OS
|
|
1886
|
+
* reclaims the VAAPI/GPU decode pool. Fire-and-forget: the returned closure is
|
|
1887
|
+
* synchronous; the frame loop and generator teardown run on their own tasks.
|
|
1888
|
+
*/
|
|
1889
|
+
function startSessionDecodePump(deps) {
|
|
1890
|
+
const format = deps.opts.format ?? "rgb";
|
|
1891
|
+
const iterator = createSessionDecodeCoordinator({
|
|
1892
|
+
fork: deps.fork,
|
|
1893
|
+
logger: deps.logger,
|
|
1894
|
+
...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
|
|
1895
|
+
...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
|
|
1896
|
+
}).generateVideoFrames(deps.source, deps.opts);
|
|
1897
|
+
let stopped = false;
|
|
1898
|
+
let releaseSpawnSlot = null;
|
|
1899
|
+
let spawnSlotReleased = false;
|
|
1900
|
+
const releaseSpawnSlotOnce = () => {
|
|
1901
|
+
if (spawnSlotReleased) return;
|
|
1902
|
+
const release = releaseSpawnSlot;
|
|
1903
|
+
if (!release) return;
|
|
1904
|
+
spawnSlotReleased = true;
|
|
1905
|
+
releaseSpawnSlot = null;
|
|
1906
|
+
release();
|
|
1907
|
+
};
|
|
1908
|
+
const pump = async () => {
|
|
1909
|
+
try {
|
|
1910
|
+
if (deps.acquireSpawnSlot) {
|
|
1911
|
+
releaseSpawnSlot = await deps.acquireSpawnSlot();
|
|
1912
|
+
if (stopped) {
|
|
1913
|
+
releaseSpawnSlotOnce();
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
let firstFrameSeen = false;
|
|
1918
|
+
for await (const frame of iterator) {
|
|
1919
|
+
if (!firstFrameSeen) {
|
|
1920
|
+
firstFrameSeen = true;
|
|
1921
|
+
releaseSpawnSlotOnce();
|
|
1922
|
+
}
|
|
1923
|
+
if (stopped) {
|
|
1924
|
+
frame.image.close();
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
try {
|
|
1928
|
+
const decoded = await toDecodedFrame(frame, format);
|
|
1929
|
+
deps.onDecodedFrame(decoded);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", { meta: {
|
|
1932
|
+
deviceId: deps.source.deviceId,
|
|
1933
|
+
format,
|
|
1934
|
+
error: require_dist.errMsg(err)
|
|
1935
|
+
} });
|
|
1936
|
+
} finally {
|
|
1937
|
+
frame.image.close();
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
deps.logger.warn("session-decode pump: frame loop ended with error", { meta: {
|
|
1942
|
+
deviceId: deps.source.deviceId,
|
|
1943
|
+
format,
|
|
1944
|
+
error: require_dist.errMsg(err)
|
|
1945
|
+
} });
|
|
1946
|
+
} finally {
|
|
1947
|
+
releaseSpawnSlotOnce();
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
pump();
|
|
1951
|
+
return () => {
|
|
1952
|
+
if (stopped) return;
|
|
1953
|
+
stopped = true;
|
|
1954
|
+
releaseSpawnSlotOnce();
|
|
1955
|
+
iterator.return().catch(() => {});
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
//#endregion
|
|
1378
1959
|
//#region src/pipeline-runner/index.ts
|
|
1379
1960
|
var DEFAULT_CONFIG = {
|
|
1380
1961
|
maxQueueDepth: 30,
|
|
1381
1962
|
maxConcurrentInferences: 16,
|
|
1382
1963
|
targetLoadPercent: 80,
|
|
1383
|
-
minThrottledFps: 1
|
|
1964
|
+
minThrottledFps: 1,
|
|
1965
|
+
useSessionDecode: true,
|
|
1966
|
+
maxConcurrentDecodeSpawns: 3
|
|
1384
1967
|
};
|
|
1385
1968
|
/**
|
|
1386
1969
|
* Pure decision helper: returns `true` when a dynamic frame-diff analyzer
|
|
@@ -1458,6 +2041,19 @@ function resolveMotionFrameSource(config, sharedWithDetection) {
|
|
|
1458
2041
|
};
|
|
1459
2042
|
}
|
|
1460
2043
|
/**
|
|
2044
|
+
* The owner hostname a session-decode worker must dial to reach this camera's
|
|
2045
|
+
* compressed restream. `undefined` (→ the broker returns a `127.0.0.1` URL,
|
|
2046
|
+
* correct for a node-local co-located worker) unless the camera's source is a
|
|
2047
|
+
* cross-node remote-restream, in which case the owner host is resolved exactly
|
|
2048
|
+
* as `remoteSourceFor`/`RemoteSourcePlane` do. Pure so the decision is
|
|
2049
|
+
* unit-testable without the addon/broker.
|
|
2050
|
+
*/
|
|
2051
|
+
function resolveSessionDecodeHostname(config, hubUrl) {
|
|
2052
|
+
const frameSource = config.frameSource;
|
|
2053
|
+
if (!isRemoteRestream(frameSource)) return void 0;
|
|
2054
|
+
return require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
1461
2057
|
* Interval for periodic metric snapshot emission. ~1 Hz strikes the
|
|
1462
2058
|
* balance between UI freshness (overlay phase / fps numbers feel live)
|
|
1463
2059
|
* and bus traffic (one snapshot per attached camera + one per node
|
|
@@ -1474,6 +2070,15 @@ var METRICS_SNAPSHOT_INTERVAL_MS = 1e3;
|
|
|
1474
2070
|
var METRICS_HEARTBEAT_MS = 3e4;
|
|
1475
2071
|
var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
1476
2072
|
runner = null;
|
|
2073
|
+
/**
|
|
2074
|
+
* Per-runner (per-node) decode-worker SPAWN-CONCURRENCY GATE (P5). A single
|
|
2075
|
+
* counting semaphore every session-decode fork (detection + motion, all
|
|
2076
|
+
* cameras on this node) passes through, sized to
|
|
2077
|
+
* `config.maxConcurrentDecodeSpawns`. Bounds a synchronized multi-camera
|
|
2078
|
+
* motion burst to N concurrent forks instead of stampeding the shared iGPU.
|
|
2079
|
+
* `resize()`d live in `onConfigChanged`. Null until `onInitialize`.
|
|
2080
|
+
*/
|
|
2081
|
+
spawnGate = null;
|
|
1477
2082
|
attached = /* @__PURE__ */ new Map();
|
|
1478
2083
|
nodeId = "unknown";
|
|
1479
2084
|
metricsSnapshotTimer = null;
|
|
@@ -1512,6 +2117,14 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1512
2117
|
*/
|
|
1513
2118
|
remotePlanes = /* @__PURE__ */ new Map();
|
|
1514
2119
|
/**
|
|
2120
|
+
* Devices whose motion currently rides the detection session-decode worker
|
|
2121
|
+
* (P4 co-active sharing) instead of a dedicated motion worker. Membership →
|
|
2122
|
+
* the detection pump also enqueues a derived gray motion frame per decoded
|
|
2123
|
+
* rgb (see `enqueueSharedDetectionFrame`). A pure flag-set: `enqueueMotionFrame`
|
|
2124
|
+
* is device-scoped, so no per-device callback object is needed.
|
|
2125
|
+
*/
|
|
2126
|
+
sharedMotionSinks = /* @__PURE__ */ new Set();
|
|
2127
|
+
/**
|
|
1515
2128
|
* Snapshot-equality cache for metrics-snapshot defer. The runner
|
|
1516
2129
|
* fires per-camera metrics every `METRICS_SNAPSHOT_INTERVAL_MS`;
|
|
1517
2130
|
* for an idle camera (no inference, queue empty, fps=0) every tick
|
|
@@ -1532,7 +2145,25 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1532
2145
|
*/
|
|
1533
2146
|
benchFrameCache = /* @__PURE__ */ new Map();
|
|
1534
2147
|
benchFrameSweeper = null;
|
|
2148
|
+
/** Per-camera single-flight: a camera never runs two occupancy bursts at once. */
|
|
1535
2149
|
occupancyBurstInFlight = /* @__PURE__ */ new Set();
|
|
2150
|
+
/**
|
|
2151
|
+
* GLOBAL occupancy admission queue. Each occupancy burst SUBSCRIBES a detection
|
|
2152
|
+
* stream (rgb) which, during the `watching` phase, has no existing consumer and
|
|
2153
|
+
* therefore makes the broker DIAL a fresh decoder. Without a global cap, N
|
|
2154
|
+
* cameras rechecking on the same interval would spawn N parallel decoder dials
|
|
2155
|
+
* (thundering herd → re-dial churn → latency). This bounds concurrent bursts to
|
|
2156
|
+
* OCCUPANCY_MAX_CONCURRENT; the rest wait in the queue and drain as slots free.
|
|
2157
|
+
* (A burst on an already-`active` camera reuses the live decoder — the broker's
|
|
2158
|
+
* demand-gate shares one session across subscribers — so only watching-phase
|
|
2159
|
+
* spawns pay the dial cost.)
|
|
2160
|
+
*/
|
|
2161
|
+
occupancyQueue = [];
|
|
2162
|
+
occupancyQueued = /* @__PURE__ */ new Set();
|
|
2163
|
+
occupancyFrames = /* @__PURE__ */ new Map();
|
|
2164
|
+
occupancyActiveCount = 0;
|
|
2165
|
+
/** Max concurrent occupancy bursts (decoder dials) across all cameras. */
|
|
2166
|
+
occupancyMaxConcurrent = 3;
|
|
1536
2167
|
constructor() {
|
|
1537
2168
|
super({ ...DEFAULT_CONFIG });
|
|
1538
2169
|
}
|
|
@@ -1549,9 +2180,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1549
2180
|
onPhaseChanged: (deviceId, phase, meta) => this.handlePhaseChanged(deviceId, phase, meta),
|
|
1550
2181
|
logger: this.ctx.logger,
|
|
1551
2182
|
onOccupancyRecheck: (deviceId, frames) => {
|
|
1552
|
-
this.
|
|
2183
|
+
this.enqueueOccupancyBurst(deviceId, frames);
|
|
1553
2184
|
}
|
|
1554
2185
|
});
|
|
2186
|
+
this.spawnGate = new Semaphore(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
1555
2187
|
this.runner.timingSampler.setLogger(this.ctx.logger.child("timing"));
|
|
1556
2188
|
this.runner.onDetectionStreamChange((deviceId, action) => {
|
|
1557
2189
|
this.handleDetectionStreamChange(deviceId, action);
|
|
@@ -1970,6 +2602,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
1970
2602
|
detectionMode: config.detectionMode,
|
|
1971
2603
|
fps: config.detectionFps,
|
|
1972
2604
|
motionCooldownMs: config.motionCooldownMs,
|
|
2605
|
+
occupancyRecheckEnabled: config.occupancyRecheckEnabled,
|
|
1973
2606
|
occupancyRecheckSec: config.occupancyRecheckSec,
|
|
1974
2607
|
occupancyRecheckFrames: config.occupancyRecheckFrames
|
|
1975
2608
|
});
|
|
@@ -2190,6 +2823,196 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2190
2823
|
return plane;
|
|
2191
2824
|
}
|
|
2192
2825
|
/**
|
|
2826
|
+
* Acquire the broker's COMPRESSED passthrough restream for one camStream and
|
|
2827
|
+
* wrap it as a {@link VideoFrameSource} the decode worker can dial. Mirrors
|
|
2828
|
+
* {@link RemoteSourcePlane}'s acquire: resolve the profile the camStream
|
|
2829
|
+
* feeds, then `getStreamWithCodec({ video: 'copy' })` (refcounted — rides the
|
|
2830
|
+
* broker's single source pull, never a second camera dial). No `hostname`:
|
|
2831
|
+
* the P1 flagged camera is HUB-LOCAL, so the returned `127.0.0.1` URL is
|
|
2832
|
+
* dialable from this hub-resident runner. Returns `null` (loudly logged) when
|
|
2833
|
+
* no profile is bound yet — the caller opens no subscription.
|
|
2834
|
+
*/
|
|
2835
|
+
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname) {
|
|
2836
|
+
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
|
|
2837
|
+
if (profile === null) {
|
|
2838
|
+
this.ctx.logger.error("session-decode: no broker profile feeds camStream — cannot acquire restream", {
|
|
2839
|
+
tags: { deviceId },
|
|
2840
|
+
meta: { streamId }
|
|
2841
|
+
});
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
2845
|
+
deviceId,
|
|
2846
|
+
video: "copy",
|
|
2847
|
+
audio: "none",
|
|
2848
|
+
profile,
|
|
2849
|
+
tag,
|
|
2850
|
+
...hostname !== void 0 ? { hostname } : {}
|
|
2851
|
+
});
|
|
2852
|
+
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
2853
|
+
const source = {
|
|
2854
|
+
restreamUrl: rtpSource.url,
|
|
2855
|
+
deviceId,
|
|
2856
|
+
codec
|
|
2857
|
+
};
|
|
2858
|
+
const pipelineKey = rtpSource.pipelineKey;
|
|
2859
|
+
let released = false;
|
|
2860
|
+
const release = async () => {
|
|
2861
|
+
if (released) return;
|
|
2862
|
+
released = true;
|
|
2863
|
+
await api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey }).catch((err) => {
|
|
2864
|
+
this.ctx.logger.debug("session-decode: releaseStreamWithCodec failed (benign on hub restart)", {
|
|
2865
|
+
tags: { deviceId },
|
|
2866
|
+
meta: {
|
|
2867
|
+
pipelineKey,
|
|
2868
|
+
error: require_dist.errMsg(err)
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
});
|
|
2872
|
+
};
|
|
2873
|
+
return {
|
|
2874
|
+
source,
|
|
2875
|
+
release
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Register this device's motion to ride the detection session-decode worker
|
|
2880
|
+
* (P4 co-active sharing) instead of forking a dedicated motion worker. While
|
|
2881
|
+
* registered, the detection pump enqueues a derived gray motion frame per
|
|
2882
|
+
* decoded rgb. Returns an idempotent teardown that unregisters it; forks
|
|
2883
|
+
* nothing and holds no restream (the detection subscription owns the worker).
|
|
2884
|
+
*/
|
|
2885
|
+
registerSharedMotionSink(deviceId) {
|
|
2886
|
+
this.sharedMotionSinks.add(deviceId);
|
|
2887
|
+
this.ctxIfReady?.logger.info("session-decode: motion rides the detection worker (shared — no 2nd fork)", { tags: { deviceId } });
|
|
2888
|
+
let released = false;
|
|
2889
|
+
return () => {
|
|
2890
|
+
if (released) return;
|
|
2891
|
+
released = true;
|
|
2892
|
+
this.sharedMotionSinks.delete(deviceId);
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Fan a decoded detection rgb frame out to the detection queue, and — when
|
|
2897
|
+
* this device's motion is riding the detection worker (P4 sharing) — also to
|
|
2898
|
+
* the motion queue as a derived gray frame (`rgbFrameToGrayFrame`, luma only:
|
|
2899
|
+
* no second dial, no second decode, no GPU). Mirrors the shm shared-motion
|
|
2900
|
+
* seam (`subscribeMotionFrames` `shared` branch).
|
|
2901
|
+
*/
|
|
2902
|
+
enqueueSharedDetectionFrame(deviceId, frame) {
|
|
2903
|
+
const runner = this.runner;
|
|
2904
|
+
if (!runner) return;
|
|
2905
|
+
runner.enqueueDetectionFrame(deviceId, frame, void 0);
|
|
2906
|
+
if (this.sharedMotionSinks.has(deviceId)) runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Epic C P1 detection path for a flagged camera: decode `rgb` in a
|
|
2910
|
+
* per-session forked worker and feed the runner's detection queue INLINE (no
|
|
2911
|
+
* `FrameHandle`), exactly like the shared shm path. Teardown stops the pump
|
|
2912
|
+
* (→ worker `stop` + `kill` + SIGKILL fallback → reclaim) and releases the
|
|
2913
|
+
* restream refcount.
|
|
2914
|
+
*
|
|
2915
|
+
* Returns the teardown SYNCHRONOUSLY (Epic C P2 I3): the restream acquire +
|
|
2916
|
+
* pump-start run in the background via {@link startGuardedSessionDecode},
|
|
2917
|
+
* so a detach that lands while the acquire is still in flight releases the
|
|
2918
|
+
* restream immediately and never forks the decode worker — no orphan
|
|
2919
|
+
* worker, no leaked refcount.
|
|
2920
|
+
*/
|
|
2921
|
+
/**
|
|
2922
|
+
* Build the spawn-gate acquire the pump calls before forking a decode
|
|
2923
|
+
* worker (P5). Logs DEFERRAL (gate saturated → this fork queues) and the
|
|
2924
|
+
* subsequent proceed — no silent capping. Returns a no-op acquire when the
|
|
2925
|
+
* gate is not yet initialised (pre-`onInitialize`), so the pump is never
|
|
2926
|
+
* blocked by a missing gate. `label` distinguishes detect vs motion forks in
|
|
2927
|
+
* the logs; the device id is already tagged on `log`.
|
|
2928
|
+
*/
|
|
2929
|
+
buildDecodeSpawnAcquire(log, label) {
|
|
2930
|
+
return async () => {
|
|
2931
|
+
const gate = this.spawnGate;
|
|
2932
|
+
if (!gate) return () => {};
|
|
2933
|
+
const wasDeferred = gate.available <= 0;
|
|
2934
|
+
if (wasDeferred) log.info("session-decode: decode-worker spawn DEFERRED — spawn gate saturated, queuing", { meta: {
|
|
2935
|
+
label,
|
|
2936
|
+
cap: gate.concurrency,
|
|
2937
|
+
inFlight: gate.concurrency - gate.available
|
|
2938
|
+
} });
|
|
2939
|
+
const release = await gate.acquire();
|
|
2940
|
+
if (wasDeferred) log.info("session-decode: deferred decode-worker spawn now proceeding", { meta: { label } });
|
|
2941
|
+
else log.debug("session-decode: decode-worker spawn slot acquired", { meta: {
|
|
2942
|
+
label,
|
|
2943
|
+
remaining: gate.available
|
|
2944
|
+
} });
|
|
2945
|
+
return release;
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
startDetectionSessionDecode(config, api) {
|
|
2949
|
+
const runner = this.runner;
|
|
2950
|
+
const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
|
|
2951
|
+
if (!runner) return () => {};
|
|
2952
|
+
return startGuardedSessionDecode({
|
|
2953
|
+
logger: log,
|
|
2954
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"])),
|
|
2955
|
+
startPump: (source) => {
|
|
2956
|
+
log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
|
|
2957
|
+
streamId: config.detectionStreamId,
|
|
2958
|
+
codec: source.codec
|
|
2959
|
+
} });
|
|
2960
|
+
return startSessionDecodePump({
|
|
2961
|
+
fork: forkDecodeWorker,
|
|
2962
|
+
logger: log,
|
|
2963
|
+
source,
|
|
2964
|
+
opts: {
|
|
2965
|
+
fps: config.detectionFps,
|
|
2966
|
+
format: "rgb"
|
|
2967
|
+
},
|
|
2968
|
+
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
2969
|
+
onDecodedFrame: (frame) => {
|
|
2970
|
+
this.enqueueSharedDetectionFrame(config.deviceId, frame);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Epic C P1 motion path for a flagged camera: a dedicated `gray` decode of
|
|
2978
|
+
* the SUB stream in its OWN per-session forked worker (independent of the
|
|
2979
|
+
* detection worker — two workers per flagged camera in P1; sharing one is a
|
|
2980
|
+
* P2 follow-up). Feeds the motion queue INLINE (no handle). Teardown stops
|
|
2981
|
+
* the pump and releases the restream refcount.
|
|
2982
|
+
*
|
|
2983
|
+
* Returns the teardown SYNCHRONOUSLY (Epic C P2 I3) — same
|
|
2984
|
+
* {@link startGuardedSessionDecode} guard as detection: a detach during the
|
|
2985
|
+
* in-flight acquire releases immediately and skips the pump/worker fork.
|
|
2986
|
+
*/
|
|
2987
|
+
startMotionSessionDecode(config, api) {
|
|
2988
|
+
const runner = this.runner;
|
|
2989
|
+
const log = this.ctx.logger.withTags({ deviceId: config.deviceId });
|
|
2990
|
+
if (!runner) return () => {};
|
|
2991
|
+
return startGuardedSessionDecode({
|
|
2992
|
+
logger: log,
|
|
2993
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.motionStreamId, `session-decode:motion:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"])),
|
|
2994
|
+
startPump: (source) => {
|
|
2995
|
+
log.info("session-decode: motion routed through per-session decode worker (gray)", { meta: {
|
|
2996
|
+
streamId: config.motionStreamId,
|
|
2997
|
+
codec: source.codec
|
|
2998
|
+
} });
|
|
2999
|
+
return startSessionDecodePump({
|
|
3000
|
+
fork: forkDecodeWorker,
|
|
3001
|
+
logger: log,
|
|
3002
|
+
source,
|
|
3003
|
+
opts: {
|
|
3004
|
+
fps: config.motionFps,
|
|
3005
|
+
format: "gray"
|
|
3006
|
+
},
|
|
3007
|
+
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "motion"),
|
|
3008
|
+
onDecodedFrame: (frame) => {
|
|
3009
|
+
runner.enqueueMotionFrame(config.deviceId, frame, void 0);
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
2193
3016
|
* Subscribe the motion analyzer to decoded frames.
|
|
2194
3017
|
*
|
|
2195
3018
|
* Two modes:
|
|
@@ -2213,6 +3036,10 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2213
3036
|
log.warn("subscribeMotionFrames: this.ctx.api not available");
|
|
2214
3037
|
return null;
|
|
2215
3038
|
}
|
|
3039
|
+
if (shouldUseSessionDecode(this.config.useSessionDecode)) {
|
|
3040
|
+
if (resolveMotionFrameSource(config, opts?.sharedWithDetection === true).shared) return this.registerSharedMotionSink(config.deviceId);
|
|
3041
|
+
return this.startMotionSessionDecode(config, api);
|
|
3042
|
+
}
|
|
2216
3043
|
const src = resolveMotionFrameSource(config, opts?.sharedWithDetection === true);
|
|
2217
3044
|
const shared = src.shared;
|
|
2218
3045
|
const streamId = src.streamId;
|
|
@@ -2305,6 +3132,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2305
3132
|
log.warn("subscribeDetectionFrames: this.ctx.api not available");
|
|
2306
3133
|
return null;
|
|
2307
3134
|
}
|
|
3135
|
+
if (shouldUseSessionDecode(this.config.useSessionDecode)) return this.startDetectionSessionDecode(config, api);
|
|
2308
3136
|
const remote = this.remoteSourceFor(config);
|
|
2309
3137
|
return startFrameHandlePoller({
|
|
2310
3138
|
api,
|
|
@@ -2440,6 +3268,35 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2440
3268
|
log.error("runMotionAnalysis failed", { meta: { error: msg } });
|
|
2441
3269
|
}
|
|
2442
3270
|
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Admit an occupancy recheck into the global queue. Deduped per camera (one
|
|
3273
|
+
* already queued or bursting is ignored; its latest frame-count wins) so a
|
|
3274
|
+
* fast recheck cadence can't stack requests. Draining is bounded by
|
|
3275
|
+
* `occupancyMaxConcurrent` so 25 cameras rechecking together don't spawn 25
|
|
3276
|
+
* parallel decoder dials.
|
|
3277
|
+
*/
|
|
3278
|
+
enqueueOccupancyBurst(deviceId, frames) {
|
|
3279
|
+
this.occupancyFrames.set(deviceId, frames);
|
|
3280
|
+
if (this.occupancyBurstInFlight.has(deviceId) || this.occupancyQueued.has(deviceId)) return;
|
|
3281
|
+
this.occupancyQueued.add(deviceId);
|
|
3282
|
+
this.occupancyQueue.push(deviceId);
|
|
3283
|
+
this.drainOccupancyQueue();
|
|
3284
|
+
}
|
|
3285
|
+
/** Start queued occupancy bursts up to the global concurrency cap. */
|
|
3286
|
+
drainOccupancyQueue() {
|
|
3287
|
+
while (this.occupancyActiveCount < this.occupancyMaxConcurrent && this.occupancyQueue.length > 0) {
|
|
3288
|
+
const deviceId = this.occupancyQueue.shift();
|
|
3289
|
+
if (deviceId === void 0) break;
|
|
3290
|
+
this.occupancyQueued.delete(deviceId);
|
|
3291
|
+
const frames = this.occupancyFrames.get(deviceId) ?? 4;
|
|
3292
|
+
this.occupancyFrames.delete(deviceId);
|
|
3293
|
+
this.occupancyActiveCount++;
|
|
3294
|
+
this.runOccupancyBurst(deviceId, frames).finally(() => {
|
|
3295
|
+
this.occupancyActiveCount--;
|
|
3296
|
+
this.drainOccupancyQueue();
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2443
3300
|
async runOccupancyBurst(deviceId, frames) {
|
|
2444
3301
|
if (this.occupancyBurstInFlight.has(deviceId)) return;
|
|
2445
3302
|
const attachment = this.attached.get(deviceId);
|
|
@@ -2635,21 +3492,44 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2635
3492
|
step: 1,
|
|
2636
3493
|
default: DEFAULT_CONFIG.minThrottledFps,
|
|
2637
3494
|
showValue: true
|
|
3495
|
+
},
|
|
3496
|
+
{
|
|
3497
|
+
type: "boolean",
|
|
3498
|
+
key: "useSessionDecode",
|
|
3499
|
+
style: "checkbox",
|
|
3500
|
+
label: "Use per-session decode workers",
|
|
3501
|
+
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.",
|
|
3502
|
+
default: DEFAULT_CONFIG.useSessionDecode
|
|
3503
|
+
},
|
|
3504
|
+
{
|
|
3505
|
+
type: "slider",
|
|
3506
|
+
key: "maxConcurrentDecodeSpawns",
|
|
3507
|
+
label: "Max concurrent decode-worker spawns",
|
|
3508
|
+
description: "Min floor of per-session decode-worker forks allowed to spin up CONCURRENTLY on this node. A synchronized multi-camera motion burst forks at most this many decode workers at once; the rest queue (FIFO) until an in-flight worker delivers its first frame or tears down. Keeps a burst from stampeding the shared GPU. Set >= your camera count to effectively disable the gate. Only applies when per-session decode workers are enabled.",
|
|
3509
|
+
min: 1,
|
|
3510
|
+
max: 16,
|
|
3511
|
+
step: 1,
|
|
3512
|
+
default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
|
|
3513
|
+
showValue: true
|
|
2638
3514
|
}
|
|
2639
3515
|
]
|
|
2640
3516
|
}] });
|
|
2641
3517
|
}
|
|
2642
3518
|
async onConfigChanged() {
|
|
2643
3519
|
this.runner?.updateLimits(this.config);
|
|
3520
|
+
this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
2644
3521
|
this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
|
|
2645
3522
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2646
3523
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
2647
3524
|
targetLoadPercent: this.config.targetLoadPercent,
|
|
2648
|
-
minThrottledFps: this.config.minThrottledFps
|
|
3525
|
+
minThrottledFps: this.config.minThrottledFps,
|
|
3526
|
+
useSessionDecode: this.config.useSessionDecode,
|
|
3527
|
+
maxConcurrentDecodeSpawns: this.config.maxConcurrentDecodeSpawns
|
|
2649
3528
|
} });
|
|
2650
3529
|
}
|
|
2651
3530
|
};
|
|
2652
3531
|
//#endregion
|
|
3532
|
+
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
2653
3533
|
exports.FrameQueue = FrameQueue;
|
|
2654
3534
|
exports.PipelineRunner = PipelineRunner;
|
|
2655
3535
|
exports.PipelineTimingSampler = PipelineTimingSampler;
|
|
@@ -2657,5 +3537,6 @@ exports.Semaphore = Semaphore;
|
|
|
2657
3537
|
exports.customActions = pipelineRunnerBenchActions;
|
|
2658
3538
|
exports.default = PipelineRunnerAddon;
|
|
2659
3539
|
exports.resolveMotionFrameSource = resolveMotionFrameSource;
|
|
3540
|
+
exports.resolveSessionDecodeHostname = resolveSessionDecodeHostname;
|
|
2660
3541
|
exports.rgbFrameToGrayFrame = rgbFrameToGrayFrame;
|
|
2661
3542
|
exports.shouldStartOnboardAnalyzer = shouldStartOnboardAnalyzer;
|