@camstack/addon-pipeline 1.2.6 → 1.2.8
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 +3 -3
- package/dist/audio-analyzer/index.mjs +2 -3
- package/dist/{chunk-D6vf50IK.js → chunk-emK7D4bc.js} +7 -0
- package/dist/detection-pipeline/index.js +104 -8
- package/dist/detection-pipeline/index.mjs +103 -8
- package/dist/{dist-Bh0H8jM6.js → dist-DNjy_sr4.js} +1374 -925
- package/dist/{dist-Bw_V5L0r.mjs → dist-owmpaeIY.mjs} +1379 -924
- package/dist/fmp4-keyframes-BzTPoPVn.js +6676 -0
- package/dist/fmp4-keyframes-qOgJBjMO.mjs +6669 -0
- package/dist/{sheet-geometry-DcpuGTCm.mjs → hub-hostname-DAJXlOgV.js} +6 -39
- package/dist/{sheet-geometry-BR-ip0CP.js → hub-hostname-cCknRYKj.mjs} +1 -56
- package/dist/{model-download-service-Cp9f4dk6-CwUb5v3Y.mjs → model-download-service-Cp9f4dk6-WO-foD7W.mjs} +26 -1
- package/dist/{model-download-service-Cp9f4dk6-Dv-oFwjN.js → model-download-service-Cp9f4dk6-fsdDExML.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +51 -132
- package/dist/pipeline-runner/index.mjs +49 -130
- package/dist/recorder/index.js +695 -974
- package/dist/recorder/index.mjs +694 -972
- package/dist/session-decode/decode-worker-child.js +9 -5
- package/dist/session-decode/decode-worker-child.mjs +8 -4
- package/dist/{step-definitions-xxdpJX9-.js → step-definitions-C2bNlww3.js} +1 -1
- package/dist/{step-definitions-aY4D9Loy.mjs → step-definitions-CkvctwuS.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DX-Ef8CP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-XdI0RgvI.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DSfbOjdi.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-BdxY9zld.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js--zEZtV2N.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DUL71Vjq.mjs → hostInit-DsMIdlwP.mjs} +2 -2
- package/dist/stream-broker/index.js +680 -15
- package/dist/stream-broker/index.mjs +679 -15
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CGWEeRkz.js → MaskShapeCanvas-DI4BY7W2-CftzR7Pj.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-4YbTa3Wp.js → MotionZonesSettings-NcxxQN8r-DPgVIdEn.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DL_tAs9-.js → PrivacyMaskSettings-APgPLF7p-D0HqiXHW.js} +1 -1
- package/embed-dist/assets/{index-CvBwcYl_.js → index-DRtbV_en.js} +17 -15
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/postprocessors/ctc.py +14 -5
- package/python/postprocessors/test_ctc.py +35 -0
- package/dist/chunk-BdkLduGY.mjs +0 -5
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-gchR4WlX.mjs +0 -26
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { H as
|
|
1
|
+
import { H as BaseAddon, K as createEvent, N as pipelineRunnerCapability, S as defineCustomActions, V as errMsg, at as lazy, b as customAction, ct as object, ft as EventCategory, j as nodePin, nt as array, rt as boolean, s as DEVICE_BACKEND_TO_FORMAT, st as number, tt as _enum, ut as string } from "../dist-owmpaeIY.mjs";
|
|
2
2
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
|
|
5
|
-
import { a as getStepDefinition } from "../step-definitions-
|
|
5
|
+
import { a as getStepDefinition } from "../step-definitions-CkvctwuS.mjs";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import sharp from "sharp";
|
|
@@ -1273,20 +1273,43 @@ function startGuardedSessionDecode(deps) {
|
|
|
1273
1273
|
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
1274
1274
|
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
1275
1275
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
1276
|
-
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) :
|
|
1276
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 160;
|
|
1277
1277
|
})() * 1024 * 1024;
|
|
1278
1278
|
/**
|
|
1279
1279
|
* How many of each device's MOST RECENT frames are protected from a busy
|
|
1280
1280
|
* neighbour's budget eviction. Sized to cover the late two-plane / cross-process
|
|
1281
|
-
* enrichment horizon (~150-500 ms)
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1281
|
+
* enrichment horizon (~150-500 ms) at the REAL delivered rate: the store writes
|
|
1282
|
+
* every DELIVERED rgb frame, and a high-fps camera delivers ~25 fps (observed on
|
|
1283
|
+
* the 4K outdoor cams), so the old floor of 6 protected only ~240 ms there and
|
|
1284
|
+
* its late captures hit "frame recycled before resolve" (2026-07-22 triage —
|
|
1285
|
+
* missing thumbnails/firstFrames concentrated on exactly those cameras).
|
|
1286
|
+
* 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
|
|
1287
|
+
* so the worst-case protected footprint stays well inside the byte budget for
|
|
1288
|
+
* realistic concurrency (4 active cams ≈ 44 MB of 96); the hard ceiling always
|
|
1289
|
+
* overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
|
|
1290
|
+
* `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
|
|
1286
1291
|
*/
|
|
1287
1292
|
var RETAINED_FRAME_PER_DEVICE_FLOOR = (() => {
|
|
1288
1293
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR"]);
|
|
1289
|
-
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) :
|
|
1294
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 16;
|
|
1295
|
+
})();
|
|
1296
|
+
/**
|
|
1297
|
+
* TIME-based per-device protection (2026-07-23 cold-window fix): a frame
|
|
1298
|
+
* YOUNGER than this survives budget eviction regardless of how many frames
|
|
1299
|
+
* its device holds. The COUNT floor above protects a fixed number of frames —
|
|
1300
|
+
* but the wall-clock window that buys depends on the delivered fps (16 frames
|
|
1301
|
+
* = 1.6 s @10 fps but only 0.64 s @25 fps, and the 4K street cams deliver
|
|
1302
|
+
* ~25). The late-capture horizon is TIME (session attach + enrichment +
|
|
1303
|
+
* round-trip ≈ 0.5–1.5 s, worst on cold on-motion dials), so the guarantee
|
|
1304
|
+
* must be time-denominated: 12/13 of the zero-media/missing-firstFrame tracks
|
|
1305
|
+
* in the 2026-07-23 audit were ≤6 s cold-session subjects whose handles were
|
|
1306
|
+
* recycled before their first captures ran. Bounded by the hard byte ceiling
|
|
1307
|
+
* (pass 2) exactly like the count floor — worst case ~26 MB per ACTIVE camera
|
|
1308
|
+
* @25 fps. `CAMSTACK_SESSION_RETAIN_FLOOR_MS`; `0` disables (count floor only).
|
|
1309
|
+
*/
|
|
1310
|
+
var RETAINED_FRAME_TIME_FLOOR_MS = (() => {
|
|
1311
|
+
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR_MS"]);
|
|
1312
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 3e3;
|
|
1290
1313
|
})();
|
|
1291
1314
|
/** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
|
|
1292
1315
|
var RETAINED_FRAME_KEY_PREFIX = "ram:";
|
|
@@ -1298,6 +1321,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1298
1321
|
nodeId;
|
|
1299
1322
|
budgetBytes;
|
|
1300
1323
|
perDeviceFloor;
|
|
1324
|
+
timeFloorMs;
|
|
1325
|
+
now;
|
|
1301
1326
|
/** Insertion-ordered (Map) → globally oldest is `keys().next()`. */
|
|
1302
1327
|
entries = /* @__PURE__ */ new Map();
|
|
1303
1328
|
/**
|
|
@@ -1314,6 +1339,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1314
1339
|
this.nodeId = options.nodeId;
|
|
1315
1340
|
this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
|
|
1316
1341
|
this.perDeviceFloor = options.perDeviceFloor ?? RETAINED_FRAME_PER_DEVICE_FLOOR;
|
|
1342
|
+
this.timeFloorMs = options.timeFloorMs ?? RETAINED_FRAME_TIME_FLOOR_MS;
|
|
1343
|
+
this.now = options.now ?? Date.now;
|
|
1317
1344
|
}
|
|
1318
1345
|
/** Number of retained frames (tests / metrics). */
|
|
1319
1346
|
get size() {
|
|
@@ -1349,7 +1376,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1349
1376
|
const key = keyOf(handle);
|
|
1350
1377
|
this.entries.set(key, {
|
|
1351
1378
|
frame,
|
|
1352
|
-
deviceId
|
|
1379
|
+
deviceId,
|
|
1380
|
+
writtenAt: this.now()
|
|
1353
1381
|
});
|
|
1354
1382
|
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
1355
1383
|
if (deviceKeys) deviceKeys.push(key);
|
|
@@ -1394,6 +1422,16 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1394
1422
|
evictToBudget() {
|
|
1395
1423
|
if (this.bytes <= this.budgetBytes) return;
|
|
1396
1424
|
const oldestFirst = [...this.entries.keys()];
|
|
1425
|
+
const nowMs = this.now();
|
|
1426
|
+
for (const key of oldestFirst) {
|
|
1427
|
+
if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
|
|
1428
|
+
const entry = this.entries.get(key);
|
|
1429
|
+
if (!entry) continue;
|
|
1430
|
+
if (this.timeFloorMs > 0 && nowMs - entry.writtenAt < this.timeFloorMs) continue;
|
|
1431
|
+
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
1432
|
+
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key);
|
|
1433
|
+
}
|
|
1434
|
+
if (this.bytes <= this.budgetBytes) return;
|
|
1397
1435
|
for (const key of oldestFirst) {
|
|
1398
1436
|
if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
|
|
1399
1437
|
const entry = this.entries.get(key);
|
|
@@ -1461,40 +1499,6 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
1461
1499
|
height: outH
|
|
1462
1500
|
};
|
|
1463
1501
|
}
|
|
1464
|
-
var ThumbSamplerPolicy = class {
|
|
1465
|
-
intervalMs;
|
|
1466
|
-
minGapMs;
|
|
1467
|
-
now;
|
|
1468
|
-
/** Last accepted-sample wall clock per device. */
|
|
1469
|
-
lastAcceptedAt = /* @__PURE__ */ new Map();
|
|
1470
|
-
constructor(options) {
|
|
1471
|
-
this.intervalMs = Math.max(0, options.intervalMs);
|
|
1472
|
-
this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
|
|
1473
|
-
this.now = options.now ?? (() => Date.now());
|
|
1474
|
-
}
|
|
1475
|
-
/**
|
|
1476
|
-
* Decide whether to sample `deviceId` now. Returns true (and records the
|
|
1477
|
-
* acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
|
|
1478
|
-
* the last accepted sample for this device; false otherwise. The FIRST call
|
|
1479
|
-
* for a device always samples (cold start).
|
|
1480
|
-
*/
|
|
1481
|
-
shouldSample(deviceId) {
|
|
1482
|
-
const now = this.now();
|
|
1483
|
-
const gate = Math.max(this.intervalMs, this.minGapMs);
|
|
1484
|
-
const last = this.lastAcceptedAt.get(deviceId);
|
|
1485
|
-
if (last !== void 0 && now - last < gate) return false;
|
|
1486
|
-
this.lastAcceptedAt.set(deviceId, now);
|
|
1487
|
-
return true;
|
|
1488
|
-
}
|
|
1489
|
-
/** Forget a device (detach / phase exit) so its next frame cold-starts. */
|
|
1490
|
-
forget(deviceId) {
|
|
1491
|
-
this.lastAcceptedAt.delete(deviceId);
|
|
1492
|
-
}
|
|
1493
|
-
/** Drop all per-device state (shutdown). */
|
|
1494
|
-
clear() {
|
|
1495
|
-
this.lastAcceptedAt.clear();
|
|
1496
|
-
}
|
|
1497
|
-
};
|
|
1498
1502
|
//#endregion
|
|
1499
1503
|
//#region src/session-decode/session-decode-coordinator.ts
|
|
1500
1504
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
@@ -2353,8 +2357,6 @@ function resolveStepDevice(input) {
|
|
|
2353
2357
|
//#region src/pipeline-runner/index.ts
|
|
2354
2358
|
var DEFAULT_CONFIG = {
|
|
2355
2359
|
maxQueueDepth: 30,
|
|
2356
|
-
thumbnailsEnabled: true,
|
|
2357
|
-
thumbnailSampleMs: 5e3,
|
|
2358
2360
|
maxConcurrentInferences: 16,
|
|
2359
2361
|
targetLoadPercent: 80,
|
|
2360
2362
|
minThrottledFps: 1,
|
|
@@ -2676,12 +2678,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2676
2678
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
2677
2679
|
*/
|
|
2678
2680
|
retainedFrames = null;
|
|
2679
|
-
/**
|
|
2680
|
-
* Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
|
|
2681
|
-
* encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
|
|
2682
|
-
* stays a couple ms of sharp every few seconds. Null until `onInitialize`.
|
|
2683
|
-
*/
|
|
2684
|
-
thumbSampler = null;
|
|
2685
2681
|
attached = /* @__PURE__ */ new Map();
|
|
2686
2682
|
nodeId = "unknown";
|
|
2687
2683
|
metricsSnapshotTimer = null;
|
|
@@ -2809,7 +2805,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2809
2805
|
nodeId: this.nodeId,
|
|
2810
2806
|
logger: this.ctx.logger.child("retained-frames")
|
|
2811
2807
|
});
|
|
2812
|
-
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
2813
2808
|
this.runner = new PipelineRunner({
|
|
2814
2809
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2815
2810
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -2900,8 +2895,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2900
2895
|
this.nativeCropRegistry.clear();
|
|
2901
2896
|
this.retainedFrames?.clear();
|
|
2902
2897
|
this.retainedFrames = null;
|
|
2903
|
-
this.thumbSampler?.clear();
|
|
2904
|
-
this.thumbSampler = null;
|
|
2905
2898
|
}
|
|
2906
2899
|
async cacheBenchFrame(input) {
|
|
2907
2900
|
const sharp = (await import("sharp")).default;
|
|
@@ -3468,59 +3461,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3468
3461
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
3469
3462
|
};
|
|
3470
3463
|
}
|
|
3471
|
-
/**
|
|
3472
|
-
* Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
|
|
3473
|
-
* detection frame: at most once per `thumbnailSampleMs` per camera (policy
|
|
3474
|
-
* enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
|
|
3475
|
-
* `recording.thumb-sampled` telemetry. The recorder (single designated node)
|
|
3476
|
-
* packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
|
|
3477
|
-
* is logged at debug and never disturbs the decode/detect path. Zero extra
|
|
3478
|
-
* decode (frame exists); one sharp encode per cadence per camera (D9: a
|
|
3479
|
-
* compressed payload at 1/5 Hz, never raw pixels at frame-rate).
|
|
3480
|
-
*/
|
|
3481
|
-
maybeSampleThumbnail(deviceId, frame) {
|
|
3482
|
-
if (!this.config.thumbnailsEnabled) return;
|
|
3483
|
-
const sampler = this.thumbSampler;
|
|
3484
|
-
if (!sampler || frame.format !== "rgb") return;
|
|
3485
|
-
if (!sampler.shouldSample(deviceId)) return;
|
|
3486
|
-
const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
|
|
3487
|
-
const width = frame.width;
|
|
3488
|
-
const height = frame.height;
|
|
3489
|
-
const data = frame.data;
|
|
3490
|
-
(async () => {
|
|
3491
|
-
try {
|
|
3492
|
-
const targetW = Math.min(480, width);
|
|
3493
|
-
const image = sharp(data, { raw: {
|
|
3494
|
-
width,
|
|
3495
|
-
height,
|
|
3496
|
-
channels: 3
|
|
3497
|
-
} });
|
|
3498
|
-
if (targetW < width) image.resize(targetW);
|
|
3499
|
-
const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
|
|
3500
|
-
const meta = {
|
|
3501
|
-
width: targetW,
|
|
3502
|
-
height: Math.round(targetW * height / width)
|
|
3503
|
-
};
|
|
3504
|
-
this.ctxIfReady?.eventBus.emit(createEvent(EventCategory.RecordingThumbSampled, {
|
|
3505
|
-
type: "pipeline",
|
|
3506
|
-
id: deviceId,
|
|
3507
|
-
nodeId: this.nodeId,
|
|
3508
|
-
deviceId
|
|
3509
|
-
}, {
|
|
3510
|
-
deviceId,
|
|
3511
|
-
capturedAt,
|
|
3512
|
-
width: meta.width,
|
|
3513
|
-
height: meta.height,
|
|
3514
|
-
jpeg: new Uint8Array(jpeg)
|
|
3515
|
-
}));
|
|
3516
|
-
} catch (err) {
|
|
3517
|
-
this.ctx?.logger.debug("thumbnail sample encode failed", {
|
|
3518
|
-
tags: { deviceId },
|
|
3519
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
3520
|
-
});
|
|
3521
|
-
}
|
|
3522
|
-
})();
|
|
3523
|
-
}
|
|
3524
3464
|
/** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
|
|
3525
3465
|
async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey, nativeCropRef) {
|
|
3526
3466
|
const api = this.ctxIfReady?.api;
|
|
@@ -3601,7 +3541,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3601
3541
|
attachment.detectionUnsubscribe?.();
|
|
3602
3542
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3603
3543
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3604
|
-
this.thumbSampler?.forget(deviceId);
|
|
3605
3544
|
this.packageGovernor.forget(deviceId);
|
|
3606
3545
|
const warnPrefix = `${deviceId}:`;
|
|
3607
3546
|
for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
|
|
@@ -3848,7 +3787,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3848
3787
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3849
3788
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
3850
3789
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
3851
|
-
this.maybeSampleThumbnail(config.deviceId, frame);
|
|
3852
3790
|
}
|
|
3853
3791
|
});
|
|
3854
3792
|
}
|
|
@@ -4516,31 +4454,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
4516
4454
|
step: 1,
|
|
4517
4455
|
default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
|
|
4518
4456
|
showValue: true
|
|
4519
|
-
},
|
|
4520
|
-
{
|
|
4521
|
-
type: "boolean",
|
|
4522
|
-
key: "thumbnailsEnabled",
|
|
4523
|
-
label: "Scrub thumbnails",
|
|
4524
|
-
description: "Sample the already-decoded detection frame into small JPEG scrub thumbnails (zero extra decode). The recorder packs them into 5-minute sprite sheets the viewer timeline scrubs. Disable to stop emitting thumbnail telemetry from this node.",
|
|
4525
|
-
default: DEFAULT_CONFIG.thumbnailsEnabled
|
|
4526
|
-
},
|
|
4527
|
-
{
|
|
4528
|
-
type: "slider",
|
|
4529
|
-
key: "thumbnailSampleMs",
|
|
4530
|
-
label: "Thumbnail cadence (ms)",
|
|
4531
|
-
description: "How often (ms) each camera contributes a scrub thumbnail. Lower = denser scrub preview + more storage; a hard 1 Hz ceiling always applies. Default 5000.",
|
|
4532
|
-
min: 1e3,
|
|
4533
|
-
max: 3e4,
|
|
4534
|
-
step: 1e3,
|
|
4535
|
-
default: DEFAULT_CONFIG.thumbnailSampleMs,
|
|
4536
|
-
showValue: true
|
|
4537
4457
|
}
|
|
4538
4458
|
]
|
|
4539
4459
|
}] });
|
|
4540
4460
|
}
|
|
4541
4461
|
async onConfigChanged() {
|
|
4542
4462
|
this.runner?.updateLimits(this.config);
|
|
4543
|
-
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
4544
4463
|
this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
4545
4464
|
this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
|
|
4546
4465
|
maxQueueDepth: this.config.maxQueueDepth,
|