@camstack/addon-pipeline 1.2.212 → 1.2.214

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 (37) 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-6FmFzp0X.mjs → default-detection-model-C2vYSX3O.mjs} +1 -1
  4. package/dist/{default-detection-model-CXpC8UoM.js → default-detection-model-f0PImZnh.js} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-B-MeGV6o.js → dist-DoHcimdu.js} +205 -2
  8. package/dist/{dist-C-elKxbr.mjs → dist-DttqKvsQ.mjs} +205 -2
  9. package/dist/{lazy-sharp-xDAJscmd.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-DPduceDa.js → node-6uF72wri.js} +1 -1
  13. package/dist/{node-DIRnjf_L.mjs → node-NtM7__8q.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +213 -24
  15. package/dist/pipeline-runner/index.mjs +212 -23
  16. package/dist/{process-memory-CUYuwW7H.js → process-memory-BRdyl5fE.js} +1 -1
  17. package/dist/{process-memory-CJ8yC9r_.mjs → process-memory-CddpCDWL.mjs} +1 -1
  18. package/dist/recorder/index.js +4 -2
  19. package/dist/recorder/index.mjs +4 -2
  20. package/dist/{segment-demux-js-CXXWV0Ph.js → segment-demux-js-CkJPdyDH.js} +1 -1
  21. package/dist/{segment-demux-js-BhBboAO1.mjs → segment-demux-js-CsFtPVzH.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +319 -8
  23. package/dist/session-decode/decode-worker-child.mjs +318 -7
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-NrRAP0ec.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbBa4iV-.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-0MF-KL1v.mjs +26 -0
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-LoLVfRky.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Uxkwkgy5.mjs} +1 -1
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-BxCqSqXk.mjs → hostInit-CagSUJH7.mjs} +3 -3
  31. package/dist/stream-broker/index.js +3 -3
  32. package/dist/stream-broker/index.mjs +3 -3
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-C0mDH3xS.mjs → worker-protocol-BKwkLZWT.mjs} +5 -2
  35. package/dist/{worker-protocol-DMNMfs2E.js → worker-protocol-BWO2WNjw.js} +5 -2
  36. package/package.json +1 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cw3x0dZG.mjs +0 -26
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-DMNMfs2E.js");
3
- const require_lazy_sharp = require("../lazy-sharp-xDAJscmd.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,153 @@ 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
364
+ //#region src/session-decode/pre-roll-burst.ts
365
+ var ADMITTED = { admitted: true };
366
+ /**
367
+ * Choose the media stride for one burst.
368
+ *
369
+ * `plannedFrames` is what the stride actually yields, so a caller can log the
370
+ * intent next to the outcome — a budget that is never reached and a budget that
371
+ * is always hit are different problems and used to read the same.
372
+ */
373
+ function planPreRollBurst(input) {
374
+ const span = Number.isFinite(input.spanMs) && input.spanMs > 0 ? input.spanMs : 0;
375
+ const fps = Number.isFinite(input.detectionFps) && input.detectionFps > 0 ? input.detectionFps : 1;
376
+ const maxFrames = Number.isFinite(input.maxFrames) && input.maxFrames > 0 ? input.maxFrames : 0;
377
+ if (span === 0 || maxFrames === 0) return {
378
+ strideMs: Number.POSITIVE_INFINITY,
379
+ plannedFrames: 0,
380
+ subsampled: false
381
+ };
382
+ const cadenceMs = 1e3 / fps;
383
+ const budgetStrideMs = span / maxFrames;
384
+ const subsampled = budgetStrideMs > cadenceMs;
385
+ const strideMs = subsampled ? budgetStrideMs : cadenceMs;
386
+ return {
387
+ strideMs,
388
+ plannedFrames: Math.min(maxFrames, Math.max(1, Math.floor(span / strideMs))),
389
+ subsampled
390
+ };
391
+ }
392
+ /**
393
+ * The live, per-session budget. Stateful by necessity — a stride is a decision
394
+ * about the PREVIOUS admission — and deliberately free of any clock of its own:
395
+ * both times are passed in, so a test can drive a catch-up burst without
396
+ * pretending 4 s of media takes 4 s to decode.
397
+ */
398
+ var PreRollBurst = class {
399
+ budget;
400
+ plan;
401
+ startedAtWall;
402
+ liveEdgeWall;
403
+ lastAdmittedMediaWall = null;
404
+ offered = 0;
405
+ inferred = 0;
406
+ done = false;
407
+ drops = {
408
+ stride: 0,
409
+ "budget-frames": 0,
410
+ "budget-wall-ms": 0,
411
+ "live-edge": 0
412
+ };
413
+ constructor(input) {
414
+ this.budget = input.budget;
415
+ this.plan = input.plan;
416
+ this.startedAtWall = input.startedAtWall;
417
+ this.liveEdgeWall = input.liveEdgeWall;
418
+ }
419
+ /** True once the burst has ended — by budget, or by reaching the live edge. */
420
+ get finished() {
421
+ return this.done;
422
+ }
423
+ /**
424
+ * Offer one decoded history frame.
425
+ *
426
+ * `mediaWall` is the frame's OWN moment (see `media-clock.ts`); `nowWall` is
427
+ * the wall clock of the offer. The two are different by construction during
428
+ * catch-up, which is the whole reason this class exists.
429
+ */
430
+ offer(mediaWall, nowWall) {
431
+ this.offered++;
432
+ if (this.done) return this.drop("budget-frames");
433
+ if (mediaWall >= this.liveEdgeWall) {
434
+ this.done = true;
435
+ return this.drop("live-edge");
436
+ }
437
+ if (nowWall - this.startedAtWall >= this.budget.maxWallMs) {
438
+ this.done = true;
439
+ return this.drop("budget-wall-ms");
440
+ }
441
+ if (this.inferred >= this.budget.maxFrames) {
442
+ this.done = true;
443
+ return this.drop("budget-frames");
444
+ }
445
+ if (this.lastAdmittedMediaWall !== null && mediaWall - this.lastAdmittedMediaWall < this.plan.strideMs) return this.drop("stride");
446
+ this.lastAdmittedMediaWall = mediaWall;
447
+ this.inferred++;
448
+ return ADMITTED;
449
+ }
450
+ /** End the burst without offering a frame (session torn down, decode ended). */
451
+ finish() {
452
+ this.done = true;
453
+ }
454
+ report() {
455
+ return {
456
+ offered: this.offered,
457
+ inferred: this.inferred,
458
+ dropped: this.offered - this.inferred,
459
+ droppedByReason: { ...this.drops },
460
+ strideMs: Math.round(this.plan.strideMs),
461
+ subsampled: this.plan.subsampled
462
+ };
463
+ }
464
+ drop(reason) {
465
+ this.drops[reason]++;
466
+ return {
467
+ admitted: false,
468
+ reason
469
+ };
470
+ }
471
+ };
472
+ //#endregion
322
473
  //#region src/session-decode/lease-activity-gate.ts
323
474
  /**
324
475
  * Demand-driven gate for the native-frame lease capture (see
@@ -1523,6 +1674,18 @@ var REDIAL_MS = 3e3;
1523
1674
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
1524
1675
  var METRICS_INTERVAL_MS = 1e4;
1525
1676
  /**
1677
+ * Wall-clock ceiling on one pre-roll catch-up burst.
1678
+ *
1679
+ * A burst is a race against the live edge it is trying to get ahead of. Two
1680
+ * seconds in, the decoder has either caught up (the burst ended on its own) or
1681
+ * something is wrong — a cold GPU, a node under load, a camera whose GOP is
1682
+ * longer than its history — and continuing to spend inference on footage that
1683
+ * is now three or four seconds stale buys an operator nothing while a live
1684
+ * frame waits. The frame budget bounds the WORK; this bounds the WINDOW in
1685
+ * which that work is still worth doing.
1686
+ */
1687
+ var PRE_ROLL_BURST_MAX_WALL_MS = 2e3;
1688
+ /**
1526
1689
  * Hard cap on the native-frame retention ring (native-res crop feature). Kept
1527
1690
  * DELIBERATELY TINY — its only job is to cover the detection→crop latency
1528
1691
  * (typically 1-4 frames), NOT the full shm ring depth. On the HW path every
@@ -2156,6 +2319,23 @@ var DecodeWorkerChild = class {
2156
2319
  sessionFormat = "rgb";
2157
2320
  minIntervalMs = 0;
2158
2321
  /**
2322
+ * Session media clock, anchored on the FIRST decoded frame. Null until that
2323
+ * frame arrives, or forever on a source whose pts cannot be read — in which
2324
+ * case every frame falls back to the wall clock, exactly as before.
2325
+ */
2326
+ mediaClock = null;
2327
+ /** History this dial asked the broker for; `0` ⇒ there is no burst. */
2328
+ preRollMs = 0;
2329
+ /** Frames the catch-up burst may send to inference. */
2330
+ preRollMaxFrames = 0;
2331
+ /** The live catch-up burst, or null once it has ended (or never started). */
2332
+ preRollBurst = null;
2333
+ /** Whether the burst's report has already been sent — it is sent exactly once. */
2334
+ preRollReported = false;
2335
+ /** Media wall clock of the OLDEST and NEWEST frames the burst admitted. */
2336
+ preRollFirstMediaWall = null;
2337
+ preRollLastMediaWall = null;
2338
+ /**
2159
2339
  * The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
2160
2340
  * pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
2161
2341
  * the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
@@ -2217,6 +2397,7 @@ var DecodeWorkerChild = class {
2217
2397
  this.metricsTimer = null;
2218
2398
  }
2219
2399
  if (this.started) this.logMetrics(true);
2400
+ this.endPreRollBurst();
2220
2401
  this.abortController?.abort();
2221
2402
  this.frames.free();
2222
2403
  this.closeInput();
@@ -2276,6 +2457,8 @@ var DecodeWorkerChild = class {
2276
2457
  this.emitStderr(`decode-worker-child: ${require_worker_protocol.formatNativeLeaseKnobs(NATIVE_LEASE_KNOBS)}\n`, "info");
2277
2458
  this.sessionFormat = opts.format ?? "rgb";
2278
2459
  this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
2460
+ this.preRollMs = opts.preRollMs && opts.preRollMs > 0 ? opts.preRollMs : 0;
2461
+ this.preRollMaxFrames = opts.preRollMaxFrames && opts.preRollMaxFrames > 0 ? opts.preRollMaxFrames : 0;
2279
2462
  this.lastMetricsAt = Date.now();
2280
2463
  this.metricsTimer = setInterval(() => this.logMetrics(false), METRICS_INTERVAL_MS);
2281
2464
  this.metricsTimer.unref?.();
@@ -3108,14 +3291,140 @@ var DecodeWorkerChild = class {
3108
3291
  }
3109
3292
  this.publishFrame(frame);
3110
3293
  }
3111
- /** Publish to the latest-wins slot; deliver immediately if a `pull` is pending. */
3294
+ /**
3295
+ * Publish to the latest-wins slot; deliver immediately if a `pull` is pending.
3296
+ *
3297
+ * ## Where the pre-roll used to die
3298
+ *
3299
+ * This method used to be one line: `this.frames.publish(frame, Date.now(),
3300
+ * this.minIntervalMs)`. `FrameSlot.publish` throttles by comparing that
3301
+ * `Date.now()` against the previous publish's, and during a catch-up burst
3302
+ * that comparison is meaningless — the restreamer replays ~4 s of retained
3303
+ * media and the decoder chews through all of it inside a few hundred
3304
+ * milliseconds of WALL clock. So the throttle admitted one or two frames of
3305
+ * the whole window and `frame.free()`d the rest, and the two frames on which
3306
+ * device 617's cyclist scored above the vehicle floor (0.5669 and 0.5366,
3307
+ * 1.68 s before the live track was born) were freed here, having been
3308
+ * received, decoded and paid for in full.
3309
+ *
3310
+ * The fix is not to weaken the throttle. A live frame still paces on wall
3311
+ * clock, unchanged. A HISTORY frame paces on its own MEDIA clock instead,
3312
+ * under an explicit budget, because media time is the only clock on which
3313
+ * "one frame per 333 ms of footage" means what it says.
3314
+ */
3112
3315
  publishFrame(frame) {
3113
- const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
3316
+ const now = Date.now();
3317
+ const mediaWall = this.mediaWallClockOfFrame(frame, now);
3318
+ const verdict = this.offerToPreRollBurst(mediaWall, now);
3319
+ if (verdict === "dropped") {
3320
+ this.framesSkipped++;
3321
+ frame.free();
3322
+ return;
3323
+ }
3324
+ const preRoll = verdict === "pre-roll";
3325
+ const reply = this.frames.publish(frame, preRoll ? mediaWall : now, preRoll ? 0 : this.minIntervalMs, {
3326
+ capturedAt: mediaWall,
3327
+ preRoll
3328
+ });
3114
3329
  if (reply) {
3115
3330
  this.pendingPull = false;
3116
3331
  this.deliverFrame(reply);
3117
3332
  }
3118
3333
  }
3334
+ /**
3335
+ * The frame's own moment, from its pts — or the wall clock when the source
3336
+ * cannot say.
3337
+ *
3338
+ * The media clock is anchored on the FIRST frame of the session: that frame
3339
+ * is `preRollMs` old by construction, because the pre-roll is what the dial
3340
+ * asked for and the restreamer serves it before anything live. A source whose
3341
+ * pts cannot be converted (no time base, a zero denominator) gets no clock at
3342
+ * all and therefore no burst — a fabricated media time would date a whole
3343
+ * burst of detections at one wrong instant, which is worse than not inferring
3344
+ * them at all.
3345
+ */
3346
+ mediaWallClockOfFrame(frame, now) {
3347
+ const ptsMs = this.frameMediaPtsMs(frame);
3348
+ if (ptsMs === null) return now;
3349
+ if (!this.mediaClock) {
3350
+ this.mediaClock = anchorMediaClock({
3351
+ firstPtsMs: ptsMs,
3352
+ preRollMs: this.preRollMs,
3353
+ nowWall: now
3354
+ });
3355
+ if (this.preRollMs > 0 && this.preRollMaxFrames > 0) this.preRollBurst = new PreRollBurst({
3356
+ budget: {
3357
+ maxFrames: this.preRollMaxFrames,
3358
+ maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
3359
+ },
3360
+ plan: planPreRollBurst({
3361
+ spanMs: this.preRollMs,
3362
+ detectionFps: this.minIntervalMs > 0 ? 1e3 / this.minIntervalMs : 1,
3363
+ maxFrames: this.preRollMaxFrames
3364
+ }),
3365
+ startedAtWall: now,
3366
+ liveEdgeWall: now
3367
+ });
3368
+ }
3369
+ return mediaWallClockOf(this.mediaClock, ptsMs, now);
3370
+ }
3371
+ /** This frame's pts in ms, or `null` when the source cannot express one. */
3372
+ frameMediaPtsMs(frame) {
3373
+ const pts = frame.pts;
3374
+ const timeBase = frame.timeBase;
3375
+ if (typeof pts !== "bigint") return null;
3376
+ if (typeof timeBase !== "object" || timeBase === null) return null;
3377
+ const num = Reflect.get(timeBase, "num");
3378
+ const den = Reflect.get(timeBase, "den");
3379
+ if (typeof num !== "number" || typeof den !== "number") return null;
3380
+ return ptsToMs(pts, {
3381
+ num,
3382
+ den
3383
+ });
3384
+ }
3385
+ /**
3386
+ * Ask the burst whether this frame is history worth inferring.
3387
+ *
3388
+ * `'live'` — no burst, or the burst has ended. The ordinary path, untouched.
3389
+ * `'pre-roll'` — inside the budget; delivered marked, to the runner's lane.
3390
+ * `'dropped'` — history the budget refused. Freed by the caller and counted.
3391
+ */
3392
+ offerToPreRollBurst(mediaWall, now) {
3393
+ const burst = this.preRollBurst;
3394
+ if (!burst) return "live";
3395
+ const verdict = burst.offer(mediaWall, now);
3396
+ if (verdict.admitted) {
3397
+ if (this.preRollFirstMediaWall === null) this.preRollFirstMediaWall = mediaWall;
3398
+ this.preRollLastMediaWall = mediaWall;
3399
+ return "pre-roll";
3400
+ }
3401
+ if (burst.finished) {
3402
+ this.endPreRollBurst();
3403
+ return verdict.reason === "live-edge" ? "live" : "dropped";
3404
+ }
3405
+ return "dropped";
3406
+ }
3407
+ /** Close the burst and send its accounting — exactly once per session. */
3408
+ endPreRollBurst() {
3409
+ const burst = this.preRollBurst;
3410
+ this.preRollBurst = null;
3411
+ if (!burst || this.preRollReported) return;
3412
+ this.preRollReported = true;
3413
+ burst.finish();
3414
+ const report = burst.report();
3415
+ const first = this.preRollFirstMediaWall;
3416
+ const last = this.preRollLastMediaWall;
3417
+ this.send({
3418
+ kind: "preRollReport",
3419
+ offered: report.offered,
3420
+ inferred: report.inferred,
3421
+ dropped: report.dropped,
3422
+ droppedByReason: report.droppedByReason,
3423
+ strideMs: report.strideMs,
3424
+ subsampled: report.subsampled,
3425
+ spanMs: first !== null && last !== null ? Math.round(last - first) : 0
3426
+ });
3427
+ }
3119
3428
  /** Send a `frame` reply to the parent and count it toward `deliveredFps`. */
3120
3429
  deliverFrame(reply) {
3121
3430
  this.framesDelivered++;
@@ -3172,7 +3481,9 @@ var DecodeWorkerChild = class {
3172
3481
  timestamp: reply.timestamp,
3173
3482
  width: reply.width,
3174
3483
  height: reply.height,
3175
- format: this.sessionFormat
3484
+ format: this.sessionFormat,
3485
+ capturedAt: reply.capturedAt,
3486
+ ...reply.preRoll ? { preRoll: true } : {}
3176
3487
  };
3177
3488
  }
3178
3489
  /**