@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
@@ -2,14 +2,13 @@ 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");
7
- const require_default_detection_model = require("../default-detection-model-BZTjgdod.js");
8
- const require_worker_protocol = require("../worker-protocol-CYopQrkK.js");
5
+ const require_dist = require("../dist-DoHcimdu.js");
6
+ const require_node = require("../node-6uF72wri.js");
7
+ const require_default_detection_model = require("../default-detection-model-f0PImZnh.js");
8
+ const require_worker_protocol = require("../worker-protocol-BWO2WNjw.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-BawPy_a9.js");
10
+ const require_lazy_sharp = require("../lazy-sharp-BeeQGCUu.js");
11
11
  const require_restream_intent = require("../restream-intent-B1Difcnq.js");
12
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
13
12
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
14
13
  let node_os = require("node:os");
15
14
  node_os = require_dist.__toESM(node_os);
@@ -18,6 +17,7 @@ let sharp = require("sharp");
18
17
  sharp = require_dist.__toESM(sharp);
19
18
  let node_url = require("node:url");
20
19
  let node_perf_hooks = require("node:perf_hooks");
20
+ let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
21
21
  //#region src/session-decode/fork-decode-worker.ts
22
22
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
23
23
  var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
@@ -1010,6 +1010,7 @@ var SessionDecodeSession = class {
1010
1010
  frameTimeoutMs;
1011
1011
  firstFrameMaxWaitMs;
1012
1012
  onDialTiming;
1013
+ onPreRollReport;
1013
1014
  pendingPull = null;
1014
1015
  pendingToBuffers = /* @__PURE__ */ new Map();
1015
1016
  /** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
@@ -1023,7 +1024,7 @@ var SessionDecodeSession = class {
1023
1024
  firstFrameReceived = false;
1024
1025
  /** `Date.now()` of the FIRST `pull` — the origin of the cold-start ceiling. */
1025
1026
  firstPullAt = 0;
1026
- constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming) {
1027
+ constructor(child, logger, deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, onDialTiming, onPreRollReport) {
1027
1028
  this.child = child;
1028
1029
  this.logger = logger;
1029
1030
  this.deviceId = deviceId;
@@ -1031,6 +1032,7 @@ var SessionDecodeSession = class {
1031
1032
  this.frameTimeoutMs = frameTimeoutMs;
1032
1033
  this.firstFrameMaxWaitMs = firstFrameMaxWaitMs;
1033
1034
  this.onDialTiming = onDialTiming;
1035
+ this.onPreRollReport = onPreRollReport;
1034
1036
  this.child.on("message", (m) => this.handleMessage(m));
1035
1037
  this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1036
1038
  }
@@ -1221,6 +1223,17 @@ var SessionDecodeSession = class {
1221
1223
  streamInfoProbed: raw.streamInfoProbed
1222
1224
  });
1223
1225
  return;
1226
+ case "preRollReport":
1227
+ this.onPreRollReport?.({
1228
+ offered: raw.offered,
1229
+ inferred: raw.inferred,
1230
+ dropped: raw.dropped,
1231
+ droppedByReason: raw.droppedByReason,
1232
+ strideMs: raw.strideMs,
1233
+ subsampled: raw.subsampled,
1234
+ spanMs: raw.spanMs
1235
+ });
1236
+ return;
1224
1237
  }
1225
1238
  }
1226
1239
  handleExit(info) {
@@ -1445,7 +1458,7 @@ function createSessionDecodeCoordinator(deps) {
1445
1458
  const frameTimeoutMs = deps.frameTimeoutMs ?? DEFAULT_FRAME_TIMEOUT_MS;
1446
1459
  const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
1447
1460
  return { async *generateVideoFrames(source, opts) {
1448
- const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming);
1461
+ const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
1449
1462
  session.start(source, opts);
1450
1463
  try {
1451
1464
  while (true) {
@@ -1454,7 +1467,9 @@ function createSessionDecodeCoordinator(deps) {
1454
1467
  const { reply } = outcome;
1455
1468
  yield {
1456
1469
  timestamp: reply.timestamp,
1457
- image: buildFrameImage(session, reply)
1470
+ image: buildFrameImage(session, reply),
1471
+ ...reply.capturedAt !== void 0 ? { capturedAt: reply.capturedAt } : {},
1472
+ ...reply.preRoll === true ? { preRoll: true } : {}
1458
1473
  };
1459
1474
  }
1460
1475
  } finally {
@@ -1468,10 +1483,18 @@ function createSessionDecodeCoordinator(deps) {
1468
1483
  * Build a {@link DecodedFrame} from one pulled {@link VideoFrame}. Downscales to
1469
1484
  * the detection working width (see {@link resolveWorkingResize}) so the ROI
1470
1485
  * scaler ships a small frame instead of the full source resolution.
1471
- * `capturedAt` is set to `Date.now()` so downstream frameAge metrics diff
1472
- * against the wall clock (mirrors the shm-ring source, which stamps commit
1473
- * time) — this is what lets Step 5 MEASURE session-decode frameAge vs the
1474
- * shm-ring cameras.
1486
+ * `capturedAt` is the frame's OWN moment: the worker's media clock when it has
1487
+ * one (`media-clock.ts`), else `Date.now()` as before, so downstream frameAge
1488
+ * metrics keep diffing against the wall clock (mirroring the shm-ring source,
1489
+ * which stamps commit time) — this is what lets Step 5 MEASURE session-decode
1490
+ * frameAge vs the shm-ring cameras.
1491
+ *
1492
+ * The fallback is deliberate and must stay: a `Date.now()` here for a frame
1493
+ * replayed out of ~4 s of retained history would date its detections, its
1494
+ * track's `firstSeen`, its media and the notification's instant at SESSION
1495
+ * OPEN. That is a silently wrong timestamp — nothing downstream can tell it
1496
+ * from a right one — and it is a worse failure than the late birth the pre-roll
1497
+ * pass exists to fix.
1475
1498
  */
1476
1499
  async function toDecodedFrame(frame, format, viewSpec) {
1477
1500
  const geometry = viewSpec ? require_default_detection_model.resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
@@ -1486,7 +1509,8 @@ async function toDecodedFrame(frame, format, viewSpec) {
1486
1509
  height: geometry?.output.height ?? frame.image.height,
1487
1510
  format: viewSpec?.format ?? format,
1488
1511
  timestamp: frame.timestamp,
1489
- capturedAt: Date.now()
1512
+ capturedAt: frame.capturedAt ?? Date.now(),
1513
+ ...frame.preRoll === true ? { preRoll: true } : {}
1490
1514
  };
1491
1515
  }
1492
1516
  /**
@@ -1507,7 +1531,8 @@ function startSessionDecodePump(deps) {
1507
1531
  dialTiming = timing;
1508
1532
  },
1509
1533
  ...deps.firstFrameTimeoutMs !== void 0 ? { firstFrameTimeoutMs: deps.firstFrameTimeoutMs } : {},
1510
- ...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {}
1534
+ ...deps.frameTimeoutMs !== void 0 ? { frameTimeoutMs: deps.frameTimeoutMs } : {},
1535
+ ...deps.onPreRollReport ? { onPreRollReport: deps.onPreRollReport } : {}
1511
1536
  }).generateVideoFrames(deps.source, deps.opts);
1512
1537
  let stopped = false;
1513
1538
  let releaseSpawnSlot = null;
@@ -1549,7 +1574,7 @@ function startSessionDecodePump(deps) {
1549
1574
  }
1550
1575
  let imageOwnershipTransferred = false;
1551
1576
  try {
1552
- if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame()) continue;
1577
+ if (deps.shouldMaterializeFrame && !deps.shouldMaterializeFrame(frame.preRoll === true)) continue;
1553
1578
  const image = frame.image;
1554
1579
  const cropFn = image.nativeCrop;
1555
1580
  const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
@@ -1558,7 +1583,7 @@ function startSessionDecodePump(deps) {
1558
1583
  markInferred: () => image.markInferred?.()
1559
1584
  }) : void 0;
1560
1585
  if (deps.frameRegistry && deps.onFrameRef && deps.inlineWhen?.() !== true) {
1561
- const capturedAt = Date.now();
1586
+ const capturedAt = frame.capturedAt ?? Date.now();
1562
1587
  const ref = deps.frameRegistry.register(image, {
1563
1588
  timestamp: frame.timestamp,
1564
1589
  capturedAt
@@ -1572,7 +1597,8 @@ function startSessionDecodePump(deps) {
1572
1597
  format: ref.format,
1573
1598
  timestamp: ref.timestamp,
1574
1599
  capturedAt,
1575
- frameRef: ref
1600
+ frameRef: ref,
1601
+ ...frame.preRoll === true ? { preRoll: true } : {}
1576
1602
  };
1577
1603
  try {
1578
1604
  if (!await deps.onFrameRef(lazyFrame, nativeCrop)) deps.frameRegistry.release(ref, "drop");
@@ -2425,7 +2451,7 @@ var EMPTY_SAMPLE = {
2425
2451
  */
2426
2452
  function applyStallTick(sample, lateByMs, gcWindow, reportThresholdMs) {
2427
2453
  if (lateByMs < reportThresholdMs) return sample;
2428
- const verdict = require_event_loop_stall_monitor.attributeStall(lateByMs, Math.round(gcWindow.totalMs), gcWindow.count, Math.round(gcWindow.longestMs));
2454
+ const verdict = (0, _camstack_system_addon_utils.attributeStall)(lateByMs, Math.round(gcWindow.totalMs), gcWindow.count, Math.round(gcWindow.longestMs));
2429
2455
  return {
2430
2456
  count: sample.count + 1,
2431
2457
  maxMs: Math.max(sample.maxMs, lateByMs),
@@ -4703,6 +4729,44 @@ function ownFrame(frame) {
4703
4729
  };
4704
4730
  }
4705
4731
  //#endregion
4732
+ //#region src/pipeline-runner/pre-roll-lane.ts
4733
+ var PreRollLane = class {
4734
+ items = [];
4735
+ capacity;
4736
+ clone;
4737
+ dispose;
4738
+ _refused = 0;
4739
+ constructor(options) {
4740
+ this.capacity = Math.max(0, options.capacity);
4741
+ this.clone = options.clone;
4742
+ this.dispose = options.dispose;
4743
+ }
4744
+ /** Append one entry. `false` ⇒ the lane is full and the caller must log it. */
4745
+ enqueue(item) {
4746
+ if (this.items.length >= this.capacity) {
4747
+ this._refused++;
4748
+ return false;
4749
+ }
4750
+ this.items.push(this.clone(item));
4751
+ return true;
4752
+ }
4753
+ /** Oldest first — the point of the lane. */
4754
+ dequeue() {
4755
+ return this.items.shift();
4756
+ }
4757
+ get size() {
4758
+ return this.items.length;
4759
+ }
4760
+ /** Entries refused because the lane was full, for the session's report line. */
4761
+ get refused() {
4762
+ return this._refused;
4763
+ }
4764
+ clear() {
4765
+ for (const item of this.items) this.dispose?.(item);
4766
+ this.items.length = 0;
4767
+ }
4768
+ };
4769
+ //#endregion
4706
4770
  //#region src/pipeline-runner/semaphore.ts
4707
4771
  /**
4708
4772
  * Counting semaphore with FIFO waiter queue. Used by the runner to bound
@@ -5021,8 +5085,6 @@ var PipelineTimingSampler = class {
5021
5085
  this.audioSamples.clear();
5022
5086
  }
5023
5087
  };
5024
- //#endregion
5025
- //#region src/pipeline-runner/runner.ts
5026
5088
  /**
5027
5089
  * Bound on one `processFrame` call. Chosen well above the slowest legitimate
5028
5090
  * inference seen in production (8 617 ms, GPU contention on the hub) so the
@@ -5197,6 +5259,14 @@ var PipelineRunner = class {
5197
5259
  registration,
5198
5260
  motionQueue,
5199
5261
  detectionQueue,
5262
+ preRollQueue: new PreRollLane({
5263
+ capacity: this.config.maxPreRollQueueDepth ?? 12,
5264
+ clone: ownDetectionEntry,
5265
+ dispose: (entry) => {
5266
+ if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5267
+ if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5268
+ }
5269
+ }),
5200
5270
  inferenceTimes: [],
5201
5271
  shedFrames: 0,
5202
5272
  inflight: 0,
@@ -5243,6 +5313,7 @@ var PipelineRunner = class {
5243
5313
  if (state.phase === "active") this.detectionStreamHandler?.(deviceId, "unsubscribe");
5244
5314
  state.motionQueue.clear();
5245
5315
  state.detectionQueue.clear();
5316
+ state.preRollQueue.clear();
5246
5317
  this.timingSampler.clearDroppedFramesSource(deviceId);
5247
5318
  this.cameras.delete(deviceId);
5248
5319
  this.lastAdmittedAt.delete(deviceId);
@@ -5324,6 +5395,59 @@ var PipelineRunner = class {
5324
5395
  maxInflightPerCamera: cap
5325
5396
  };
5326
5397
  }
5398
+ /**
5399
+ * Queue one PRE-ROLL frame — a frame decoded out of the restreamer's retained
5400
+ * history at session open, carrying its own capture moment
5401
+ * (`session-decode/media-clock.ts`), not the moment it was decoded.
5402
+ *
5403
+ * Deliberately NOT routed through {@link reserveDetectionFrame}: that gate
5404
+ * paces on WALL CLOCK, and a catch-up burst delivers seconds of media inside
5405
+ * milliseconds of wall clock, so the governor would reject the whole burst
5406
+ * bar one frame. The burst's own bound is a MEDIA-time stride applied
5407
+ * upstream, under an explicit frame + wall-clock budget
5408
+ * (`session-decode/pre-roll-burst.ts`); by the time a frame reaches here it
5409
+ * has already been chosen.
5410
+ *
5411
+ * `false` means the lane is full or the camera is not taking detection
5412
+ * frames. The caller LOGS that: a burst silently truncated by the lane and a
5413
+ * burst that fit read identically otherwise, and this is a branch that drops
5414
+ * work.
5415
+ */
5416
+ enqueuePreRollFrame(deviceId, frame, handle) {
5417
+ const state = this.cameras.get(deviceId);
5418
+ if (!state || state.phase !== "active") return false;
5419
+ return state.preRollQueue.enqueue({
5420
+ frame,
5421
+ handle
5422
+ });
5423
+ }
5424
+ /**
5425
+ * Whether one more history frame would be taken. Asked BEFORE pixels are
5426
+ * materialized so a burst that has already filled the lane costs no RGB
5427
+ * conversion, no IPC and no retention work — the same pixel-free discipline
5428
+ * the live path gets from `reserveDetectionFrame`.
5429
+ */
5430
+ preRollLaneAccepting(deviceId) {
5431
+ const state = this.cameras.get(deviceId);
5432
+ if (!state || state.phase !== "active") return false;
5433
+ return state.preRollQueue.size < (this.config.maxPreRollQueueDepth ?? 12);
5434
+ }
5435
+ /** Frames of history waiting for a spare permit, for the session report line. */
5436
+ preRollQueueDepth(deviceId) {
5437
+ return this.cameras.get(deviceId)?.preRollQueue.size ?? 0;
5438
+ }
5439
+ /** Frames this camera lane REFUSED because it was full. */
5440
+ preRollRefusedCount(deviceId) {
5441
+ return this.cameras.get(deviceId)?.preRollQueue.refused ?? 0;
5442
+ }
5443
+ /**
5444
+ * Whether the node has a permit to spare for history right now — i.e. one
5445
+ * would still be free after the burst took one. See
5446
+ * {@link PRE_ROLL_PERMIT_RESERVE}.
5447
+ */
5448
+ preRollDispatchable() {
5449
+ return this.semaphore.available > 1;
5450
+ }
5327
5451
  enqueueReservedDetectionFrame(deviceId, frame, handle) {
5328
5452
  const state = this.cameras.get(deviceId);
5329
5453
  if (!state || state.phase !== "active") return false;
@@ -5754,7 +5878,7 @@ var PipelineRunner = class {
5754
5878
  entry: state.detectionQueue.dequeue(),
5755
5879
  state
5756
5880
  };
5757
- if (this.defaultRoundRobinKeys.length === 0) return null;
5881
+ if (this.defaultRoundRobinKeys.length === 0) return this.pickNextPreRollFrame();
5758
5882
  const startIndex = this.defaultRoundRobinIndex;
5759
5883
  for (let i = 0; i < this.defaultRoundRobinKeys.length; i++) {
5760
5884
  const idx = (startIndex + i) % this.defaultRoundRobinKeys.length;
@@ -5773,6 +5897,30 @@ var PipelineRunner = class {
5773
5897
  };
5774
5898
  }
5775
5899
  }
5900
+ return this.pickNextPreRollFrame();
5901
+ }
5902
+ /**
5903
+ * The catch-up lane, reached ONLY after every camera's live queue came back
5904
+ * empty. That ordering IS the priority rule: a live frame is always picked
5905
+ * first, so a pre-roll frame can never delay one — and the burst simply
5906
+ * stops progressing while the camera is busy, which is the correct behaviour
5907
+ * for one-shot work whose value does not decay.
5908
+ */
5909
+ pickNextPreRollFrame() {
5910
+ if (!this.preRollDispatchable()) return null;
5911
+ for (const [deviceId, state] of this.cameras) {
5912
+ if (state.phase !== "active") continue;
5913
+ if (state.preRollQueue.size === 0) continue;
5914
+ if (state.detectionQueue.size > 0) continue;
5915
+ if (state.inflight >= this.maxInflightPerCamera()) continue;
5916
+ const entry = state.preRollQueue.dequeue();
5917
+ if (!entry) continue;
5918
+ return {
5919
+ deviceId,
5920
+ entry,
5921
+ state
5922
+ };
5923
+ }
5776
5924
  return null;
5777
5925
  }
5778
5926
  };
@@ -8576,10 +8724,23 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8576
8724
  /** Attach → first decoded detection frame, one line per attach. See
8577
8725
  * {@link FirstDetectionFrameTimer}. */
8578
8726
  firstDetectionFrame = new FirstDetectionFrameTimer();
8727
+ /**
8728
+ * Frames this camera's pre-roll catch-up burst may send to inference.
8729
+ *
8730
+ * The switch and the budget are ONE number here on purpose: `false` and
8731
+ * `0` mean the same thing to every layer below (no burst is constructed, the
8732
+ * history is decoded to reach a GOP and then discarded exactly as it was
8733
+ * before D411), and two independent ways to say it is two ways to disagree.
8734
+ */
8735
+ preRollBudgetFor(config) {
8736
+ if (!config.preRollInferenceEnabled) return 0;
8737
+ const frames = config.preRollInferenceFrames;
8738
+ return Number.isFinite(frames) && frames > 0 ? frames : 0;
8739
+ }
8579
8740
  enqueueSharedDetectionFrame(deviceId, frame, handle, reservation) {
8580
8741
  const runner = this.runner;
8581
8742
  if (!runner) return false;
8582
- const admitted = reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
8743
+ const admitted = frame.preRoll === true ? runner.enqueuePreRollFrame(deviceId, frame, handle) : reservation ? reservation.detection && runner.enqueueReservedDetectionFrame(deviceId, frame, handle) : runner.enqueueDetectionFrame(deviceId, frame, handle);
8583
8744
  if (admitted) {
8584
8745
  const latencyMs = this.firstDetectionFrame.takeLatency(deviceId, Date.now());
8585
8746
  if (latencyMs !== null) this.ctx?.logger.info("session-decode: first detection frame after attach", {
@@ -8697,9 +8858,19 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8697
8858
  source,
8698
8859
  opts: {
8699
8860
  fps: config.detectionFps,
8700
- format: "rgb"
8861
+ format: "rgb",
8862
+ preRollMs: require_restream_intent.preRollRequestOf(source.restreamUrl),
8863
+ preRollMaxFrames: this.preRollBudgetFor(config)
8701
8864
  },
8702
- shouldMaterializeFrame: () => {
8865
+ shouldMaterializeFrame: (preRoll) => {
8866
+ if (preRoll) {
8867
+ const accepting = runner.preRollLaneAccepting(config.deviceId);
8868
+ reservation = {
8869
+ detection: accepting,
8870
+ motion: false
8871
+ };
8872
+ return accepting;
8873
+ }
8703
8874
  reservation = this.reserveSharedDetectionFrame(config.deviceId);
8704
8875
  const materialize = reservation.detection || reservation.motion;
8705
8876
  this.retention?.noteFrame({
@@ -8733,7 +8904,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8733
8904
  materialized: false,
8734
8905
  workerToRunnerBytes: 0
8735
8906
  });
8736
- const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
8907
+ const admitted = current.detection && (frame.preRoll === true ? runner.enqueuePreRollFrame(config.deviceId, frame, handle) : runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle));
8737
8908
  if (admitted) nativeCrop?.markInferred();
8738
8909
  if (current.motion) {
8739
8910
  const motion = await resolveMotionFrameView(require_default_detection_model.localFrameRegistry, frame.frameRef);
@@ -8750,6 +8921,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8750
8921
  }
8751
8922
  } : { inlineViewSpec: SPLIT_DETECTION_VIEW },
8752
8923
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
8924
+ onPreRollReport: (report) => {
8925
+ log.info("session-decode: pre-roll catch-up burst", {
8926
+ tags: { deviceId: config.deviceId },
8927
+ meta: {
8928
+ streamId: config.detectionStreamId,
8929
+ requestedMs: require_restream_intent.preRollRequestOf(source.restreamUrl),
8930
+ spanMs: report.spanMs,
8931
+ offered: report.offered,
8932
+ inferred: report.inferred,
8933
+ dropped: report.dropped,
8934
+ droppedByReason: report.droppedByReason,
8935
+ strideMs: report.strideMs,
8936
+ subsampled: report.subsampled,
8937
+ laneRefused: runner.preRollRefusedCount(config.deviceId),
8938
+ budgetFrames: this.preRollBudgetFor(config)
8939
+ }
8940
+ });
8941
+ },
8753
8942
  onFirstFrame: (timing) => {
8754
8943
  const phaseActiveAt = this.lastMotionAt.get(config.deviceId);
8755
8944
  log.info("session-decode: detection first frame", {
@@ -9698,6 +9887,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9698
9887
  frame: result,
9699
9888
  nodeId: this.nodeId,
9700
9889
  frameHandle: handle,
9890
+ emittedAt: Date.now(),
9701
9891
  ...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
9702
9892
  ...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
9703
9893
  };