@camstack/addon-pipeline 1.2.237 → 1.2.239

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.
@@ -5791,19 +5791,27 @@ var PipelineRunner = class {
5791
5791
  if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5792
5792
  });
5793
5793
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
5794
+ const preRollQueue = new PreRollLane({
5795
+ capacity: this.config.maxPreRollQueueDepth ?? 12,
5796
+ clone: ownDetectionEntry,
5797
+ dispose: (entry) => {
5798
+ if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5799
+ if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5800
+ }
5801
+ });
5802
+ this.logger?.info("camera registered: root detectors", {
5803
+ tags: { deviceId },
5804
+ meta: {
5805
+ detectionSources: registration.detectionSources ?? null,
5806
+ ...planDetectionSources(registration.detectionSources)
5807
+ }
5808
+ });
5794
5809
  const state = {
5795
5810
  registration,
5796
5811
  onboardSlot: null,
5797
5812
  motionQueue,
5798
5813
  detectionQueue,
5799
- preRollQueue: new PreRollLane({
5800
- capacity: this.config.maxPreRollQueueDepth ?? 12,
5801
- clone: ownDetectionEntry,
5802
- dispose: (entry) => {
5803
- if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5804
- if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5805
- }
5806
- }),
5814
+ preRollQueue,
5807
5815
  inferenceTimes: [],
5808
5816
  shedFrames: 0,
5809
5817
  inflight: 0,
@@ -5783,19 +5783,27 @@ var PipelineRunner = class {
5783
5783
  if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5784
5784
  });
5785
5785
  const initialPhase = registration.detectionMode === "disabled" ? "idle" : registration.detectionMode === "always-on" ? "active" : "watching";
5786
+ const preRollQueue = new PreRollLane({
5787
+ capacity: this.config.maxPreRollQueueDepth ?? 12,
5788
+ clone: ownDetectionEntry,
5789
+ dispose: (entry) => {
5790
+ if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5791
+ if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5792
+ }
5793
+ });
5794
+ this.logger?.info("camera registered: root detectors", {
5795
+ tags: { deviceId },
5796
+ meta: {
5797
+ detectionSources: registration.detectionSources ?? null,
5798
+ ...planDetectionSources(registration.detectionSources)
5799
+ }
5800
+ });
5786
5801
  const state = {
5787
5802
  registration,
5788
5803
  onboardSlot: null,
5789
5804
  motionQueue,
5790
5805
  detectionQueue,
5791
- preRollQueue: new PreRollLane({
5792
- capacity: this.config.maxPreRollQueueDepth ?? 12,
5793
- clone: ownDetectionEntry,
5794
- dispose: (entry) => {
5795
- if (entry.handle) this.config.releaseRetention?.(entry.handle, "drop");
5796
- if (entry.frame.frameRef) this.config.releaseFrameRef?.(entry.frame.frameRef, "drop");
5797
- }
5798
- }),
5806
+ preRollQueue,
5799
5807
  inferenceTimes: [],
5800
5808
  shedFrames: 0,
5801
5809
  inflight: 0,
@@ -1323,6 +1323,126 @@ function resolveRgbCropSource(plane, planeStride, region) {
1323
1323
  stride: tightStride
1324
1324
  };
1325
1325
  }
1326
+ var ADMITTED = { admitted: true };
1327
+ /**
1328
+ * Choose the media stride for one burst.
1329
+ *
1330
+ * `plannedFrames` is what the stride actually yields, so a caller can log the
1331
+ * intent next to the outcome — a budget that is never reached and a budget that
1332
+ * is always hit are different problems and used to read the same.
1333
+ */
1334
+ function planPreRollBurst(input) {
1335
+ const span = Number.isFinite(input.spanMs) && input.spanMs > 0 ? input.spanMs : 0;
1336
+ const fps = Number.isFinite(input.detectionFps) && input.detectionFps > 0 ? input.detectionFps : 1;
1337
+ const maxFrames = Number.isFinite(input.maxFrames) && input.maxFrames > 0 ? input.maxFrames : 0;
1338
+ if (span === 0 || maxFrames === 0) return {
1339
+ strideMs: Number.POSITIVE_INFINITY,
1340
+ plannedFrames: 0,
1341
+ subsampled: false
1342
+ };
1343
+ const cadenceMs = 1e3 / fps;
1344
+ const budgetStrideMs = span / maxFrames;
1345
+ const subsampled = budgetStrideMs > cadenceMs;
1346
+ const strideMs = subsampled ? budgetStrideMs : cadenceMs;
1347
+ return {
1348
+ strideMs,
1349
+ plannedFrames: Math.min(maxFrames, Math.max(1, Math.floor(span / strideMs))),
1350
+ subsampled
1351
+ };
1352
+ }
1353
+ /**
1354
+ * The live, per-session budget. Stateful by necessity — a stride is a decision
1355
+ * about the PREVIOUS admission — and deliberately free of any clock of its own:
1356
+ * both times are passed in, so a test can drive a catch-up burst without
1357
+ * pretending 4 s of media takes 4 s to decode.
1358
+ */
1359
+ var PreRollBurst = class {
1360
+ budget;
1361
+ plan;
1362
+ startedAtWall;
1363
+ /** Widest lead a frame's moment took over the wall clock — the SERVED span. */
1364
+ maxLeadMs = 0;
1365
+ lastAdmittedMediaWall = null;
1366
+ offered = 0;
1367
+ inferred = 0;
1368
+ done = false;
1369
+ drops = {
1370
+ stride: 0,
1371
+ "budget-frames": 0,
1372
+ "budget-wall-ms": 0,
1373
+ "live-edge": 0
1374
+ };
1375
+ constructor(input) {
1376
+ this.budget = input.budget;
1377
+ this.plan = input.plan;
1378
+ this.startedAtWall = input.startedAtWall;
1379
+ }
1380
+ /**
1381
+ * How much history this burst actually carried, in ms of media — the number
1382
+ * the broker knows as `preRollSpanMs` and that never reached this process.
1383
+ *
1384
+ * `null` until a frame has led the wall clock: a burst that saw none did not
1385
+ * measure a short span, it measured NOTHING, and the difference is the whole
1386
+ * reason the anchor was left alone rather than fabricated (D393).
1387
+ */
1388
+ servedSpanMs() {
1389
+ return this.maxLeadMs > 0 ? this.maxLeadMs : null;
1390
+ }
1391
+ /** True once the burst has ended — by budget, or by reaching the live edge. */
1392
+ get finished() {
1393
+ return this.done;
1394
+ }
1395
+ /**
1396
+ * Offer one decoded history frame.
1397
+ *
1398
+ * `mediaWall` is the frame's OWN moment (see `media-clock.ts`); `nowWall` is
1399
+ * the wall clock of the offer. The two are different by construction during
1400
+ * catch-up, which is the whole reason this class exists.
1401
+ */
1402
+ offer(mediaWall, nowWall) {
1403
+ this.offered++;
1404
+ if (this.done) return this.drop("budget-frames");
1405
+ const leadMs = mediaWall - nowWall;
1406
+ if (this.maxLeadMs > 0 && leadMs <= 100) {
1407
+ this.done = true;
1408
+ return this.drop("live-edge");
1409
+ }
1410
+ if (leadMs > this.maxLeadMs) this.maxLeadMs = leadMs;
1411
+ if (nowWall - this.startedAtWall >= this.budget.maxWallMs) {
1412
+ this.done = true;
1413
+ return this.drop("budget-wall-ms");
1414
+ }
1415
+ if (this.inferred >= this.budget.maxFrames) {
1416
+ this.done = true;
1417
+ return this.drop("budget-frames");
1418
+ }
1419
+ if (this.lastAdmittedMediaWall !== null && mediaWall - this.lastAdmittedMediaWall < this.plan.strideMs) return this.drop("stride");
1420
+ this.lastAdmittedMediaWall = mediaWall;
1421
+ this.inferred++;
1422
+ return ADMITTED;
1423
+ }
1424
+ /** End the burst without offering a frame (session torn down, decode ended). */
1425
+ finish() {
1426
+ this.done = true;
1427
+ }
1428
+ report() {
1429
+ return {
1430
+ offered: this.offered,
1431
+ inferred: this.inferred,
1432
+ dropped: this.offered - this.inferred,
1433
+ droppedByReason: { ...this.drops },
1434
+ strideMs: Math.round(this.plan.strideMs),
1435
+ subsampled: this.plan.subsampled
1436
+ };
1437
+ }
1438
+ drop(reason) {
1439
+ this.drops[reason]++;
1440
+ return {
1441
+ admitted: false,
1442
+ reason
1443
+ };
1444
+ }
1445
+ };
1326
1446
  //#endregion
1327
1447
  //#region src/session-decode/pull-demuxer-open.ts
1328
1448
  /**
@@ -1577,6 +1697,18 @@ var REDIAL_MS = 3e3;
1577
1697
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
1578
1698
  var METRICS_INTERVAL_MS = 1e4;
1579
1699
  /**
1700
+ * Wall-clock ceiling on one pre-roll catch-up burst.
1701
+ *
1702
+ * A burst is a race against the live edge it is trying to get ahead of. Two
1703
+ * seconds in, the decoder has either caught up (the burst ended on its own) or
1704
+ * something is wrong — a cold GPU, a node under load, a camera whose GOP is
1705
+ * longer than its history — and continuing to spend inference on footage that
1706
+ * is now three or four seconds stale buys an operator nothing while a live
1707
+ * frame waits. The frame budget bounds the WORK; this bounds the WINDOW in
1708
+ * which that work is still worth doing.
1709
+ */
1710
+ var PRE_ROLL_BURST_MAX_WALL_MS = 2e3;
1711
+ /**
1580
1712
  * Hard cap on the native-frame retention ring (native-res crop feature). Kept
1581
1713
  * DELIBERATELY TINY — its only job is to cover the detection→crop latency
1582
1714
  * (typically 1-4 frames), NOT the full shm ring depth. On the HW path every
@@ -3290,13 +3422,26 @@ var DecodeWorkerChild = class {
3290
3422
  const ptsMs = this.frameMediaPtsMs(frame);
3291
3423
  if (ptsMs === null) return now;
3292
3424
  if (this.mediaClock === null || this.mediaClock.dialSeq !== this.dialSeq) {
3293
- const servedPreRollMs = 0;
3294
- 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)`);
3295
3425
  this.mediaClock = mediaClockForDial(this.mediaClock, this.dialSeq, {
3296
3426
  firstPtsMs: ptsMs,
3297
- preRollMs: servedPreRollMs,
3427
+ preRollMs: 0,
3298
3428
  nowWall: now
3299
3429
  });
3430
+ if (this.preRollMs > 0 && this.preRollMaxFrames > 0) {
3431
+ const preRollCadenceMs = this.minIntervalMs > 0 ? this.minIntervalMs : 1e3;
3432
+ this.preRollBurst = new PreRollBurst({
3433
+ budget: {
3434
+ maxFrames: this.preRollMaxFrames,
3435
+ maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
3436
+ },
3437
+ plan: planPreRollBurst({
3438
+ spanMs: this.preRollMaxFrames * preRollCadenceMs,
3439
+ detectionFps: 1e3 / preRollCadenceMs,
3440
+ maxFrames: this.preRollMaxFrames
3441
+ }),
3442
+ startedAtWall: now
3443
+ });
3444
+ }
3300
3445
  }
3301
3446
  return mediaWallClockOf(this.mediaClock.anchor, ptsMs, now);
3302
3447
  }
@@ -1322,6 +1322,126 @@ function resolveRgbCropSource(plane, planeStride, region) {
1322
1322
  stride: tightStride
1323
1323
  };
1324
1324
  }
1325
+ var ADMITTED = { admitted: true };
1326
+ /**
1327
+ * Choose the media stride for one burst.
1328
+ *
1329
+ * `plannedFrames` is what the stride actually yields, so a caller can log the
1330
+ * intent next to the outcome — a budget that is never reached and a budget that
1331
+ * is always hit are different problems and used to read the same.
1332
+ */
1333
+ function planPreRollBurst(input) {
1334
+ const span = Number.isFinite(input.spanMs) && input.spanMs > 0 ? input.spanMs : 0;
1335
+ const fps = Number.isFinite(input.detectionFps) && input.detectionFps > 0 ? input.detectionFps : 1;
1336
+ const maxFrames = Number.isFinite(input.maxFrames) && input.maxFrames > 0 ? input.maxFrames : 0;
1337
+ if (span === 0 || maxFrames === 0) return {
1338
+ strideMs: Number.POSITIVE_INFINITY,
1339
+ plannedFrames: 0,
1340
+ subsampled: false
1341
+ };
1342
+ const cadenceMs = 1e3 / fps;
1343
+ const budgetStrideMs = span / maxFrames;
1344
+ const subsampled = budgetStrideMs > cadenceMs;
1345
+ const strideMs = subsampled ? budgetStrideMs : cadenceMs;
1346
+ return {
1347
+ strideMs,
1348
+ plannedFrames: Math.min(maxFrames, Math.max(1, Math.floor(span / strideMs))),
1349
+ subsampled
1350
+ };
1351
+ }
1352
+ /**
1353
+ * The live, per-session budget. Stateful by necessity — a stride is a decision
1354
+ * about the PREVIOUS admission — and deliberately free of any clock of its own:
1355
+ * both times are passed in, so a test can drive a catch-up burst without
1356
+ * pretending 4 s of media takes 4 s to decode.
1357
+ */
1358
+ var PreRollBurst = class {
1359
+ budget;
1360
+ plan;
1361
+ startedAtWall;
1362
+ /** Widest lead a frame's moment took over the wall clock — the SERVED span. */
1363
+ maxLeadMs = 0;
1364
+ lastAdmittedMediaWall = null;
1365
+ offered = 0;
1366
+ inferred = 0;
1367
+ done = false;
1368
+ drops = {
1369
+ stride: 0,
1370
+ "budget-frames": 0,
1371
+ "budget-wall-ms": 0,
1372
+ "live-edge": 0
1373
+ };
1374
+ constructor(input) {
1375
+ this.budget = input.budget;
1376
+ this.plan = input.plan;
1377
+ this.startedAtWall = input.startedAtWall;
1378
+ }
1379
+ /**
1380
+ * How much history this burst actually carried, in ms of media — the number
1381
+ * the broker knows as `preRollSpanMs` and that never reached this process.
1382
+ *
1383
+ * `null` until a frame has led the wall clock: a burst that saw none did not
1384
+ * measure a short span, it measured NOTHING, and the difference is the whole
1385
+ * reason the anchor was left alone rather than fabricated (D393).
1386
+ */
1387
+ servedSpanMs() {
1388
+ return this.maxLeadMs > 0 ? this.maxLeadMs : null;
1389
+ }
1390
+ /** True once the burst has ended — by budget, or by reaching the live edge. */
1391
+ get finished() {
1392
+ return this.done;
1393
+ }
1394
+ /**
1395
+ * Offer one decoded history frame.
1396
+ *
1397
+ * `mediaWall` is the frame's OWN moment (see `media-clock.ts`); `nowWall` is
1398
+ * the wall clock of the offer. The two are different by construction during
1399
+ * catch-up, which is the whole reason this class exists.
1400
+ */
1401
+ offer(mediaWall, nowWall) {
1402
+ this.offered++;
1403
+ if (this.done) return this.drop("budget-frames");
1404
+ const leadMs = mediaWall - nowWall;
1405
+ if (this.maxLeadMs > 0 && leadMs <= 100) {
1406
+ this.done = true;
1407
+ return this.drop("live-edge");
1408
+ }
1409
+ if (leadMs > this.maxLeadMs) this.maxLeadMs = leadMs;
1410
+ if (nowWall - this.startedAtWall >= this.budget.maxWallMs) {
1411
+ this.done = true;
1412
+ return this.drop("budget-wall-ms");
1413
+ }
1414
+ if (this.inferred >= this.budget.maxFrames) {
1415
+ this.done = true;
1416
+ return this.drop("budget-frames");
1417
+ }
1418
+ if (this.lastAdmittedMediaWall !== null && mediaWall - this.lastAdmittedMediaWall < this.plan.strideMs) return this.drop("stride");
1419
+ this.lastAdmittedMediaWall = mediaWall;
1420
+ this.inferred++;
1421
+ return ADMITTED;
1422
+ }
1423
+ /** End the burst without offering a frame (session torn down, decode ended). */
1424
+ finish() {
1425
+ this.done = true;
1426
+ }
1427
+ report() {
1428
+ return {
1429
+ offered: this.offered,
1430
+ inferred: this.inferred,
1431
+ dropped: this.offered - this.inferred,
1432
+ droppedByReason: { ...this.drops },
1433
+ strideMs: Math.round(this.plan.strideMs),
1434
+ subsampled: this.plan.subsampled
1435
+ };
1436
+ }
1437
+ drop(reason) {
1438
+ this.drops[reason]++;
1439
+ return {
1440
+ admitted: false,
1441
+ reason
1442
+ };
1443
+ }
1444
+ };
1325
1445
  //#endregion
1326
1446
  //#region src/session-decode/pull-demuxer-open.ts
1327
1447
  /**
@@ -1576,6 +1696,18 @@ var REDIAL_MS = 3e3;
1576
1696
  /** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
1577
1697
  var METRICS_INTERVAL_MS = 1e4;
1578
1698
  /**
1699
+ * Wall-clock ceiling on one pre-roll catch-up burst.
1700
+ *
1701
+ * A burst is a race against the live edge it is trying to get ahead of. Two
1702
+ * seconds in, the decoder has either caught up (the burst ended on its own) or
1703
+ * something is wrong — a cold GPU, a node under load, a camera whose GOP is
1704
+ * longer than its history — and continuing to spend inference on footage that
1705
+ * is now three or four seconds stale buys an operator nothing while a live
1706
+ * frame waits. The frame budget bounds the WORK; this bounds the WINDOW in
1707
+ * which that work is still worth doing.
1708
+ */
1709
+ var PRE_ROLL_BURST_MAX_WALL_MS = 2e3;
1710
+ /**
1579
1711
  * Hard cap on the native-frame retention ring (native-res crop feature). Kept
1580
1712
  * DELIBERATELY TINY — its only job is to cover the detection→crop latency
1581
1713
  * (typically 1-4 frames), NOT the full shm ring depth. On the HW path every
@@ -3289,13 +3421,26 @@ var DecodeWorkerChild = class {
3289
3421
  const ptsMs = this.frameMediaPtsMs(frame);
3290
3422
  if (ptsMs === null) return now;
3291
3423
  if (this.mediaClock === null || this.mediaClock.dialSeq !== this.dialSeq) {
3292
- const servedPreRollMs = 0;
3293
- 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)`);
3294
3424
  this.mediaClock = mediaClockForDial(this.mediaClock, this.dialSeq, {
3295
3425
  firstPtsMs: ptsMs,
3296
- preRollMs: servedPreRollMs,
3426
+ preRollMs: 0,
3297
3427
  nowWall: now
3298
3428
  });
3429
+ if (this.preRollMs > 0 && this.preRollMaxFrames > 0) {
3430
+ const preRollCadenceMs = this.minIntervalMs > 0 ? this.minIntervalMs : 1e3;
3431
+ this.preRollBurst = new PreRollBurst({
3432
+ budget: {
3433
+ maxFrames: this.preRollMaxFrames,
3434
+ maxWallMs: PRE_ROLL_BURST_MAX_WALL_MS
3435
+ },
3436
+ plan: planPreRollBurst({
3437
+ spanMs: this.preRollMaxFrames * preRollCadenceMs,
3438
+ detectionFps: 1e3 / preRollCadenceMs,
3439
+ maxFrames: this.preRollMaxFrames
3440
+ }),
3441
+ startedAtWall: now
3442
+ });
3443
+ }
3299
3444
  }
3300
3445
  return mediaWallClockOf(this.mediaClock.anchor, ptsMs, now);
3301
3446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.237",
3
+ "version": "1.2.239",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",