@camstack/addon-pipeline 1.1.57 → 1.1.59
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 +4 -4
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +9 -9
- package/dist/detection-pipeline/index.mjs +3 -3
- package/dist/{dist-CK8Ur-OS.js → dist-B1u-QN05.js} +4822 -4650
- package/dist/{dist-CL531uVA.mjs → dist-DKeNH_5z.mjs} +4823 -4651
- package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-Cp9f4dk6-0wh7OK3s.mjs} +2 -2
- package/dist/{model-download-service-C-IHWnXx-D326YNnt.js → model-download-service-Cp9f4dk6-Cp_GTKTw.js} +2 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +147 -12
- package/dist/pipeline-runner/index.mjs +146 -11
- package/dist/recorder/index.js +993 -7
- package/dist/recorder/index.mjs +990 -5
- package/dist/session-decode/decode-worker-child.js +110 -2
- package/dist/session-decode/decode-worker-child.mjs +110 -2
- package/dist/{hub-hostname-cCknRYKj.mjs → sheet-geometry-BN80zpCa.js} +56 -1
- package/dist/{hub-hostname-DAJXlOgV.js → sheet-geometry-Bk3fR4s7.mjs} +39 -6
- package/dist/{step-definitions-DHAxruAQ.js → step-definitions-CPch37vh.js} +1 -1
- package/dist/{step-definitions-1d3_vQ6S.mjs → step-definitions-DUB27V8Y.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cm_B-hQT.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DdkfKDgw.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D3h_W9uu.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-DSXctoaw.mjs +26 -0
- package/dist/stream-broker/{hostInit-DEaqJpGA.mjs → hostInit-BLn2Pv8Q.mjs} +3 -3
- package/dist/stream-broker/index.js +626 -152
- package/dist/stream-broker/index.mjs +626 -152
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CZUzu3MF.js → MaskShapeCanvas-DI4BY7W2-B0_nx15B.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CmZbzLQi.js → MotionZonesSettings-NcxxQN8r-B8uM-VeO.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BOKDrH0N.js → PrivacyMaskSettings-APgPLF7p-DMOs4Hj9.js} +1 -1
- package/embed-dist/assets/index-W9-H5LTj.js +81 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ba7Eyd47.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-TWCUWhCm.mjs +0 -26
- package/embed-dist/assets/index-DOsK8CZb.js +0 -81
|
@@ -3,7 +3,7 @@ import { createReadStream, promises } from "node:fs";
|
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import "node:crypto";
|
|
6
|
-
//#region ../system/dist/model-download-service-
|
|
6
|
+
//#region ../system/dist/model-download-service-Cp9f4dk6.mjs
|
|
7
7
|
/**
|
|
8
8
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
9
9
|
* path stays within (traversal guard). The handler serves the first candidate
|
|
@@ -121,7 +121,7 @@ function createFileDataPlaneHandler(opts) {
|
|
|
121
121
|
const baseHeaders = {
|
|
122
122
|
"content-type": contentTypeFor(absPath, opts.contentTypes),
|
|
123
123
|
"accept-ranges": "bytes",
|
|
124
|
-
"cache-control": "no-cache"
|
|
124
|
+
"cache-control": opts.cacheControl?.(rel) ?? "no-cache"
|
|
125
125
|
};
|
|
126
126
|
const range = parseRangeHeader(req.headers.range, size);
|
|
127
127
|
if (range) {
|
|
@@ -4,7 +4,7 @@ node_fs = require_chunk.__toESM(node_fs, 1);
|
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_chunk.__toESM(node_path, 1);
|
|
6
6
|
require("node:crypto");
|
|
7
|
-
//#region ../system/dist/model-download-service-
|
|
7
|
+
//#region ../system/dist/model-download-service-Cp9f4dk6.mjs
|
|
8
8
|
/**
|
|
9
9
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
10
10
|
* path stays within (traversal guard). The handler serves the first candidate
|
|
@@ -122,7 +122,7 @@ function createFileDataPlaneHandler(opts) {
|
|
|
122
122
|
const baseHeaders = {
|
|
123
123
|
"content-type": contentTypeFor(absPath, opts.contentTypes),
|
|
124
124
|
"accept-ranges": "bytes",
|
|
125
|
-
"cache-control": "no-cache"
|
|
125
|
+
"cache-control": opts.cacheControl?.(rel) ?? "no-cache"
|
|
126
126
|
};
|
|
127
127
|
const range = parseRangeHeader(req.headers.range, size);
|
|
128
128
|
if (range) {
|
|
@@ -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-B1u-QN05.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 { B as DeviceType, E as motionDetectionCapability,
|
|
1
|
+
import { B as DeviceType, E as motionDetectionCapability, H as hydrateSchema, L as BaseAddon, x as evaluateZoneRules } from "../dist-DKeNH_5z.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
|
|
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-B1u-QN05.js");
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
|
-
const
|
|
8
|
+
const require_sheet_geometry = require("../sheet-geometry-BN80zpCa.js");
|
|
9
9
|
const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
|
|
10
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
+
const require_step_definitions = require("../step-definitions-CPch37vh.js");
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
13
|
let sharp = require("sharp");
|
|
@@ -1279,6 +1279,40 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
1279
1279
|
height: outH
|
|
1280
1280
|
};
|
|
1281
1281
|
}
|
|
1282
|
+
var ThumbSamplerPolicy = class {
|
|
1283
|
+
intervalMs;
|
|
1284
|
+
minGapMs;
|
|
1285
|
+
now;
|
|
1286
|
+
/** Last accepted-sample wall clock per device. */
|
|
1287
|
+
lastAcceptedAt = /* @__PURE__ */ new Map();
|
|
1288
|
+
constructor(options) {
|
|
1289
|
+
this.intervalMs = Math.max(0, options.intervalMs);
|
|
1290
|
+
this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
|
|
1291
|
+
this.now = options.now ?? (() => Date.now());
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Decide whether to sample `deviceId` now. Returns true (and records the
|
|
1295
|
+
* acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
|
|
1296
|
+
* the last accepted sample for this device; false otherwise. The FIRST call
|
|
1297
|
+
* for a device always samples (cold start).
|
|
1298
|
+
*/
|
|
1299
|
+
shouldSample(deviceId) {
|
|
1300
|
+
const now = this.now();
|
|
1301
|
+
const gate = Math.max(this.intervalMs, this.minGapMs);
|
|
1302
|
+
const last = this.lastAcceptedAt.get(deviceId);
|
|
1303
|
+
if (last !== void 0 && now - last < gate) return false;
|
|
1304
|
+
this.lastAcceptedAt.set(deviceId, now);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
/** Forget a device (detach / phase exit) so its next frame cold-starts. */
|
|
1308
|
+
forget(deviceId) {
|
|
1309
|
+
this.lastAcceptedAt.delete(deviceId);
|
|
1310
|
+
}
|
|
1311
|
+
/** Drop all per-device state (shutdown). */
|
|
1312
|
+
clear() {
|
|
1313
|
+
this.lastAcceptedAt.clear();
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1282
1316
|
//#endregion
|
|
1283
1317
|
//#region src/session-decode/session-decode-coordinator.ts
|
|
1284
1318
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
@@ -2029,6 +2063,8 @@ async function runDetailSubtree(deps, input) {
|
|
|
2029
2063
|
//#region src/pipeline-runner/index.ts
|
|
2030
2064
|
var DEFAULT_CONFIG = {
|
|
2031
2065
|
maxQueueDepth: 30,
|
|
2066
|
+
thumbnailsEnabled: true,
|
|
2067
|
+
thumbnailSampleMs: 5e3,
|
|
2032
2068
|
maxConcurrentInferences: 16,
|
|
2033
2069
|
targetLoadPercent: 80,
|
|
2034
2070
|
minThrottledFps: 1,
|
|
@@ -2244,7 +2280,7 @@ function resolveSessionDecodeOwnerNodeId(config) {
|
|
|
2244
2280
|
function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
2245
2281
|
const reachable = frameSource.ownerReachableHost?.trim();
|
|
2246
2282
|
if (reachable !== void 0 && reachable.length > 0) return reachable;
|
|
2247
|
-
return
|
|
2283
|
+
return require_sheet_geometry.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
|
|
2248
2284
|
}
|
|
2249
2285
|
/**
|
|
2250
2286
|
* Acquire the broker's COMPRESSED passthrough restream for one camStream and
|
|
@@ -2350,6 +2386,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2350
2386
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
2351
2387
|
*/
|
|
2352
2388
|
retainedFrames = null;
|
|
2389
|
+
/**
|
|
2390
|
+
* Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
|
|
2391
|
+
* encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
|
|
2392
|
+
* stays a couple ms of sharp every few seconds. Null until `onInitialize`.
|
|
2393
|
+
*/
|
|
2394
|
+
thumbSampler = null;
|
|
2353
2395
|
attached = /* @__PURE__ */ new Map();
|
|
2354
2396
|
nodeId = "unknown";
|
|
2355
2397
|
metricsSnapshotTimer = null;
|
|
@@ -2470,6 +2512,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2470
2512
|
nodeId: this.nodeId,
|
|
2471
2513
|
logger: this.ctx.logger.child("retained-frames")
|
|
2472
2514
|
});
|
|
2515
|
+
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
2473
2516
|
this.runner = new PipelineRunner({
|
|
2474
2517
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2475
2518
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -2559,6 +2602,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2559
2602
|
this.nativeCropRegistry.clear();
|
|
2560
2603
|
this.retainedFrames?.clear();
|
|
2561
2604
|
this.retainedFrames = null;
|
|
2605
|
+
this.thumbSampler?.clear();
|
|
2606
|
+
this.thumbSampler = null;
|
|
2562
2607
|
}
|
|
2563
2608
|
async cacheBenchFrame(input) {
|
|
2564
2609
|
const sharp$2 = (await import("sharp")).default;
|
|
@@ -3021,6 +3066,59 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3021
3066
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
3022
3067
|
};
|
|
3023
3068
|
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
|
|
3071
|
+
* detection frame: at most once per `thumbnailSampleMs` per camera (policy
|
|
3072
|
+
* enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
|
|
3073
|
+
* `recording.thumb-sampled` telemetry. The recorder (single designated node)
|
|
3074
|
+
* packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
|
|
3075
|
+
* is logged at debug and never disturbs the decode/detect path. Zero extra
|
|
3076
|
+
* decode (frame exists); one sharp encode per cadence per camera (D9: a
|
|
3077
|
+
* compressed payload at 1/5 Hz, never raw pixels at frame-rate).
|
|
3078
|
+
*/
|
|
3079
|
+
maybeSampleThumbnail(deviceId, frame) {
|
|
3080
|
+
if (!this.config.thumbnailsEnabled) return;
|
|
3081
|
+
const sampler = this.thumbSampler;
|
|
3082
|
+
if (!sampler || frame.format !== "rgb") return;
|
|
3083
|
+
if (!sampler.shouldSample(deviceId)) return;
|
|
3084
|
+
const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
|
|
3085
|
+
const width = frame.width;
|
|
3086
|
+
const height = frame.height;
|
|
3087
|
+
const data = frame.data;
|
|
3088
|
+
(async () => {
|
|
3089
|
+
try {
|
|
3090
|
+
const targetW = Math.min(320, width);
|
|
3091
|
+
const image = (0, sharp.default)(data, { raw: {
|
|
3092
|
+
width,
|
|
3093
|
+
height,
|
|
3094
|
+
channels: 3
|
|
3095
|
+
} });
|
|
3096
|
+
if (targetW < width) image.resize(targetW);
|
|
3097
|
+
const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
|
|
3098
|
+
const meta = {
|
|
3099
|
+
width: targetW,
|
|
3100
|
+
height: Math.round(targetW * height / width)
|
|
3101
|
+
};
|
|
3102
|
+
this.ctxIfReady?.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.RecordingThumbSampled, {
|
|
3103
|
+
type: "pipeline",
|
|
3104
|
+
id: deviceId,
|
|
3105
|
+
nodeId: this.nodeId,
|
|
3106
|
+
deviceId
|
|
3107
|
+
}, {
|
|
3108
|
+
deviceId,
|
|
3109
|
+
capturedAt,
|
|
3110
|
+
width: meta.width,
|
|
3111
|
+
height: meta.height,
|
|
3112
|
+
jpeg: new Uint8Array(jpeg)
|
|
3113
|
+
}));
|
|
3114
|
+
} catch (err) {
|
|
3115
|
+
this.ctx?.logger.debug("thumbnail sample encode failed", {
|
|
3116
|
+
tags: { deviceId },
|
|
3117
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
})();
|
|
3121
|
+
}
|
|
3024
3122
|
/** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
|
|
3025
3123
|
async runDetailPipeline(steps, imageJpeg, deviceId) {
|
|
3026
3124
|
const api = this.ctxIfReady?.api;
|
|
@@ -3098,6 +3196,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3098
3196
|
attachment.detectionUnsubscribe?.();
|
|
3099
3197
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3100
3198
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3199
|
+
this.thumbSampler?.forget(deviceId);
|
|
3101
3200
|
this.attached.delete(deviceId);
|
|
3102
3201
|
this.lastMotionAt.delete(deviceId);
|
|
3103
3202
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -3339,6 +3438,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3339
3438
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3340
3439
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
3341
3440
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
3441
|
+
this.maybeSampleThumbnail(config.deviceId, frame);
|
|
3342
3442
|
}
|
|
3343
3443
|
});
|
|
3344
3444
|
}
|
|
@@ -3516,14 +3616,16 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3516
3616
|
}
|
|
3517
3617
|
if (steps.length === 0) return null;
|
|
3518
3618
|
let image;
|
|
3519
|
-
if (
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3619
|
+
if (!this.executorCoLocated()) {
|
|
3620
|
+
if (frame.format === "rgb") try {
|
|
3621
|
+
image = await (0, sharp.default)(frame.data, { raw: {
|
|
3622
|
+
width: frame.width,
|
|
3623
|
+
height: frame.height,
|
|
3624
|
+
channels: 3
|
|
3625
|
+
} }).jpeg({ quality: DETECTION_HOP_JPEG_QUALITY }).toBuffer();
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
log.debug("runInference: frame JPEG encode failed — falling back to raw pixels", { meta: { error: require_dist.errMsg(err) } });
|
|
3628
|
+
}
|
|
3527
3629
|
}
|
|
3528
3630
|
try {
|
|
3529
3631
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
@@ -3538,6 +3640,20 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3538
3640
|
return null;
|
|
3539
3641
|
}
|
|
3540
3642
|
}
|
|
3643
|
+
/** Sticky true once the executor's provider shows up in THIS process' local
|
|
3644
|
+
* registry (the `detection` execution.group) — from then on the per-frame
|
|
3645
|
+
* call is in-process and raw pixels are zero-copy. Re-checks while false
|
|
3646
|
+
* (a co-located sibling may register after the runner's first frames). */
|
|
3647
|
+
executorCoLocatedSticky = false;
|
|
3648
|
+
executorCoLocated() {
|
|
3649
|
+
if (this.executorCoLocatedSticky) return true;
|
|
3650
|
+
if (this.ctx?.capabilities?.get("pipeline-executor") !== void 0) {
|
|
3651
|
+
this.executorCoLocatedSticky = true;
|
|
3652
|
+
this.ctx?.logger.info("executor co-located in this runner — raw in-process frame transport");
|
|
3653
|
+
return true;
|
|
3654
|
+
}
|
|
3655
|
+
return false;
|
|
3656
|
+
}
|
|
3541
3657
|
async runMotionAnalysis(deviceId, frame, handle) {
|
|
3542
3658
|
const ctx = this.ctx;
|
|
3543
3659
|
const runner = this.runner;
|
|
@@ -3977,12 +4093,31 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3977
4093
|
step: 1,
|
|
3978
4094
|
default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
|
|
3979
4095
|
showValue: true
|
|
4096
|
+
},
|
|
4097
|
+
{
|
|
4098
|
+
type: "boolean",
|
|
4099
|
+
key: "thumbnailsEnabled",
|
|
4100
|
+
label: "Scrub thumbnails",
|
|
4101
|
+
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.",
|
|
4102
|
+
default: DEFAULT_CONFIG.thumbnailsEnabled
|
|
4103
|
+
},
|
|
4104
|
+
{
|
|
4105
|
+
type: "slider",
|
|
4106
|
+
key: "thumbnailSampleMs",
|
|
4107
|
+
label: "Thumbnail cadence (ms)",
|
|
4108
|
+
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.",
|
|
4109
|
+
min: 1e3,
|
|
4110
|
+
max: 3e4,
|
|
4111
|
+
step: 1e3,
|
|
4112
|
+
default: DEFAULT_CONFIG.thumbnailSampleMs,
|
|
4113
|
+
showValue: true
|
|
3980
4114
|
}
|
|
3981
4115
|
]
|
|
3982
4116
|
}] });
|
|
3983
4117
|
}
|
|
3984
4118
|
async onConfigChanged() {
|
|
3985
4119
|
this.runner?.updateLimits(this.config);
|
|
4120
|
+
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
3986
4121
|
this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
3987
4122
|
this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
|
|
3988
4123
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as nodePin, I as errMsg, L as BaseAddon, Q as boolean, V as createEvent, X as _enum, Z as array, _ as customAction, at as string, et as lazy, k as pipelineRunnerCapability, nt as number, rt as object, st as EventCategory, y as defineCustomActions } from "../dist-DKeNH_5z.mjs";
|
|
2
2
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as resolveHubHostname } from "../sheet-geometry-Bk3fR4s7.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-CyVJTZEO.mjs";
|
|
5
|
-
import { a as getStepDefinition } from "../step-definitions-
|
|
5
|
+
import { a as getStepDefinition } from "../step-definitions-DUB27V8Y.mjs";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import sharp from "sharp";
|
|
@@ -1273,6 +1273,40 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
1273
1273
|
height: outH
|
|
1274
1274
|
};
|
|
1275
1275
|
}
|
|
1276
|
+
var ThumbSamplerPolicy = class {
|
|
1277
|
+
intervalMs;
|
|
1278
|
+
minGapMs;
|
|
1279
|
+
now;
|
|
1280
|
+
/** Last accepted-sample wall clock per device. */
|
|
1281
|
+
lastAcceptedAt = /* @__PURE__ */ new Map();
|
|
1282
|
+
constructor(options) {
|
|
1283
|
+
this.intervalMs = Math.max(0, options.intervalMs);
|
|
1284
|
+
this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
|
|
1285
|
+
this.now = options.now ?? (() => Date.now());
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Decide whether to sample `deviceId` now. Returns true (and records the
|
|
1289
|
+
* acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
|
|
1290
|
+
* the last accepted sample for this device; false otherwise. The FIRST call
|
|
1291
|
+
* for a device always samples (cold start).
|
|
1292
|
+
*/
|
|
1293
|
+
shouldSample(deviceId) {
|
|
1294
|
+
const now = this.now();
|
|
1295
|
+
const gate = Math.max(this.intervalMs, this.minGapMs);
|
|
1296
|
+
const last = this.lastAcceptedAt.get(deviceId);
|
|
1297
|
+
if (last !== void 0 && now - last < gate) return false;
|
|
1298
|
+
this.lastAcceptedAt.set(deviceId, now);
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
/** Forget a device (detach / phase exit) so its next frame cold-starts. */
|
|
1302
|
+
forget(deviceId) {
|
|
1303
|
+
this.lastAcceptedAt.delete(deviceId);
|
|
1304
|
+
}
|
|
1305
|
+
/** Drop all per-device state (shutdown). */
|
|
1306
|
+
clear() {
|
|
1307
|
+
this.lastAcceptedAt.clear();
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1276
1310
|
//#endregion
|
|
1277
1311
|
//#region src/session-decode/session-decode-coordinator.ts
|
|
1278
1312
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
@@ -2023,6 +2057,8 @@ async function runDetailSubtree(deps, input) {
|
|
|
2023
2057
|
//#region src/pipeline-runner/index.ts
|
|
2024
2058
|
var DEFAULT_CONFIG = {
|
|
2025
2059
|
maxQueueDepth: 30,
|
|
2060
|
+
thumbnailsEnabled: true,
|
|
2061
|
+
thumbnailSampleMs: 5e3,
|
|
2026
2062
|
maxConcurrentInferences: 16,
|
|
2027
2063
|
targetLoadPercent: 80,
|
|
2028
2064
|
minThrottledFps: 1,
|
|
@@ -2344,6 +2380,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2344
2380
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
2345
2381
|
*/
|
|
2346
2382
|
retainedFrames = null;
|
|
2383
|
+
/**
|
|
2384
|
+
* Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
|
|
2385
|
+
* encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
|
|
2386
|
+
* stays a couple ms of sharp every few seconds. Null until `onInitialize`.
|
|
2387
|
+
*/
|
|
2388
|
+
thumbSampler = null;
|
|
2347
2389
|
attached = /* @__PURE__ */ new Map();
|
|
2348
2390
|
nodeId = "unknown";
|
|
2349
2391
|
metricsSnapshotTimer = null;
|
|
@@ -2464,6 +2506,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2464
2506
|
nodeId: this.nodeId,
|
|
2465
2507
|
logger: this.ctx.logger.child("retained-frames")
|
|
2466
2508
|
});
|
|
2509
|
+
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
2467
2510
|
this.runner = new PipelineRunner({
|
|
2468
2511
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2469
2512
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -2553,6 +2596,8 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2553
2596
|
this.nativeCropRegistry.clear();
|
|
2554
2597
|
this.retainedFrames?.clear();
|
|
2555
2598
|
this.retainedFrames = null;
|
|
2599
|
+
this.thumbSampler?.clear();
|
|
2600
|
+
this.thumbSampler = null;
|
|
2556
2601
|
}
|
|
2557
2602
|
async cacheBenchFrame(input) {
|
|
2558
2603
|
const sharp = (await import("sharp")).default;
|
|
@@ -3015,6 +3060,59 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3015
3060
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
3016
3061
|
};
|
|
3017
3062
|
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
|
|
3065
|
+
* detection frame: at most once per `thumbnailSampleMs` per camera (policy
|
|
3066
|
+
* enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
|
|
3067
|
+
* `recording.thumb-sampled` telemetry. The recorder (single designated node)
|
|
3068
|
+
* packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
|
|
3069
|
+
* is logged at debug and never disturbs the decode/detect path. Zero extra
|
|
3070
|
+
* decode (frame exists); one sharp encode per cadence per camera (D9: a
|
|
3071
|
+
* compressed payload at 1/5 Hz, never raw pixels at frame-rate).
|
|
3072
|
+
*/
|
|
3073
|
+
maybeSampleThumbnail(deviceId, frame) {
|
|
3074
|
+
if (!this.config.thumbnailsEnabled) return;
|
|
3075
|
+
const sampler = this.thumbSampler;
|
|
3076
|
+
if (!sampler || frame.format !== "rgb") return;
|
|
3077
|
+
if (!sampler.shouldSample(deviceId)) return;
|
|
3078
|
+
const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
|
|
3079
|
+
const width = frame.width;
|
|
3080
|
+
const height = frame.height;
|
|
3081
|
+
const data = frame.data;
|
|
3082
|
+
(async () => {
|
|
3083
|
+
try {
|
|
3084
|
+
const targetW = Math.min(320, width);
|
|
3085
|
+
const image = sharp(data, { raw: {
|
|
3086
|
+
width,
|
|
3087
|
+
height,
|
|
3088
|
+
channels: 3
|
|
3089
|
+
} });
|
|
3090
|
+
if (targetW < width) image.resize(targetW);
|
|
3091
|
+
const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
|
|
3092
|
+
const meta = {
|
|
3093
|
+
width: targetW,
|
|
3094
|
+
height: Math.round(targetW * height / width)
|
|
3095
|
+
};
|
|
3096
|
+
this.ctxIfReady?.eventBus.emit(createEvent(EventCategory.RecordingThumbSampled, {
|
|
3097
|
+
type: "pipeline",
|
|
3098
|
+
id: deviceId,
|
|
3099
|
+
nodeId: this.nodeId,
|
|
3100
|
+
deviceId
|
|
3101
|
+
}, {
|
|
3102
|
+
deviceId,
|
|
3103
|
+
capturedAt,
|
|
3104
|
+
width: meta.width,
|
|
3105
|
+
height: meta.height,
|
|
3106
|
+
jpeg: new Uint8Array(jpeg)
|
|
3107
|
+
}));
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
this.ctx?.logger.debug("thumbnail sample encode failed", {
|
|
3110
|
+
tags: { deviceId },
|
|
3111
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
})();
|
|
3115
|
+
}
|
|
3018
3116
|
/** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
|
|
3019
3117
|
async runDetailPipeline(steps, imageJpeg, deviceId) {
|
|
3020
3118
|
const api = this.ctxIfReady?.api;
|
|
@@ -3092,6 +3190,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3092
3190
|
attachment.detectionUnsubscribe?.();
|
|
3093
3191
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3094
3192
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3193
|
+
this.thumbSampler?.forget(deviceId);
|
|
3095
3194
|
this.attached.delete(deviceId);
|
|
3096
3195
|
this.lastMotionAt.delete(deviceId);
|
|
3097
3196
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -3333,6 +3432,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3333
3432
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3334
3433
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
3335
3434
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
3435
|
+
this.maybeSampleThumbnail(config.deviceId, frame);
|
|
3336
3436
|
}
|
|
3337
3437
|
});
|
|
3338
3438
|
}
|
|
@@ -3510,14 +3610,16 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3510
3610
|
}
|
|
3511
3611
|
if (steps.length === 0) return null;
|
|
3512
3612
|
let image;
|
|
3513
|
-
if (
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3613
|
+
if (!this.executorCoLocated()) {
|
|
3614
|
+
if (frame.format === "rgb") try {
|
|
3615
|
+
image = await sharp(frame.data, { raw: {
|
|
3616
|
+
width: frame.width,
|
|
3617
|
+
height: frame.height,
|
|
3618
|
+
channels: 3
|
|
3619
|
+
} }).jpeg({ quality: DETECTION_HOP_JPEG_QUALITY }).toBuffer();
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
log.debug("runInference: frame JPEG encode failed — falling back to raw pixels", { meta: { error: errMsg(err) } });
|
|
3622
|
+
}
|
|
3521
3623
|
}
|
|
3522
3624
|
try {
|
|
3523
3625
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
@@ -3532,6 +3634,20 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3532
3634
|
return null;
|
|
3533
3635
|
}
|
|
3534
3636
|
}
|
|
3637
|
+
/** Sticky true once the executor's provider shows up in THIS process' local
|
|
3638
|
+
* registry (the `detection` execution.group) — from then on the per-frame
|
|
3639
|
+
* call is in-process and raw pixels are zero-copy. Re-checks while false
|
|
3640
|
+
* (a co-located sibling may register after the runner's first frames). */
|
|
3641
|
+
executorCoLocatedSticky = false;
|
|
3642
|
+
executorCoLocated() {
|
|
3643
|
+
if (this.executorCoLocatedSticky) return true;
|
|
3644
|
+
if (this.ctx?.capabilities?.get("pipeline-executor") !== void 0) {
|
|
3645
|
+
this.executorCoLocatedSticky = true;
|
|
3646
|
+
this.ctx?.logger.info("executor co-located in this runner — raw in-process frame transport");
|
|
3647
|
+
return true;
|
|
3648
|
+
}
|
|
3649
|
+
return false;
|
|
3650
|
+
}
|
|
3535
3651
|
async runMotionAnalysis(deviceId, frame, handle) {
|
|
3536
3652
|
const ctx = this.ctx;
|
|
3537
3653
|
const runner = this.runner;
|
|
@@ -3971,12 +4087,31 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3971
4087
|
step: 1,
|
|
3972
4088
|
default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
|
|
3973
4089
|
showValue: true
|
|
4090
|
+
},
|
|
4091
|
+
{
|
|
4092
|
+
type: "boolean",
|
|
4093
|
+
key: "thumbnailsEnabled",
|
|
4094
|
+
label: "Scrub thumbnails",
|
|
4095
|
+
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.",
|
|
4096
|
+
default: DEFAULT_CONFIG.thumbnailsEnabled
|
|
4097
|
+
},
|
|
4098
|
+
{
|
|
4099
|
+
type: "slider",
|
|
4100
|
+
key: "thumbnailSampleMs",
|
|
4101
|
+
label: "Thumbnail cadence (ms)",
|
|
4102
|
+
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.",
|
|
4103
|
+
min: 1e3,
|
|
4104
|
+
max: 3e4,
|
|
4105
|
+
step: 1e3,
|
|
4106
|
+
default: DEFAULT_CONFIG.thumbnailSampleMs,
|
|
4107
|
+
showValue: true
|
|
3974
4108
|
}
|
|
3975
4109
|
]
|
|
3976
4110
|
}] });
|
|
3977
4111
|
}
|
|
3978
4112
|
async onConfigChanged() {
|
|
3979
4113
|
this.runner?.updateLimits(this.config);
|
|
4114
|
+
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
3980
4115
|
this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
3981
4116
|
this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
|
|
3982
4117
|
maxQueueDepth: this.config.maxQueueDepth,
|