@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
|
@@ -45,43 +45,10 @@ function extractHost(raw) {
|
|
|
45
45
|
const host = authority.split(":")[0] ?? "";
|
|
46
46
|
return host.length > 0 ? host : void 0;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Compute the grid + sheet dimensions for `tileCount` tiles. `tileCount` must
|
|
50
|
-
* be ≥ 1 (an empty window produces no sheet). Columns = min(tileCount, maxCols)
|
|
51
|
-
* so a partial window packs tightly instead of leaving a full-width row.
|
|
52
|
-
*/
|
|
53
|
-
function computeSheetGeometry(tileCount, options = {}) {
|
|
54
|
-
if (!Number.isInteger(tileCount) || tileCount < 1) throw new Error(`computeSheetGeometry: tileCount must be a positive integer, got ${tileCount}`);
|
|
55
|
-
const tileWidth = options.tileWidth ?? 480;
|
|
56
|
-
const tileHeight = options.tileHeight ?? 270;
|
|
57
|
-
const maxCols = Math.max(1, options.maxCols ?? 10);
|
|
58
|
-
const cols = Math.min(tileCount, maxCols);
|
|
59
|
-
const rows = Math.ceil(tileCount / cols);
|
|
60
|
-
return {
|
|
61
|
-
cols,
|
|
62
|
-
rows,
|
|
63
|
-
tileWidth,
|
|
64
|
-
tileHeight,
|
|
65
|
-
sheetWidth: cols * tileWidth,
|
|
66
|
-
sheetHeight: rows * tileHeight
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/** Pixel placement of tile `index` (0-based) in row-major order. */
|
|
70
|
-
function tilePlacement(index, geometry) {
|
|
71
|
-
if (!Number.isInteger(index) || index < 0) throw new Error(`tilePlacement: index must be a non-negative integer, got ${index}`);
|
|
72
|
-
const col = index % geometry.cols;
|
|
73
|
-
const row = Math.floor(index / geometry.cols);
|
|
74
|
-
return {
|
|
75
|
-
index,
|
|
76
|
-
x: col * geometry.tileWidth,
|
|
77
|
-
y: row * geometry.tileHeight
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/** All tile placements for a full sheet, index 0..tileCount-1. */
|
|
81
|
-
function allTilePlacements(tileCount, geometry) {
|
|
82
|
-
const out = [];
|
|
83
|
-
for (let i = 0; i < tileCount; i += 1) out.push(tilePlacement(i, geometry));
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
48
|
//#endregion
|
|
87
|
-
|
|
49
|
+
Object.defineProperty(exports, "resolveHubHostname", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
get: function() {
|
|
52
|
+
return resolveHubHostname;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -45,60 +45,5 @@ function extractHost(raw) {
|
|
|
45
45
|
const host = authority.split(":")[0] ?? "";
|
|
46
46
|
return host.length > 0 ? host : void 0;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Compute the grid + sheet dimensions for `tileCount` tiles. `tileCount` must
|
|
50
|
-
* be ≥ 1 (an empty window produces no sheet). Columns = min(tileCount, maxCols)
|
|
51
|
-
* so a partial window packs tightly instead of leaving a full-width row.
|
|
52
|
-
*/
|
|
53
|
-
function computeSheetGeometry(tileCount, options = {}) {
|
|
54
|
-
if (!Number.isInteger(tileCount) || tileCount < 1) throw new Error(`computeSheetGeometry: tileCount must be a positive integer, got ${tileCount}`);
|
|
55
|
-
const tileWidth = options.tileWidth ?? 480;
|
|
56
|
-
const tileHeight = options.tileHeight ?? 270;
|
|
57
|
-
const maxCols = Math.max(1, options.maxCols ?? 10);
|
|
58
|
-
const cols = Math.min(tileCount, maxCols);
|
|
59
|
-
const rows = Math.ceil(tileCount / cols);
|
|
60
|
-
return {
|
|
61
|
-
cols,
|
|
62
|
-
rows,
|
|
63
|
-
tileWidth,
|
|
64
|
-
tileHeight,
|
|
65
|
-
sheetWidth: cols * tileWidth,
|
|
66
|
-
sheetHeight: rows * tileHeight
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/** Pixel placement of tile `index` (0-based) in row-major order. */
|
|
70
|
-
function tilePlacement(index, geometry) {
|
|
71
|
-
if (!Number.isInteger(index) || index < 0) throw new Error(`tilePlacement: index must be a non-negative integer, got ${index}`);
|
|
72
|
-
const col = index % geometry.cols;
|
|
73
|
-
const row = Math.floor(index / geometry.cols);
|
|
74
|
-
return {
|
|
75
|
-
index,
|
|
76
|
-
x: col * geometry.tileWidth,
|
|
77
|
-
y: row * geometry.tileHeight
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/** All tile placements for a full sheet, index 0..tileCount-1. */
|
|
81
|
-
function allTilePlacements(tileCount, geometry) {
|
|
82
|
-
const out = [];
|
|
83
|
-
for (let i = 0; i < tileCount; i += 1) out.push(tilePlacement(i, geometry));
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
48
|
//#endregion
|
|
87
|
-
|
|
88
|
-
enumerable: true,
|
|
89
|
-
get: function() {
|
|
90
|
-
return allTilePlacements;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
Object.defineProperty(exports, "computeSheetGeometry", {
|
|
94
|
-
enumerable: true,
|
|
95
|
-
get: function() {
|
|
96
|
-
return computeSheetGeometry;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
Object.defineProperty(exports, "resolveHubHostname", {
|
|
100
|
-
enumerable: true,
|
|
101
|
-
get: function() {
|
|
102
|
-
return resolveHubHostname;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
49
|
+
export { resolveHubHostname as t };
|
|
@@ -1,8 +1,33 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import { createReadStream, promises } from "node:fs";
|
|
3
4
|
import * as path$1 from "node:path";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import "node:crypto";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
17
|
+
key = keys[i];
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
19
|
+
get: ((k) => from[k]).bind(null, key),
|
|
20
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
26
|
+
value: mod,
|
|
27
|
+
enumerable: true
|
|
28
|
+
}) : target, mod));
|
|
29
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
|
+
//#endregion
|
|
6
31
|
//#region ../system/dist/model-download-service-Cp9f4dk6.mjs
|
|
7
32
|
/**
|
|
8
33
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
@@ -328,4 +353,4 @@ function deleteModelFromDisk(modelsDir, entry, format) {
|
|
|
328
353
|
return true;
|
|
329
354
|
}
|
|
330
355
|
//#endregion
|
|
331
|
-
export { ensureModel as a, downloadFile as i, createFileDataPlaneHandler as n, isModelDownloaded as o, deleteModelFromDisk as r, parseRangeHeader as s, contentTypeFor as t };
|
|
356
|
+
export { ensureModel as a, __commonJSMin as c, downloadFile as i, __require as l, createFileDataPlaneHandler as n, isModelDownloaded as o, deleteModelFromDisk as r, parseRangeHeader as s, contentTypeFor as t, __toESM as u };
|
|
@@ -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-DNjy_sr4.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 { A as motionDetectionCapability,
|
|
1
|
+
import { A as motionDetectionCapability, G as DeviceType, H as BaseAddon, T as evaluateZoneRules, q as hydrateSchema } from "../dist-owmpaeIY.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_chunk = require("../chunk-
|
|
6
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
+
const require_dist = require("../dist-DNjy_sr4.js");
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
|
-
const
|
|
8
|
+
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
9
|
const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
|
|
10
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
+
const require_step_definitions = require("../step-definitions-C2bNlww3.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,20 +1279,43 @@ function startGuardedSessionDecode(deps) {
|
|
|
1279
1279
|
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
1280
1280
|
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
1281
1281
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
1282
|
-
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) :
|
|
1282
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 160;
|
|
1283
1283
|
})() * 1024 * 1024;
|
|
1284
1284
|
/**
|
|
1285
1285
|
* How many of each device's MOST RECENT frames are protected from a busy
|
|
1286
1286
|
* neighbour's budget eviction. Sized to cover the late two-plane / cross-process
|
|
1287
|
-
* enrichment horizon (~150-500 ms)
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1287
|
+
* enrichment horizon (~150-500 ms) at the REAL delivered rate: the store writes
|
|
1288
|
+
* every DELIVERED rgb frame, and a high-fps camera delivers ~25 fps (observed on
|
|
1289
|
+
* the 4K outdoor cams), so the old floor of 6 protected only ~240 ms there and
|
|
1290
|
+
* its late captures hit "frame recycled before resolve" (2026-07-22 triage —
|
|
1291
|
+
* missing thumbnails/firstFrames concentrated on exactly those cameras).
|
|
1292
|
+
* 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
|
|
1293
|
+
* so the worst-case protected footprint stays well inside the byte budget for
|
|
1294
|
+
* realistic concurrency (4 active cams ≈ 44 MB of 96); the hard ceiling always
|
|
1295
|
+
* overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
|
|
1296
|
+
* `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
|
|
1292
1297
|
*/
|
|
1293
1298
|
var RETAINED_FRAME_PER_DEVICE_FLOOR = (() => {
|
|
1294
1299
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR"]);
|
|
1295
|
-
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) :
|
|
1300
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 16;
|
|
1301
|
+
})();
|
|
1302
|
+
/**
|
|
1303
|
+
* TIME-based per-device protection (2026-07-23 cold-window fix): a frame
|
|
1304
|
+
* YOUNGER than this survives budget eviction regardless of how many frames
|
|
1305
|
+
* its device holds. The COUNT floor above protects a fixed number of frames —
|
|
1306
|
+
* but the wall-clock window that buys depends on the delivered fps (16 frames
|
|
1307
|
+
* = 1.6 s @10 fps but only 0.64 s @25 fps, and the 4K street cams deliver
|
|
1308
|
+
* ~25). The late-capture horizon is TIME (session attach + enrichment +
|
|
1309
|
+
* round-trip ≈ 0.5–1.5 s, worst on cold on-motion dials), so the guarantee
|
|
1310
|
+
* must be time-denominated: 12/13 of the zero-media/missing-firstFrame tracks
|
|
1311
|
+
* in the 2026-07-23 audit were ≤6 s cold-session subjects whose handles were
|
|
1312
|
+
* recycled before their first captures ran. Bounded by the hard byte ceiling
|
|
1313
|
+
* (pass 2) exactly like the count floor — worst case ~26 MB per ACTIVE camera
|
|
1314
|
+
* @25 fps. `CAMSTACK_SESSION_RETAIN_FLOOR_MS`; `0` disables (count floor only).
|
|
1315
|
+
*/
|
|
1316
|
+
var RETAINED_FRAME_TIME_FLOOR_MS = (() => {
|
|
1317
|
+
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR_MS"]);
|
|
1318
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 3e3;
|
|
1296
1319
|
})();
|
|
1297
1320
|
/** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
|
|
1298
1321
|
var RETAINED_FRAME_KEY_PREFIX = "ram:";
|
|
@@ -1304,6 +1327,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1304
1327
|
nodeId;
|
|
1305
1328
|
budgetBytes;
|
|
1306
1329
|
perDeviceFloor;
|
|
1330
|
+
timeFloorMs;
|
|
1331
|
+
now;
|
|
1307
1332
|
/** Insertion-ordered (Map) → globally oldest is `keys().next()`. */
|
|
1308
1333
|
entries = /* @__PURE__ */ new Map();
|
|
1309
1334
|
/**
|
|
@@ -1320,6 +1345,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1320
1345
|
this.nodeId = options.nodeId;
|
|
1321
1346
|
this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
|
|
1322
1347
|
this.perDeviceFloor = options.perDeviceFloor ?? RETAINED_FRAME_PER_DEVICE_FLOOR;
|
|
1348
|
+
this.timeFloorMs = options.timeFloorMs ?? RETAINED_FRAME_TIME_FLOOR_MS;
|
|
1349
|
+
this.now = options.now ?? Date.now;
|
|
1323
1350
|
}
|
|
1324
1351
|
/** Number of retained frames (tests / metrics). */
|
|
1325
1352
|
get size() {
|
|
@@ -1355,7 +1382,8 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1355
1382
|
const key = keyOf(handle);
|
|
1356
1383
|
this.entries.set(key, {
|
|
1357
1384
|
frame,
|
|
1358
|
-
deviceId
|
|
1385
|
+
deviceId,
|
|
1386
|
+
writtenAt: this.now()
|
|
1359
1387
|
});
|
|
1360
1388
|
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
1361
1389
|
if (deviceKeys) deviceKeys.push(key);
|
|
@@ -1400,6 +1428,16 @@ var RetainedFrameStore = class RetainedFrameStore {
|
|
|
1400
1428
|
evictToBudget() {
|
|
1401
1429
|
if (this.bytes <= this.budgetBytes) return;
|
|
1402
1430
|
const oldestFirst = [...this.entries.keys()];
|
|
1431
|
+
const nowMs = this.now();
|
|
1432
|
+
for (const key of oldestFirst) {
|
|
1433
|
+
if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
|
|
1434
|
+
const entry = this.entries.get(key);
|
|
1435
|
+
if (!entry) continue;
|
|
1436
|
+
if (this.timeFloorMs > 0 && nowMs - entry.writtenAt < this.timeFloorMs) continue;
|
|
1437
|
+
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
1438
|
+
if (deviceKeys && deviceKeys.length > this.perDeviceFloor) this.dropKey(key);
|
|
1439
|
+
}
|
|
1440
|
+
if (this.bytes <= this.budgetBytes) return;
|
|
1403
1441
|
for (const key of oldestFirst) {
|
|
1404
1442
|
if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
|
|
1405
1443
|
const entry = this.entries.get(key);
|
|
@@ -1467,40 +1505,6 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
1467
1505
|
height: outH
|
|
1468
1506
|
};
|
|
1469
1507
|
}
|
|
1470
|
-
var ThumbSamplerPolicy = class {
|
|
1471
|
-
intervalMs;
|
|
1472
|
-
minGapMs;
|
|
1473
|
-
now;
|
|
1474
|
-
/** Last accepted-sample wall clock per device. */
|
|
1475
|
-
lastAcceptedAt = /* @__PURE__ */ new Map();
|
|
1476
|
-
constructor(options) {
|
|
1477
|
-
this.intervalMs = Math.max(0, options.intervalMs);
|
|
1478
|
-
this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
|
|
1479
|
-
this.now = options.now ?? (() => Date.now());
|
|
1480
|
-
}
|
|
1481
|
-
/**
|
|
1482
|
-
* Decide whether to sample `deviceId` now. Returns true (and records the
|
|
1483
|
-
* acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
|
|
1484
|
-
* the last accepted sample for this device; false otherwise. The FIRST call
|
|
1485
|
-
* for a device always samples (cold start).
|
|
1486
|
-
*/
|
|
1487
|
-
shouldSample(deviceId) {
|
|
1488
|
-
const now = this.now();
|
|
1489
|
-
const gate = Math.max(this.intervalMs, this.minGapMs);
|
|
1490
|
-
const last = this.lastAcceptedAt.get(deviceId);
|
|
1491
|
-
if (last !== void 0 && now - last < gate) return false;
|
|
1492
|
-
this.lastAcceptedAt.set(deviceId, now);
|
|
1493
|
-
return true;
|
|
1494
|
-
}
|
|
1495
|
-
/** Forget a device (detach / phase exit) so its next frame cold-starts. */
|
|
1496
|
-
forget(deviceId) {
|
|
1497
|
-
this.lastAcceptedAt.delete(deviceId);
|
|
1498
|
-
}
|
|
1499
|
-
/** Drop all per-device state (shutdown). */
|
|
1500
|
-
clear() {
|
|
1501
|
-
this.lastAcceptedAt.clear();
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
1504
1508
|
//#endregion
|
|
1505
1509
|
//#region src/session-decode/session-decode-coordinator.ts
|
|
1506
1510
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
@@ -2359,8 +2363,6 @@ function resolveStepDevice(input) {
|
|
|
2359
2363
|
//#region src/pipeline-runner/index.ts
|
|
2360
2364
|
var DEFAULT_CONFIG = {
|
|
2361
2365
|
maxQueueDepth: 30,
|
|
2362
|
-
thumbnailsEnabled: true,
|
|
2363
|
-
thumbnailSampleMs: 5e3,
|
|
2364
2366
|
maxConcurrentInferences: 16,
|
|
2365
2367
|
targetLoadPercent: 80,
|
|
2366
2368
|
minThrottledFps: 1,
|
|
@@ -2576,7 +2578,7 @@ function resolveSessionDecodeOwnerNodeId(config) {
|
|
|
2576
2578
|
function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
2577
2579
|
const reachable = frameSource.ownerReachableHost?.trim();
|
|
2578
2580
|
if (reachable !== void 0 && reachable.length > 0) return reachable;
|
|
2579
|
-
return
|
|
2581
|
+
return require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
|
|
2580
2582
|
}
|
|
2581
2583
|
/**
|
|
2582
2584
|
* Acquire the broker's COMPRESSED passthrough restream for one camStream and
|
|
@@ -2682,12 +2684,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2682
2684
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
2683
2685
|
*/
|
|
2684
2686
|
retainedFrames = null;
|
|
2685
|
-
/**
|
|
2686
|
-
* Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
|
|
2687
|
-
* encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
|
|
2688
|
-
* stays a couple ms of sharp every few seconds. Null until `onInitialize`.
|
|
2689
|
-
*/
|
|
2690
|
-
thumbSampler = null;
|
|
2691
2687
|
attached = /* @__PURE__ */ new Map();
|
|
2692
2688
|
nodeId = "unknown";
|
|
2693
2689
|
metricsSnapshotTimer = null;
|
|
@@ -2815,7 +2811,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2815
2811
|
nodeId: this.nodeId,
|
|
2816
2812
|
logger: this.ctx.logger.child("retained-frames")
|
|
2817
2813
|
});
|
|
2818
|
-
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
2819
2814
|
this.runner = new PipelineRunner({
|
|
2820
2815
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
2821
2816
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -2906,8 +2901,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2906
2901
|
this.nativeCropRegistry.clear();
|
|
2907
2902
|
this.retainedFrames?.clear();
|
|
2908
2903
|
this.retainedFrames = null;
|
|
2909
|
-
this.thumbSampler?.clear();
|
|
2910
|
-
this.thumbSampler = null;
|
|
2911
2904
|
}
|
|
2912
2905
|
async cacheBenchFrame(input) {
|
|
2913
2906
|
const sharp$2 = (await import("sharp")).default;
|
|
@@ -3474,59 +3467,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3474
3467
|
frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
|
|
3475
3468
|
};
|
|
3476
3469
|
}
|
|
3477
|
-
/**
|
|
3478
|
-
* Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
|
|
3479
|
-
* detection frame: at most once per `thumbnailSampleMs` per camera (policy
|
|
3480
|
-
* enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
|
|
3481
|
-
* `recording.thumb-sampled` telemetry. The recorder (single designated node)
|
|
3482
|
-
* packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
|
|
3483
|
-
* is logged at debug and never disturbs the decode/detect path. Zero extra
|
|
3484
|
-
* decode (frame exists); one sharp encode per cadence per camera (D9: a
|
|
3485
|
-
* compressed payload at 1/5 Hz, never raw pixels at frame-rate).
|
|
3486
|
-
*/
|
|
3487
|
-
maybeSampleThumbnail(deviceId, frame) {
|
|
3488
|
-
if (!this.config.thumbnailsEnabled) return;
|
|
3489
|
-
const sampler = this.thumbSampler;
|
|
3490
|
-
if (!sampler || frame.format !== "rgb") return;
|
|
3491
|
-
if (!sampler.shouldSample(deviceId)) return;
|
|
3492
|
-
const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
|
|
3493
|
-
const width = frame.width;
|
|
3494
|
-
const height = frame.height;
|
|
3495
|
-
const data = frame.data;
|
|
3496
|
-
(async () => {
|
|
3497
|
-
try {
|
|
3498
|
-
const targetW = Math.min(480, width);
|
|
3499
|
-
const image = (0, sharp.default)(data, { raw: {
|
|
3500
|
-
width,
|
|
3501
|
-
height,
|
|
3502
|
-
channels: 3
|
|
3503
|
-
} });
|
|
3504
|
-
if (targetW < width) image.resize(targetW);
|
|
3505
|
-
const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
|
|
3506
|
-
const meta = {
|
|
3507
|
-
width: targetW,
|
|
3508
|
-
height: Math.round(targetW * height / width)
|
|
3509
|
-
};
|
|
3510
|
-
this.ctxIfReady?.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.RecordingThumbSampled, {
|
|
3511
|
-
type: "pipeline",
|
|
3512
|
-
id: deviceId,
|
|
3513
|
-
nodeId: this.nodeId,
|
|
3514
|
-
deviceId
|
|
3515
|
-
}, {
|
|
3516
|
-
deviceId,
|
|
3517
|
-
capturedAt,
|
|
3518
|
-
width: meta.width,
|
|
3519
|
-
height: meta.height,
|
|
3520
|
-
jpeg: new Uint8Array(jpeg)
|
|
3521
|
-
}));
|
|
3522
|
-
} catch (err) {
|
|
3523
|
-
this.ctx?.logger.debug("thumbnail sample encode failed", {
|
|
3524
|
-
tags: { deviceId },
|
|
3525
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
3526
|
-
});
|
|
3527
|
-
}
|
|
3528
|
-
})();
|
|
3529
|
-
}
|
|
3530
3470
|
/** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
|
|
3531
3471
|
async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey, nativeCropRef) {
|
|
3532
3472
|
const api = this.ctxIfReady?.api;
|
|
@@ -3607,7 +3547,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3607
3547
|
attachment.detectionUnsubscribe?.();
|
|
3608
3548
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3609
3549
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3610
|
-
this.thumbSampler?.forget(deviceId);
|
|
3611
3550
|
this.packageGovernor.forget(deviceId);
|
|
3612
3551
|
const warnPrefix = `${deviceId}:`;
|
|
3613
3552
|
for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
|
|
@@ -3854,7 +3793,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3854
3793
|
const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
|
|
3855
3794
|
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
3856
3795
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
3857
|
-
this.maybeSampleThumbnail(config.deviceId, frame);
|
|
3858
3796
|
}
|
|
3859
3797
|
});
|
|
3860
3798
|
}
|
|
@@ -4522,31 +4460,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
4522
4460
|
step: 1,
|
|
4523
4461
|
default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
|
|
4524
4462
|
showValue: true
|
|
4525
|
-
},
|
|
4526
|
-
{
|
|
4527
|
-
type: "boolean",
|
|
4528
|
-
key: "thumbnailsEnabled",
|
|
4529
|
-
label: "Scrub thumbnails",
|
|
4530
|
-
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.",
|
|
4531
|
-
default: DEFAULT_CONFIG.thumbnailsEnabled
|
|
4532
|
-
},
|
|
4533
|
-
{
|
|
4534
|
-
type: "slider",
|
|
4535
|
-
key: "thumbnailSampleMs",
|
|
4536
|
-
label: "Thumbnail cadence (ms)",
|
|
4537
|
-
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.",
|
|
4538
|
-
min: 1e3,
|
|
4539
|
-
max: 3e4,
|
|
4540
|
-
step: 1e3,
|
|
4541
|
-
default: DEFAULT_CONFIG.thumbnailSampleMs,
|
|
4542
|
-
showValue: true
|
|
4543
4463
|
}
|
|
4544
4464
|
]
|
|
4545
4465
|
}] });
|
|
4546
4466
|
}
|
|
4547
4467
|
async onConfigChanged() {
|
|
4548
4468
|
this.runner?.updateLimits(this.config);
|
|
4549
|
-
this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
|
|
4550
4469
|
this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
|
|
4551
4470
|
this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
|
|
4552
4471
|
maxQueueDepth: this.config.maxQueueDepth,
|