@camstack/addon-pipeline 1.2.7 → 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.
Files changed (41) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +2 -3
  3. package/dist/{chunk-D6vf50IK.js → chunk-emK7D4bc.js} +7 -0
  4. package/dist/detection-pipeline/index.js +104 -8
  5. package/dist/detection-pipeline/index.mjs +103 -8
  6. package/dist/{dist-CAT3VF7A.js → dist-DNjy_sr4.js} +1239 -1058
  7. package/dist/{dist-BwyGTnMu.mjs → dist-owmpaeIY.mjs} +867 -680
  8. package/dist/fmp4-keyframes-BzTPoPVn.js +6676 -0
  9. package/dist/fmp4-keyframes-qOgJBjMO.mjs +6669 -0
  10. package/dist/{sheet-geometry-DcpuGTCm.mjs → hub-hostname-DAJXlOgV.js} +6 -39
  11. package/dist/{sheet-geometry-BR-ip0CP.js → hub-hostname-cCknRYKj.mjs} +1 -56
  12. package/dist/{model-download-service-Cp9f4dk6-CwUb5v3Y.mjs → model-download-service-Cp9f4dk6-WO-foD7W.mjs} +26 -1
  13. package/dist/{model-download-service-Cp9f4dk6-Dv-oFwjN.js → model-download-service-Cp9f4dk6-fsdDExML.js} +1 -1
  14. package/dist/motion-wasm/index.js +1 -1
  15. package/dist/motion-wasm/index.mjs +1 -1
  16. package/dist/pipeline-runner/index.js +40 -126
  17. package/dist/pipeline-runner/index.mjs +38 -124
  18. package/dist/recorder/index.js +695 -974
  19. package/dist/recorder/index.mjs +694 -972
  20. package/dist/session-decode/decode-worker-child.js +9 -5
  21. package/dist/session-decode/decode-worker-child.mjs +8 -4
  22. package/dist/{step-definitions-BPEExRug.js → step-definitions-C2bNlww3.js} +1 -1
  23. package/dist/{step-definitions-DN1obXS7.mjs → step-definitions-CkvctwuS.mjs} +1 -1
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BtN6sZcL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-XdI0RgvI.mjs} +2 -2
  26. 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
  27. 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
  28. package/dist/stream-broker/{hostInit-BIZvt2bk.mjs → hostInit-DsMIdlwP.mjs} +2 -2
  29. package/dist/stream-broker/index.js +646 -10
  30. package/dist/stream-broker/index.mjs +645 -10
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-3uL2OmvC.js → MaskShapeCanvas-DI4BY7W2-CftzR7Pj.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-JkIkJTeI.js → MotionZonesSettings-NcxxQN8r-DPgVIdEn.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-AKGABxQk.js → PrivacyMaskSettings-APgPLF7p-D0HqiXHW.js} +1 -1
  35. package/embed-dist/assets/{index-DfcKIHhA.js → index-DRtbV_en.js} +17 -15
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/python/postprocessors/ctc.py +14 -5
  39. package/python/postprocessors/test_ctc.py +35 -0
  40. package/dist/chunk-BdkLduGY.mjs +0 -5
  41. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D34ivtL2.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
- export { computeSheetGeometry as n, resolveHubHostname as r, allTilePlacements as t };
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
- Object.defineProperty(exports, "allTilePlacements", {
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 };
@@ -1,4 +1,4 @@
1
- const require_chunk = require("./chunk-D6vf50IK.js");
1
+ const require_chunk = require("./chunk-emK7D4bc.js");
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_chunk.__toESM(node_fs, 1);
4
4
  let node_path = require("node:path");
@@ -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-CAT3VF7A.js");
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, J as hydrateSchema, K as DeviceType, T as evaluateZoneRules, U as BaseAddon } from "../dist-BwyGTnMu.mjs";
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-D6vf50IK.js");
6
- const require_dist = require("../dist-CAT3VF7A.js");
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 require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
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-BPEExRug.js");
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,7 +1279,7 @@ 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) : 96;
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
@@ -1299,6 +1299,24 @@ var RETAINED_FRAME_PER_DEVICE_FLOOR = (() => {
1299
1299
  const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR"]);
1300
1300
  return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 16;
1301
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;
1319
+ })();
1302
1320
  /** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
1303
1321
  var RETAINED_FRAME_KEY_PREFIX = "ram:";
1304
1322
  /** A retention frame is uniquely identified by its key + slot + committed seq. */
@@ -1309,6 +1327,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1309
1327
  nodeId;
1310
1328
  budgetBytes;
1311
1329
  perDeviceFloor;
1330
+ timeFloorMs;
1331
+ now;
1312
1332
  /** Insertion-ordered (Map) → globally oldest is `keys().next()`. */
1313
1333
  entries = /* @__PURE__ */ new Map();
1314
1334
  /**
@@ -1325,6 +1345,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1325
1345
  this.nodeId = options.nodeId;
1326
1346
  this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
1327
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;
1328
1350
  }
1329
1351
  /** Number of retained frames (tests / metrics). */
1330
1352
  get size() {
@@ -1360,7 +1382,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1360
1382
  const key = keyOf(handle);
1361
1383
  this.entries.set(key, {
1362
1384
  frame,
1363
- deviceId
1385
+ deviceId,
1386
+ writtenAt: this.now()
1364
1387
  });
1365
1388
  const deviceKeys = this.perDeviceKeys.get(deviceId);
1366
1389
  if (deviceKeys) deviceKeys.push(key);
@@ -1405,6 +1428,16 @@ var RetainedFrameStore = class RetainedFrameStore {
1405
1428
  evictToBudget() {
1406
1429
  if (this.bytes <= this.budgetBytes) return;
1407
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;
1408
1441
  for (const key of oldestFirst) {
1409
1442
  if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
1410
1443
  const entry = this.entries.get(key);
@@ -1472,40 +1505,6 @@ function cropRgb(frame, bbox, maxWidth) {
1472
1505
  height: outH
1473
1506
  };
1474
1507
  }
1475
- var ThumbSamplerPolicy = class {
1476
- intervalMs;
1477
- minGapMs;
1478
- now;
1479
- /** Last accepted-sample wall clock per device. */
1480
- lastAcceptedAt = /* @__PURE__ */ new Map();
1481
- constructor(options) {
1482
- this.intervalMs = Math.max(0, options.intervalMs);
1483
- this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
1484
- this.now = options.now ?? (() => Date.now());
1485
- }
1486
- /**
1487
- * Decide whether to sample `deviceId` now. Returns true (and records the
1488
- * acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
1489
- * the last accepted sample for this device; false otherwise. The FIRST call
1490
- * for a device always samples (cold start).
1491
- */
1492
- shouldSample(deviceId) {
1493
- const now = this.now();
1494
- const gate = Math.max(this.intervalMs, this.minGapMs);
1495
- const last = this.lastAcceptedAt.get(deviceId);
1496
- if (last !== void 0 && now - last < gate) return false;
1497
- this.lastAcceptedAt.set(deviceId, now);
1498
- return true;
1499
- }
1500
- /** Forget a device (detach / phase exit) so its next frame cold-starts. */
1501
- forget(deviceId) {
1502
- this.lastAcceptedAt.delete(deviceId);
1503
- }
1504
- /** Drop all per-device state (shutdown). */
1505
- clear() {
1506
- this.lastAcceptedAt.clear();
1507
- }
1508
- };
1509
1508
  //#endregion
1510
1509
  //#region src/session-decode/session-decode-coordinator.ts
1511
1510
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
@@ -2364,8 +2363,6 @@ function resolveStepDevice(input) {
2364
2363
  //#region src/pipeline-runner/index.ts
2365
2364
  var DEFAULT_CONFIG = {
2366
2365
  maxQueueDepth: 30,
2367
- thumbnailsEnabled: true,
2368
- thumbnailSampleMs: 5e3,
2369
2366
  maxConcurrentInferences: 16,
2370
2367
  targetLoadPercent: 80,
2371
2368
  minThrottledFps: 1,
@@ -2581,7 +2578,7 @@ function resolveSessionDecodeOwnerNodeId(config) {
2581
2578
  function resolveOwnerRestreamHost(frameSource, hubUrl) {
2582
2579
  const reachable = frameSource.ownerReachableHost?.trim();
2583
2580
  if (reachable !== void 0 && reachable.length > 0) return reachable;
2584
- return require_sheet_geometry.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
2581
+ return require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
2585
2582
  }
2586
2583
  /**
2587
2584
  * Acquire the broker's COMPRESSED passthrough restream for one camStream and
@@ -2687,12 +2684,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2687
2684
  * Initialised in {@link onInitialize} once `nodeId` is known.
2688
2685
  */
2689
2686
  retainedFrames = null;
2690
- /**
2691
- * Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
2692
- * encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
2693
- * stays a couple ms of sharp every few seconds. Null until `onInitialize`.
2694
- */
2695
- thumbSampler = null;
2696
2687
  attached = /* @__PURE__ */ new Map();
2697
2688
  nodeId = "unknown";
2698
2689
  metricsSnapshotTimer = null;
@@ -2820,7 +2811,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2820
2811
  nodeId: this.nodeId,
2821
2812
  logger: this.ctx.logger.child("retained-frames")
2822
2813
  });
2823
- this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
2824
2814
  this.runner = new PipelineRunner({
2825
2815
  maxQueueDepth: this.config.maxQueueDepth,
2826
2816
  maxConcurrentInferences: this.config.maxConcurrentInferences,
@@ -2911,8 +2901,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2911
2901
  this.nativeCropRegistry.clear();
2912
2902
  this.retainedFrames?.clear();
2913
2903
  this.retainedFrames = null;
2914
- this.thumbSampler?.clear();
2915
- this.thumbSampler = null;
2916
2904
  }
2917
2905
  async cacheBenchFrame(input) {
2918
2906
  const sharp$2 = (await import("sharp")).default;
@@ -3479,59 +3467,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3479
3467
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
3480
3468
  };
3481
3469
  }
3482
- /**
3483
- * Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
3484
- * detection frame: at most once per `thumbnailSampleMs` per camera (policy
3485
- * enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
3486
- * `recording.thumb-sampled` telemetry. The recorder (single designated node)
3487
- * packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
3488
- * is logged at debug and never disturbs the decode/detect path. Zero extra
3489
- * decode (frame exists); one sharp encode per cadence per camera (D9: a
3490
- * compressed payload at 1/5 Hz, never raw pixels at frame-rate).
3491
- */
3492
- maybeSampleThumbnail(deviceId, frame) {
3493
- if (!this.config.thumbnailsEnabled) return;
3494
- const sampler = this.thumbSampler;
3495
- if (!sampler || frame.format !== "rgb") return;
3496
- if (!sampler.shouldSample(deviceId)) return;
3497
- const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
3498
- const width = frame.width;
3499
- const height = frame.height;
3500
- const data = frame.data;
3501
- (async () => {
3502
- try {
3503
- const targetW = Math.min(480, width);
3504
- const image = (0, sharp.default)(data, { raw: {
3505
- width,
3506
- height,
3507
- channels: 3
3508
- } });
3509
- if (targetW < width) image.resize(targetW);
3510
- const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
3511
- const meta = {
3512
- width: targetW,
3513
- height: Math.round(targetW * height / width)
3514
- };
3515
- this.ctxIfReady?.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.RecordingThumbSampled, {
3516
- type: "pipeline",
3517
- id: deviceId,
3518
- nodeId: this.nodeId,
3519
- deviceId
3520
- }, {
3521
- deviceId,
3522
- capturedAt,
3523
- width: meta.width,
3524
- height: meta.height,
3525
- jpeg: new Uint8Array(jpeg)
3526
- }));
3527
- } catch (err) {
3528
- this.ctx?.logger.debug("thumbnail sample encode failed", {
3529
- tags: { deviceId },
3530
- meta: { error: err instanceof Error ? err.message : String(err) }
3531
- });
3532
- }
3533
- })();
3534
- }
3535
3470
  /** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
3536
3471
  async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey, nativeCropRef) {
3537
3472
  const api = this.ctxIfReady?.api;
@@ -3612,7 +3547,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3612
3547
  attachment.detectionUnsubscribe?.();
3613
3548
  this.lastMotionFrameAt.delete(deviceId);
3614
3549
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
3615
- this.thumbSampler?.forget(deviceId);
3616
3550
  this.packageGovernor.forget(deviceId);
3617
3551
  const warnPrefix = `${deviceId}:`;
3618
3552
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
@@ -3859,7 +3793,6 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3859
3793
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
3860
3794
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
3861
3795
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
3862
- this.maybeSampleThumbnail(config.deviceId, frame);
3863
3796
  }
3864
3797
  });
3865
3798
  }
@@ -4527,31 +4460,12 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
4527
4460
  step: 1,
4528
4461
  default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
4529
4462
  showValue: true
4530
- },
4531
- {
4532
- type: "boolean",
4533
- key: "thumbnailsEnabled",
4534
- label: "Scrub thumbnails",
4535
- 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.",
4536
- default: DEFAULT_CONFIG.thumbnailsEnabled
4537
- },
4538
- {
4539
- type: "slider",
4540
- key: "thumbnailSampleMs",
4541
- label: "Thumbnail cadence (ms)",
4542
- 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.",
4543
- min: 1e3,
4544
- max: 3e4,
4545
- step: 1e3,
4546
- default: DEFAULT_CONFIG.thumbnailSampleMs,
4547
- showValue: true
4548
4463
  }
4549
4464
  ]
4550
4465
  }] });
4551
4466
  }
4552
4467
  async onConfigChanged() {
4553
4468
  this.runner?.updateLimits(this.config);
4554
- this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
4555
4469
  this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
4556
4470
  this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
4557
4471
  maxQueueDepth: this.config.maxQueueDepth,