@blockcast/mmt-container 0.1.0-main.fcefcc30604d → 0.1.0-main.ffdace838275

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 (64) hide show
  1. package/dist/browser-runtime.js +859 -115
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/carp-catalog.d.ts +1 -2
  4. package/dist/carp-catalog.d.ts.map +1 -1
  5. package/dist/carp-catalog.js +32 -23
  6. package/dist/carp-catalog.js.map +2 -2
  7. package/dist/catalog-fec-params.d.ts +2 -4
  8. package/dist/catalog-fec-params.d.ts.map +1 -1
  9. package/dist/catalog-fec-params.js +3 -4
  10. package/dist/catalog-fec-params.js.map +2 -2
  11. package/dist/delivery-gap.d.ts +75 -0
  12. package/dist/delivery-gap.d.ts.map +1 -0
  13. package/dist/delivery-gap.js +88 -0
  14. package/dist/delivery-gap.js.map +7 -0
  15. package/dist/fec-manager.d.ts +80 -2
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +528 -56
  18. package/dist/fec-manager.js.map +3 -3
  19. package/dist/fec-worker-setup.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +2 -2
  24. package/dist/media-router.d.ts +12 -4
  25. package/dist/media-router.d.ts.map +1 -1
  26. package/dist/media-router.js +22 -4
  27. package/dist/media-router.js.map +2 -2
  28. package/dist/mmtp-container-parser.no-modules.js +32 -23
  29. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  30. package/dist/mmtp-router.d.ts +15 -4
  31. package/dist/mmtp-router.d.ts.map +1 -1
  32. package/dist/mmtp-router.js +51 -4
  33. package/dist/mmtp-router.js.map +2 -2
  34. package/dist/multicast-manager.d.ts +48 -6
  35. package/dist/multicast-manager.d.ts.map +1 -1
  36. package/dist/multicast-manager.js +710 -72
  37. package/dist/multicast-manager.js.map +4 -4
  38. package/dist/multicast-transport.d.ts +34 -5
  39. package/dist/multicast-transport.d.ts.map +1 -1
  40. package/dist/multicast-transport.js +87 -11
  41. package/dist/multicast-transport.js.map +2 -2
  42. package/dist/no-modules.js +32 -23
  43. package/dist/no-modules.js.map +2 -2
  44. package/dist/repair-fetch.d.ts +51 -2
  45. package/dist/repair-fetch.d.ts.map +1 -1
  46. package/dist/repair-fetch.js +76 -3
  47. package/dist/repair-fetch.js.map +2 -2
  48. package/dist/track-session.d.ts +15 -9
  49. package/dist/track-session.d.ts.map +1 -1
  50. package/dist/track-session.js +15 -9
  51. package/dist/track-session.js.map +2 -2
  52. package/package.json +6 -6
  53. package/src/carp-catalog.ts +34 -29
  54. package/src/catalog-fec-params.ts +6 -9
  55. package/src/delivery-gap.ts +147 -0
  56. package/src/fec-manager.ts +587 -67
  57. package/src/index.ts +3 -0
  58. package/src/media-router.ts +36 -8
  59. package/src/mmtp-router.ts +81 -8
  60. package/src/multicast-manager.ts +78 -3
  61. package/src/multicast-transport.ts +104 -14
  62. package/src/repair-fetch.ts +170 -8
  63. package/src/track-session.ts +15 -9
  64. package/test-vectors/catalog-validation.json +469 -25
@@ -6308,10 +6308,15 @@ function validateCmsfMmtpCatalog(value) {
6308
6308
  if (!Array.isArray(catalog["tracks"])) {
6309
6309
  catalogError("catalog_tracks", "$.tracks", "tracks must be an array");
6310
6310
  }
6311
- const common = typeof catalog["commonTrackFields"] === "object" && catalog["commonTrackFields"] !== null && !Array.isArray(catalog["commonTrackFields"]) ? catalog["commonTrackFields"] : {};
6311
+ if ("commonTrackFields" in catalog) {
6312
+ catalogError(
6313
+ "catalog_common_track_fields",
6314
+ "$.commonTrackFields",
6315
+ "commonTrackFields is retired; every track carries its own fields"
6316
+ );
6317
+ }
6312
6318
  const tracks = catalog["tracks"];
6313
6319
  const trackRecords = [];
6314
- const effective = (track, field) => track[field] ?? common[field];
6315
6320
  const positiveInteger2 = (input) => Number.isSafeInteger(input) && input > 0;
6316
6321
  const positiveNumber = (input) => typeof input === "number" && Number.isFinite(input) && input > 0;
6317
6322
  for (let index = 0; index < tracks.length; index++) {
@@ -6326,28 +6331,32 @@ function validateCmsfMmtpCatalog(value) {
6326
6331
  catalogError("track_name", `${path}.name`, "name must be a non-empty string");
6327
6332
  }
6328
6333
  trackRecords.push(track);
6329
- const packaging = effective(track, "packaging");
6334
+ const packaging = track["packaging"];
6330
6335
  if (typeof packaging !== "string" || packaging.length === 0) {
6331
6336
  catalogError("track_packaging", `${path}.packaging`, "packaging must be a non-empty string");
6332
6337
  }
6333
- const trackHasIsLive = Object.prototype.hasOwnProperty.call(track, "isLive");
6334
- const hasIsLive = trackHasIsLive || Object.prototype.hasOwnProperty.call(common, "isLive");
6335
- const isLive = trackHasIsLive ? track["isLive"] : common["isLive"];
6338
+ const hasIsLive = Object.prototype.hasOwnProperty.call(track, "isLive");
6339
+ const isLive = track["isLive"];
6336
6340
  if (hasIsLive && typeof isLive !== "boolean") {
6337
6341
  catalogError("track_is_live", `${path}.isLive`, "isLive must be a boolean");
6338
6342
  }
6339
6343
  track["isLive"] = isLive ?? true;
6340
- const selectionValue = effective(track, "selectionParams");
6341
- const selection = typeof selectionValue === "object" && selectionValue !== null && !Array.isArray(selectionValue) ? selectionValue : void 0;
6342
- const codec = selection?.["codec"] ?? effective(track, "codec");
6344
+ if ("selectionParams" in track) {
6345
+ catalogError(
6346
+ "track_selection_params",
6347
+ `${path}.selectionParams`,
6348
+ "selectionParams is retired; codec and the other selection fields are flat on the track"
6349
+ );
6350
+ }
6351
+ const codec = track["codec"];
6343
6352
  if (packaging !== "timeline" && packaging !== "mediatimeline" && packaging !== "eventtimeline" && packaging !== "fec-repair" && (typeof codec !== "string" || codec.length === 0)) {
6344
6353
  catalogError("track_codec", `${path}.codec`, `codec is required for packaging=${packaging}`);
6345
6354
  }
6346
- const timescale = effective(track, "timescale");
6347
- const groupDurationMs = effective(track, "groupDurationMs");
6348
- const groupDurationTicks = effective(track, "groupDurationTicks");
6349
- const keyframeIntervalMs = effective(track, "keyframeIntervalMs");
6350
- const keyframeIntervalTicks = effective(track, "keyframeIntervalTicks");
6355
+ const timescale = track["timescale"];
6356
+ const groupDurationMs = track["groupDurationMs"];
6357
+ const groupDurationTicks = track["groupDurationTicks"];
6358
+ const keyframeIntervalMs = track["keyframeIntervalMs"];
6359
+ const keyframeIntervalTicks = track["keyframeIntervalTicks"];
6351
6360
  if (groupDurationMs !== void 0 && !positiveInteger2(groupDurationMs)) {
6352
6361
  catalogError("track_group_duration_ms", `${path}.groupDurationMs`, "must be a positive integer");
6353
6362
  }
@@ -6364,7 +6373,7 @@ function validateCmsfMmtpCatalog(value) {
6364
6373
  rejectRepairFieldsOnMediaTrack(path, track, catalogError);
6365
6374
  }
6366
6375
  if (packaging === "mmtp") {
6367
- const mmtpMode = effective(track, "mmtpMode");
6376
+ const mmtpMode = track["mmtpMode"];
6368
6377
  if (mmtpMode !== "mpu" && mmtpMode !== "mfu") {
6369
6378
  catalogError("track_mmtp_mode", `${path}.mmtpMode`, "must be mpu or mfu");
6370
6379
  }
@@ -6418,10 +6427,10 @@ function validateCmsfMmtpCatalog(value) {
6418
6427
  }
6419
6428
  }
6420
6429
  const repairDescriptors = trackRecords.flatMap(
6421
- (track, index) => effective(track, "packaging") === "fec-repair" ? [describeRepairTrack(track["name"], `$.tracks[${index}]`, track)] : []
6430
+ (track, index) => track["packaging"] === "fec-repair" ? [describeRepairTrack(track["name"], `$.tracks[${index}]`, track)] : []
6422
6431
  );
6423
6432
  const reachability = repairReachability(
6424
- trackRecords.filter((track) => effective(track, "packaging") !== "fec-repair").map((track) => track["fec"])
6433
+ trackRecords.filter((track) => track["packaging"] !== "fec-repair").map((track) => track["fec"])
6425
6434
  );
6426
6435
  for (const descriptor of repairDescriptors) {
6427
6436
  assertRepairTrackReachable(descriptor, reachability, catalogError);
@@ -6429,7 +6438,7 @@ function validateCmsfMmtpCatalog(value) {
6429
6438
  if (target === void 0) continue;
6430
6439
  const source = trackRecords.find((candidate) => candidate["name"] === target);
6431
6440
  const sourceFec = source?.["fec"];
6432
- if (!source || effective(source, "packaging") !== "mmtp" || !sourceFec || sourceFec["algorithm"] === "none") {
6441
+ if (!source || source["packaging"] !== "mmtp" || !sourceFec || sourceFec["algorithm"] === "none") {
6433
6442
  catalogError(
6434
6443
  "track_repair_depends",
6435
6444
  `${descriptor.path}.depends`,
@@ -6458,12 +6467,12 @@ function validateCmsfMmtpCatalog(value) {
6458
6467
  const switchingSets = /* @__PURE__ */ new Map();
6459
6468
  for (let index = 0; index < trackRecords.length; index++) {
6460
6469
  const track = trackRecords[index];
6461
- if (effective(track, "packaging") !== "mmtp") continue;
6462
- const altGroup = effective(track, "altGroup");
6470
+ if (track["packaging"] !== "mmtp") continue;
6471
+ const altGroup = track["altGroup"];
6463
6472
  if (!Number.isSafeInteger(altGroup)) continue;
6464
- const timescale = BigInt(effective(track, "timescale"));
6465
- const durationTicks = effective(track, "groupDurationTicks");
6466
- const ticks = durationTicks !== void 0 ? BigInt(durationTicks) : BigInt(effective(track, "groupDurationMs")) * timescale / 1000n;
6473
+ const timescale = BigInt(track["timescale"]);
6474
+ const durationTicks = track["groupDurationTicks"];
6475
+ const ticks = durationTicks !== void 0 ? BigInt(durationTicks) : BigInt(track["groupDurationMs"]) * timescale / 1000n;
6467
6476
  const anchor = switchingSets.get(altGroup);
6468
6477
  if (anchor && ticks * anchor.timescale !== anchor.ticks * timescale) {
6469
6478
  catalogError("track_switching_set", `$.tracks[${index}]`, `group duration does not match ${JSON.stringify(anchor.name)} in altGroup ${altGroup}`);
@@ -9522,15 +9531,21 @@ var SessionManager = class {
9522
9531
  * deadline before the subscription existed and abandoned the join on the
9523
9532
  * happy path. BLO-34289.
9524
9533
  *
9525
- * Flip it true when the subscribe settles, never later. Over AMT a settled
9526
- * subscribe means the gateway has sent its Membership Update, and the relay's
9527
- * upstream join plus a path RTT still precede the first packet, but that
9528
- * latency belongs inside this budget, not in front of it: RFC 7450 gives a
9529
- * gateway no signal that the relay has traffic for its (S,G), so a join whose
9530
- * source never sends is bounded only by this deadline. Holding the flag until
9531
- * MulticastTransport.onDataPlaneLive would park such a join in `joining`
9532
- * forever. MulticastTransport's silence window, not this deadline, is what
9533
- * starts at the first packet.
9534
+ * The deadline is a media budget (group cadence plus FEC delivery window) and
9535
+ * budgets nothing for reaching the stream, so ideally it starts at
9536
+ * MulticastTransport.onDataPlaneLive, the join's first accepted packet. Over
9537
+ * AMT that is measurably later than a settled subscribe: the subscribe
9538
+ * settles once the gateway has sent its Membership Update, and the relay's
9539
+ * upstream join plus a path RTT (0.4-0.8 s measured to the prod relay) still
9540
+ * precede the first packet. But RFC 7450 gives a gateway no signal that the
9541
+ * relay has traffic for its (S,G), so a join whose source never sends never
9542
+ * reaches that edge. Starting the deadline there is therefore sound only when
9543
+ * the provider bounds the join phase itself, with a typed subscription error
9544
+ * for a join that delivers nothing (MulticastTransport forwards it to
9545
+ * onFailback). A host whose provider makes no such guarantee flips this true
9546
+ * when the subscribe settles and accepts that join latency is charged to the
9547
+ * media budget; holding it until the first packet would park a silent join
9548
+ * in `joining` forever.
9534
9549
  *
9535
9550
  * The flag tracks the host's primary join, not per-track transport state. A
9536
9551
  * track that fails over to the next transport in its own chain while this is
@@ -9767,6 +9782,91 @@ var SessionManager = class {
9767
9782
  }
9768
9783
  };
9769
9784
 
9785
+ // src/delivery-gap.ts
9786
+ var DeliveryGapEstimator = class {
9787
+ /** Most recent arrival, or undefined before the first one since reset. */
9788
+ #lastArrivalMs;
9789
+ /** Last MMTP packet_sequence_number seen per packet_id. */
9790
+ #lastSequence = /* @__PURE__ */ new Map();
9791
+ /**
9792
+ * Monotonic deque of (arrivalMs, gapMs): gaps strictly decrease from head to
9793
+ * tail, so the head is the window maximum. Amortized O(1) per arrival.
9794
+ */
9795
+ #times = [];
9796
+ #gaps = [];
9797
+ #head = 0;
9798
+ #windowMs;
9799
+ /**
9800
+ * @param windowMs trailing window in ms, which also caps the reported gap,
9801
+ * or undefined when the catalog gives none (no gap is reported).
9802
+ */
9803
+ setWindowMs(windowMs) {
9804
+ if (windowMs !== void 0 && !(Number.isFinite(windowMs) && windowMs > 0)) {
9805
+ throw new Error(
9806
+ `[DeliveryGap] window must be a finite positive ms value or undefined (got ${windowMs})`
9807
+ );
9808
+ }
9809
+ this.#windowMs = windowMs;
9810
+ }
9811
+ get windowMs() {
9812
+ return this.#windowMs;
9813
+ }
9814
+ /**
9815
+ * Record one multicast datagram arrival at receiver time `nowMs`, carrying
9816
+ * MMTP `packetSequenceNumber` (32-bit, per packet_id) on `packetId`.
9817
+ */
9818
+ noteArrival(nowMs, packetId, packetSequenceNumber) {
9819
+ const last = this.#lastArrivalMs;
9820
+ const previousSequence = this.#lastSequence.get(packetId);
9821
+ this.#lastArrivalMs = nowMs;
9822
+ this.#lastSequence.set(packetId, packetSequenceNumber);
9823
+ if (last === void 0 || previousSequence === void 0) return;
9824
+ if (this.#windowMs === void 0) return;
9825
+ if (previousSequence + 1 >>> 0 !== packetSequenceNumber >>> 0) return;
9826
+ const gap = nowMs - last;
9827
+ this.#evict(nowMs);
9828
+ while (this.#gaps.length > this.#head && this.#gaps[this.#gaps.length - 1] <= gap) {
9829
+ this.#gaps.pop();
9830
+ this.#times.pop();
9831
+ }
9832
+ this.#gaps.push(gap);
9833
+ this.#times.push(nowMs);
9834
+ this.#compact();
9835
+ }
9836
+ /**
9837
+ * Largest gap observed within the window ending at `nowMs`, at most the
9838
+ * window itself; 0 if none or when no window is set.
9839
+ */
9840
+ peakGapMs(nowMs) {
9841
+ const windowMs = this.#windowMs;
9842
+ if (windowMs === void 0) return 0;
9843
+ this.#evict(nowMs);
9844
+ return this.#gaps.length > this.#head ? Math.min(this.#gaps[this.#head], windowMs) : 0;
9845
+ }
9846
+ /** Forget everything: a new delivery path starts with no measurement. */
9847
+ reset() {
9848
+ this.#lastArrivalMs = void 0;
9849
+ this.#lastSequence.clear();
9850
+ this.#times = [];
9851
+ this.#gaps = [];
9852
+ this.#head = 0;
9853
+ }
9854
+ #evict(nowMs) {
9855
+ const windowMs = this.#windowMs;
9856
+ if (windowMs === void 0) return;
9857
+ while (this.#times.length > this.#head && this.#times[this.#head] < nowMs - windowMs) {
9858
+ this.#head++;
9859
+ }
9860
+ }
9861
+ #compact() {
9862
+ if (this.#head > 0 && this.#head * 2 >= this.#times.length) {
9863
+ this.#times = this.#times.slice(this.#head);
9864
+ this.#gaps = this.#gaps.slice(this.#head);
9865
+ this.#head = 0;
9866
+ }
9867
+ }
9868
+ };
9869
+
9770
9870
  // src/block-policy.ts
9771
9871
  function repairPriorityForClass(blockClass) {
9772
9872
  switch (blockClass) {
@@ -10231,6 +10331,9 @@ var WORKER_ERROR_LOG_CAP = 5;
10231
10331
  var WORKER_ERROR_KIND_CAP = 32;
10232
10332
  var WORKER_ERROR_OVERFLOW_KIND = "other";
10233
10333
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
10334
+ function isMulticastSymbolDelivery(value) {
10335
+ return value === "datagram" || value === "ordered-relay";
10336
+ }
10234
10337
  function newFecCounters() {
10235
10338
  return {
10236
10339
  sourcePackets: 0,
@@ -10296,6 +10399,17 @@ var FECManager = class {
10296
10399
  // deadline; the timer re-checks it when it fires (see DeadlineTimer).
10297
10400
  this.#activeBlocks = /* @__PURE__ */ new Map();
10298
10401
  this.#activeBlockDeadlinesMs = /* @__PURE__ */ new Map();
10402
+ // The two terms #activeBlockDeadlinesMs is the later of (BLO-35597):
10403
+ // - the multicast late-symbol wait, refreshed by every accepted symbol;
10404
+ // - a D-28 repair hold, set by the repair scheduler only while a repair
10405
+ // FETCH it admitted can still land before the block's presentation
10406
+ // deadline. The multicast span elapsing never ends a fetch the scheduler
10407
+ // judged able to arrive in time.
10408
+ this.#blockWaitDeadlinesMs = /* @__PURE__ */ new Map();
10409
+ this.#repairHoldUntilMs = /* @__PURE__ */ new Map();
10410
+ // Receiver-measured multicast delivery gap: the late-symbol wait's jitter
10411
+ // term (see DeliveryGapEstimator for the statistic and why).
10412
+ this.#deliveryGaps = new DeliveryGapEstimator();
10299
10413
  this.#lifecycleTrace = [];
10300
10414
  // Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
10301
10415
  // Failure remains pending until cleanupComplete acknowledges that queue drain.
@@ -10353,6 +10467,9 @@ var FECManager = class {
10353
10467
  this.#lastStreamKey = "";
10354
10468
  this.#seenSymbols = /* @__PURE__ */ new Set();
10355
10469
  this.#blockPathCounts = /* @__PURE__ */ new Map();
10470
+ // Receiver-local first-accepted-symbol time per block, bounded like
10471
+ // #blockPathCounts (see blockAnchorAtMs).
10472
+ this.#blockAnchorsMs = /* @__PURE__ */ new Map();
10356
10473
  // Exact catalog identity observed on accepted symbols for each FEC block.
10357
10474
  // Conflicting identities collapse to null so recovery fails closed.
10358
10475
  this.#blockCatalogTrackNames = /* @__PURE__ */ new Map();
@@ -10400,6 +10517,19 @@ var FECManager = class {
10400
10517
  // Worker recovery emits all K source symbols. Track recovery once per block
10401
10518
  // while routing only ESI slots that were absent from the direct source path.
10402
10519
  this.#workerRecoveredBlocks = /* @__PURE__ */ new Set();
10520
+ // How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
10521
+ // provider declares it per transport join; absence keeps datagram semantics.
10522
+ this.#multicastSymbolDelivery = "datagram";
10523
+ // ordered-relay: per stream, the highest SBN any multicast symbol carried.
10524
+ // The relay preserves the receiving socket's order, so every block more than
10525
+ // the decoder's interleave horizon behind this frontier has received every
10526
+ // symbol it will ever receive over multicast.
10527
+ this.#orderedFrontiers = /* @__PURE__ */ new Map();
10528
+ // ordered-relay: blocks with multicast symbols that are neither terminal nor
10529
+ // handed to a decoder cleanup, per stream, keyed by SBN. They carry no
10530
+ // timer; the frontier settles them. At most one interleave horizon of blocks
10531
+ // per stream is ever here.
10532
+ this.#orderedPendingBlocks = /* @__PURE__ */ new Map();
10403
10533
  // ── Keyframe-protected repair policy (block-policy.ts) ──
10404
10534
  // One classifier per physical stream, built from that stream's geometry.
10405
10535
  this.#classifiers = /* @__PURE__ */ new Map();
@@ -10506,6 +10636,9 @@ var FECManager = class {
10506
10636
  #fecStatsObservers;
10507
10637
  #activeBlocks;
10508
10638
  #activeBlockDeadlinesMs;
10639
+ #blockWaitDeadlinesMs;
10640
+ #repairHoldUntilMs;
10641
+ #deliveryGaps;
10509
10642
  #lifecycleTrace;
10510
10643
  #pendingWorkerCleanups;
10511
10644
  #pendingWorkerCleanupClients;
@@ -10529,6 +10662,7 @@ var FECManager = class {
10529
10662
  #lastStreamKey;
10530
10663
  #seenSymbols;
10531
10664
  #blockPathCounts;
10665
+ #blockAnchorsMs;
10532
10666
  #blockCatalogTrackNames;
10533
10667
  #startupAlignedPackets;
10534
10668
  #startupPartialBlocks;
@@ -10541,6 +10675,9 @@ var FECManager = class {
10541
10675
  #repairDeadlines;
10542
10676
  #repairDeadlineAtMs;
10543
10677
  #workerRecoveredBlocks;
10678
+ #multicastSymbolDelivery;
10679
+ #orderedFrontiers;
10680
+ #orderedPendingBlocks;
10544
10681
  #classifiers;
10545
10682
  #streamGeometries;
10546
10683
  #failedBlocks;
@@ -10640,6 +10777,7 @@ var FECManager = class {
10640
10777
  #markTerminal(key, sbn, packetId, clean) {
10641
10778
  const nowMs = performance.now();
10642
10779
  this.#retireTerminalBlocks(packetId, nowMs);
10780
+ this.#forgetOrderedPending(key);
10643
10781
  this.#completedBlocks.add(key);
10644
10782
  if (clean) this.#cleanlyCompletedBlocks.add(key);
10645
10783
  this.#indexBlockKey(key, sbn, packetId);
@@ -10657,8 +10795,10 @@ var FECManager = class {
10657
10795
  if (!queue) return;
10658
10796
  for (let record = queue.peek(); record !== void 0; record = queue.peek()) {
10659
10797
  if (record.expiresAtMs > nowMs) return;
10798
+ const current = this.#terminalExpiryMs.get(record.key) === record.expiresAtMs;
10799
+ if (current && this.#withinOrderedHorizon(record.key)) return;
10660
10800
  queue.shift();
10661
- if (this.#terminalExpiryMs.get(record.key) !== record.expiresAtMs) continue;
10801
+ if (!current) continue;
10662
10802
  this.#terminalExpiryMs.delete(record.key);
10663
10803
  this.#completedBlocks.delete(record.key);
10664
10804
  this.#cleanlyCompletedBlocks.delete(record.key);
@@ -10669,12 +10809,143 @@ var FECManager = class {
10669
10809
  this.#unindexBlockKey(record.key);
10670
10810
  }
10671
10811
  }
10812
+ // -----------------------------------------------------------------------
10813
+ // Ordered-relay settlement (MulticastSymbolDelivery 'ordered-relay')
10814
+ // -----------------------------------------------------------------------
10815
+ /**
10816
+ * The decoder's interleave horizon for one stream, in blocks: the same
10817
+ * `interleaveDepth` the Worker and the WASM decoder are configured with
10818
+ * (fec-worker-setup derives it from the catalog interleaveDepthMs and the
10819
+ * media-unit duration). The decoder already retires every block more than
10820
+ * this many SBNs behind the newest one it saw (`cleanup_by_sbn`), so it is
10821
+ * the stream-order reorder allowance the receiver has already committed to.
10822
+ */
10823
+ #orderedHorizonBlocksFor(packetId, config) {
10824
+ const horizon = this.#derivableOrderedHorizon(config);
10825
+ if (horizon === void 0) {
10826
+ throw new Error(
10827
+ `[FECManager] packetId=${packetId}: ordered-relay settlement derives the decoder's interleave horizon from the catalog interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS}ms) and the media-unit rate; got interleaveDepthMs=${config.interleaveDepthMs} fps=${String(config.fps)}.`
10828
+ );
10829
+ }
10830
+ return horizon;
10831
+ }
10832
+ #derivableOrderedHorizon(config) {
10833
+ if (!(config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) || config.fps === void 0 || !Number.isFinite(config.fps) || config.fps <= 0) {
10834
+ return void 0;
10835
+ }
10836
+ return deriveFecInterleaveDepth(config.interleaveDepthMs, 1e3 / config.fps);
10837
+ }
10838
+ /**
10839
+ * The stream's interleave horizon, or undefined when its geometry cannot
10840
+ * define one. Explicitly configured tracks are refused that case at config
10841
+ * time (#upsertTrackConfig, setMulticastSymbolDelivery); only a packetId that
10842
+ * was never configured, and so resolves to the manager defaults, reaches it
10843
+ * here, and such a stream keeps datagram semantics rather than throwing from
10844
+ * the packet-feed path. Those defaults can change under a stream that already
10845
+ * holds blocks, so a caller cannot assume a horizon that was defined when a
10846
+ * block was held is still defined (see #advanceOrderedFrontier).
10847
+ */
10848
+ #orderedHorizonBlocks(packetId) {
10849
+ return this.#derivableOrderedHorizon(this.#resolveTrackConfig(packetId));
10850
+ }
10851
+ /** True while a block of an ordered-relay stream can still receive symbols. */
10852
+ #withinOrderedHorizon(key) {
10853
+ if (this.#multicastSymbolDelivery !== "ordered-relay") return false;
10854
+ const block = this.#parseBlockKey(key);
10855
+ const frontier = this.#orderedFrontiers.get(
10856
+ this.#streamKey(block.packetId, block.catalogTrackName)
10857
+ );
10858
+ if (frontier === void 0) return false;
10859
+ const horizon = this.#orderedHorizonBlocks(block.packetId);
10860
+ if (horizon === void 0) return false;
10861
+ return block.sbn >= frontier - horizon;
10862
+ }
10863
+ /** Whether a block's fate is decided by stream order rather than a timer. */
10864
+ #settlesByOrder(key) {
10865
+ return this.#multicastSymbolDelivery === "ordered-relay" && (this.#blockPathCounts.get(key)?.multicastSymbols ?? 0) > 0 && this.#orderedHorizonBlocks(this.#parseBlockKey(key).packetId) !== void 0;
10866
+ }
10867
+ #forgetOrderedPending(key) {
10868
+ if (this.#orderedPendingBlocks.size === 0) return;
10869
+ const block = this.#parseBlockKey(key);
10870
+ const streamKey = this.#streamKey(block.packetId, block.catalogTrackName);
10871
+ const pending = this.#orderedPendingBlocks.get(streamKey);
10872
+ if (pending?.get(block.sbn) !== key) return;
10873
+ pending.delete(block.sbn);
10874
+ if (pending.size === 0) this.#orderedPendingBlocks.delete(streamKey);
10875
+ }
10876
+ /**
10877
+ * Record the stream position of a multicast symbol and settle every pending
10878
+ * block it proves final: a block more than the interleave horizon behind the
10879
+ * frontier has already been handed every symbol the relay will deliver.
10880
+ */
10881
+ #advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName) {
10882
+ const previous = this.#orderedFrontiers.get(streamKey);
10883
+ const frontier = previous === void 0 || sbn > previous ? sbn : previous;
10884
+ if (frontier !== previous) this.#orderedFrontiers.set(streamKey, frontier);
10885
+ const pending = this.#orderedPendingBlocks.get(streamKey);
10886
+ if (!pending) return;
10887
+ const blocks = this.#orderedHorizonBlocks(packetId);
10888
+ if (blocks === void 0) {
10889
+ this.#orderedPendingBlocks.delete(streamKey);
10890
+ for (const pendingSbn of pending.keys()) {
10891
+ this.noteBlockActivity(pendingSbn, packetId, catalogTrackName);
10892
+ }
10893
+ return;
10894
+ }
10895
+ const horizon = frontier - blocks;
10896
+ for (const [pendingSbn, key] of pending) {
10897
+ if (pendingSbn >= horizon) continue;
10898
+ pending.delete(pendingSbn);
10899
+ this.#settleOrderedBlock(key, pendingSbn, packetId, catalogTrackName);
10900
+ }
10901
+ if (pending.size === 0 && this.#orderedPendingBlocks.get(streamKey) === pending) {
10902
+ this.#orderedPendingBlocks.delete(streamKey);
10903
+ }
10904
+ }
10905
+ /**
10906
+ * Decide a block the ordered relay has finished delivering. Its accounting
10907
+ * is final: SSB_length (from any repair symbol) against the source and
10908
+ * repair symbols it holds says whether the decoder can still recover it.
10909
+ * A block short of that asks for repair when D-28 is wired, under the same
10910
+ * post-request fetch budget the datagram path grants; otherwise, and for a
10911
+ * block the decoder holds enough symbols for, the decoder finalizes it
10912
+ * exactly as a datagram deadline would (a Worker acknowledges the cleanup
10913
+ * only after every earlier symbol, so an in-flight recovery still wins).
10914
+ */
10915
+ #settleOrderedBlock(key, sbn, packetId, catalogTrackName) {
10916
+ if (this.#completedBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairRequestedBlocks.has(key)) return;
10917
+ const authoredLength = this.#blockAuthoredLengths.get(key);
10918
+ const received = (this.#blockSourceCounts.get(key) ?? 0) + (this.#blockRepairRsIds.get(key)?.size ?? 0);
10919
+ if (this.onRepairNeeded && (authoredLength === void 0 || received < authoredLength)) {
10920
+ this.#requestRepair(sbn, packetId, catalogTrackName, true);
10921
+ return;
10922
+ }
10923
+ this.#traceLifecycle("horizon-settled", sbn, packetId, catalogTrackName);
10924
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
10925
+ }
10926
+ /** Register a block whose multicast symbols arrive over an ordered relay. */
10927
+ #holdForOrderedSettlement(key, sbn, packetId, catalogTrackName) {
10928
+ this.#clearRepairDeadline(key);
10929
+ if (this.#activeBlocks.has(key)) {
10930
+ this.#clearBlockTimer(key);
10931
+ this.#unindexBlockKey(key);
10932
+ }
10933
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
10934
+ let pending = this.#orderedPendingBlocks.get(streamKey);
10935
+ if (!pending) {
10936
+ pending = /* @__PURE__ */ new Map();
10937
+ this.#orderedPendingBlocks.set(streamKey, pending);
10938
+ }
10939
+ pending.set(sbn, key);
10940
+ }
10672
10941
  /** Cancel and forget a block's inactivity timer and deadline. */
10673
10942
  #clearBlockTimer(key) {
10674
10943
  const timer = this.#activeBlocks.get(key);
10675
10944
  if (timer?.handle != null) clearTimeout(timer.handle);
10676
10945
  this.#activeBlocks.delete(key);
10677
10946
  this.#activeBlockDeadlinesMs.delete(key);
10947
+ this.#blockWaitDeadlinesMs.delete(key);
10948
+ this.#repairHoldUntilMs.delete(key);
10678
10949
  }
10679
10950
  #parseBlockKey(key) {
10680
10951
  const [packetIdText, sbnText, ...catalogParts] = key.split(":");
@@ -10799,7 +11070,7 @@ var FECManager = class {
10799
11070
  timer.handle = setTimeout(onFire, delayMs);
10800
11071
  this.#repairDeadlines.set(key, timer);
10801
11072
  }
10802
- #requestRepair(sbn, packetId, catalogTrackName) {
11073
+ #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
10803
11074
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
10804
11075
  if (this.#completedBlocks.has(key) || !this.onRepairNeeded || this.#repairRequestedBlocks.has(key)) return;
10805
11076
  const deadline = this.#repairDeadlines.get(key);
@@ -10807,16 +11078,7 @@ var FECManager = class {
10807
11078
  this.#repairDeadlines.set(key, null);
10808
11079
  this.#repairDeadlineAtMs.delete(key);
10809
11080
  this.#repairRequestedBlocks.add(key);
10810
- const config = this.#resolveTrackConfig(packetId);
10811
- const graceMs = this.#repairGraceMs(config);
10812
- const now = performance.now();
10813
- const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now;
10814
- this.#armBlockTimeout(
10815
- sbn,
10816
- packetId,
10817
- catalogTrackName,
10818
- Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now))
10819
- );
11081
+ if (settledByOrder) this.#armBlockTimeout(sbn, packetId, catalogTrackName, 0);
10820
11082
  this.#traceLifecycle("repair-requested", sbn, packetId, catalogTrackName);
10821
11083
  this.onRepairNeeded(
10822
11084
  sbn,
@@ -11062,8 +11324,55 @@ var FECManager = class {
11062
11324
  }
11063
11325
  return this.#fecTimeoutMs;
11064
11326
  }
11065
- #repairDeadlineMs(config) {
11066
- const halfFailBoundMs = Math.ceil(config.timeoutMs / 2);
11327
+ /**
11328
+ * The delivery-gap window is the longest catalog keyframe interval among
11329
+ * the configured video streams. A failed video block costs every frame up
11330
+ * to the next keyframe, so a receiver must remember a stall for at least
11331
+ * that long: forgetting it sooner would let the next recurrence fail a
11332
+ * block whose damage outlives the memory of the stall that caused it. It
11333
+ * comes from the catalog's `keyframeIntervalTicks`/`keyframeIntervalMs`
11334
+ * (via the classifier geometry), never from a constant. It also caps the
11335
+ * gap the wait may absorb; with no catalog cadence no gap is reported and
11336
+ * the catalog tolerance alone applies (see DeliveryGapEstimator).
11337
+ */
11338
+ #refreshDeliveryGapWindow() {
11339
+ let windowMs;
11340
+ const consider = (geometry) => {
11341
+ if (geometry?.trackKind !== "video" || geometry.mode !== "classifying") return;
11342
+ const { ticks, timescale } = geometry.keyframeInterval;
11343
+ const ms = ticks / timescale * 1e3;
11344
+ if (Number.isFinite(ms) && ms > 0 && (windowMs === void 0 || ms > windowMs)) windowMs = ms;
11345
+ };
11346
+ for (const geometry of this.#streamGeometries.values()) consider(geometry);
11347
+ for (const config of this.#trackConfigs.values()) consider(config.geometry);
11348
+ this.#deliveryGaps.setWindowMs(windowMs);
11349
+ }
11350
+ /**
11351
+ * The multicast late-symbol wait: the block span plus the larger of the
11352
+ * catalog's signaled tolerance and the receiver-measured delivery gap.
11353
+ *
11354
+ * `config.timeoutMs` already is span + `fec.reorderToleranceMs` (or the
11355
+ * wider envelope configure() applies), so it stays the floor; the measured
11356
+ * gap widens it only when the path stalls for longer than the catalog
11357
+ * allowed for. Datagram delivery only: an ordered relay settles blocks by
11358
+ * stream order and never feeds the estimator.
11359
+ */
11360
+ #multicastWaitMs(config) {
11361
+ return this.#waitForGapMs(config, this.#deliveryGaps.peakGapMs(performance.now()));
11362
+ }
11363
+ /**
11364
+ * The widest #multicastWaitMs can become: the estimator reports at most its
11365
+ * catalog window, so this is a catalog quantity that no measurement moves.
11366
+ */
11367
+ #maxMulticastWaitMs(config) {
11368
+ return this.#waitForGapMs(config, this.#deliveryGaps.windowMs ?? 0);
11369
+ }
11370
+ #waitForGapMs(config, gapMs) {
11371
+ if (!(gapMs > 0) || !(config.interleaveDepthMs > 0)) return config.timeoutMs;
11372
+ return Math.max(config.timeoutMs, Math.ceil(config.interleaveDepthMs + gapMs));
11373
+ }
11374
+ #repairDeadlineMs(config, waitMs = this.#multicastWaitMs(config)) {
11375
+ const halfFailBoundMs = Math.ceil(waitMs / 2);
11067
11376
  if (config.k > 0 && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0) {
11068
11377
  const mediaUnitMs = 1e3 / config.fps;
11069
11378
  const interleaveUnits = deriveFecInterleaveDepth(
@@ -11071,21 +11380,52 @@ var FECManager = class {
11071
11380
  mediaUnitMs
11072
11381
  );
11073
11382
  const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs);
11074
- if (sourceHorizonMs < config.timeoutMs) return sourceHorizonMs;
11383
+ if (sourceHorizonMs < waitMs) return sourceHorizonMs;
11075
11384
  }
11076
11385
  return halfFailBoundMs;
11077
11386
  }
11078
- /** One block lifetime: the inactivity fail bound plus the D-28 repair grace. */
11387
+ /**
11388
+ * One block lifetime: the inactivity fail bound plus the D-28 repair grace,
11389
+ * at the widest wait the catalog allows (#maxMulticastWaitMs). Everything
11390
+ * derived from it (terminal retention, the release gate's hold ceiling, the
11391
+ * duplicate ledgers, the owed-cleanup bound) is compared across time, so it
11392
+ * must not follow the live measurement.
11393
+ */
11079
11394
  #blockLifetimeMs(config) {
11080
- return config.timeoutMs + this.#repairGraceMs(config);
11395
+ const waitMs = this.#maxMulticastWaitMs(config);
11396
+ return waitMs + this.#repairGraceMs(config, waitMs);
11081
11397
  }
11082
- #repairGraceMs(config) {
11083
- return Math.max(1, config.timeoutMs - this.#repairDeadlineMs(config));
11398
+ /**
11399
+ * The lifetime at the live wait, capped at #blockLifetimeMs: lifetime is not
11400
+ * monotone in the wait (the repair deadline switches from half the wait to
11401
+ * the source horizon), so the cap is what keeps it inside the retention.
11402
+ */
11403
+ #liveBlockLifetimeMs(config) {
11404
+ const waitMs = this.#multicastWaitMs(config);
11405
+ return Math.min(waitMs + this.#repairGraceMs(config, waitMs), this.#blockLifetimeMs(config));
11406
+ }
11407
+ #repairGraceMs(config, waitMs) {
11408
+ return Math.max(1, waitMs - this.#repairDeadlineMs(config, waitMs));
11084
11409
  }
11085
11410
  #armBlockTimeout(sbn, packetId, catalogTrackName, delayMs) {
11086
11411
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
11087
- const boundedDelayMs = Math.max(1, Math.ceil(delayMs));
11088
- const deadlineMs = performance.now() + boundedDelayMs;
11412
+ const waitAtMs = performance.now() + Math.max(1, Math.ceil(delayMs));
11413
+ const holdAtMs = this.#repairHoldUntilMs.get(key);
11414
+ this.#blockWaitDeadlinesMs.set(key, holdAtMs === void 0 ? waitAtMs : Math.min(waitAtMs, holdAtMs));
11415
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
11416
+ }
11417
+ /**
11418
+ * Arm (or move) a block's one fail timer at the later of its multicast
11419
+ * wait and its repair hold. Moving it later costs no timer operation: the
11420
+ * armed timer wakes no later than the new deadline and re-arms for the
11421
+ * remainder.
11422
+ */
11423
+ #applyBlockDeadline(key, sbn, packetId, catalogTrackName) {
11424
+ const nowMs = performance.now();
11425
+ const deadlineMs = Math.max(
11426
+ this.#blockWaitDeadlinesMs.get(key) ?? nowMs + 1,
11427
+ this.#repairHoldUntilMs.get(key) ?? -Infinity
11428
+ );
11089
11429
  this.#activeBlockDeadlinesMs.set(key, deadlineMs);
11090
11430
  const armed = this.#activeBlocks.get(key);
11091
11431
  if (armed?.handle != null) {
@@ -11096,45 +11436,55 @@ var FECManager = class {
11096
11436
  const onFire = () => {
11097
11437
  if (this.#activeBlocks.get(key) !== timer) return;
11098
11438
  const currentDeadlineMs = this.#activeBlockDeadlinesMs.get(key);
11099
- const nowMs = performance.now();
11100
- if (currentDeadlineMs !== void 0 && currentDeadlineMs > nowMs) {
11439
+ const firedAtMs = performance.now();
11440
+ if (currentDeadlineMs !== void 0 && currentDeadlineMs > firedAtMs) {
11101
11441
  timer.fireAtMs = currentDeadlineMs;
11102
- timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - nowMs));
11442
+ timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - firedAtMs));
11103
11443
  return;
11104
11444
  }
11105
11445
  this.#activeBlocks.delete(key);
11106
11446
  this.#activeBlockDeadlinesMs.delete(key);
11447
+ this.#blockWaitDeadlinesMs.delete(key);
11448
+ this.#repairHoldUntilMs.delete(key);
11107
11449
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
11108
- if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
11109
- this.#failBlock(sbn, packetId, catalogTrackName);
11110
- return;
11111
- }
11112
- const syncOwner = this.#syncDecoderBlockOwners.get(key);
11113
- if (syncOwner) {
11114
- this.#routeSyncCleanup(syncOwner, sbn, packetId);
11115
- this.#failBlock(sbn, packetId, catalogTrackName);
11116
- return;
11117
- }
11118
- const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
11119
- if (workerClient) {
11120
- this.#pendingWorkerCleanups.add(key);
11121
- this.#pendingWorkerCleanupClients.set(key, workerClient);
11122
- this.#boundOwedCleanup(key, packetId, workerClient);
11123
- workerClient.cleanup(sbn);
11124
- } else {
11125
- const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
11126
- if (decoder) {
11127
- this.#routeSyncCleanup(decoder, sbn, packetId);
11128
- this.#failBlock(sbn, packetId, catalogTrackName);
11129
- } else {
11130
- this.#failBlock(sbn, packetId, catalogTrackName);
11131
- }
11132
- }
11450
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
11133
11451
  };
11134
- timer.handle = setTimeout(onFire, boundedDelayMs);
11452
+ timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
11135
11453
  this.#activeBlocks.set(key, timer);
11136
11454
  this.#indexBlockKey(key, sbn, packetId);
11137
11455
  }
11456
+ /**
11457
+ * Hand a block whose delivery is over to its decoder for finalization: the
11458
+ * datagram deadline fired, or the ordered relay moved past it.
11459
+ */
11460
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
11461
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
11462
+ if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
11463
+ this.#failBlock(sbn, packetId, catalogTrackName);
11464
+ return;
11465
+ }
11466
+ const syncOwner = this.#syncDecoderBlockOwners.get(key);
11467
+ if (syncOwner) {
11468
+ this.#routeSyncCleanup(syncOwner, sbn, packetId);
11469
+ this.#failBlock(sbn, packetId, catalogTrackName);
11470
+ return;
11471
+ }
11472
+ const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
11473
+ if (workerClient) {
11474
+ this.#pendingWorkerCleanups.add(key);
11475
+ this.#pendingWorkerCleanupClients.set(key, workerClient);
11476
+ this.#boundOwedCleanup(key, packetId, workerClient);
11477
+ workerClient.cleanup(sbn);
11478
+ } else {
11479
+ const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
11480
+ if (decoder) {
11481
+ this.#routeSyncCleanup(decoder, sbn, packetId);
11482
+ this.#failBlock(sbn, packetId, catalogTrackName);
11483
+ } else {
11484
+ this.#failBlock(sbn, packetId, catalogTrackName);
11485
+ }
11486
+ }
11487
+ }
11138
11488
  /**
11139
11489
  * Upsert one packetId's track configuration. With `catalogTrackName`, the
11140
11490
  * classifier geometry in `config` is that stream's own and is kept per
@@ -11159,12 +11509,17 @@ var FECManager = class {
11159
11509
  ...(config.geometry ?? previous.geometry) === void 0 ? {} : { geometry: config.geometry ?? previous.geometry }
11160
11510
  };
11161
11511
  next.timeoutMs = this.#computeTimeout(next);
11512
+ if (this.#multicastSymbolDelivery === "ordered-relay") {
11513
+ this.#orderedHorizonBlocksFor(packetId, next);
11514
+ }
11162
11515
  this.#trackConfigs.set(packetId, next);
11516
+ this.#refreshDeliveryGapWindow();
11163
11517
  let policyChanged = config.geometry !== void 0 && JSON.stringify(config.geometry) !== JSON.stringify(previous.geometry) || config.trackKind !== void 0 && config.trackKind !== previous.trackKind;
11164
11518
  if (streamScopedGeometry !== void 0) {
11165
11519
  const streamKey = this.#streamKey(packetId, catalogTrackName);
11166
11520
  if (JSON.stringify(streamScopedGeometry) !== JSON.stringify(this.#streamGeometries.get(streamKey))) {
11167
11521
  this.#streamGeometries.set(streamKey, streamScopedGeometry);
11522
+ this.#refreshDeliveryGapWindow();
11168
11523
  policyChanged = true;
11169
11524
  }
11170
11525
  }
@@ -11231,6 +11586,9 @@ var FECManager = class {
11231
11586
  pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
11232
11587
  decoderUnavailableEvents: this.#decoderUnavailableEvents,
11233
11588
  repairTrackedBlocks: this.#blockRepairRsIds.size,
11589
+ multicastSymbolDelivery: this.#multicastSymbolDelivery,
11590
+ orderedFrontiers: Object.fromEntries(this.#orderedFrontiers),
11591
+ orderedPendingBlocks: [...this.#orderedPendingBlocks.values()].reduce((total, pending) => total + pending.size, 0),
11234
11592
  lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry }))
11235
11593
  };
11236
11594
  }
@@ -11289,6 +11647,7 @@ var FECManager = class {
11289
11647
  }
11290
11648
  /** Start a new multicast transport join without discarding track/decoder state. */
11291
11649
  beginMulticastJoin() {
11650
+ this.#orderedFrontiers.clear();
11292
11651
  this.#startupAlignedPackets.clear();
11293
11652
  this.#startupPartialBlocks.clear();
11294
11653
  this.#startupRequireRepairReady = true;
@@ -11296,6 +11655,45 @@ var FECManager = class {
11296
11655
  this.#startupRepairBoundaryCounts.clear();
11297
11656
  this.#startupRepairFailOpenPackets.clear();
11298
11657
  }
11658
+ /** How the multicast leg currently delivers symbols. */
11659
+ get multicastSymbolDelivery() {
11660
+ return this.#multicastSymbolDelivery;
11661
+ }
11662
+ /**
11663
+ * Declare how the multicast leg delivers symbols (see
11664
+ * MulticastSymbolDelivery). The multicast provider owns this fact:
11665
+ * MulticastManager applies what the window.multicast subscription declares
11666
+ * on every join, and a consumer feeding packets itself declares its own leg.
11667
+ *
11668
+ * Switching to 'ordered-relay' requires every configured track to define
11669
+ * its interleave horizon and throws otherwise. Switching back to 'datagram'
11670
+ * gives every block awaiting ordered settlement the datagram inactivity
11671
+ * deadline from now.
11672
+ */
11673
+ setMulticastSymbolDelivery(delivery) {
11674
+ if (!isMulticastSymbolDelivery(delivery)) {
11675
+ throw new Error(
11676
+ `[FECManager] multicast symbol delivery must be 'datagram' or 'ordered-relay', got ${String(delivery)}`
11677
+ );
11678
+ }
11679
+ if (delivery === this.#multicastSymbolDelivery) return;
11680
+ this.#deliveryGaps.reset();
11681
+ if (delivery === "ordered-relay") {
11682
+ for (const [packetId, config] of this.#trackConfigs) {
11683
+ this.#orderedHorizonBlocksFor(packetId, config);
11684
+ }
11685
+ this.#multicastSymbolDelivery = delivery;
11686
+ return;
11687
+ }
11688
+ this.#multicastSymbolDelivery = delivery;
11689
+ const pending = [...this.#orderedPendingBlocks.values()].flatMap((blocks) => [...blocks.values()]);
11690
+ this.#orderedPendingBlocks.clear();
11691
+ this.#orderedFrontiers.clear();
11692
+ for (const key of pending) {
11693
+ const block = this.#parseBlockKey(key);
11694
+ this.noteBlockActivity(block.sbn, block.packetId, block.catalogTrackName);
11695
+ }
11696
+ }
11299
11697
  /** Begin a fresh decode epoch for one physical stream. */
11300
11698
  resetTrack(packetId, catalogTrackName) {
11301
11699
  if (!Number.isInteger(packetId) || packetId < 0) {
@@ -11325,6 +11723,8 @@ var FECManager = class {
11325
11723
  resetTimerMap(this.#activeBlocks);
11326
11724
  resetTimerMap(this.#repairDeadlines);
11327
11725
  resetMap(this.#activeBlockDeadlinesMs);
11726
+ resetMap(this.#blockWaitDeadlinesMs);
11727
+ resetMap(this.#repairHoldUntilMs);
11328
11728
  resetMap(this.#repairDeadlineAtMs);
11329
11729
  resetMap(this.#terminalExpiryMs);
11330
11730
  resetMap(this.#pendingWorkerCleanupClients);
@@ -11336,9 +11736,12 @@ var FECManager = class {
11336
11736
  resetMap(this.#blockAuthoredLengths);
11337
11737
  resetMap(this.#blockRepairRsIds);
11338
11738
  resetMap(this.#blockPathCounts);
11739
+ resetMap(this.#blockAnchorsMs);
11339
11740
  resetMap(this.#blockCatalogTrackNames);
11340
11741
  resetMap(this.#syncDecoderBlockOwners);
11341
11742
  resetMap(this.#startupRepairBoundaryCounts);
11743
+ resetMap(this.#orderedFrontiers);
11744
+ resetMap(this.#orderedPendingBlocks);
11342
11745
  resetSet(this.#pendingWorkerCleanups);
11343
11746
  resetSet(this.#completedBlocks);
11344
11747
  resetSet(this.#cleanlyCompletedBlocks);
@@ -11398,6 +11801,9 @@ var FECManager = class {
11398
11801
  feedSymbolFromSource(packetId, sbn, esi, _data, source, catalogTrackName) {
11399
11802
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
11400
11803
  const streamKey = this.#streamKey(packetId, catalogTrackName);
11804
+ if (source === "multicast" && this.#multicastSymbolDelivery === "ordered-relay") {
11805
+ this.#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName);
11806
+ }
11401
11807
  const k = this.kForPacket(packetId);
11402
11808
  const isSourceSymbol = esi >= 0 && esi < k;
11403
11809
  const isStartupPartial = this.#startupPartialBlocks.has(blockKey);
@@ -11452,6 +11858,7 @@ var FECManager = class {
11452
11858
  moqSymbols: 0,
11453
11859
  complete: false
11454
11860
  };
11861
+ if (!this.#blockAnchorsMs.has(blockKey)) this.#blockAnchorsMs.set(blockKey, performance.now());
11455
11862
  if (source === "multicast") counts.multicastSymbols++;
11456
11863
  else counts.moqSymbols++;
11457
11864
  this.#blockPathCounts.set(blockKey, counts);
@@ -11462,6 +11869,9 @@ var FECManager = class {
11462
11869
  if (this.#blockPathCounts.size > 512) {
11463
11870
  dropOldest(this.#blockPathCounts, this.#blockPathCounts.size - 256);
11464
11871
  }
11872
+ if (this.#blockAnchorsMs.size > 512) {
11873
+ dropOldest(this.#blockAnchorsMs, this.#blockAnchorsMs.size - 256);
11874
+ }
11465
11875
  if (this.#blockCatalogTrackNames.size > 512) {
11466
11876
  for (const key of dropOldest(
11467
11877
  this.#blockCatalogTrackNames,
@@ -11550,9 +11960,75 @@ var FECManager = class {
11550
11960
  interleaveDepthMsForPacket(packetId) {
11551
11961
  return this.#resolveTrackConfig(packetId).interleaveDepthMs;
11552
11962
  }
11553
- /** Resolved per-track block fail bound in milliseconds. */
11963
+ /**
11964
+ * Record one multicast MMTP packet arrival (every packet of the leg, FEC
11965
+ * symbol or not) for the delivery-gap measurement. `packetSequenceNumber`
11966
+ * is the MMTP packet_sequence_number, which increments by one per packet of
11967
+ * a packet_id: it is what separates a stall from a loss (see
11968
+ * DeliveryGapEstimator). Datagram delivery only.
11969
+ */
11970
+ noteMulticastArrival(packetId, packetSequenceNumber) {
11971
+ if (this.#multicastSymbolDelivery !== "datagram") return;
11972
+ this.#deliveryGaps.noteArrival(performance.now(), packetId, packetSequenceNumber);
11973
+ }
11974
+ /**
11975
+ * Resolved per-track multicast late-symbol wait in milliseconds: the span
11976
+ * plus the larger of the catalog tolerance and the measured delivery gap.
11977
+ */
11554
11978
  timeoutMsForPacket(packetId) {
11555
- return this.#resolveTrackConfig(packetId).timeoutMs;
11979
+ return this.#multicastWaitMs(this.#resolveTrackConfig(packetId));
11980
+ }
11981
+ /**
11982
+ * Largest multicast delivery gap measured over the current window (ms), and
11983
+ * that window (undefined: held since the last reset). Diagnostics.
11984
+ */
11985
+ multicastDeliveryGap() {
11986
+ return {
11987
+ peakGapMs: this.#deliveryGaps.peakGapMs(performance.now()),
11988
+ windowMs: this.#deliveryGaps.windowMs
11989
+ };
11990
+ }
11991
+ /**
11992
+ * Receiver-local time of a block's first accepted symbol, or undefined for
11993
+ * a block no symbol reached (one materialized because a later SBN arrived).
11994
+ */
11995
+ blockAnchorAtMs(sbn, packetId, catalogTrackName) {
11996
+ return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
11997
+ }
11998
+ /** Absolute fail deadline of a still-open block, or undefined. */
11999
+ blockDeadlineAtMs(sbn, packetId, catalogTrackName) {
12000
+ return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
12001
+ }
12002
+ /**
12003
+ * Keep a block open until `untilAtMs` while a repair FETCH the scheduler
12004
+ * admitted is in flight. Never shortens the multicast wait already armed;
12005
+ * while set, the hold is the block's whole deadline, so no later symbol
12006
+ * extends it. Returns false when the block is no longer open (resolved, or
12007
+ * not yet tracked).
12008
+ */
12009
+ holdBlockForRepair(sbn, packetId, catalogTrackName, untilAtMs) {
12010
+ if (!Number.isFinite(untilAtMs)) {
12011
+ throw new Error(`[FECManager] repair hold must be a finite time (got ${untilAtMs})`);
12012
+ }
12013
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
12014
+ if (!this.#activeBlocks.has(key)) return false;
12015
+ const previous = this.#repairHoldUntilMs.get(key);
12016
+ if (previous !== void 0 && previous >= untilAtMs) return true;
12017
+ this.#repairHoldUntilMs.set(
12018
+ key,
12019
+ Math.max(untilAtMs, this.#blockWaitDeadlinesMs.get(key) ?? -Infinity)
12020
+ );
12021
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
12022
+ return true;
12023
+ }
12024
+ /**
12025
+ * Drop a block's repair hold: its FETCH ended without delivering. The block
12026
+ * falls back to its multicast wait, which may already have passed.
12027
+ */
12028
+ releaseRepairHold(sbn, packetId, catalogTrackName) {
12029
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
12030
+ if (!this.#repairHoldUntilMs.delete(key) || !this.#activeBlocks.has(key)) return;
12031
+ this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
11556
12032
  }
11557
12033
  /** Resolved per-track D-28 repair request deadline in milliseconds. */
11558
12034
  repairDeadlineMsForPacket(packetId) {
@@ -11921,7 +12397,7 @@ var FECManager = class {
11921
12397
  return;
11922
12398
  }
11923
12399
  this.#settleOwedCleanup(key, "cleanup-ack-timeout");
11924
- }, Math.ceil(this.#blockLifetimeMs(this.#resolveTrackConfig(packetId))))
12400
+ }, Math.ceil(this.#liveBlockLifetimeMs(this.#resolveTrackConfig(packetId))))
11925
12401
  };
11926
12402
  this.#pendingWorkerCleanupBounds.set(key, bound);
11927
12403
  }
@@ -12239,14 +12715,19 @@ var FECManager = class {
12239
12715
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
12240
12716
  if (this.#completedBlocks.has(key)) return;
12241
12717
  if (this.#pendingWorkerCleanups.has(key)) return;
12242
- if (this.#repairRequestedBlocks.has(key)) return;
12718
+ const repairRequested = this.#repairRequestedBlocks.has(key);
12719
+ if (repairRequested && this.#settlesByOrder(key)) return;
12720
+ if (this.#settlesByOrder(key)) {
12721
+ this.#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName);
12722
+ return;
12723
+ }
12243
12724
  const trackConfig = this.#resolveTrackConfig(packetId);
12244
- const timeoutMs = trackConfig.timeoutMs;
12725
+ const timeoutMs = this.#multicastWaitMs(trackConfig);
12245
12726
  if (!(timeoutMs > 0)) {
12246
12727
  throw new Error(`[FECManager] packetId=${packetId} has no positive fail bound (timeoutMs=${timeoutMs})`);
12247
12728
  }
12248
12729
  const repairDeadline = this.#repairDeadlines.get(key);
12249
- if (this.onRepairNeeded && (repairDeadline === void 0 || sourceProgress)) {
12730
+ if (this.onRepairNeeded && !repairRequested && (repairDeadline === void 0 || sourceProgress)) {
12250
12731
  this.#armRepairDeadline(
12251
12732
  key,
12252
12733
  sbn,
@@ -12377,6 +12858,9 @@ var FECManager = class {
12377
12858
  }
12378
12859
  this.#activeBlocks.clear();
12379
12860
  this.#activeBlockDeadlinesMs.clear();
12861
+ this.#blockWaitDeadlinesMs.clear();
12862
+ this.#repairHoldUntilMs.clear();
12863
+ this.#deliveryGaps.reset();
12380
12864
  this.#pendingWorkerCleanups.clear();
12381
12865
  this.#pendingWorkerCleanupClients.clear();
12382
12866
  for (const bound of this.#pendingWorkerCleanupBounds.values()) clearTimeout(bound.handle);
@@ -12404,8 +12888,11 @@ var FECManager = class {
12404
12888
  this.#startupRepairReadyPackets.clear();
12405
12889
  this.#startupRepairBoundaryCounts.clear();
12406
12890
  this.#startupRepairFailOpenPackets.clear();
12891
+ this.#orderedFrontiers.clear();
12892
+ this.#orderedPendingBlocks.clear();
12407
12893
  this.#seenSymbols.clear();
12408
12894
  this.#blockPathCounts.clear();
12895
+ this.#blockAnchorsMs.clear();
12409
12896
  this.#blockCatalogTrackNames.clear();
12410
12897
  this.#classifiers.clear();
12411
12898
  this.#streamGeometries.clear();
@@ -14798,6 +15285,7 @@ var StreamDuplicateLedgers = class {
14798
15285
 
14799
15286
  // src/mmtp-router.ts
14800
15287
  var KEYFRAME_LOST_TIMEOUT_SLACK = 1.5;
15288
+ var MAX_TIMER_DELAY_MS = 2147483647;
14801
15289
  function isRouterDebugEnabled() {
14802
15290
  const globalDebug = globalThis.__MMTP_ROUTER_DEBUG;
14803
15291
  if (globalDebug === true || globalDebug === "true" || globalDebug === "1") return true;
@@ -14967,8 +15455,10 @@ var MmtpRouter = class {
14967
15455
  #moqRepairOwnerCounts = {};
14968
15456
  #parserDiagnostics = /* @__PURE__ */ new Map();
14969
15457
  // ── Keyframe loss detection state ───────────────────────────────
14970
- /** Catalog-derived keyframe loss timeout (ms). null = not configured (throws on video track registration). */
15458
+ /** Catalog-derived keyframe loss timeout (ms), or a reader evaluated at each arm. null = not configured (warns on video track registration; Phase-13 keyframe repair stays off). */
14971
15459
  #keyframeLostTimeoutMs;
15460
+ /** Why the last reader evaluation left the timer unarmed; null while arming. Dedups the warning. */
15461
+ #keyframeLostUnarmedReason = null;
14972
15462
  /** Active keyframe loss timer handle. Single timer per video track — cleared on each rapFlag and on dispose(). */
14973
15463
  #keyframeLostTimer = null;
14974
15464
  /** Monotonic counter incremented on each emitted video keyframe. Tracks current GOP position. */
@@ -15229,11 +15719,19 @@ var MmtpRouter = class {
15229
15719
  /**
15230
15720
  * Set the keyframe loss timeout after construction.
15231
15721
  * Must be called BEFORE setPacketIdType('video').
15232
- * Derive from explicit keyframe/group duration:
15722
+ * A fixed timeout derives from explicit keyframe/group duration:
15233
15723
  * Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).
15724
+ * A timeout whose inputs are measured after configuration (e.g. keyframe
15725
+ * cadence plus measured playout lateness) is passed as a reader, evaluated
15726
+ * each time the timer is armed. Both forms pass the same check at each arm:
15727
+ * a throw, or a value that is not finite, positive and within the timer
15728
+ * maximum, leaves that keyframe unarmed (fail closed per keyframe, not per
15729
+ * session). A fixed NaN, e.g. derived from a catalog without a group
15730
+ * duration, therefore never reaches setTimeout.
15234
15731
  */
15235
15732
  setKeyframeLostTimeoutMs(ms) {
15236
15733
  this.#keyframeLostTimeoutMs = ms;
15734
+ this.#keyframeLostUnarmedReason = null;
15237
15735
  }
15238
15736
  /** Update MFU reassembly timeout. Source and FEC-recovery paths share the
15239
15737
  * parser's single reassembler (BLO-8151), so one update covers both. */
@@ -15727,16 +16225,49 @@ var MmtpRouter = class {
15727
16225
  // ── Private: keyframe loss detection ────────────────────────────
15728
16226
  /** Arm (or re-arm) the keyframe loss timer.
15729
16227
  * Clears any existing timer and sets a new one for keyframeLostTimeoutMs.
15730
- * On timeout, fires onKeyframeLost with the next expected groupId. */
16228
+ * On timeout, fires onKeyframeLost with the next expected groupId.
16229
+ * A reader without a valid value leaves the timer cleared and unarmed. */
15731
16230
  #armKeyframeLostTimer() {
15732
16231
  if (this.#keyframeLostTimer) {
15733
16232
  clearTimeout(this.#keyframeLostTimer);
16233
+ this.#keyframeLostTimer = null;
15734
16234
  }
16235
+ const timeoutMs = this.#readKeyframeLostTimeoutMs();
16236
+ if (timeoutMs === null) return;
15735
16237
  const expectedNext = this.#lastKeyframeGroupId + 1;
15736
16238
  this.#keyframeLostTimer = setTimeout(() => {
15737
16239
  this.#keyframeLostTimer = null;
15738
16240
  this.#onKeyframeLost?.(expectedNext);
15739
- }, this.#keyframeLostTimeoutMs);
16241
+ }, timeoutMs);
16242
+ }
16243
+ /** The configured number, or the reader's current value; null when the
16244
+ * reader throws or either form yields a value no timer can honour. Warns
16245
+ * on entering that state or when its cause changes (a different exception,
16246
+ * or a different class of bad value), not once per keyframe. */
16247
+ #readKeyframeLostTimeoutMs() {
16248
+ const timeout = this.#keyframeLostTimeoutMs;
16249
+ let reason;
16250
+ let detail;
16251
+ try {
16252
+ const ms = typeof timeout === "function" ? timeout() : timeout;
16253
+ if (Number.isFinite(ms) && ms > 0 && ms <= MAX_TIMER_DELAY_MS) {
16254
+ this.#keyframeLostUnarmedReason = null;
16255
+ return ms;
16256
+ }
16257
+ reason = typeof ms !== "number" ? "yielded a non-number" : Number.isNaN(ms) ? "yielded NaN" : !Number.isFinite(ms) ? "yielded an infinite value" : ms <= 0 ? "yielded a non-positive value" : `exceeds the ${MAX_TIMER_DELAY_MS} ms timer maximum`;
16258
+ detail = ms;
16259
+ } catch (error) {
16260
+ reason = `threw ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`;
16261
+ detail = error;
16262
+ }
16263
+ if (reason !== this.#keyframeLostUnarmedReason) {
16264
+ this.#keyframeLostUnarmedReason = reason;
16265
+ console.warn(
16266
+ `[MmtpRouter] keyframeLostTimeoutMs ${reason} \u2014 keyframe loss timer not armed until it yields a finite positive value:`,
16267
+ detail
16268
+ );
16269
+ }
16270
+ return null;
15740
16271
  }
15741
16272
  #noteEmittedKeyframe(packetId, keyframe) {
15742
16273
  if (!keyframe) return;
@@ -15950,6 +16481,9 @@ var MmtpRouter = class {
15950
16481
  const parsed = this.#parser.parsePacket(packet);
15951
16482
  if (!parsed) return;
15952
16483
  const info = parsed.info;
16484
+ if (source === "multicast" && !manifestAdmitted) {
16485
+ this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
16486
+ }
15953
16487
  if (source === "multicast" && !ingressHasPacketId(ingress, info.packetId)) {
15954
16488
  debugRouter(
15955
16489
  `[MmtpRouter] Dropping multicast packet pid=${info.packetId}: absent from ingress tracks`
@@ -18901,6 +19435,8 @@ var MulticastTransport = class _MulticastTransport {
18901
19435
  #dataPlaneLive = false;
18902
19436
  #onDataPlaneLive = null;
18903
19437
  #errorListener = null;
19438
+ #onDelivery = null;
19439
+ #delivery;
18904
19440
  #disposed = false;
18905
19441
  #stats = {
18906
19442
  packetsReceived: 0,
@@ -18910,6 +19446,11 @@ var MulticastTransport = class _MulticastTransport {
18910
19446
  packetsDroppedSignaling: 0
18911
19447
  };
18912
19448
  #droppedPidCounts = /* @__PURE__ */ new Map();
19449
+ // Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each
19450
+ // value is a mutable cell, so the per-packet update is one lookup and an
19451
+ // in-place add: no insertion once the key exists, and no re-boxed number
19452
+ // per packet once a count outgrows the small-integer range.
19453
+ #acceptedBytesByPacketId = /* @__PURE__ */ new Map();
18913
19454
  #packetIds = null;
18914
19455
  #joinGeneration = 0;
18915
19456
  constructor(onPacket) {
@@ -18971,7 +19512,22 @@ var MulticastTransport = class _MulticastTransport {
18971
19512
  this.#subscription = subscription;
18972
19513
  this.#packetListener = packetListener;
18973
19514
  this.#packetIds = packetIds;
19515
+ if (packetIds) {
19516
+ for (const packetId of packetIds) {
19517
+ if (!this.#acceptedBytesByPacketId.has(packetId)) {
19518
+ this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 });
19519
+ }
19520
+ }
19521
+ }
18974
19522
  this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
19523
+ const delivery = subscription.delivery ?? "datagram";
19524
+ if (delivery !== "datagram" && delivery !== "ordered-relay") {
19525
+ throw new Error(
19526
+ `window.multicast subscription declared unknown delivery ${String(delivery)}`
19527
+ );
19528
+ }
19529
+ this.#delivery = delivery;
19530
+ this.#onDelivery?.(delivery);
18975
19531
  const onError = (event) => {
18976
19532
  if (this.#disposed || generation !== this.#joinGeneration) return;
18977
19533
  this.#failBackOnSubscriptionError(event.detail);
@@ -19005,8 +19561,6 @@ var MulticastTransport = class _MulticastTransport {
19005
19561
  return false;
19006
19562
  }
19007
19563
  if (!this.#acceptPacket(packet)) return false;
19008
- this.#stats.packetsReceived++;
19009
- this.#stats.bytesReceived += packet.byteLength;
19010
19564
  this.#onPacket(packet, context);
19011
19565
  return true;
19012
19566
  }
@@ -19052,8 +19606,6 @@ var MulticastTransport = class _MulticastTransport {
19052
19606
  if (this.#disposed) return;
19053
19607
  const packet = data instanceof Uint8Array ? data : new Uint8Array(data);
19054
19608
  if (!this.#acceptPacket(packet)) return;
19055
- this.#stats.packetsReceived++;
19056
- this.#stats.bytesReceived += packet.byteLength;
19057
19609
  this.#onPacket(packet, context);
19058
19610
  this.#resetWatchdog();
19059
19611
  }
@@ -19071,14 +19623,25 @@ var MulticastTransport = class _MulticastTransport {
19071
19623
  onFailback(cb) {
19072
19624
  this.#onFailback = cb;
19073
19625
  }
19626
+ /**
19627
+ * Called on every successful join, before the joined subscription can
19628
+ * deliver a packet, with how that subscription delivers packets.
19629
+ */
19630
+ onDelivery(cb) {
19631
+ this.#onDelivery = cb;
19632
+ }
19633
+ /** How the current subscription delivers packets, once joined. */
19634
+ get delivery() {
19635
+ return this.#delivery;
19636
+ }
19074
19637
  /**
19075
19638
  * Fires once per join, at the first packet the join accepts (after the PID
19076
19639
  * filter; a rejected packet is not liveness). This is the data-plane-live
19077
- * edge, where this transport's silence window starts. It reports progress; it
19078
- * arms nothing else. In particular, a host's takeover deadline starts when
19079
- * the subscribe settles, not here: a join whose source never sends never
19080
- * reaches this edge, and that deadline is what bounds it (see
19081
- * SessionManager.joinActive).
19640
+ * edge, where this transport's silence window starts. A join whose source
19641
+ * never sends never reaches it, so a host may start a media-arrival budget
19642
+ * here only if the provider bounds the join phase with a typed subscription
19643
+ * error (see SessionManager.joinActive); otherwise the host starts it when
19644
+ * the subscribe settles.
19082
19645
  */
19083
19646
  onDataPlaneLive(cb) {
19084
19647
  this.#onDataPlaneLive = cb;
@@ -19103,6 +19666,33 @@ var MulticastTransport = class _MulticastTransport {
19103
19666
  get stats() {
19104
19667
  return this.#stats;
19105
19668
  }
19669
+ /**
19670
+ * Accepted bytes summed over `packetIds`: the `bytesReceived` share of those
19671
+ * MMTP packet_ids. Counted by the same rule, on every path that counts
19672
+ * `bytesReceived` (only packets that pass the ingress filter; packet_id 0
19673
+ * signaling never), and cumulative for the life of the transport, across
19674
+ * rejoins, as `bytesReceived` is. An id never accepted contributes 0. An
19675
+ * unparseable packet admitted by a join without an allow-list has no
19676
+ * packet_id, so it counts toward `bytesReceived` only.
19677
+ *
19678
+ * Use it to scope an ABR throughput sample to the active tier's source and
19679
+ * repair packet_ids when the join admits more than the active tier (a
19680
+ * shared group joined across rendition switches); `bytesReceived` is then
19681
+ * the whole allow-list's arrival, not the active tier's. A Set, so an id
19682
+ * shared by two tracks of the tier is counted once.
19683
+ *
19684
+ * Monotonic only for a fixed set: each id has counted since it was first
19685
+ * admitted, so after a tier switch the new set's sum is not a continuation
19686
+ * of the old one. A cumulative consumer takes a fresh baseline for the new
19687
+ * set and never differences across sets.
19688
+ */
19689
+ bytesReceivedForPacketIds(packetIds) {
19690
+ let bytes = 0;
19691
+ for (const packetId of packetIds) {
19692
+ bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0;
19693
+ }
19694
+ return bytes;
19695
+ }
19106
19696
  /**
19107
19697
  * Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct
19108
19698
  * PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`
@@ -19138,12 +19728,17 @@ var MulticastTransport = class _MulticastTransport {
19138
19728
  this.#packetListener = null;
19139
19729
  this.#errorListener = null;
19140
19730
  this.#resolvedTransport = void 0;
19731
+ this.#delivery = void 0;
19141
19732
  this.#packetIds = null;
19142
19733
  }
19734
+ /** The ingress filter; an accepted packet is counted here, once. */
19143
19735
  #acceptPacket(packet) {
19144
19736
  const packetId = readMMTPPacketId(packet);
19145
19737
  if (packetId === null) {
19146
- if (!this.#packetIds) return true;
19738
+ if (!this.#packetIds) {
19739
+ this.#countAccepted(packet, null);
19740
+ return true;
19741
+ }
19147
19742
  this.#stats.packetsDroppedUnparseable++;
19148
19743
  return false;
19149
19744
  }
@@ -19151,10 +19746,25 @@ var MulticastTransport = class _MulticastTransport {
19151
19746
  this.#stats.packetsDroppedSignaling++;
19152
19747
  return false;
19153
19748
  }
19154
- if (!this.#packetIds || this.#packetIds.has(packetId)) return true;
19749
+ if (!this.#packetIds || this.#packetIds.has(packetId)) {
19750
+ this.#countAccepted(packet, packetId);
19751
+ return true;
19752
+ }
19155
19753
  this.#recordUnregisteredPidDrop(packetId);
19156
19754
  return false;
19157
19755
  }
19756
+ /** Receive counters for one accepted packet; `packetId` is null when it has none. */
19757
+ #countAccepted(packet, packetId) {
19758
+ this.#stats.packetsReceived++;
19759
+ this.#stats.bytesReceived += packet.byteLength;
19760
+ if (packetId === null) return;
19761
+ const counter = this.#acceptedBytesByPacketId.get(packetId);
19762
+ if (counter !== void 0) {
19763
+ counter.bytes += packet.byteLength;
19764
+ } else {
19765
+ this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength });
19766
+ }
19767
+ }
19158
19768
  #recordUnregisteredPidDrop(packetId) {
19159
19769
  this.#stats.packetsDroppedUnregisteredPid++;
19160
19770
  const seen = this.#droppedPidCounts.get(packetId);
@@ -19240,6 +19850,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
19240
19850
  deltaBlocksRecoveredQuic: 0,
19241
19851
  deltaRepairsPreempted: 0,
19242
19852
  deltaRepairsDeferred: 0,
19853
+ repairsSkipped: 0,
19243
19854
  deltaBlocksFailed: 0,
19244
19855
  audioBlocksRecoveredInband: 0,
19245
19856
  audioBlocksRecoveredQuic: 0,
@@ -19630,6 +20241,9 @@ var MulticastManager = class {
19630
20241
  this.#transport = new MulticastTransport((packet, ingress) => {
19631
20242
  this.#router.feedPacket(packet, "multicast", void 0, ingress);
19632
20243
  });
20244
+ this.#transport.onDelivery((delivery) => {
20245
+ this.#fec.setMulticastSymbolDelivery(delivery);
20246
+ });
19633
20247
  this.#fec.onBlockRecovered = (sbn, packetId, data, meta, recoveredTrackName) => {
19634
20248
  const recovered = new Uint8Array(data);
19635
20249
  const catalogTrackName = recoveredTrackName ?? this.#fec.catalogTrackNameForBlock(sbn, packetId);
@@ -19736,6 +20350,16 @@ var MulticastManager = class {
19736
20350
  get stats() {
19737
20351
  return this.#transport.stats;
19738
20352
  }
20353
+ /**
20354
+ * Accepted bytes for these MMTP packet_ids, cumulative like
20355
+ * `stats.bytesReceived`; see MulticastTransport.bytesReceivedForPacketIds.
20356
+ * Scopes an ABR throughput sample to the active tier when the join admits
20357
+ * more packet_ids than that tier's. Monotonic only while the set is fixed:
20358
+ * re-baseline when the queried set changes (e.g. on a tier switch).
20359
+ */
20360
+ bytesReceivedForPacketIds(packetIds) {
20361
+ return this.#transport.bytesReceivedForPacketIds(packetIds);
20362
+ }
19739
20363
  get fecStats() {
19740
20364
  return this.#fec.fecStats;
19741
20365
  }
@@ -20011,7 +20635,8 @@ var MulticastManager = class {
20011
20635
  * Applies only to tracks WITHOUT FEC: an FEC track never arms the timer,
20012
20636
  * because its vanished blocks resolve through catalog-derived block
20013
20637
  * deadlines and the release gate's stall decision.
20014
- * Must be called BEFORE setPacketIdType('video').
20638
+ * Must be called BEFORE setPacketIdType('video'). A reader is passed
20639
+ * through; see MmtpRouter.setKeyframeLostTimeoutMs.
20015
20640
  */
20016
20641
  setKeyframeLostTimeoutMs(ms) {
20017
20642
  this.#router.setKeyframeLostTimeoutMs(ms);
@@ -20115,6 +20740,14 @@ var MulticastManager = class {
20115
20740
  groupAddress: config.groupAddress,
20116
20741
  port: config.port,
20117
20742
  mode: config.mode,
20743
+ // Precedence between two sources, not a tolerant alias: `amtRelay*` is
20744
+ // the player/UI override and `relay*` is the catalog endpoint's relay
20745
+ // (REQUIRED for `protocol: "amt"`; see multicast-input.ts). The override
20746
+ // wins, else the catalog value. This is the only inlet for the
20747
+ // endpoint-level catalog relay (`relayAddress` / `relayPort`); a catalog
20748
+ // `multicast.networkSource` AMT entry reaches the transport separately,
20749
+ // through `findAmtRelay`. Retiring `relayAddress` therefore belongs to
20750
+ // the coordinated `networkSource` migration.
20118
20751
  amtRelay: config.amtRelay ?? config.relayAddress,
20119
20752
  amtRelayPort: config.amtRelayPort ?? config.relayPort,
20120
20753
  packetIds: config.packetIds,
@@ -20133,6 +20766,14 @@ var MulticastManager = class {
20133
20766
  feedPacket(data, ingress) {
20134
20767
  this.#transport.feedPacket(data, ingress);
20135
20768
  }
20769
+ /**
20770
+ * Declare how packets given to feedPacket() reach this manager. A consumer
20771
+ * that owns the multicast socket itself declares its own leg; join()
20772
+ * applies what the window.multicast subscription declares instead.
20773
+ */
20774
+ setMulticastSymbolDelivery(delivery) {
20775
+ this.#fec.setMulticastSymbolDelivery(delivery);
20776
+ }
20136
20777
  /** Process a repair packet from MoQ (reliable QUIC). */
20137
20778
  processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
20138
20779
  this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
@@ -20349,6 +20990,18 @@ var MulticastManager = class {
20349
20990
  reason
20350
20991
  });
20351
20992
  }
20993
+ /** Record a repair request the scheduler sent no FETCH for. */
20994
+ noteRepairSkipped(reason, measurement, sbn, packetId, catalogTrackName) {
20995
+ this.#statsFor(packetId, catalogTrackName).repairsSkipped++;
20996
+ this.#emitRepairLifecycle({
20997
+ version: 2,
20998
+ type: "repair-skipped",
20999
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
21000
+ reason,
21001
+ rttMs: measurement.rttMs,
21002
+ budgetMs: measurement.budgetMs
21003
+ });
21004
+ }
20352
21005
  /**
20353
21006
  * Record a repair-scheduler decision about a delta block: preempted for
20354
21007
  * keyframe/audio work, or never dispatched before its block resolved.
@@ -20622,7 +21275,7 @@ var MediaRouter = class {
20622
21275
  // ── Keyframe repair state ────────────────────────────────────────
20623
21276
  /** Groups currently being repaired — idempotent guard per Pitfall 2 (T-13-01 mitigation) */
20624
21277
  #repairInFlight = /* @__PURE__ */ new Set();
20625
- /** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation) */
21278
+ /** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation). A reader is evaluated per decision. */
20626
21279
  #maxRepairInFlight;
20627
21280
  /** Callback to request MoQ subscribe for a specific group (wired by player) */
20628
21281
  #onRepairRequest = null;
@@ -20668,7 +21321,7 @@ var MediaRouter = class {
20668
21321
  this.#crossfadeFrames = options?.crossfadeFrames ?? DEFAULT_CROSSFADE_FRAMES;
20669
21322
  this.#maxOverlayBuffer = options?.maxOverlayBuffer ?? DEFAULT_MAX_OVERLAY_BUFFER;
20670
21323
  const maxRepairInFlight = options?.maxRepairInFlight;
20671
- if (maxRepairInFlight !== void 0 && (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1)) {
21324
+ if (maxRepairInFlight !== void 0 && typeof maxRepairInFlight !== "function" && (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1)) {
20672
21325
  throw new Error(
20673
21326
  `[MediaRouter] maxRepairInFlight=${maxRepairInFlight} must be a positive integer`
20674
21327
  );
@@ -20704,6 +21357,8 @@ var MediaRouter = class {
20704
21357
  /**
20705
21358
  * Request repair for a lost keyframe group. Only fires when activeSource is 'multicast'.
20706
21359
  * Idempotent per groupId — duplicate calls for the same group are suppressed.
21360
+ * A reader `maxRepairInFlight` is evaluated here; without a valid value the
21361
+ * group is skipped and a later call reads it again.
20707
21362
  * @param expectedGroupId - The group ID expected but not received
20708
21363
  * @param trackName - Track name for the SUBSCRIBE (e.g. 'video'). Default: 'video'.
20709
21364
  */
@@ -20711,9 +21366,25 @@ var MediaRouter = class {
20711
21366
  if (this.#active !== "multicast") return;
20712
21367
  if (this.#maxRepairInFlight === null) return;
20713
21368
  if (this.#repairInFlight.has(expectedGroupId)) return;
20714
- if (this.#repairInFlight.size >= this.#maxRepairInFlight) {
21369
+ let maxRepairInFlight;
21370
+ try {
21371
+ maxRepairInFlight = typeof this.#maxRepairInFlight === "function" ? this.#maxRepairInFlight() : this.#maxRepairInFlight;
21372
+ } catch (error) {
21373
+ console.warn(
21374
+ `[MediaRouter] maxRepairInFlight unmeasured, skipping repair for group ${expectedGroupId}:`,
21375
+ error
21376
+ );
21377
+ return;
21378
+ }
21379
+ if (!Number.isSafeInteger(maxRepairInFlight) || maxRepairInFlight < 1) {
21380
+ console.warn(
21381
+ `[MediaRouter] maxRepairInFlight=${maxRepairInFlight} is not a positive integer, skipping repair for group ${expectedGroupId}`
21382
+ );
21383
+ return;
21384
+ }
21385
+ if (this.#repairInFlight.size >= maxRepairInFlight) {
20715
21386
  console.warn(
20716
- `[MediaRouter] max repair in flight (${this.#maxRepairInFlight}), dropping repair for group ${expectedGroupId}`
21387
+ `[MediaRouter] max repair in flight (${maxRepairInFlight}), dropping repair for group ${expectedGroupId}`
20717
21388
  );
20718
21389
  return;
20719
21390
  }
@@ -22193,8 +22864,7 @@ function recordOf(value) {
22193
22864
  function msfTracks(catalog) {
22194
22865
  const raw = catalog.tracks;
22195
22866
  if (!Array.isArray(raw)) return void 0;
22196
- const common = recordOf(catalog.commonTrackFields) ?? {};
22197
- return { tracks: raw.map((t) => recordOf(t) ?? {}), common };
22867
+ return { tracks: raw.map((t) => recordOf(t) ?? {}) };
22198
22868
  }
22199
22869
  function resolveRepairLayers(catalog, source) {
22200
22870
  if (!catalog) return void 0;
@@ -22250,7 +22920,7 @@ function resolveRepairLayers(catalog, source) {
22250
22920
  if (!msf || msfIndex < 0) return void 0;
22251
22921
  const track = msf.tracks[msfIndex];
22252
22922
  const fec = recordOf(track["fec"]);
22253
- const packagingOf = (t) => t["packaging"] ?? msf.common["packaging"];
22923
+ const packagingOf = (t) => t["packaging"];
22254
22924
  const fecOfTrack = (t) => {
22255
22925
  const trackFec = recordOf(t?.["fec"]);
22256
22926
  return trackFec !== void 0 && trackFec["algorithm"] !== "none" ? trackFec : void 0;
@@ -22296,7 +22966,7 @@ function catalogRepairTarget(catalog, source) {
22296
22966
  const sourceName = typeof source === "string" ? source : source.name;
22297
22967
  const msf = msfTracks(catalog);
22298
22968
  const entry = msf?.tracks.find((t) => t["name"] === sourceName);
22299
- const sourcePriority = entry === void 0 ? null : moqtPriority2(entry["priority"] ?? msf.common["priority"]);
22969
+ const sourcePriority = entry === void 0 ? null : moqtPriority2(entry["priority"]);
22300
22970
  return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
22301
22971
  }
22302
22972
  function geometryOf(name, path, fec) {
@@ -22430,6 +23100,12 @@ function wireRepairFetch(mc, opts) {
22430
23100
  `[RepairFetch] attemptLimit=${String(opts.attemptLimit)} must be a positive integer`
22431
23101
  );
22432
23102
  }
23103
+ if (typeof opts.reliableRttMs !== "function") {
23104
+ throw new Error("[RepairFetch] reliableRttMs is required: repair admission needs the measured reliable-path RTT");
23105
+ }
23106
+ if (typeof opts.presentationDelayMs !== "function") {
23107
+ throw new Error("[RepairFetch] presentationDelayMs is required: a repair FETCH is due by the block's presentation deadline");
23108
+ }
22433
23109
  const entries = /* @__PURE__ */ new Map();
22434
23110
  const warnedNoTarget = /* @__PURE__ */ new Set();
22435
23111
  let disposed = false;
@@ -22443,12 +23119,41 @@ function wireRepairFetch(mc, opts) {
22443
23119
  const note = (type, entry) => {
22444
23120
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
22445
23121
  };
23122
+ const releaseHold = (entry) => {
23123
+ if (entry.catalogTrackName === void 0) mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId);
23124
+ else mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId, entry.catalogTrackName);
23125
+ };
22446
23126
  const stop = (entry, reason) => {
22447
23127
  const run = entry.running;
22448
23128
  if (!run) return;
22449
23129
  entry.running = null;
22450
23130
  run.stopped = reason;
22451
23131
  run.controller.abort();
23132
+ if (reason === "preempted") releaseHold(entry);
23133
+ };
23134
+ const readRtt = () => {
23135
+ try {
23136
+ const rtt = opts.reliableRttMs();
23137
+ return Number.isFinite(rtt) && rtt > 0 ? rtt : null;
23138
+ } catch {
23139
+ return null;
23140
+ }
23141
+ };
23142
+ const admit = (entry, now) => {
23143
+ const budgetMs = entry.deadlineAt - now;
23144
+ const rttMs = readRtt();
23145
+ if (rttMs !== null && rttMs < budgetMs) return true;
23146
+ retire(entry);
23147
+ releaseHold(entry);
23148
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
23149
+ mc.noteRepairSkipped(
23150
+ rttMs === null ? "rtt-unmeasured" : "rtt-exceeds-budget",
23151
+ { rttMs, budgetMs },
23152
+ entry.sbn,
23153
+ entry.packetId,
23154
+ entry.catalogTrackName
23155
+ );
23156
+ return false;
22452
23157
  };
22453
23158
  const retire = (entry) => {
22454
23159
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
@@ -22480,9 +23185,16 @@ function wireRepairFetch(mc, opts) {
22480
23185
  return derived.kind === "omit" ? { subscriberPriority: void 0 } : null;
22481
23186
  };
22482
23187
  const start = (entry, counted = true) => {
23188
+ if (!admit(entry, performance.now())) return;
22483
23189
  if (counted) entry.attempts++;
22484
23190
  const run = { controller: new AbortController(), stopped: null };
22485
23191
  entry.running = run;
23192
+ mc.fecManager.holdBlockForRepair(
23193
+ entry.sbn,
23194
+ entry.packetId,
23195
+ entry.catalogTrackName,
23196
+ entry.deadlineAt
23197
+ );
22486
23198
  void execute(entry, run);
22487
23199
  };
22488
23200
  const dispatch = () => {
@@ -22519,7 +23231,10 @@ function wireRepairFetch(mc, opts) {
22519
23231
  entry.running = null;
22520
23232
  if (error === void 0) {
22521
23233
  retire(entry);
22522
- if (fed === 0) clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
23234
+ if (fed === 0) {
23235
+ releaseHold(entry);
23236
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
23237
+ }
22523
23238
  dispatch();
22524
23239
  return;
22525
23240
  }
@@ -22529,6 +23244,7 @@ function wireRepairFetch(mc, opts) {
22529
23244
  return;
22530
23245
  }
22531
23246
  retire(entry);
23247
+ if (fed === 0) releaseHold(entry);
22532
23248
  clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
22533
23249
  console.warn(
22534
23250
  `[RepairFetch] fetch failed pid=${entry.packetId} sbn=${entry.sbn} track=${entry.repairTrack}:`,
@@ -22599,6 +23315,20 @@ function wireRepairFetch(mc, opts) {
22599
23315
  else if (entry.priority === 2 && entry.attempts === 0) note("repair-deferred", entry);
22600
23316
  dispatch();
22601
23317
  });
23318
+ const presentationDeadlineAt = (sbn, packetId, catalogTrackName, now) => {
23319
+ const waitDeadlineAt = now + mc.fecManager.repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName);
23320
+ const anchor = mc.fecManager.blockAnchorAtMs(sbn, packetId, catalogTrackName);
23321
+ if (anchor === void 0) return waitDeadlineAt;
23322
+ let delayMs;
23323
+ try {
23324
+ delayMs = opts.presentationDelayMs(packetId, catalogTrackName);
23325
+ } catch {
23326
+ return null;
23327
+ }
23328
+ if (!(Number.isFinite(delayMs) && delayMs > 0)) return null;
23329
+ const ceilingAt = anchor + mc.fecManager.terminalRetentionMsForPacket(packetId);
23330
+ return Math.max(waitDeadlineAt, Math.min(anchor + delayMs, ceilingAt));
23331
+ };
22602
23332
  mc.onRepairSubscribe((sbn, packetId, catalogTrackName, blockClass) => {
22603
23333
  if (disposed) {
22604
23334
  clearRepair(sbn, packetId, catalogTrackName);
@@ -22633,14 +23363,26 @@ function wireRepairFetch(mc, opts) {
22633
23363
  clearRepair(sbn, packetId, catalogTrackName);
22634
23364
  return;
22635
23365
  }
22636
- const budgetMs = mc.fecManager.repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName);
23366
+ const now = performance.now();
23367
+ const deadlineAt = presentationDeadlineAt(sbn, packetId, catalogTrackName, now);
23368
+ if (deadlineAt === null) {
23369
+ clearRepair(sbn, packetId, catalogTrackName);
23370
+ mc.noteRepairSkipped(
23371
+ "presentation-delay-unavailable",
23372
+ { rttMs: readRtt(), budgetMs: 0 },
23373
+ sbn,
23374
+ packetId,
23375
+ catalogTrackName
23376
+ );
23377
+ return;
23378
+ }
22637
23379
  const entry = {
22638
23380
  key,
22639
23381
  ...identity,
22640
23382
  repairTrack: target.repairTrack,
22641
23383
  priority: repairPriorityForClass(blockClass),
22642
23384
  subscriberPriority: wire.subscriberPriority,
22643
- deadlineAt: performance.now() + budgetMs,
23385
+ deadlineAt,
22644
23386
  order: enqueueOrder++,
22645
23387
  attempts: 0,
22646
23388
  running: null
@@ -22655,6 +23397,7 @@ function wireRepairFetch(mc, opts) {
22655
23397
  removeLifecycleObserver();
22656
23398
  for (const entry of [...entries.values()]) {
22657
23399
  clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
23400
+ if (entry.running) releaseHold(entry);
22658
23401
  stop(entry, "disposed");
22659
23402
  }
22660
23403
  entries.clear();
@@ -22670,6 +23413,7 @@ export {
22670
23413
  CmcdProducer,
22671
23414
  CmsfCatalogValidationError,
22672
23415
  DEFAULT_REPAIR_PRIORITY,
23416
+ DeliveryGapEstimator,
22673
23417
  FECManager,
22674
23418
  FEC_ESI_MAX,
22675
23419
  FEC_SBN_MAX,