@camstack/addon-pipeline 1.2.7 → 1.2.9

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-Bg8aaZzc.mjs} +3 -3
  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-BBLfhYBl.mjs} +3 -3
  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
@@ -1,8 +1,8 @@
1
- import { H as errMsg, N as pipelineRunnerCapability, S as defineCustomActions, U as BaseAddon, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT } from "../dist-BwyGTnMu.mjs";
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 { r as resolveHubHostname } from "../sheet-geometry-DcpuGTCm.mjs";
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-DN1obXS7.mjs";
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,7 +1273,7 @@ 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) : 96;
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
@@ -1293,6 +1293,24 @@ var RETAINED_FRAME_PER_DEVICE_FLOOR = (() => {
1293
1293
  const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR"]);
1294
1294
  return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 16;
1295
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;
1313
+ })();
1296
1314
  /** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
1297
1315
  var RETAINED_FRAME_KEY_PREFIX = "ram:";
1298
1316
  /** A retention frame is uniquely identified by its key + slot + committed seq. */
@@ -1303,6 +1321,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1303
1321
  nodeId;
1304
1322
  budgetBytes;
1305
1323
  perDeviceFloor;
1324
+ timeFloorMs;
1325
+ now;
1306
1326
  /** Insertion-ordered (Map) → globally oldest is `keys().next()`. */
1307
1327
  entries = /* @__PURE__ */ new Map();
1308
1328
  /**
@@ -1319,6 +1339,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1319
1339
  this.nodeId = options.nodeId;
1320
1340
  this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
1321
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;
1322
1344
  }
1323
1345
  /** Number of retained frames (tests / metrics). */
1324
1346
  get size() {
@@ -1354,7 +1376,8 @@ var RetainedFrameStore = class RetainedFrameStore {
1354
1376
  const key = keyOf(handle);
1355
1377
  this.entries.set(key, {
1356
1378
  frame,
1357
- deviceId
1379
+ deviceId,
1380
+ writtenAt: this.now()
1358
1381
  });
1359
1382
  const deviceKeys = this.perDeviceKeys.get(deviceId);
1360
1383
  if (deviceKeys) deviceKeys.push(key);
@@ -1399,6 +1422,16 @@ var RetainedFrameStore = class RetainedFrameStore {
1399
1422
  evictToBudget() {
1400
1423
  if (this.bytes <= this.budgetBytes) return;
1401
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;
1402
1435
  for (const key of oldestFirst) {
1403
1436
  if (this.bytes <= this.budgetBytes || this.entries.size <= 1) break;
1404
1437
  const entry = this.entries.get(key);
@@ -1466,40 +1499,6 @@ function cropRgb(frame, bbox, maxWidth) {
1466
1499
  height: outH
1467
1500
  };
1468
1501
  }
1469
- var ThumbSamplerPolicy = class {
1470
- intervalMs;
1471
- minGapMs;
1472
- now;
1473
- /** Last accepted-sample wall clock per device. */
1474
- lastAcceptedAt = /* @__PURE__ */ new Map();
1475
- constructor(options) {
1476
- this.intervalMs = Math.max(0, options.intervalMs);
1477
- this.minGapMs = Math.max(0, options.minGapMs ?? 1e3);
1478
- this.now = options.now ?? (() => Date.now());
1479
- }
1480
- /**
1481
- * Decide whether to sample `deviceId` now. Returns true (and records the
1482
- * acceptance) when the larger of `intervalMs` / `minGapMs` has elapsed since
1483
- * the last accepted sample for this device; false otherwise. The FIRST call
1484
- * for a device always samples (cold start).
1485
- */
1486
- shouldSample(deviceId) {
1487
- const now = this.now();
1488
- const gate = Math.max(this.intervalMs, this.minGapMs);
1489
- const last = this.lastAcceptedAt.get(deviceId);
1490
- if (last !== void 0 && now - last < gate) return false;
1491
- this.lastAcceptedAt.set(deviceId, now);
1492
- return true;
1493
- }
1494
- /** Forget a device (detach / phase exit) so its next frame cold-starts. */
1495
- forget(deviceId) {
1496
- this.lastAcceptedAt.delete(deviceId);
1497
- }
1498
- /** Drop all per-device state (shutdown). */
1499
- clear() {
1500
- this.lastAcceptedAt.clear();
1501
- }
1502
- };
1503
1502
  //#endregion
1504
1503
  //#region src/session-decode/session-decode-coordinator.ts
1505
1504
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
@@ -2358,8 +2357,6 @@ function resolveStepDevice(input) {
2358
2357
  //#region src/pipeline-runner/index.ts
2359
2358
  var DEFAULT_CONFIG = {
2360
2359
  maxQueueDepth: 30,
2361
- thumbnailsEnabled: true,
2362
- thumbnailSampleMs: 5e3,
2363
2360
  maxConcurrentInferences: 16,
2364
2361
  targetLoadPercent: 80,
2365
2362
  minThrottledFps: 1,
@@ -2681,12 +2678,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
2681
2678
  * Initialised in {@link onInitialize} once `nodeId` is known.
2682
2679
  */
2683
2680
  retainedFrames = null;
2684
- /**
2685
- * Scrub-thumbnail sampler policy (2026-07-16). Gates the per-camera JPEG
2686
- * encode to ~1 / `thumbnailSampleMs` (with a hard 1 Hz ceiling) so the cost
2687
- * stays a couple ms of sharp every few seconds. Null until `onInitialize`.
2688
- */
2689
- thumbSampler = null;
2690
2681
  attached = /* @__PURE__ */ new Map();
2691
2682
  nodeId = "unknown";
2692
2683
  metricsSnapshotTimer = null;
@@ -2814,7 +2805,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
2814
2805
  nodeId: this.nodeId,
2815
2806
  logger: this.ctx.logger.child("retained-frames")
2816
2807
  });
2817
- this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
2818
2808
  this.runner = new PipelineRunner({
2819
2809
  maxQueueDepth: this.config.maxQueueDepth,
2820
2810
  maxConcurrentInferences: this.config.maxConcurrentInferences,
@@ -2905,8 +2895,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
2905
2895
  this.nativeCropRegistry.clear();
2906
2896
  this.retainedFrames?.clear();
2907
2897
  this.retainedFrames = null;
2908
- this.thumbSampler?.clear();
2909
- this.thumbSampler = null;
2910
2898
  }
2911
2899
  async cacheBenchFrame(input) {
2912
2900
  const sharp = (await import("sharp")).default;
@@ -3473,59 +3461,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
3473
3461
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
3474
3462
  };
3475
3463
  }
3476
- /**
3477
- * Scrub-thumbnail sampler (2026-07-16). Rides the already-decoded rgb
3478
- * detection frame: at most once per `thumbnailSampleMs` per camera (policy
3479
- * enforces a 1 Hz hard ceiling), downscale to a ~320px JPEG and emit it as
3480
- * `recording.thumb-sampled` telemetry. The recorder (single designated node)
3481
- * packs 5-minute sprite sheets from these. Fire-and-forget — a failed encode
3482
- * is logged at debug and never disturbs the decode/detect path. Zero extra
3483
- * decode (frame exists); one sharp encode per cadence per camera (D9: a
3484
- * compressed payload at 1/5 Hz, never raw pixels at frame-rate).
3485
- */
3486
- maybeSampleThumbnail(deviceId, frame) {
3487
- if (!this.config.thumbnailsEnabled) return;
3488
- const sampler = this.thumbSampler;
3489
- if (!sampler || frame.format !== "rgb") return;
3490
- if (!sampler.shouldSample(deviceId)) return;
3491
- const capturedAt = typeof frame.capturedAt === "number" && frame.capturedAt > 0 ? frame.capturedAt : Date.now();
3492
- const width = frame.width;
3493
- const height = frame.height;
3494
- const data = frame.data;
3495
- (async () => {
3496
- try {
3497
- const targetW = Math.min(480, width);
3498
- const image = sharp(data, { raw: {
3499
- width,
3500
- height,
3501
- channels: 3
3502
- } });
3503
- if (targetW < width) image.resize(targetW);
3504
- const jpeg = await image.jpeg({ quality: 60 }).toBuffer();
3505
- const meta = {
3506
- width: targetW,
3507
- height: Math.round(targetW * height / width)
3508
- };
3509
- this.ctxIfReady?.eventBus.emit(createEvent(EventCategory.RecordingThumbSampled, {
3510
- type: "pipeline",
3511
- id: deviceId,
3512
- nodeId: this.nodeId,
3513
- deviceId
3514
- }, {
3515
- deviceId,
3516
- capturedAt,
3517
- width: meta.width,
3518
- height: meta.height,
3519
- jpeg: new Uint8Array(jpeg)
3520
- }));
3521
- } catch (err) {
3522
- this.ctx?.logger.debug("thumbnail sample encode failed", {
3523
- tags: { deviceId },
3524
- meta: { error: err instanceof Error ? err.message : String(err) }
3525
- });
3526
- }
3527
- })();
3528
- }
3529
3464
  /** Real `runPipeline` dep: node-local `pipelineExecutor.runPipeline` on the `'full'` plane. */
3530
3465
  async runDetailPipeline(steps, imageJpeg, deviceId, stepDeviceKey, nativeCropRef) {
3531
3466
  const api = this.ctxIfReady?.api;
@@ -3606,7 +3541,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
3606
3541
  attachment.detectionUnsubscribe?.();
3607
3542
  this.lastMotionFrameAt.delete(deviceId);
3608
3543
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
3609
- this.thumbSampler?.forget(deviceId);
3610
3544
  this.packageGovernor.forget(deviceId);
3611
3545
  const warnPrefix = `${deviceId}:`;
3612
3546
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
@@ -3853,7 +3787,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
3853
3787
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
3854
3788
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
3855
3789
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
3856
- this.maybeSampleThumbnail(config.deviceId, frame);
3857
3790
  }
3858
3791
  });
3859
3792
  }
@@ -4521,31 +4454,12 @@ var PipelineRunnerAddon = class extends BaseAddon {
4521
4454
  step: 1,
4522
4455
  default: DEFAULT_CONFIG.maxConcurrentDecodeSpawns,
4523
4456
  showValue: true
4524
- },
4525
- {
4526
- type: "boolean",
4527
- key: "thumbnailsEnabled",
4528
- label: "Scrub thumbnails",
4529
- 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.",
4530
- default: DEFAULT_CONFIG.thumbnailsEnabled
4531
- },
4532
- {
4533
- type: "slider",
4534
- key: "thumbnailSampleMs",
4535
- label: "Thumbnail cadence (ms)",
4536
- 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.",
4537
- min: 1e3,
4538
- max: 3e4,
4539
- step: 1e3,
4540
- default: DEFAULT_CONFIG.thumbnailSampleMs,
4541
- showValue: true
4542
4457
  }
4543
4458
  ]
4544
4459
  }] });
4545
4460
  }
4546
4461
  async onConfigChanged() {
4547
4462
  this.runner?.updateLimits(this.config);
4548
- this.thumbSampler = new ThumbSamplerPolicy({ intervalMs: this.config.thumbnailSampleMs });
4549
4463
  this.spawnGate?.resize(Math.max(1, this.config.maxConcurrentDecodeSpawns));
4550
4464
  this.ctx.logger.info("pipeline-runner tuning updated", { meta: {
4551
4465
  maxQueueDepth: this.config.maxQueueDepth,