@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,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-CYopQrkK.js");
3
- const require_lazy_sharp = require("../lazy-sharp-BawPy_a9.js");
2
+ const require_worker_protocol = require("../worker-protocol-BWO2WNjw.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-BeeQGCUu.js");
4
4
  //#region src/session-decode/color-conversion.ts
5
5
  /**
6
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -208,7 +208,9 @@ function toReply(held) {
208
208
  frameId: held.frameId,
209
209
  timestamp: held.timestamp,
210
210
  width: held.width,
211
- height: held.height
211
+ height: held.height,
212
+ capturedAt: held.capturedAt,
213
+ preRoll: held.preRoll
212
214
  };
213
215
  }
214
216
  /**
@@ -252,7 +254,7 @@ var FrameSlot = class {
252
254
  * immediately and reserving the frame); otherwise returns `null` and the
253
255
  * frame simply waits in the slot for the next `pull`.
254
256
  */
255
- publish(frame, timestamp, minIntervalMs) {
257
+ publish(frame, timestamp, minIntervalMs, meta) {
256
258
  if (minIntervalMs > 0 && this.lastPublishedAt !== null && timestamp - this.lastPublishedAt < minIntervalMs) {
257
259
  this.throttledCount++;
258
260
  frame.free();
@@ -266,7 +268,9 @@ var FrameSlot = class {
266
268
  frame,
267
269
  timestamp,
268
270
  width: frame.width,
269
- height: frame.height
271
+ height: frame.height,
272
+ capturedAt: meta?.capturedAt ?? timestamp,
273
+ preRoll: meta?.preRoll ?? false
270
274
  };
271
275
  if (previous) this.supersededCount++;
272
276
  previous?.frame.free();
@@ -319,6 +323,44 @@ var FrameSlot = class {
319
323
  }
320
324
  };
321
325
  //#endregion
326
+ //#region src/session-decode/media-clock.ts
327
+ /**
328
+ * Convert a container pts to milliseconds.
329
+ *
330
+ * Returns `null` — never a fabricated `0` — for a time base that cannot
331
+ * describe a duration. A zero here would place every frame of the session at
332
+ * the anchor, i.e. a whole burst of detections sharing one instant, which is
333
+ * exactly the kind of plausible-looking wrong timestamp this module exists to
334
+ * refuse. The caller degrades to the wall clock instead.
335
+ */
336
+ function ptsToMs(pts, timeBase) {
337
+ if (!Number.isFinite(timeBase.num) || !Number.isFinite(timeBase.den)) return null;
338
+ if (timeBase.num <= 0 || timeBase.den <= 0) return null;
339
+ return Number(pts) * timeBase.num * 1e3 / timeBase.den;
340
+ }
341
+ /** Anchor the session's media clock on its first decoded frame. */
342
+ function anchorMediaClock(input) {
343
+ const preRoll = Number.isFinite(input.preRollMs) && input.preRollMs > 0 ? input.preRollMs : 0;
344
+ return {
345
+ firstPtsMs: input.firstPtsMs,
346
+ wallAtFirstPts: input.nowWall - preRoll
347
+ };
348
+ }
349
+ /**
350
+ * The wall clock at which the frame with `ptsMs` was shot.
351
+ *
352
+ * Clamped to `nowWall`: a container whose pts runs past the anchor (a clock
353
+ * jump, a source whose pts is not wall-clock-shaped, a re-dial that reset it)
354
+ * must never produce a track that starts in the FUTURE. A future `firstSeen`
355
+ * sorts a track ahead of everything, and no later pass can detect that it is
356
+ * wrong. Clamping loses precision on a pathological source and keeps the
357
+ * ordering property every consumer relies on.
358
+ */
359
+ function mediaWallClockOf(anchor, ptsMs, nowWall) {
360
+ const shotAt = anchor.wallAtFirstPts + (ptsMs - anchor.firstPtsMs);
361
+ return shotAt > nowWall ? nowWall : shotAt;
362
+ }
363
+ //#endregion
322
364
  //#region src/session-decode/lease-activity-gate.ts
323
365
  /**
324
366
  * Demand-driven gate for the native-frame lease capture (see
@@ -2156,6 +2198,23 @@ var DecodeWorkerChild = class {
2156
2198
  sessionFormat = "rgb";
2157
2199
  minIntervalMs = 0;
2158
2200
  /**
2201
+ * Session media clock, anchored on the FIRST decoded frame. Null until that
2202
+ * frame arrives, or forever on a source whose pts cannot be read — in which
2203
+ * case every frame falls back to the wall clock, exactly as before.
2204
+ */
2205
+ mediaClock = null;
2206
+ /** History this dial asked the broker for; `0` ⇒ there is no burst. */
2207
+ preRollMs = 0;
2208
+ /** Frames the catch-up burst may send to inference. */
2209
+ preRollMaxFrames = 0;
2210
+ /** The live catch-up burst, or null once it has ended (or never started). */
2211
+ preRollBurst = null;
2212
+ /** Whether the burst's report has already been sent — it is sent exactly once. */
2213
+ preRollReported = false;
2214
+ /** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
2215
+ preRollFirstMediaWall = null;
2216
+ preRollLastMediaWall = null;
2217
+ /**
2159
2218
  * The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
2160
2219
  * pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
2161
2220
  * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
@@ -2217,6 +2276,7 @@ var DecodeWorkerChild = class {
2217
2276
  this.metricsTimer = null;
2218
2277
  }
2219
2278
  if (this.started) this.logMetrics(true);
2279
+ this.endPreRollBurst();
2220
2280
  this.abortController?.abort();
2221
2281
  this.frames.free();
2222
2282
  this.closeInput();
@@ -2276,6 +2336,8 @@ var DecodeWorkerChild = class {
2276
2336
  this.emitStderr(`decode-worker-child: ${require_worker_protocol.formatNativeLeaseKnobs(NATIVE_LEASE_KNOBS)}\n`, "info");
2277
2337
  this.sessionFormat = opts.format ?? "rgb";
2278
2338
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
2339
+ this.preRollMs = opts.preRollMs && opts.preRollMs > 0 ? opts.preRollMs : 0;
2340
+ this.preRollMaxFrames = opts.preRollMaxFrames && opts.preRollMaxFrames > 0 ? opts.preRollMaxFrames : 0;
2279
2341
  this.lastMetricsAt = Date.now();
2280
2342
  this.metricsTimer = setInterval(() => this.logMetrics(false), METRICS_INTERVAL_MS);
2281
2343
  this.metricsTimer.unref?.();
@@ -3108,14 +3170,129 @@ var DecodeWorkerChild = class {
3108
3170
  }
3109
3171
  this.publishFrame(frame);
3110
3172
  }
3111
- /** Publish to the latest-wins slot; deliver immediately if a `pull` is pending. */
3173
+ /**
3174
+ * Publish to the latest-wins slot; deliver immediately if a `pull` is pending.
3175
+ *
3176
+ * ## Where the pre-roll used to die
3177
+ *
3178
+ * This method used to be one line: `this.frames.publish(frame, Date.now(),
3179
+ * this.minIntervalMs)`. `FrameSlot.publish` throttles by comparing that
3180
+ * `Date.now()` against the previous publish's, and during a catch-up burst
3181
+ * that comparison is meaningless — the restreamer replays ~4 s of retained
3182
+ * media and the decoder chews through all of it inside a few hundred
3183
+ * milliseconds of WALL clock. So the throttle admitted one or two frames of
3184
+ * the whole window and `frame.free()`d the rest, and the two frames on which
3185
+ * device 617's cyclist scored above the vehicle floor (0.5669 and 0.5366,
3186
+ * 1.68 s before the live track was born) were freed here, having been
3187
+ * received, decoded and paid for in full.
3188
+ *
3189
+ * The fix is not to weaken the throttle. A live frame still paces on wall
3190
+ * clock, unchanged. A HISTORY frame paces on its own MEDIA clock instead,
3191
+ * under an explicit budget, because media time is the only clock on which
3192
+ * "one frame per 333 ms of footage" means what it says.
3193
+ */
3112
3194
  publishFrame(frame) {
3113
- const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
3195
+ const now = Date.now();
3196
+ const mediaWall = this.mediaWallClockOfFrame(frame, now);
3197
+ const verdict = this.offerToPreRollBurst(mediaWall, now);
3198
+ if (verdict === "dropped") {
3199
+ this.framesSkipped++;
3200
+ frame.free();
3201
+ return;
3202
+ }
3203
+ const preRoll = verdict === "pre-roll";
3204
+ const reply = this.frames.publish(frame, preRoll ? mediaWall : now, preRoll ? 0 : this.minIntervalMs, {
3205
+ capturedAt: mediaWall,
3206
+ preRoll
3207
+ });
3114
3208
  if (reply) {
3115
3209
  this.pendingPull = false;
3116
3210
  this.deliverFrame(reply);
3117
3211
  }
3118
3212
  }
3213
+ /**
3214
+ * The frame's own moment, from its pts — or the wall clock when the source
3215
+ * cannot say.
3216
+ *
3217
+ * The media clock is anchored on the FIRST frame of the session: that frame
3218
+ * is `preRollMs` old by construction, because the pre-roll is what the dial
3219
+ * asked for and the restreamer serves it before anything live. A source whose
3220
+ * pts cannot be converted (no time base, a zero denominator) gets no clock at
3221
+ * all and therefore no burst — a fabricated media time would date a whole
3222
+ * burst of detections at one wrong instant, which is worse than not inferring
3223
+ * them at all.
3224
+ */
3225
+ mediaWallClockOfFrame(frame, now) {
3226
+ const ptsMs = this.frameMediaPtsMs(frame);
3227
+ if (ptsMs === null) return now;
3228
+ if (!this.mediaClock) {
3229
+ const servedPreRollMs = 0;
3230
+ if (this.preRollMs > 0) this.emitStderr(`[decode] pre-roll requested ${this.preRollMs}ms but the SERVED span does not reach this worker — burst suppressed, clock anchored at now (D411 follow-up)`);
3231
+ this.mediaClock = anchorMediaClock({
3232
+ firstPtsMs: ptsMs,
3233
+ preRollMs: servedPreRollMs,
3234
+ nowWall: now
3235
+ });
3236
+ }
3237
+ return mediaWallClockOf(this.mediaClock, ptsMs, now);
3238
+ }
3239
+ /** This frame's pts in ms, or `null` when the source cannot express one. */
3240
+ frameMediaPtsMs(frame) {
3241
+ const pts = frame.pts;
3242
+ const timeBase = frame.timeBase;
3243
+ if (typeof pts !== "bigint") return null;
3244
+ if (typeof timeBase !== "object" || timeBase === null) return null;
3245
+ const num = Reflect.get(timeBase, "num");
3246
+ const den = Reflect.get(timeBase, "den");
3247
+ if (typeof num !== "number" || typeof den !== "number") return null;
3248
+ return ptsToMs(pts, {
3249
+ num,
3250
+ den
3251
+ });
3252
+ }
3253
+ /**
3254
+ * Ask the burst whether this frame is history worth inferring.
3255
+ *
3256
+ * `'live'` — no burst, or the burst has ended. The ordinary path, untouched.
3257
+ * `'pre-roll'` — inside the budget; delivered marked, to the runner's lane.
3258
+ * `'dropped'` — history the budget refused. Freed by the caller and counted.
3259
+ */
3260
+ offerToPreRollBurst(mediaWall, now) {
3261
+ const burst = this.preRollBurst;
3262
+ if (!burst) return "live";
3263
+ const verdict = burst.offer(mediaWall, now);
3264
+ if (verdict.admitted) {
3265
+ if (this.preRollFirstMediaWall === null) this.preRollFirstMediaWall = mediaWall;
3266
+ this.preRollLastMediaWall = mediaWall;
3267
+ return "pre-roll";
3268
+ }
3269
+ if (burst.finished) {
3270
+ this.endPreRollBurst();
3271
+ return verdict.reason === "live-edge" ? "live" : "dropped";
3272
+ }
3273
+ return "dropped";
3274
+ }
3275
+ /** Close the burst and send its accounting — exactly once per session. */
3276
+ endPreRollBurst() {
3277
+ const burst = this.preRollBurst;
3278
+ this.preRollBurst = null;
3279
+ if (!burst || this.preRollReported) return;
3280
+ this.preRollReported = true;
3281
+ burst.finish();
3282
+ const report = burst.report();
3283
+ const first = this.preRollFirstMediaWall;
3284
+ const last = this.preRollLastMediaWall;
3285
+ this.send({
3286
+ kind: "preRollReport",
3287
+ offered: report.offered,
3288
+ inferred: report.inferred,
3289
+ dropped: report.dropped,
3290
+ droppedByReason: report.droppedByReason,
3291
+ strideMs: report.strideMs,
3292
+ subsampled: report.subsampled,
3293
+ spanMs: first !== null && last !== null ? Math.round(last - first) : 0
3294
+ });
3295
+ }
3119
3296
  /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
3120
3297
  deliverFrame(reply) {
3121
3298
  this.framesDelivered++;
@@ -3172,7 +3349,9 @@ var DecodeWorkerChild = class {
3172
3349
  timestamp: reply.timestamp,
3173
3350
  width: reply.width,
3174
3351
  height: reply.height,
3175
- format: this.sessionFormat
3352
+ format: this.sessionFormat,
3353
+ capturedAt: reply.capturedAt,
3354
+ ...reply.preRoll ? { preRoll: true } : {}
3176
3355
  };
3177
3356
  }
3178
3357
  /**
@@ -1,4 +1,4 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-DNqdtZzZ.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-BKwkLZWT.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -207,7 +207,9 @@ function toReply(held) {
207
207
  frameId: held.frameId,
208
208
  timestamp: held.timestamp,
209
209
  width: held.width,
210
- height: held.height
210
+ height: held.height,
211
+ capturedAt: held.capturedAt,
212
+ preRoll: held.preRoll
211
213
  };
212
214
  }
213
215
  /**
@@ -251,7 +253,7 @@ var FrameSlot = class {
251
253
  * immediately and reserving the frame); otherwise returns `null` and the
252
254
  * frame simply waits in the slot for the next `pull`.
253
255
  */
254
- publish(frame, timestamp, minIntervalMs) {
256
+ publish(frame, timestamp, minIntervalMs, meta) {
255
257
  if (minIntervalMs > 0 && this.lastPublishedAt !== null && timestamp - this.lastPublishedAt < minIntervalMs) {
256
258
  this.throttledCount++;
257
259
  frame.free();
@@ -265,7 +267,9 @@ var FrameSlot = class {
265
267
  frame,
266
268
  timestamp,
267
269
  width: frame.width,
268
- height: frame.height
270
+ height: frame.height,
271
+ capturedAt: meta?.capturedAt ?? timestamp,
272
+ preRoll: meta?.preRoll ?? false
269
273
  };
270
274
  if (previous) this.supersededCount++;
271
275
  previous?.frame.free();
@@ -318,6 +322,44 @@ var FrameSlot = class {
318
322
  }
319
323
  };
320
324
  //#endregion
325
+ //#region src/session-decode/media-clock.ts
326
+ /**
327
+ * Convert a container pts to milliseconds.
328
+ *
329
+ * Returns `null` — never a fabricated `0` — for a time base that cannot
330
+ * describe a duration. A zero here would place every frame of the session at
331
+ * the anchor, i.e. a whole burst of detections sharing one instant, which is
332
+ * exactly the kind of plausible-looking wrong timestamp this module exists to
333
+ * refuse. The caller degrades to the wall clock instead.
334
+ */
335
+ function ptsToMs(pts, timeBase) {
336
+ if (!Number.isFinite(timeBase.num) || !Number.isFinite(timeBase.den)) return null;
337
+ if (timeBase.num <= 0 || timeBase.den <= 0) return null;
338
+ return Number(pts) * timeBase.num * 1e3 / timeBase.den;
339
+ }
340
+ /** Anchor the session's media clock on its first decoded frame. */
341
+ function anchorMediaClock(input) {
342
+ const preRoll = Number.isFinite(input.preRollMs) && input.preRollMs > 0 ? input.preRollMs : 0;
343
+ return {
344
+ firstPtsMs: input.firstPtsMs,
345
+ wallAtFirstPts: input.nowWall - preRoll
346
+ };
347
+ }
348
+ /**
349
+ * The wall clock at which the frame with `ptsMs` was shot.
350
+ *
351
+ * Clamped to `nowWall`: a container whose pts runs past the anchor (a clock
352
+ * jump, a source whose pts is not wall-clock-shaped, a re-dial that reset it)
353
+ * must never produce a track that starts in the FUTURE. A future `firstSeen`
354
+ * sorts a track ahead of everything, and no later pass can detect that it is
355
+ * wrong. Clamping loses precision on a pathological source and keeps the
356
+ * ordering property every consumer relies on.
357
+ */
358
+ function mediaWallClockOf(anchor, ptsMs, nowWall) {
359
+ const shotAt = anchor.wallAtFirstPts + (ptsMs - anchor.firstPtsMs);
360
+ return shotAt > nowWall ? nowWall : shotAt;
361
+ }
362
+ //#endregion
321
363
  //#region src/session-decode/lease-activity-gate.ts
322
364
  /**
323
365
  * Demand-driven gate for the native-frame lease capture (see
@@ -2155,6 +2197,23 @@ var DecodeWorkerChild = class {
2155
2197
  sessionFormat = "rgb";
2156
2198
  minIntervalMs = 0;
2157
2199
  /**
2200
+ * Session media clock, anchored on the FIRST decoded frame. Null until that
2201
+ * frame arrives, or forever on a source whose pts cannot be read — in which
2202
+ * case every frame falls back to the wall clock, exactly as before.
2203
+ */
2204
+ mediaClock = null;
2205
+ /** History this dial asked the broker for; `0` ⇒ there is no burst. */
2206
+ preRollMs = 0;
2207
+ /** Frames the catch-up burst may send to inference. */
2208
+ preRollMaxFrames = 0;
2209
+ /** The live catch-up burst, or null once it has ended (or never started). */
2210
+ preRollBurst = null;
2211
+ /** Whether the burst's report has already been sent — it is sent exactly once. */
2212
+ preRollReported = false;
2213
+ /** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
2214
+ preRollFirstMediaWall = null;
2215
+ preRollLastMediaWall = null;
2216
+ /**
2158
2217
  * The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
2159
2218
  * pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
2160
2219
  * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
@@ -2216,6 +2275,7 @@ var DecodeWorkerChild = class {
2216
2275
  this.metricsTimer = null;
2217
2276
  }
2218
2277
  if (this.started) this.logMetrics(true);
2278
+ this.endPreRollBurst();
2219
2279
  this.abortController?.abort();
2220
2280
  this.frames.free();
2221
2281
  this.closeInput();
@@ -2275,6 +2335,8 @@ var DecodeWorkerChild = class {
2275
2335
  this.emitStderr(`decode-worker-child: ${formatNativeLeaseKnobs(NATIVE_LEASE_KNOBS)}\n`, "info");
2276
2336
  this.sessionFormat = opts.format ?? "rgb";
2277
2337
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
2338
+ this.preRollMs = opts.preRollMs && opts.preRollMs > 0 ? opts.preRollMs : 0;
2339
+ this.preRollMaxFrames = opts.preRollMaxFrames && opts.preRollMaxFrames > 0 ? opts.preRollMaxFrames : 0;
2278
2340
  this.lastMetricsAt = Date.now();
2279
2341
  this.metricsTimer = setInterval(() => this.logMetrics(false), METRICS_INTERVAL_MS);
2280
2342
  this.metricsTimer.unref?.();
@@ -3107,14 +3169,129 @@ var DecodeWorkerChild = class {
3107
3169
  }
3108
3170
  this.publishFrame(frame);
3109
3171
  }
3110
- /** Publish to the latest-wins slot; deliver immediately if a `pull` is pending. */
3172
+ /**
3173
+ * Publish to the latest-wins slot; deliver immediately if a `pull` is pending.
3174
+ *
3175
+ * ## Where the pre-roll used to die
3176
+ *
3177
+ * This method used to be one line: `this.frames.publish(frame, Date.now(),
3178
+ * this.minIntervalMs)`. `FrameSlot.publish` throttles by comparing that
3179
+ * `Date.now()` against the previous publish's, and during a catch-up burst
3180
+ * that comparison is meaningless — the restreamer replays ~4 s of retained
3181
+ * media and the decoder chews through all of it inside a few hundred
3182
+ * milliseconds of WALL clock. So the throttle admitted one or two frames of
3183
+ * the whole window and `frame.free()`d the rest, and the two frames on which
3184
+ * device 617's cyclist scored above the vehicle floor (0.5669 and 0.5366,
3185
+ * 1.68 s before the live track was born) were freed here, having been
3186
+ * received, decoded and paid for in full.
3187
+ *
3188
+ * The fix is not to weaken the throttle. A live frame still paces on wall
3189
+ * clock, unchanged. A HISTORY frame paces on its own MEDIA clock instead,
3190
+ * under an explicit budget, because media time is the only clock on which
3191
+ * "one frame per 333 ms of footage" means what it says.
3192
+ */
3111
3193
  publishFrame(frame) {
3112
- const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
3194
+ const now = Date.now();
3195
+ const mediaWall = this.mediaWallClockOfFrame(frame, now);
3196
+ const verdict = this.offerToPreRollBurst(mediaWall, now);
3197
+ if (verdict === "dropped") {
3198
+ this.framesSkipped++;
3199
+ frame.free();
3200
+ return;
3201
+ }
3202
+ const preRoll = verdict === "pre-roll";
3203
+ const reply = this.frames.publish(frame, preRoll ? mediaWall : now, preRoll ? 0 : this.minIntervalMs, {
3204
+ capturedAt: mediaWall,
3205
+ preRoll
3206
+ });
3113
3207
  if (reply) {
3114
3208
  this.pendingPull = false;
3115
3209
  this.deliverFrame(reply);
3116
3210
  }
3117
3211
  }
3212
+ /**
3213
+ * The frame's own moment, from its pts — or the wall clock when the source
3214
+ * cannot say.
3215
+ *
3216
+ * The media clock is anchored on the FIRST frame of the session: that frame
3217
+ * is `preRollMs` old by construction, because the pre-roll is what the dial
3218
+ * asked for and the restreamer serves it before anything live. A source whose
3219
+ * pts cannot be converted (no time base, a zero denominator) gets no clock at
3220
+ * all and therefore no burst — a fabricated media time would date a whole
3221
+ * burst of detections at one wrong instant, which is worse than not inferring
3222
+ * them at all.
3223
+ */
3224
+ mediaWallClockOfFrame(frame, now) {
3225
+ const ptsMs = this.frameMediaPtsMs(frame);
3226
+ if (ptsMs === null) return now;
3227
+ if (!this.mediaClock) {
3228
+ const servedPreRollMs = 0;
3229
+ if (this.preRollMs > 0) this.emitStderr(`[decode] pre-roll requested ${this.preRollMs}ms but the SERVED span does not reach this worker — burst suppressed, clock anchored at now (D411 follow-up)`);
3230
+ this.mediaClock = anchorMediaClock({
3231
+ firstPtsMs: ptsMs,
3232
+ preRollMs: servedPreRollMs,
3233
+ nowWall: now
3234
+ });
3235
+ }
3236
+ return mediaWallClockOf(this.mediaClock, ptsMs, now);
3237
+ }
3238
+ /** This frame's pts in ms, or `null` when the source cannot express one. */
3239
+ frameMediaPtsMs(frame) {
3240
+ const pts = frame.pts;
3241
+ const timeBase = frame.timeBase;
3242
+ if (typeof pts !== "bigint") return null;
3243
+ if (typeof timeBase !== "object" || timeBase === null) return null;
3244
+ const num = Reflect.get(timeBase, "num");
3245
+ const den = Reflect.get(timeBase, "den");
3246
+ if (typeof num !== "number" || typeof den !== "number") return null;
3247
+ return ptsToMs(pts, {
3248
+ num,
3249
+ den
3250
+ });
3251
+ }
3252
+ /**
3253
+ * Ask the burst whether this frame is history worth inferring.
3254
+ *
3255
+ * `'live'` — no burst, or the burst has ended. The ordinary path, untouched.
3256
+ * `'pre-roll'` — inside the budget; delivered marked, to the runner's lane.
3257
+ * `'dropped'` — history the budget refused. Freed by the caller and counted.
3258
+ */
3259
+ offerToPreRollBurst(mediaWall, now) {
3260
+ const burst = this.preRollBurst;
3261
+ if (!burst) return "live";
3262
+ const verdict = burst.offer(mediaWall, now);
3263
+ if (verdict.admitted) {
3264
+ if (this.preRollFirstMediaWall === null) this.preRollFirstMediaWall = mediaWall;
3265
+ this.preRollLastMediaWall = mediaWall;
3266
+ return "pre-roll";
3267
+ }
3268
+ if (burst.finished) {
3269
+ this.endPreRollBurst();
3270
+ return verdict.reason === "live-edge" ? "live" : "dropped";
3271
+ }
3272
+ return "dropped";
3273
+ }
3274
+ /** Close the burst and send its accounting — exactly once per session. */
3275
+ endPreRollBurst() {
3276
+ const burst = this.preRollBurst;
3277
+ this.preRollBurst = null;
3278
+ if (!burst || this.preRollReported) return;
3279
+ this.preRollReported = true;
3280
+ burst.finish();
3281
+ const report = burst.report();
3282
+ const first = this.preRollFirstMediaWall;
3283
+ const last = this.preRollLastMediaWall;
3284
+ this.send({
3285
+ kind: "preRollReport",
3286
+ offered: report.offered,
3287
+ inferred: report.inferred,
3288
+ dropped: report.dropped,
3289
+ droppedByReason: report.droppedByReason,
3290
+ strideMs: report.strideMs,
3291
+ subsampled: report.subsampled,
3292
+ spanMs: first !== null && last !== null ? Math.round(last - first) : 0
3293
+ });
3294
+ }
3118
3295
  /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
3119
3296
  deliverFrame(reply) {
3120
3297
  this.framesDelivered++;
@@ -3171,7 +3348,9 @@ var DecodeWorkerChild = class {
3171
3348
  timestamp: reply.timestamp,
3172
3349
  width: reply.width,
3173
3350
  height: reply.height,
3174
- format: this.sessionFormat
3351
+ format: this.sessionFormat,
3352
+ capturedAt: reply.capturedAt,
3353
+ ...reply.preRoll ? { preRoll: true } : {}
3175
3354
  };
3176
3355
  }
3177
3356
  /**
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.91",
6
+ version: "1.2.93",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.171",
21
+ version: "1.2.173",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.139",
36
+ version: "1.2.141",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",
@@ -1,4 +1,4 @@
1
- const require_segment_demux_js = require("../segment-demux-js-oOpXzk2J.js");
1
+ const require_segment_demux_js = require("../segment-demux-js-CkJPdyDH.js");
2
2
  let node_worker_threads = require("node:worker_threads");
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as demuxSegmentFullJs } from "../segment-demux-js-CBvM6zIM.mjs";
1
+ import { t as demuxSegmentFullJs } from "../segment-demux-js-CsFtPVzH.mjs";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.171",
39
+ version: "1.2.173",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.91",
48
+ version: "1.2.93",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.139",
84
+ version: "1.2.141",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,12 +2,12 @@ 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_restream_intent = require("../restream-intent-B1Difcnq.js");
8
8
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
9
9
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
10
- const require_segment_demux_js = require("../segment-demux-js-oOpXzk2J.js");
10
+ const require_segment_demux_js = require("../segment-demux-js-CkJPdyDH.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_dist.__toESM(node_os);
13
13
  let node_crypto = require("node:crypto");
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { A as addonWidgetsSourceCapability, Bt as invocationFromEncodeProfile, C as RATE_CONTROL_RELAXED, F as cameraStreamsCapability, Ft as webrtcSessionCapability, Gt as DeviceFeature, H as deriveBatteryPresence, Ht as BOOT_RECOVERY_BACKOFF_MS, I as createHwAccelCache, It as errMsg, K as egressTranscodeSharingKey, Kt as DeviceType, L as createLogChannelsProvider, Lt as AUDIO_PRESETS, Mt as streamBrokerCapability, O as RingBuffer, P as batteryCapability, Qt as nodePin, Tt as resolveEgressDecodeHwAccel, Ut as BaseAddon, Vt as isSoftwareDecode, Wt as CAM_PROFILE_ORDER, Xt as makeProfileBrokerId, Yt as isEvent, Zt as makeSourceBrokerId, an as boolean, at as logChannelsCapability, cn as literal, en as parseProfileBrokerId, fn as record, h as EncodeProfileSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, ln as number, mn as union, on as discriminatedUnion, pn as string, q as egressTransportFromRequest, qt as createEvent, rn as _enum, st as maskUrlCredentials, un as object, w as RATE_CONTROL_TIGHT, z as declareLogChannel, zt as buildFfmpegArgs$1 } from "../dist-CsI90TVi.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-6Pb_t1Fm.mjs";
2
+ import { A as addonWidgetsSourceCapability, Bt as invocationFromEncodeProfile, C as RATE_CONTROL_RELAXED, F as cameraStreamsCapability, Ft as webrtcSessionCapability, Gt as DeviceFeature, H as deriveBatteryPresence, Ht as BOOT_RECOVERY_BACKOFF_MS, I as createHwAccelCache, It as errMsg, K as egressTranscodeSharingKey, Kt as DeviceType, L as createLogChannelsProvider, Lt as AUDIO_PRESETS, Mt as streamBrokerCapability, O as RingBuffer, P as batteryCapability, Qt as nodePin, Tt as resolveEgressDecodeHwAccel, Ut as BaseAddon, Vt as isSoftwareDecode, Wt as CAM_PROFILE_ORDER, Xt as makeProfileBrokerId, Yt as isEvent, Zt as makeSourceBrokerId, an as boolean, at as logChannelsCapability, cn as literal, en as parseProfileBrokerId, fn as record, h as EncodeProfileSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, ln as number, mn as union, on as discriminatedUnion, pn as string, q as egressTransportFromRequest, qt as createEvent, rn as _enum, st as maskUrlCredentials, un as object, w as RATE_CONTROL_TIGHT, z as declareLogChannel, zt as buildFfmpegArgs$1 } from "../dist-DttqKvsQ.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-NtM7__8q.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-DWe2kInY.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-CBvM6zIM.mjs";
7
+ import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-CsFtPVzH.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cf0O5S13.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZIja2AOe.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -1,4 +1,4 @@
1
- import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-CsI90TVi.mjs";
1
+ import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-DttqKvsQ.mjs";
2
2
  //#region src/session-decode/native-lease-config.ts
3
3
  /**
4
4
  * Resolution of the native-frame hold / tile knobs for one decode worker.
@@ -225,6 +225,8 @@ function isVideoFrameGenOptions(value) {
225
225
  if (!isRecord(value)) return false;
226
226
  if (value.fps !== void 0 && !isFiniteNumber(value.fps)) return false;
227
227
  if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
228
+ if (value.preRollMs !== void 0 && !isFiniteNumber(value.preRollMs)) return false;
229
+ if (value.preRollMaxFrames !== void 0 && !isFiniteNumber(value.preRollMaxFrames)) return false;
228
230
  return true;
229
231
  }
230
232
  /** Discriminated-union guard for {@link WorkerRequest} — no `as` casts. */
@@ -246,13 +248,14 @@ function isWorkerRequest(value) {
246
248
  function isWorkerReply(value) {
247
249
  if (!isRecord(value)) return false;
248
250
  switch (value.kind) {
249
- case "frame": return isFiniteNumber(value.frameId) && isFiniteNumber(value.timestamp) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && isWorkerFrameFormat(value.format);
251
+ case "frame": return isFiniteNumber(value.frameId) && isFiniteNumber(value.timestamp) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && isWorkerFrameFormat(value.format) && (value.capturedAt === void 0 || isFiniteNumber(value.capturedAt)) && (value.preRoll === void 0 || typeof value.preRoll === "boolean");
250
252
  case "buffer": return isFiniteNumber(value.frameId) && value.bytes instanceof Uint8Array;
251
253
  case "nativeCropResult": return isFiniteNumber(value.requestId) && value.bytes instanceof Uint8Array && isFiniteNumber(value.width) && isFiniteNumber(value.height) && (value.source === void 0 || value.source === "lease" || value.source === "tile");
252
254
  case "nativeCropMiss": return isFiniteNumber(value.requestId) && (value.reason === void 0 || typeof value.reason === "string");
253
255
  case "ended": return true;
254
256
  case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
255
257
  case "log": return isWorkerLogLevel(value.level) && typeof value.message === "string";
258
+ case "preRollReport": return isFiniteNumber(value.offered) && isFiniteNumber(value.inferred) && isFiniteNumber(value.dropped) && isRecord(value.droppedByReason) && isFiniteNumber(value.strideMs) && typeof value.subsampled === "boolean" && isFiniteNumber(value.spanMs);
256
259
  case "dialTiming": return isFiniteNumber(value.hwContextMs) && isFiniteNumber(value.demuxerOpenMs) && isFiniteNumber(value.decoderCreateMs) && isFiniteNumber(value.firstDecodeMs) && typeof value.streamInfoProbed === "boolean";
257
260
  default: return false;
258
261
  }