@camstack/addon-pipeline 1.2.213 → 1.2.215

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-B-PccVsO.mjs → default-detection-model-C2vYSX3O.mjs} +1 -1
  4. package/dist/{default-detection-model-BZTjgdod.js → default-detection-model-f0PImZnh.js} +1 -1
  5. package/dist/detection-pipeline/index.js +6 -7
  6. package/dist/detection-pipeline/index.mjs +5 -6
  7. package/dist/{dist-CyfP2nFh.js → dist-DoHcimdu.js} +42 -1
  8. package/dist/{dist-CsI90TVi.mjs → dist-DttqKvsQ.mjs} +42 -1
  9. package/dist/{lazy-sharp-BawPy_a9.js → lazy-sharp-BeeQGCUu.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-CkBqyM-0.js → node-6uF72wri.js} +1 -1
  13. package/dist/{node-6Pb_t1Fm.mjs → node-NtM7__8q.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +216 -26
  15. package/dist/pipeline-runner/index.mjs +214 -24
  16. package/dist/{process-memory-B8yoOaEl.js → process-memory-BRdyl5fE.js} +1 -1
  17. package/dist/{process-memory-B_-QzMHn.mjs → process-memory-CddpCDWL.mjs} +1 -1
  18. package/dist/recorder/index.js +14 -9
  19. package/dist/recorder/index.mjs +14 -9
  20. package/dist/{segment-demux-js-oOpXzk2J.js → segment-demux-js-CkJPdyDH.js} +1 -1
  21. package/dist/{segment-demux-js-CBvM6zIM.mjs → segment-demux-js-CsFtPVzH.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +187 -8
  23. package/dist/session-decode/decode-worker-child.mjs +186 -7
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cf0O5S13.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZIja2AOe.mjs} +3 -3
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-3eT4CIOk.mjs → hostInit-DpatMS4g.mjs} +3 -3
  28. package/dist/stream-broker/index.js +3 -3
  29. package/dist/stream-broker/index.mjs +3 -3
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/dist/{worker-protocol-DNqdtZzZ.mjs → worker-protocol-BKwkLZWT.mjs} +5 -2
  32. package/dist/{worker-protocol-CYopQrkK.js → worker-protocol-BWO2WNjw.js} +5 -2
  33. package/package.json +1 -1
  34. package/dist/event-loop-stall-monitor-DXLNBMkY.mjs +0 -138
  35. package/dist/event-loop-stall-monitor-DaUBcb13.js +0 -149
@@ -1,17 +1,17 @@
1
- import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-CsI90TVi.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-6Pb_t1Fm.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-B-PccVsO.mjs";
4
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-DNqdtZzZ.mjs";
1
+ import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-DttqKvsQ.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-NtM7__8q.mjs";
3
+ import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-C2vYSX3O.mjs";
4
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BKwkLZWT.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
7
7
  import { a as preRollRequestOf, o as withDetectionIntent, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DWe2kInY.mjs";
8
- import { t as attributeStall } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
9
8
  import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
10
9
  import * as os from "node:os";
11
10
  import { fork } from "node:child_process";
12
11
  import sharp from "sharp";
13
12
  import { fileURLToPath } from "node:url";
14
13
  import { PerformanceObserver } from "node:perf_hooks";
14
+ import { attributeStall } from "@camstack/system/addon-utils";
15
15
  //#region src/session-decode/fork-decode-worker.ts
16
16
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
17
17
  var CHILD_ENTRY_PATH = fileURLToPath(new URL("../session-decode/decode-worker-child.js", import.meta.url));
@@ -1004,6 +1004,7 @@ var SessionDecodeSession = class {
1004
1004
  frameTimeoutMs;
1005
1005
  firstFrameMaxWaitMs;
1006
1006
  onDialTiming;
1007
+ onPreRollReport;
1007
1008
  pendingPull = null;
1008
1009
  pendingToBuffers = /* @__PURE__ */ new Map();
1009
1010
  /** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
@@ -1017,7 +1018,7 @@ var SessionDecodeSession = class {
1017
1018
  firstFrameReceived = false;
1018
1019
  /** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
1019
1020
  firstPullAt = 0;
1020
- constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
1021
+ constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming, onPreRollReport) {
1021
1022
  this.child = child;
1022
1023
  this.logger = logger;
1023
1024
  this.deviceId = deviceId;
@@ -1025,6 +1026,7 @@ var SessionDecodeSession = class {
1025
1026
  this.frameTimeoutMs = frameTimeoutMs;
1026
1027
  this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
1027
1028
  this.onDialTiming = onDialTiming;
1029
+ this.onPreRollReport = onPreRollReport;
1028
1030
  this.child.on("message", (m) => this.handleMessage(m));
1029
1031
  this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1030
1032
  }
@@ -1215,6 +1217,17 @@ var SessionDecodeSession = class {
1215
1217
  streamInfoProbed: raw.streamInfoProbed
1216
1218
  });
1217
1219
  return;
1220
+ case "preRollReport":
1221
+ this.onPreRollReport?.({
1222
+ offered: raw.offered,
1223
+ inferred: raw.inferred,
1224
+ dropped: raw.dropped,
1225
+ droppedByReason: raw.droppedByReason,
1226
+ strideMs: raw.strideMs,
1227
+ subsampled: raw.subsampled,
1228
+ spanMs: raw.spanMs
1229
+ });
1230
+ return;
1218
1231
  }
1219
1232
  }
1220
1233
  handleExit(info) {
@@ -1439,7 +1452,7 @@ function createSessionDecodeCoordinator(deps) {
1439
1452
  const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
1440
1453
  const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
1441
1454
  return { async *generateVideoFrames(source, opts) {
1442
- const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
1455
+ const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
1443
1456
  session.start(source, opts);
1444
1457
  try {
1445
1458
  while (true) {
@@ -1448,7 +1461,9 @@ function createSessionDecodeCoordinator(deps) {
1448
1461
  const { reply } = outcome;
1449
1462
  yield {
1450
1463
  timestamp: reply.timestamp,
1451
- image: buildFrameImage(session, reply)
1464
+ image: buildFrameImage(session, reply),
1465
+ ...reply.capturedAt !== void 0 ? { capturedAt: reply.capturedAt } : {},
1466
+ ...reply.preRoll === true ? { preRoll: true } : {}
1452
1467
  };
1453
1468
  }
1454
1469
  } finally {
@@ -1462,10 +1477,18 @@ function createSessionDecodeCoordinator(deps) {
1462
1477
  * Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
1463
1478
  * the detection working width (see {@link resolveWorkingResize}) so the ROI
1464
1479
  * scaler ships a small frame instead of the full source resolution.
1465
- * `capturedAt` is set to `Date.now()` so downstream frameAge metrics diff
1466
- * against the wall clock (mirrors the shm-ring source, which stamps commit
1467
- * time) — this is what lets Step 5 MEASURE session-decode frameAge vs the
1468
- * shm-ring cameras.
1480
+ * `capturedAt` is the frame's OWN moment: the worker's media clock when it has
1481
+ * one (`media-clock.ts`), else `Date.now()` as before, so downstream frameAge
1482
+ * metrics keep diffing against the wall clock (mirroring the shm-ring source,
1483
+ * which stamps commit time) — this is what lets Step 5 MEASURE session-decode
1484
+ * frameAge vs the shm-ring cameras.
1485
+ *
1486
+ * The fallback is deliberate and must stay: a `Date.now()` here for a frame
1487
+ * replayed out of ~4 s of retained history would date its detections, its
1488
+ * track's `firstSeen`, its media and the notification's instant at SESSION
1489
+ * OPEN. That is a silently wrong timestamp — nothing downstream can tell it
1490
+ * from a right one — and it is a worse failure than the late birth the pre-roll
1491
+ * pass exists to fix.
1469
1492
  */
1470
1493
  async function toDecodedFrame(frame, format, viewSpec) {
1471
1494
  const geometry = viewSpec ? resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
@@ -1480,7 +1503,8 @@ async function toDecodedFrame(frame, format, viewSpec) {
1480
1503
  height: geometry?.output.height ?? frame.image.height,
1481
1504
  format: viewSpec?.format ?? format,
1482
1505
  timestamp: frame.timestamp,
1483
- capturedAt: Date.now()
1506
+ capturedAt: frame.capturedAt ?? Date.now(),
1507
+ ...frame.preRoll === true ? { preRoll: true } : {}
1484
1508
  };
1485
1509
  }
1486
1510
  /**
@@ -1501,7 +1525,8 @@ function startSessionDecodePump(deps) {
1501
1525
  dialTiming = timing;
1502
1526
  },
1503
1527
  ...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
1504
- ...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
1528
+ ...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {},
1529
+ ...deps.onPreRollReport ? { onPreRollReport: deps.onPreRollReport } : {}
1505
1530
  }).generateVideoFrames(deps.source, deps.opts);
1506
1531
  let stopped = false;
1507
1532
  let releaseSpawnSlot = null;
@@ -1543,7 +1568,7 @@ function startSessionDecodePump(deps) {
1543
1568
  }
1544
1569
  let imageOwnershipTransferred = false;
1545
1570
  try {
1546
- if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame()) continue;
1571
+ if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame(frame.preRoll === true)) continue;
1547
1572
  const image = frame.image;
1548
1573
  const cropFn = image.nativeCrop;
1549
1574
  const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
@@ -1552,7 +1577,7 @@ function startSessionDecodePump(deps) {
1552
1577
  markInferred: () => image.markInferred?.()
1553
1578
  }) : void 0;
1554
1579
  if (deps.frameRegistry && deps.onFrameRef && deps.inlineWhen?.() !== true) {
1555
- const capturedAt = Date.now();
1580
+ const capturedAt = frame.capturedAt ?? Date.now();
1556
1581
  const ref = deps.frameRegistry.register(image, {
1557
1582
  timestamp: frame.timestamp,
1558
1583
  capturedAt
@@ -1566,7 +1591,8 @@ function startSessionDecodePump(deps) {
1566
1591
  format: ref.format,
1567
1592
  timestamp: ref.timestamp,
1568
1593
  capturedAt,
1569
- frameRef: ref
1594
+ frameRef: ref,
1595
+ ...frame.preRoll === true ? { preRoll: true } : {}
1570
1596
  };
1571
1597
  try {
1572
1598
  if (!await deps.onFrameRef(lazyFrame, nativeCrop)) deps.frameRegistry.release(ref, "drop");
@@ -4696,6 +4722,44 @@ function ownFrame(frame) {
4696
4722
  };
4697
4723
  }
4698
4724
  //#endregion
4725
+ //#region src/pipeline-runner/pre-roll-lane.ts
4726
+ var PreRollLane = class {
4727
+ items = [];
4728
+ capacity;
4729
+ clone;
4730
+ dispose;
4731
+ _refused = 0;
4732
+ constructor(options) {
4733
+ this.capacity = Math.max(0, options.capacity);
4734
+ this.clone = options.clone;
4735
+ this.dispose = options.dispose;
4736
+ }
4737
+ /** Append one entry. `false` ⇒ the lane is full and the caller must log it. */
4738
+ enqueue(item) {
4739
+ if (this.items.length >= this.capacity) {
4740
+ this._refused++;
4741
+ return false;
4742
+ }
4743
+ this.items.push(this.clone(item));
4744
+ return true;
4745
+ }
4746
+ /** Oldest first — the point of the lane. */
4747
+ dequeue() {
4748
+ return this.items.shift();
4749
+ }
4750
+ get size() {
4751
+ return this.items.length;
4752
+ }
4753
+ /** Entries refused because the lane was full, for the session's report line. */
4754
+ get refused() {
4755
+ return this._refused;
4756
+ }
4757
+ clear() {
4758
+ for (const item of this.items) this.dispose?.(item);
4759
+ this.items.length = 0;
4760
+ }
4761
+ };
4762
+ //#endregion
4699
4763
  //#region src/pipeline-runner/semaphore.ts
4700
4764
  /**
4701
4765
  * Counting semaphore with FIFO waiter queue. Used by the runner to bound
@@ -5014,8 +5078,6 @@ var PipelineTimingSampler = class {
5014
5078
  this.audioSamples.clear();
5015
5079
  }
5016
5080
  };
5017
- //#endregion
5018
- //#region src/pipeline-runner/runner.ts
5019
5081
  /**
5020
5082
  * Bound on one `processFrame` call. Chosen well above the slowest legitimate
5021
5083
  * inference seen in production (8 617 ms, GPU contention on the hub) so the
@@ -5190,6 +5252,14 @@ var PipelineRunner = class {
5190
5252
  registration,
5191
5253
  motionQueue,
5192
5254
  detectionQueue,
5255
+ preRollQueue: new PreRollLane({
5256
+ capacity: this.config.maxPreRollQueueDepth ?? 12,
5257
+ clone: ownDetectionEntry,
5258
+ dispose: (entry) => {
5259
+ if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5260
+ if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5261
+ }
5262
+ }),
5193
5263
  inferenceTimes: [],
5194
5264
  shedFrames: 0,
5195
5265
  inflight: 0,
@@ -5236,6 +5306,7 @@ var PipelineRunner = class {
5236
5306
  if (state.phase === "active") this.detectionStreamHandler?.(deviceId, "unsubscribe");
5237
5307
  state.motionQueue.clear();
5238
5308
  state.detectionQueue.clear();
5309
+ state.preRollQueue.clear();
5239
5310
  this.timingSampler.clearDroppedFramesSource(deviceId);
5240
5311
  this.cameras.delete(deviceId);
5241
5312
  this.lastAdmittedAt.delete(deviceId);
@@ -5317,6 +5388,59 @@ var PipelineRunner = class {
5317
5388
  maxInflightPerCamera: cap
5318
5389
  };
5319
5390
  }
5391
+ /**
5392
+ * Queue one PRE-ROLL frame — a frame decoded out of the restreamer's retained
5393
+ * history at session open, carrying its own capture moment
5394
+ * (`session-decode/media-clock.ts`), not the moment it was decoded.
5395
+ *
5396
+ * Deliberately NOT routed through {@link reserveDetectionFrame}: that gate
5397
+ * paces on WALL CLOCK, and a catch-up burst delivers seconds of media inside
5398
+ * milliseconds of wall clock, so the governor would reject the whole burst
5399
+ * bar one frame. The burst's own bound is a MEDIA-time stride applied
5400
+ * upstream, under an explicit frame + wall-clock budget
5401
+ * (`session-decode/pre-roll-burst.ts`); by the time a frame reaches here it
5402
+ * has already been chosen.
5403
+ *
5404
+ * `false` means the lane is full or the camera is not taking detection
5405
+ * frames. The caller LOGS that: a burst silently truncated by the lane and a
5406
+ * burst that fit read identically otherwise, and this is a branch that drops
5407
+ * work.
5408
+ */
5409
+ enqueuePreRollFrame(deviceId, frame, handle) {
5410
+ const state = this.cameras.get(deviceId);
5411
+ if (!state || state.phase !== "active") return false;
5412
+ return state.preRollQueue.enqueue({
5413
+ frame,
5414
+ handle
5415
+ });
5416
+ }
5417
+ /**
5418
+ * Whether one more history frame would be taken. Asked BEFORE pixels are
5419
+ * materialized so a burst that has already filled the lane costs no RGB
5420
+ * conversion, no IPC and no retention work — the same pixel-free discipline
5421
+ * the live path gets from `reserveDetectionFrame`.
5422
+ */
5423
+ preRollLaneAccepting(deviceId) {
5424
+ const state = this.cameras.get(deviceId);
5425
+ if (!state || state.phase !== "active") return false;
5426
+ return state.preRollQueue.size < (this.config.maxPreRollQueueDepth ?? 12);
5427
+ }
5428
+ /** Frames of history waiting for a spare permit, for the session report line. */
5429
+ preRollQueueDepth(deviceId) {
5430
+ return this.cameras.get(deviceId)?.preRollQueue.size ?? 0;
5431
+ }
5432
+ /** Frames this camera lane REFUSED because it was full. */
5433
+ preRollRefusedCount(deviceId) {
5434
+ return this.cameras.get(deviceId)?.preRollQueue.refused ?? 0;
5435
+ }
5436
+ /**
5437
+ * Whether the node has a permit to spare for history right now — i.e. one
5438
+ * would still be free after the burst took one. See
5439
+ * {@link PRE_ROLL_PERMIT_RESERVE}.
5440
+ */
5441
+ preRollDispatchable() {
5442
+ return this.semaphore.available > 1;
5443
+ }
5320
5444
  enqueueReservedDetectionFrame(deviceId, frame, handle) {
5321
5445
  const state = this.cameras.get(deviceId);
5322
5446
  if (!state || state.phase !== "active") return false;
@@ -5747,7 +5871,7 @@ var PipelineRunner = class {
5747
5871
  entry: state.detectionQueue.dequeue(),
5748
5872
  state
5749
5873
  };
5750
- if (this.defaultRoundRobinKeys.length === 0) return null;
5874
+ if (this.defaultRoundRobinKeys.length === 0) return this.pickNextPreRollFrame();
5751
5875
  const startIndex = this.defaultRoundRobinIndex;
5752
5876
  for (let i = 0; i < this.defaultRoundRobinKeys.length; i++) {
5753
5877
  const idx = (startIndex + i) % this.defaultRoundRobinKeys.length;
@@ -5766,6 +5890,30 @@ var PipelineRunner = class {
5766
5890
  };
5767
5891
  }
5768
5892
  }
5893
+ return this.pickNextPreRollFrame();
5894
+ }
5895
+ /**
5896
+ * The catch-up lane, reached ONLY after every camera's live queue came back
5897
+ * empty. That ordering IS the priority rule: a live frame is always picked
5898
+ * first, so a pre-roll frame can never delay one — and the burst simply
5899
+ * stops progressing while the camera is busy, which is the correct behaviour
5900
+ * for one-shot work whose value does not decay.
5901
+ */
5902
+ pickNextPreRollFrame() {
5903
+ if (!this.preRollDispatchable()) return null;
5904
+ for (const [deviceId, state] of this.cameras) {
5905
+ if (state.phase !== "active") continue;
5906
+ if (state.preRollQueue.size === 0) continue;
5907
+ if (state.detectionQueue.size > 0) continue;
5908
+ if (state.inflight >= this.maxInflightPerCamera()) continue;
5909
+ const entry = state.preRollQueue.dequeue();
5910
+ if (!entry) continue;
5911
+ return {
5912
+ deviceId,
5913
+ entry,
5914
+ state
5915
+ };
5916
+ }
5769
5917
  return null;
5770
5918
  }
5771
5919
  };
@@ -8569,10 +8717,23 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8569
8717
  /** Attach → first decoded detection frame, one line per attach. See
8570
8718
  * {@link FirstDetectionFrameTimer}. */
8571
8719
  firstDetectionFrame = new FirstDetectionFrameTimer();
8720
+ /**
8721
+ * Frames this camera's pre-roll catch-up burst may send to inference.
8722
+ *
8723
+ * The switch and the budget are ONE number here on purpose: `false` and
8724
+ * `0` mean the same thing to every layer below (no burst is constructed, the
8725
+ * history is decoded to reach a GOP and then discarded exactly as it was
8726
+ * before D411), and two independent ways to say it is two ways to disagree.
8727
+ */
8728
+ preRollBudgetFor(config) {
8729
+ if (!config.preRollInferenceEnabled) return 0;
8730
+ const frames = config.preRollInferenceFrames;
8731
+ return Number.isFinite(frames) && frames > 0 ? frames : 0;
8732
+ }
8572
8733
  enqueueSharedDetectionFrame(deviceId, frame, handle, reservation) {
8573
8734
  const runner = this.runner;
8574
8735
  if (!runner) return false;
8575
- const admitted = reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
8736
+ const admitted = frame.preRoll === true ? runner.enqueuePreRollFrame(deviceId, frame, handle) : reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
8576
8737
  if (admitted) {
8577
8738
  const latencyMs = this.firstDetectionFrame.takeLatency(deviceId, Date.now());
8578
8739
  if (latencyMs !== null) this.ctx?.logger.info("session-decode: first detection frame after attach", {
@@ -8690,9 +8851,19 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8690
8851
  source,
8691
8852
  opts: {
8692
8853
  fps: config.detectionFps,
8693
- format: "rgb"
8854
+ format: "rgb",
8855
+ preRollMs: preRollRequestOf(source.restreamUrl),
8856
+ preRollMaxFrames: this.preRollBudgetFor(config)
8694
8857
  },
8695
- shouldMaterializeFrame: () => {
8858
+ shouldMaterializeFrame: (preRoll) => {
8859
+ if (preRoll) {
8860
+ const accepting = runner.preRollLaneAccepting(config.deviceId);
8861
+ reservation = {
8862
+ detection: accepting,
8863
+ motion: false
8864
+ };
8865
+ return accepting;
8866
+ }
8696
8867
  reservation = this.reserveSharedDetectionFrame(config.deviceId);
8697
8868
  const materialize = reservation.detection || reservation.motion;
8698
8869
  this.retention?.noteFrame({
@@ -8726,7 +8897,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8726
8897
  materialized: false,
8727
8898
  workerToRunnerBytes: 0
8728
8899
  });
8729
- const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
8900
+ const admitted = current.detection && (frame.preRoll === true ? runner.enqueuePreRollFrame(config.deviceId, frame, handle) : runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle));
8730
8901
  if (admitted) nativeCrop?.markInferred();
8731
8902
  if (current.motion) {
8732
8903
  const motion = await resolveMotionFrameView(localFrameRegistry, frame.frameRef);
@@ -8743,6 +8914,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8743
8914
  }
8744
8915
  } : { inlineViewSpec: SPLIT_DETECTION_VIEW },
8745
8916
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
8917
+ onPreRollReport: (report) => {
8918
+ log.info("session-decode: pre-roll catch-up burst", {
8919
+ tags: { deviceId: config.deviceId },
8920
+ meta: {
8921
+ streamId: config.detectionStreamId,
8922
+ requestedMs: preRollRequestOf(source.restreamUrl),
8923
+ spanMs: report.spanMs,
8924
+ offered: report.offered,
8925
+ inferred: report.inferred,
8926
+ dropped: report.dropped,
8927
+ droppedByReason: report.droppedByReason,
8928
+ strideMs: report.strideMs,
8929
+ subsampled: report.subsampled,
8930
+ laneRefused: runner.preRollRefusedCount(config.deviceId),
8931
+ budgetFrames: this.preRollBudgetFor(config)
8932
+ }
8933
+ });
8934
+ },
8746
8935
  onFirstFrame: (timing) => {
8747
8936
  const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
8748
8937
  log.info("session-decode: detection first frame", {
@@ -9691,6 +9880,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9691
9880
  frame: result,
9692
9881
  nodeId: this.nodeId,
9693
9882
  frameHandle: handle,
9883
+ emittedAt: Date.now(),
9694
9884
  ...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
9695
9885
  ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
9696
9886
  };
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-CyfP2nFh.js");
1
+ const require_dist = require("./dist-DoHcimdu.js");
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_dist.__toESM(node_fs);
4
4
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { mt as parseProcStatus } from "./dist-CsI90TVi.mjs";
1
+ import { mt as parseProcStatus } from "./dist-DttqKvsQ.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -2,11 +2,10 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CyfP2nFh.js");
6
- const require_node = require("../node-CkBqyM-0.js");
5
+ const require_dist = require("../dist-DoHcimdu.js");
6
+ const require_node = require("../node-6uF72wri.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  const require_restream_intent = require("../restream-intent-B1Difcnq.js");
9
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
10
9
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
10
  let node_crypto = require("node:crypto");
12
11
  let node_fs = require("node:fs");
@@ -10468,6 +10467,8 @@ Object.freeze({
10468
10467
  "pipelineEnabled": true,
10469
10468
  "zones": [],
10470
10469
  "onboardMotionDrivesAnalyzer": true,
10470
+ "preRollInferenceEnabled": true,
10471
+ "preRollInferenceFrames": 12,
10471
10472
  "occupancyRecheckEnabled": true,
10472
10473
  "occupancyRecheckSec": 300,
10473
10474
  "occupancyRecheckFrames": 4,
@@ -16981,11 +16982,15 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16981
16982
  /**
16982
16983
  * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
16983
16984
  *
16984
- * REUSED, not re-implemented, and not promoted: `startEventLoopStallMonitor`
16985
- * already ships inside `@camstack/addon-pipeline` — the same bundle as this
16986
- * addon — and moving it into `@camstack/system` would duplicate a mechanism
16987
- * that already exists (forbidden) or relocate it across a package boundary,
16988
- * which is a refactor with a slower ship chain, not a measurement.
16985
+ * REUSED, never re-implemented. It used to live next door in this bundle;
16986
+ * on 2026-09-09 it moved to `@camstack/system/addon-utils` (the subpath, never
16987
+ * the barrel — that one drags the kernel into the runner) because two more
16988
+ * processes that do NOT share this bundle needed it — `pipeline-analytics` and
16989
+ * hub-main — and a per-bundle copy is how two diagnostics end up disagreeing
16990
+ * about the same freeze. The cost of the move is a slower ship chain: this
16991
+ * import is host-resolved, so a `camstack deploy` of this addon onto a hub
16992
+ * whose server closure predates the export fails to load. Deploy order is
16993
+ * closure first, addons second. See D417.
16989
16994
  */
16990
16995
  stopStallMonitor = null;
16991
16996
  /**
@@ -17186,7 +17191,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17186
17191
  } });
17187
17192
  }
17188
17193
  async onInitialize() {
17189
- this.stopStallMonitor = require_event_loop_stall_monitor.startEventLoopStallMonitor(this.ctx.logger);
17194
+ this.stopStallMonitor = (0, _camstack_system_addon_utils.startEventLoopStallMonitor)(this.ctx.logger);
17190
17195
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
17191
17196
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
17192
17197
  const configuredNode = this.config.recordingNodeId;
@@ -1,15 +1,14 @@
1
- import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-CsI90TVi.mjs";
2
- import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-6Pb_t1Fm.mjs";
1
+ import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-DttqKvsQ.mjs";
2
+ import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-NtM7__8q.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, r as RECORDING_PRE_ROLL_REQUEST_MS, s as withRecordingIntent } from "../restream-intent-DWe2kInY.mjs";
5
- import { n as startEventLoopStallMonitor } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
6
5
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
6
  import { randomUUID } from "node:crypto";
8
7
  import { createReadStream, promises, realpathSync } from "node:fs";
9
8
  import path from "node:path";
10
9
  import { spawn } from "node:child_process";
11
10
  import { open } from "node:fs/promises";
12
- import { contentTypeFor, createFileDataPlaneHandler, parseRangeHeader } from "@camstack/system/addon-utils";
11
+ import { contentTypeFor, createFileDataPlaneHandler, parseRangeHeader, startEventLoopStallMonitor } from "@camstack/system/addon-utils";
13
12
  //#region src/recorder/event-capture.ts
14
13
  /**
15
14
  * La sottoscrizione motion del recorder: bus → `onTrigger(deviceId, atMs)`.
@@ -10462,6 +10461,8 @@ Object.freeze({
10462
10461
  "pipelineEnabled": true,
10463
10462
  "zones": [],
10464
10463
  "onboardMotionDrivesAnalyzer": true,
10464
+ "preRollInferenceEnabled": true,
10465
+ "preRollInferenceFrames": 12,
10465
10466
  "occupancyRecheckEnabled": true,
10466
10467
  "occupancyRecheckSec": 300,
10467
10468
  "occupancyRecheckFrames": 4,
@@ -16975,11 +16976,15 @@ var RecorderV2Addon = class extends BaseAddon {
16975
16976
  /**
16976
16977
  * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
16977
16978
  *
16978
- * REUSED, not re-implemented, and not promoted: `startEventLoopStallMonitor`
16979
- * already ships inside `@camstack/addon-pipeline` — the same bundle as this
16980
- * addon — and moving it into `@camstack/system` would duplicate a mechanism
16981
- * that already exists (forbidden) or relocate it across a package boundary,
16982
- * which is a refactor with a slower ship chain, not a measurement.
16979
+ * REUSED, never re-implemented. It used to live next door in this bundle;
16980
+ * on 2026-09-09 it moved to `@camstack/system/addon-utils` (the subpath, never
16981
+ * the barrel — that one drags the kernel into the runner) because two more
16982
+ * processes that do NOT share this bundle needed it — `pipeline-analytics` and
16983
+ * hub-main — and a per-bundle copy is how two diagnostics end up disagreeing
16984
+ * about the same freeze. The cost of the move is a slower ship chain: this
16985
+ * import is host-resolved, so a `camstack deploy` of this addon onto a hub
16986
+ * whose server closure predates the export fails to load. Deploy order is
16987
+ * closure first, addons second. See D417.
16983
16988
  */
16984
16989
  stopStallMonitor = null;
16985
16990
  /**
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-CyfP2nFh.js");
1
+ const require_dist = require("./dist-DoHcimdu.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
4
4
  var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { It as errMsg } from "./dist-CsI90TVi.mjs";
2
+ import { It as errMsg } from "./dist-DttqKvsQ.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {