@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 2.2.0-main.e75bbd7576a6

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 (73) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +703 -175
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +33 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +222 -6
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +9 -0
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +17 -0
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +37 -2
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +86 -9
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +89 -11
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +85 -9
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +18 -1
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +304 -39
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.js +526 -45
  48. package/dist/multicast-manager.js.map +2 -2
  49. package/dist/multicast-transport.js +26 -16
  50. package/dist/multicast-transport.js.map +2 -2
  51. package/dist/no-modules.js +85 -9
  52. package/dist/no-modules.js.map +2 -2
  53. package/dist/repair-layer-subscriptions.d.ts +6 -2
  54. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  55. package/dist/repair-layer-subscriptions.js +6 -3
  56. package/dist/repair-layer-subscriptions.js.map +2 -2
  57. package/dist/track-session.d.ts +1 -1
  58. package/dist/track-session.js.map +1 -1
  59. package/dist/wasm-mmtp-container-parser.js +85 -9
  60. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  61. package/package.json +8 -5
  62. package/schemas/catalog.schema.json +4 -0
  63. package/src/catalog-fec-params.ts +36 -1
  64. package/src/cmcd-producer.ts +189 -179
  65. package/src/fec-manager.ts +277 -6
  66. package/src/fec-worker-setup.ts +2 -2
  67. package/src/index.ts +7 -2
  68. package/src/iso-block-locator.ts +19 -0
  69. package/src/iso-bmff-timing.ts +40 -1
  70. package/src/mmtp-container-parser.ts +96 -8
  71. package/src/mmtp-router.ts +350 -13
  72. package/src/repair-layer-subscriptions.ts +13 -5
  73. package/src/track-session.ts +1 -1
@@ -1210,6 +1210,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1210
1210
  #retentionBlocks;
1211
1211
  #heldEvicted;
1212
1212
  #newest;
1213
+ #newestSource;
1213
1214
  #holes;
1214
1215
  static #checkedRetention(retentionBlocks) {
1215
1216
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -1247,6 +1248,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
1247
1248
  observe(position) {
1248
1249
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
1249
1250
  }
1251
+ /**
1252
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
1253
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
1254
+ * a source cannot run ahead of the sources sent before it except by
1255
+ * reordering.
1256
+ */
1257
+ get newestSource() {
1258
+ return this.#newestSource;
1259
+ }
1260
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
1261
+ noteSource(ssId) {
1262
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
1263
+ this.#newestSource = ssId;
1264
+ return true;
1265
+ }
1250
1266
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
1251
1267
  get heldEvicted() {
1252
1268
  return this.#heldEvicted;
@@ -1479,6 +1495,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1479
1495
  this.#held.clear();
1480
1496
  this.#coverageEnd = void 0;
1481
1497
  this.#newest = void 0;
1498
+ this.#newestSource = void 0;
1482
1499
  }
1483
1500
  #claim(block) {
1484
1501
  const placed = [];
@@ -2067,6 +2084,7 @@ var WORKER_ERROR_LOG_CAP = 5;
2067
2084
  var WORKER_ERROR_KIND_CAP = 32;
2068
2085
  var WORKER_ERROR_OVERFLOW_KIND = "other";
2069
2086
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
2087
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
2070
2088
  function isMulticastSymbolDelivery(value) {
2071
2089
  return value === "datagram" || value === "ordered-relay";
2072
2090
  }
@@ -2164,6 +2182,10 @@ var FECManager = class _FECManager {
2164
2182
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
2165
2183
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
2166
2184
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
2185
+ // Source ESIs per block that another FEC instance rebuilt and fed in
2186
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
2187
+ // not network arrivals and never make a block clean.
2188
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
2167
2189
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
2168
2190
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
2169
2191
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -2292,6 +2314,10 @@ var FECManager = class _FECManager {
2292
2314
  // ── Keyframe-protected repair policy (block-policy.ts) ──
2293
2315
  // One classifier per physical stream, built from that stream's geometry.
2294
2316
  this.#classifiers = /* @__PURE__ */ new Map();
2317
+ // A stream without catalog timing has no classifier, so the last RAP it
2318
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
2319
+ // a second RAP is released inside one MPU (BLO-52085).
2320
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
2295
2321
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
2296
2322
  // rungs can share a packetId with a different framerate or GOP, so the
2297
2323
  // packet-level geometry is only the geometry of a packet whose streams
@@ -2449,6 +2475,7 @@ var FECManager = class _FECManager {
2449
2475
  #pendingWorkerCleanupBounds;
2450
2476
  #blockSourceCounts;
2451
2477
  #blockSourceEsis;
2478
+ #blockRebuiltEsis;
2452
2479
  #blockAuthoredLengths;
2453
2480
  #ssbLengthConflictCount;
2454
2481
  #blockRepairRsIds;
@@ -2491,6 +2518,7 @@ var FECManager = class _FECManager {
2491
2518
  #orderedFrontiers;
2492
2519
  #orderedPendingBlocks;
2493
2520
  #classifiers;
2521
+ #noSpanRaps;
2494
2522
  #streamGeometries;
2495
2523
  #failedBlocks;
2496
2524
  #resolvedBlockClasses;
@@ -3152,6 +3180,7 @@ var FECManager = class _FECManager {
3152
3180
  };
3153
3181
  const discontinuity = () => {
3154
3182
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
3183
+ this.#noSpanRaps.delete(streamKey);
3155
3184
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
3156
3185
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
3157
3186
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -3564,6 +3593,9 @@ var FECManager = class _FECManager {
3564
3593
  }
3565
3594
  if (policyChanged) {
3566
3595
  this.#bumpEpoch(packetId);
3596
+ for (const key of [...this.#noSpanRaps.keys()]) {
3597
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
3598
+ }
3567
3599
  for (const [key, classifier] of [...this.#classifiers]) {
3568
3600
  if (!this.#belongsToPacket(key, packetId)) continue;
3569
3601
  const stream = this.#parseStreamKey(key);
@@ -3766,6 +3798,7 @@ var FECManager = class _FECManager {
3766
3798
  }
3767
3799
  resetMap(this.#blockSourceCounts);
3768
3800
  resetMap(this.#blockSourceEsis);
3801
+ resetMap(this.#blockRebuiltEsis);
3769
3802
  resetMap(this.#blockAuthoredLengths);
3770
3803
  resetMap(this.#blockRepairRsIds);
3771
3804
  resetMap(this.#blockRsbLengths);
@@ -3799,6 +3832,7 @@ var FECManager = class _FECManager {
3799
3832
  resetMap(this.#isoLocators);
3800
3833
  resetMap(this.#sourceAnchors);
3801
3834
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
3835
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
3802
3836
  this.#bumpEpoch(packetId);
3803
3837
  for (const [sbn, keys] of this.#blockKeysBySbn) {
3804
3838
  for (const key of keys.keys()) {
@@ -3889,6 +3923,7 @@ var FECManager = class _FECManager {
3889
3923
  dropMap(this.#pendingWorkerCleanupClients);
3890
3924
  dropMap(this.#blockSourceCounts);
3891
3925
  dropMap(this.#blockSourceEsis);
3926
+ dropMap(this.#blockRebuiltEsis);
3892
3927
  dropMap(this.#blockAuthoredLengths);
3893
3928
  dropMap(this.#blockRepairRsIds);
3894
3929
  dropMap(this.#blockRsbLengths);
@@ -3905,6 +3940,7 @@ var FECManager = class _FECManager {
3905
3940
  dropMap(this.#isoLocators);
3906
3941
  dropMap(this.#sourceAnchors);
3907
3942
  dropMap(this.#classifiers);
3943
+ dropMap(this.#noSpanRaps);
3908
3944
  dropMap(this.#streamGeometries);
3909
3945
  dropSet(this.#pendingWorkerCleanups);
3910
3946
  dropSet(this.#completedBlocks);
@@ -4050,6 +4086,9 @@ var FECManager = class _FECManager {
4050
4086
  this.#unindexBlockKey(key);
4051
4087
  }
4052
4088
  }
4089
+ if (isSourceSymbol && source === "multicast") {
4090
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
4091
+ }
4053
4092
  return true;
4054
4093
  }
4055
4094
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -4367,6 +4406,7 @@ var FECManager = class _FECManager {
4367
4406
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4368
4407
  const evictedBefore = locator.heldEvicted;
4369
4408
  const held = locator.hold(ssId, source);
4409
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
4370
4410
  const evicted = locator.heldEvicted - evictedBefore;
4371
4411
  if (evicted > 0) {
4372
4412
  const before = this.#isoHeldEvictedCount;
@@ -4535,11 +4575,14 @@ var FECManager = class _FECManager {
4535
4575
  /**
4536
4576
  * Whether an MPU boundary still bounds this stream's RAPs (see
4537
4577
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
4538
- * it does not. A stream with no classifier yet has shown nothing against it.
4578
+ * it does not. A stream with no classifier yet has shown nothing against it,
4579
+ * and neither has a stream without catalog timing until it releases two
4580
+ * RAPs inside one MPU.
4539
4581
  */
4540
4582
  mpuBoundsRapsFor(packetId, catalogTrackName) {
4541
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
4542
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
4583
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4584
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
4585
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
4543
4586
  }
4544
4587
  /** A block's class now, or frozen at its resolution. */
4545
4588
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -4565,7 +4608,14 @@ var FECManager = class _FECManager {
4565
4608
  * invariant (consecutive MPUs exactly one keyframe interval apart).
4566
4609
  */
4567
4610
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
4568
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
4611
+ if (!this.#trackConfigs.has(packetId)) return;
4612
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
4613
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4614
+ const last = this.#noSpanRaps.get(streamKey);
4615
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
4616
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
4617
+ return;
4618
+ }
4569
4619
  this.#applyClassifierChanges(
4570
4620
  packetId,
4571
4621
  catalogTrackName,
@@ -4952,7 +5002,15 @@ var FECManager = class _FECManager {
4952
5002
  this.#isoSourceOversizeCount++;
4953
5003
  return null;
4954
5004
  }
4955
- const sbn = located.block.start;
5005
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
5006
+ }
5007
+ /**
5008
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
5009
+ * answers through onFrame (null here), or its synchronous decoder, whose
5010
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
5011
+ * stated for `ssId`.
5012
+ */
5013
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
4956
5014
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
4957
5015
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
4958
5016
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -4981,6 +5039,74 @@ var FECManager = class _FECManager {
4981
5039
  }
4982
5040
  return recovered;
4983
5041
  }
5042
+ /**
5043
+ * Feed a source symbol of this stream that another FEC instance over the
5044
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
5045
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
5046
+ * BLO-52085). It joins its block's decoder as a received source would, so
5047
+ * it reduces the erasures the block still has to solve, but it is not a
5048
+ * network arrival: no transport accounting, no classification evidence,
5049
+ * no deadline refresh, and it never makes a block clean.
5050
+ *
5051
+ * Only a block a repair stated takes one (a repair-less span has no
5052
+ * decoder block). A block that is terminal, or already holds that source,
5053
+ * does not. The symbol is the exact protected packet, unpadded, as
5054
+ * feedSourceSymbol takes it.
5055
+ *
5056
+ * `recovered` is the block the synchronous decoder completed with it; the
5057
+ * caller routes it and completes the block, as for any recovery. When
5058
+ * every source of the block is then present, received or rebuilt, and no
5059
+ * decoder returned it (a Worker answers asynchronously, and reports such a
5060
+ * block as clean on its own side), the block is completed here as
5061
+ * recovered, with nothing left to route.
5062
+ */
5063
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
5064
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
5065
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
5066
+ if (located === null || located.block.span) return NOT_TAKEN;
5067
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
5068
+ const sbn = located.block.start;
5069
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
5070
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
5071
+ let rebuilt = this.#blockRebuiltEsis.get(key);
5072
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
5073
+ if (rebuilt === void 0) {
5074
+ rebuilt = /* @__PURE__ */ new Set();
5075
+ this.#blockRebuiltEsis.set(key, rebuilt);
5076
+ }
5077
+ const config = this.#trackConfigs.get(packetId);
5078
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
5079
+ rebuilt.add(located.esi);
5080
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
5081
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
5082
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
5083
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
5084
+ }
5085
+ }
5086
+ return { taken: true, recovered };
5087
+ }
5088
+ /**
5089
+ * A block that holds every one of its `length` sources, received or
5090
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
5091
+ * every packet of it was delivered, by its own arrival or by the instance
5092
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
5093
+ */
5094
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
5095
+ const rebuilt = this.#blockRebuiltEsis.get(key);
5096
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
5097
+ let present = rebuilt.size;
5098
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
5099
+ if (present < length) return false;
5100
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
5101
+ this.countRecovery(packetId, sbn, catalogTrackName);
5102
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5103
+ if (catalogTrackName === void 0) {
5104
+ this.onBlockComplete?.(sbn, packetId);
5105
+ } else {
5106
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
5107
+ }
5108
+ return true;
5109
+ }
4984
5110
  /**
4985
5111
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
4986
5112
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -5183,8 +5309,75 @@ var FECManager = class _FECManager {
5183
5309
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
5184
5310
  return null;
5185
5311
  }
5312
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
5186
5313
  return recovered;
5187
5314
  }
5315
+ /**
5316
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
5317
+ * its sources in send order, so one that moves the stream's newest source
5318
+ * position past a block leaves the sources that block still misses
5319
+ * reordered behind it at best: it shortens the wait of every block it
5320
+ * passed that only a late source can complete. MoQ is left out: its streams
5321
+ * can overtake one another, so a MoQ source shows nothing about the sources
5322
+ * sent before it.
5323
+ */
5324
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
5325
+ const previous = locator.newestSource;
5326
+ if (!locator.noteSource(ssId) || previous === void 0) return;
5327
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
5328
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
5329
+ this.#shortenUnrecoverableWait(
5330
+ this.#blockKey(block.start, packetId, catalogTrackName),
5331
+ block.start,
5332
+ packetId,
5333
+ catalogTrackName
5334
+ );
5335
+ }
5336
+ }
5337
+ /**
5338
+ * The wait of a block that only a late source can complete (BLO-52085): a
5339
+ * repair stated it, its received sources plus every symbol its repair group
5340
+ * carries (RSB_length) are short of K' (the repair scheduler's
5341
+ * group-cannot-complete), and a later source of the stream has arrived, so
5342
+ * a source it misses is lost or reordered behind that one. The span, which
5343
+ * covers repair symbols paced after its sources, buys it nothing: it waits
5344
+ * the late-symbol allowance alone, the multicast wait less the span (the
5345
+ * catalog tolerance or the measured delivery gap). Undefined for any other
5346
+ * block.
5347
+ */
5348
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
5349
+ const authoredLength = this.#blockAuthoredLengths.get(key);
5350
+ const rsbLength = this.#blockRsbLengths.get(key);
5351
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
5352
+ let present = this.#blockSourceCounts.get(key) ?? 0;
5353
+ const received = this.#blockSourceEsis.get(key);
5354
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
5355
+ if (present + rsbLength >= authoredLength) return void 0;
5356
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
5357
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
5358
+ return void 0;
5359
+ }
5360
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
5361
+ }
5362
+ /**
5363
+ * Bring an open block's wait in to its late-source wait, when it has one.
5364
+ * Never later than it is armed: only a source can complete such a block,
5365
+ * so a repair of its group (paced across the span) must not keep it open.
5366
+ */
5367
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
5368
+ if (!this.#activeBlocks.has(key)) return;
5369
+ const config = this.#trackConfigs.get(packetId);
5370
+ if (config === void 0) return;
5371
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
5372
+ if (waitMs === void 0) return;
5373
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
5374
+ this.#armBlockTimeout(
5375
+ sbn,
5376
+ packetId,
5377
+ catalogTrackName,
5378
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
5379
+ );
5380
+ }
5188
5381
  /**
5189
5382
  * Record a block's authored SSB_length from a repair symbol. Returns false,
5190
5383
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -5339,6 +5532,15 @@ var FECManager = class _FECManager {
5339
5532
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
5340
5533
  return;
5341
5534
  }
5535
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
5536
+ key,
5537
+ sbn,
5538
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
5539
+ packetId,
5540
+ catalogTrackName
5541
+ )) {
5542
+ return;
5543
+ }
5342
5544
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
5343
5545
  }
5344
5546
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -5347,6 +5549,7 @@ var FECManager = class _FECManager {
5347
5549
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5348
5550
  this.#blockSourceCounts.delete(key);
5349
5551
  this.#blockSourceEsis.delete(key);
5552
+ this.#blockRebuiltEsis.delete(key);
5350
5553
  this.#blockAuthoredLengths.delete(key);
5351
5554
  this.#blockRepairRsIds.delete(key);
5352
5555
  this.#blockRsbLengths.delete(key);
@@ -5413,7 +5616,16 @@ var FECManager = class _FECManager {
5413
5616
  this.#repairDeadlineMs(trackConfig)
5414
5617
  );
5415
5618
  }
5416
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
5619
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
5620
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
5621
+ return;
5622
+ }
5623
+ this.#armBlockTimeout(
5624
+ sbn,
5625
+ packetId,
5626
+ catalogTrackName,
5627
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
5628
+ );
5417
5629
  }
5418
5630
  #failBlock(sbn, packetId, catalogTrackName) {
5419
5631
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -5426,6 +5638,7 @@ var FECManager = class _FECManager {
5426
5638
  }
5427
5639
  this.#blockSourceCounts.delete(key);
5428
5640
  this.#blockSourceEsis.delete(key);
5641
+ this.#blockRebuiltEsis.delete(key);
5429
5642
  this.#blockAuthoredLengths.delete(key);
5430
5643
  this.#blockRepairRsIds.delete(key);
5431
5644
  this.#blockRsbLengths.delete(key);
@@ -5464,6 +5677,7 @@ var FECManager = class _FECManager {
5464
5677
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
5465
5678
  this.#blockSourceCounts.delete(key);
5466
5679
  this.#blockSourceEsis.delete(key);
5680
+ this.#blockRebuiltEsis.delete(key);
5467
5681
  this.#blockAuthoredLengths.delete(key);
5468
5682
  this.#blockRepairRsIds.delete(key);
5469
5683
  this.#blockRsbLengths.delete(key);
@@ -5555,6 +5769,7 @@ var FECManager = class _FECManager {
5555
5769
  this.#blockKeysBySbn.clear();
5556
5770
  this.#blockSourceCounts.clear();
5557
5771
  this.#blockSourceEsis.clear();
5772
+ this.#blockRebuiltEsis.clear();
5558
5773
  this.#blockAuthoredLengths.clear();
5559
5774
  this.#blockRepairRsIds.clear();
5560
5775
  this.#blockRsbLengths.clear();
@@ -5580,6 +5795,7 @@ var FECManager = class _FECManager {
5580
5795
  this.#blockAnchorsMs.clear();
5581
5796
  this.#blockCatalogTrackNames.clear();
5582
5797
  this.#classifiers.clear();
5798
+ this.#noSpanRaps.clear();
5583
5799
  this.#streamGeometries.clear();
5584
5800
  this.#failedBlocks.clear();
5585
5801
  this.#resolvedBlockClasses.clear();
@@ -7054,14 +7270,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7054
7270
  });
7055
7271
 
7056
7272
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7273
+ var MAX_PENDING_PACKETS = 256;
7057
7274
  var AMTGatewayManager = class {
7058
7275
  subscriptions = /* @__PURE__ */ new Map();
7059
7276
  messageHandler;
7277
+ messageListenerRegistered = false;
7060
7278
  pendingPackets = /* @__PURE__ */ new Map();
7061
7279
  constructor() {
7062
7280
  this.messageHandler = (event) => {
7063
7281
  const data = event.data;
7064
- console.log(`[AMT] SW message received:`, data.type);
7065
7282
  if (data.type === "UDP_PACKET") {
7066
7283
  this.handlePacket(data);
7067
7284
  } else if (data.type === "SOCKET_ERROR") {
@@ -7070,12 +7287,22 @@ var AMTGatewayManager = class {
7070
7287
  console.log(`[AMT SW Debug] ${data.message}`);
7071
7288
  }
7072
7289
  };
7073
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
7074
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
7075
- console.log("[AMT] Registered service worker message handler");
7076
- } else {
7077
- console.warn("[AMT] Service worker not available");
7290
+ }
7291
+ ensureMessageListener() {
7292
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
7293
+ return;
7078
7294
  }
7295
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
7296
+ this.messageListenerRegistered = true;
7297
+ console.log("[AMT] Registered service worker message handler");
7298
+ }
7299
+ removeMessageListener() {
7300
+ if (!this.messageListenerRegistered)
7301
+ return;
7302
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7303
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7304
+ }
7305
+ this.messageListenerRegistered = false;
7079
7306
  }
7080
7307
  /**
7081
7308
  * Subscribe via AMT tunnel
@@ -7085,6 +7312,7 @@ var AMTGatewayManager = class {
7085
7312
  if (config.protected) {
7086
7313
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
7087
7314
  }
7315
+ this.ensureMessageListener();
7088
7316
  const keyStr = this.getKeyString(key);
7089
7317
  if (this.subscriptions.has(keyStr)) {
7090
7318
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -7120,6 +7348,8 @@ var AMTGatewayManager = class {
7120
7348
  this.subscriptions.delete(keyStr);
7121
7349
  this.pendingPackets.delete(subscription.subscriptionId);
7122
7350
  }
7351
+ if (this.subscriptions.size === 0)
7352
+ this.removeMessageListener();
7123
7353
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
7124
7354
  }
7125
7355
  }
@@ -7141,6 +7371,8 @@ var AMTGatewayManager = class {
7141
7371
  } finally {
7142
7372
  this.subscriptions.delete(handle);
7143
7373
  this.pendingPackets.delete(subscription.subscriptionId);
7374
+ if (this.subscriptions.size === 0)
7375
+ this.removeMessageListener();
7144
7376
  }
7145
7377
  }
7146
7378
  /**
@@ -7157,14 +7389,12 @@ var AMTGatewayManager = class {
7157
7389
  if (!subscriptionId)
7158
7390
  return;
7159
7391
  const subscription = this.subscriptions.get(subscriptionId);
7160
- if (!subscription) {
7161
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
7392
+ if (!subscription)
7162
7393
  return;
7163
- }
7164
7394
  if (!subscription.handshakeComplete) {
7165
7395
  const queue = this.pendingPackets.get(subscriptionId);
7166
7396
  const packetData2 = data;
7167
- if (queue) {
7397
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7168
7398
  queue.push({
7169
7399
  data: packetData2.data,
7170
7400
  remoteAddress: packetData2.remoteAddress,
@@ -7190,19 +7420,15 @@ var AMTGatewayManager = class {
7190
7420
  if (!subscriptionId)
7191
7421
  return;
7192
7422
  const subscription = this.subscriptions.get(subscriptionId);
7193
- if (!subscription) {
7194
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
7423
+ if (!subscription)
7195
7424
  return;
7196
- }
7197
7425
  subscription.config.onError(new Error(error || "Unknown error"));
7198
7426
  }
7199
7427
  /**
7200
7428
  * Cleanup on destroy
7201
7429
  */
7202
7430
  destroy() {
7203
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7204
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7205
- }
7431
+ this.removeMessageListener();
7206
7432
  const handles = Array.from(this.subscriptions.keys());
7207
7433
  for (const handle of handles) {
7208
7434
  this.unsubscribe(handle).catch((err) => {
@@ -13111,9 +13337,21 @@ var IsoBmffTimingResolver = class {
13111
13337
  }
13112
13338
  #track = null;
13113
13339
  #fragments = /* @__PURE__ */ new Map();
13340
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
13341
+ #firstSamples = /* @__PURE__ */ new Map();
13342
+ /**
13343
+ * First sample and sample count of the newest fragments whose every slot was
13344
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
13345
+ * matches the combiner, since it retires an abandoned sample as well.
13346
+ */
13347
+ #delivered = /* @__PURE__ */ new Map();
13114
13348
  #pendingSamples = 0;
13115
13349
  #evictedSamples = 0;
13116
13350
  #lastMovieFragmentRejection = null;
13351
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
13352
+ get initialized() {
13353
+ return this.#track !== null;
13354
+ }
13117
13355
  get pendingSampleCount() {
13118
13356
  return this.#pendingSamples;
13119
13357
  }
@@ -13137,7 +13375,14 @@ var IsoBmffTimingResolver = class {
13137
13375
  this.#track = timing;
13138
13376
  return true;
13139
13377
  }
13140
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
13378
+ /**
13379
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
13380
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
13381
+ * fragment whose samples are all done is a duplicate: admitting it again
13382
+ * would charge stale timing and could displace the live MPU's. A source
13383
+ * copy is still admitted again (a rendition switch re-arms timing that way).
13384
+ */
13385
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
13141
13386
  this.#lastMovieFragmentRejection = null;
13142
13387
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13143
13388
  this.#lastMovieFragmentRejection = "invalid";
@@ -13163,6 +13408,8 @@ var IsoBmffTimingResolver = class {
13163
13408
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13164
13409
  );
13165
13410
  }
13411
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
13412
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
13166
13413
  let freed = 0;
13167
13414
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13168
13415
  freed += this.#evictOldestFragment();
@@ -13176,6 +13423,7 @@ var IsoBmffTimingResolver = class {
13176
13423
  return false;
13177
13424
  }
13178
13425
  this.#fragments.set(key, [...parsed.samples]);
13426
+ this.#firstSamples.set(key, parsed.samples[0]);
13179
13427
  this.#pendingSamples += parsed.samples.length;
13180
13428
  return true;
13181
13429
  }
@@ -13233,6 +13481,7 @@ var IsoBmffTimingResolver = class {
13233
13481
  let live = 0;
13234
13482
  for (const sample of samples) if (sample !== null) live++;
13235
13483
  this.#fragments.delete(oldest);
13484
+ this.#firstSamples.delete(oldest);
13236
13485
  this.#pendingSamples -= live;
13237
13486
  this.#evictedSamples += live;
13238
13487
  return live;
@@ -13248,12 +13497,23 @@ var IsoBmffTimingResolver = class {
13248
13497
  if (!samples || !samples[index]) return false;
13249
13498
  samples[index] = null;
13250
13499
  this.#pendingSamples--;
13251
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
13500
+ if (samples.every((value) => value === null)) {
13501
+ this.#fragments.delete(key);
13502
+ const first = this.#firstSamples.get(key);
13503
+ this.#firstSamples.delete(key);
13504
+ this.#delivered.delete(key);
13505
+ this.#delivered.set(key, { first, sampleCount: samples.length });
13506
+ if (this.#delivered.size > this.maxPendingSamples) {
13507
+ this.#delivered.delete(this.#delivered.keys().next().value);
13508
+ }
13509
+ }
13252
13510
  return true;
13253
13511
  }
13254
13512
  reset() {
13255
13513
  this.#track = null;
13256
13514
  this.#fragments.clear();
13515
+ this.#firstSamples.clear();
13516
+ this.#delivered.clear();
13257
13517
  this.#pendingSamples = 0;
13258
13518
  }
13259
13519
  };
@@ -13735,6 +13995,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13735
13995
  this.#reclaimedTimingSamples = 0;
13736
13996
  this.#evictedTimingSamples = 0;
13737
13997
  this.#rejectedMovieFragments = 0;
13998
+ this.#recoveredHeadsAwaitingTiming = {
13999
+ held: 0,
14000
+ expired: 0,
14001
+ discarded: 0,
14002
+ superseded: 0
14003
+ };
13738
14004
  this.#sequencers = /* @__PURE__ */ new Map();
13739
14005
  this.#fecGate = null;
13740
14006
  this.#fecSequencer = null;
@@ -13843,6 +14109,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13843
14109
  #reclaimedTimingSamples;
13844
14110
  #evictedTimingSamples;
13845
14111
  #rejectedMovieFragments;
14112
+ #recoveredHeadsAwaitingTiming;
13846
14113
  #sequencers;
13847
14114
  #fecGate;
13848
14115
  #fecSequencer;
@@ -13963,6 +14230,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13963
14230
  rejectedMovieFragments: this.#rejectedMovieFragments
13964
14231
  };
13965
14232
  }
14233
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14234
+ get recoveredHeadsAwaitingTiming() {
14235
+ let pending = 0;
14236
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14237
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14238
+ }
14239
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14240
+ }
13966
14241
  get reorderStats() {
13967
14242
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13968
14243
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14136,7 +14411,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14136
14411
  const now = Date.now();
14137
14412
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14138
14413
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14139
- awaitingTiming.shift();
14414
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14140
14415
  }
14141
14416
  }
14142
14417
  }
@@ -14254,11 +14529,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14254
14529
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14255
14530
  return this.#drop("fragment-unexpected-fragmentation");
14256
14531
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14257
- if (!resolver2) return this.#drop("fragment-no-resolver");
14532
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14258
14533
  const pendingBefore = resolver2.pendingSampleCount;
14259
14534
  const evictedBefore = resolver2.evictedSampleCount;
14260
14535
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14261
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14536
+ const admitted = resolver2.addMovieFragment(
14537
+ mpu.mpuSequenceNumber,
14538
+ data,
14539
+ remaining,
14540
+ options?.recovered === true
14541
+ );
14262
14542
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14263
14543
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14264
14544
  if (!admitted) {
@@ -14278,7 +14558,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14278
14558
  };
14279
14559
  }
14280
14560
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14281
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14561
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14282
14562
  if (!resolver) return this.#drop("mfu-no-resolver");
14283
14563
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14284
14564
  return this.#drop("mfu-empty-payload");
@@ -14299,6 +14579,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14299
14579
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14300
14580
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14301
14581
  sampleNumber: mfuDuHeader.sampleNumber,
14582
+ sequenceNumber: info.sequenceNumber,
14302
14583
  arrivedAt: Date.now()
14303
14584
  });
14304
14585
  return this.#drop("mfu-sample-timing-missing");
@@ -14622,6 +14903,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
14622
14903
  }
14623
14904
  this.#emitFrame(data, meta);
14624
14905
  }
14906
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
14907
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
14908
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
14909
+ }
14625
14910
  /**
14626
14911
  * Claim and return the authored timing for a recovered sample. Recovery does
14627
14912
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -14683,6 +14968,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14683
14968
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14684
14969
  this.#sequencers.clear();
14685
14970
  this.#fecSequencer?.discard();
14971
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14972
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14973
+ }
14686
14974
  this.#timingStreams.clear();
14687
14975
  this.#pendingTimingSamples = 0;
14688
14976
  this.#reliableMoqStartupGate.clear();
@@ -14701,10 +14989,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14701
14989
  return resolver;
14702
14990
  }
14703
14991
  #awaitTiming(streamKey, head) {
14704
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14705
- if (!awaitingTiming) return;
14992
+ const state = this.#timingStreams.get(streamKey);
14993
+ if (!state) return;
14994
+ if (!head.options?.recovered) {
14995
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
14996
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
14997
+ state.awaitingTiming = kept;
14998
+ }
14999
+ const awaitingTiming = state.awaitingTiming;
14706
15000
  awaitingTiming.push(head);
14707
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
15001
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
15002
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
15003
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
15004
+ }
15005
+ }
15006
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
15007
+ #leaveAwaitingTiming(head, outcome) {
15008
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14708
15009
  }
14709
15010
  /**
14710
15011
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14732,6 +15033,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14732
15033
  const state = this.#timingStreams.get(streamKey);
14733
15034
  if (!state) return;
14734
15035
  this.#timingStreams.delete(streamKey);
15036
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14735
15037
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14736
15038
  if (this.#pendingTimingSamples < 0) {
14737
15039
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -17937,6 +18239,16 @@ function keyframeOverlaySsId(header) {
17937
18239
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
17938
18240
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
17939
18241
  }
18242
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
18243
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
18244
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
18245
+ }
18246
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
18247
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
18248
+ return headerPsn + offset >>> 0;
18249
+ }
18250
+ var OVERLAY_ROUTED_LIMIT = 1024;
18251
+ var OVERLAY_PENDING_LIMIT = 256;
17940
18252
  var OVERLAY_OFFSET_MPUS = 16;
17941
18253
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
17942
18254
  function assertStreamIdentity(role, stream) {
@@ -18048,6 +18360,7 @@ var MmtpRouter = class {
18048
18360
  #provenance = /* @__PURE__ */ new Map();
18049
18361
  #onFlowProtected = null;
18050
18362
  #unadmittedListedDrops = 0;
18363
+ #admittedSourceDatagrams = 0;
18051
18364
  #unlistedRepairSetAside = 0;
18052
18365
  #assistBlocks = /* @__PURE__ */ new Map();
18053
18366
  #assistFailures = /* @__PURE__ */ new Map();
@@ -18104,6 +18417,15 @@ var MmtpRouter = class {
18104
18417
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
18105
18418
  */
18106
18419
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18420
+ /**
18421
+ * Per stream, the SS_ID offset at which a base recovery last showed the
18422
+ * sender's own packet_sequence_number: a recovered packet's header psn
18423
+ * equal to its SS_ID minus that offset. While it equals the stream's
18424
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
18425
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
18426
+ * an MPU no member of which arrived).
18427
+ */
18428
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18107
18429
  /**
18108
18430
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
18109
18431
  * not parsed, typically because its overlap copy was parsed first
@@ -18173,7 +18495,9 @@ var MmtpRouter = class {
18173
18495
  recovered: 0,
18174
18496
  restarts: 0,
18175
18497
  unsequencedDropped: 0,
18176
- protectedDropped: 0
18498
+ protectedDropped: 0,
18499
+ baseFed: 0,
18500
+ baseUndetermined: 0
18177
18501
  };
18178
18502
  /** Callback fired when a delta-track FEC block completes or fails. */
18179
18503
  #onDeltaBlockCompleteCb = null;
@@ -18346,7 +18670,7 @@ var MmtpRouter = class {
18346
18670
  const cb = this.#getFrameCallback(packetId);
18347
18671
  if (isVideo && !assembler) {
18348
18672
  if (frameMeta.recovered) {
18349
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
18673
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
18350
18674
  } else {
18351
18675
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
18352
18676
  }
@@ -18736,6 +19060,10 @@ var MmtpRouter = class {
18736
19060
  get keyframeOverlayStats() {
18737
19061
  return { ...this.#overlayStats };
18738
19062
  }
19063
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
19064
+ get recoveredHeadsAwaitingTiming() {
19065
+ return this.#parser.recoveredHeadsAwaitingTiming;
19066
+ }
18739
19067
  #isOverlayRepair(repairPacketId, repairTrackName) {
18740
19068
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18741
19069
  }
@@ -18786,6 +19114,11 @@ var MmtpRouter = class {
18786
19114
  */
18787
19115
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
18788
19116
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
19117
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
19118
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
19119
+ stream.overlayToBase.routed.clear();
19120
+ stream.overlayToBase.pending.clear();
19121
+ }
18789
19122
  }
18790
19123
  #resetOverlay(inst) {
18791
19124
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -18842,9 +19175,10 @@ var MmtpRouter = class {
18842
19175
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
18843
19176
  * every overlay installed for its source, at that SS_ID, with the bytes the
18844
19177
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
18845
- * included (7.1, 7.4.3).
19178
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
19179
+ * FEC reads it.
18846
19180
  */
18847
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
19181
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
18848
19182
  if (this.#overlays.size === 0) return;
18849
19183
  const ssId = keyframeOverlaySsId(header);
18850
19184
  if (ssId === void 0) return;
@@ -18853,7 +19187,11 @@ var MmtpRouter = class {
18853
19187
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
18854
19188
  if (identity !== null) {
18855
19189
  inst.offsets.delete(identity.mpuSequenceNumber);
18856
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
19190
+ inst.offsets.set(identity.mpuSequenceNumber, {
19191
+ psnOffset: ssId - sequenceNumber >>> 0,
19192
+ overlaySsId: ssId,
19193
+ ...baseSsId === void 0 ? {} : { baseSsId }
19194
+ });
18857
19195
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
18858
19196
  }
18859
19197
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -18932,14 +19270,27 @@ var MmtpRouter = class {
18932
19270
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
18933
19271
  const restamped = this.#restampedStreams.has(streamKey);
18934
19272
  const restored = /* @__PURE__ */ new Set();
19273
+ const baseSsIds = /* @__PURE__ */ new Map();
18935
19274
  const symbols = result.symbols.flatMap((sym) => {
18936
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
18937
- if (offset !== void 0) {
18938
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
19275
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
19276
+ if (anchor !== void 0) {
19277
+ const overlaySsId = serial32Add(blockStart, sym.index);
19278
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
18939
19279
  restored.add(packetSequenceNumber);
18940
- return [{ ...sym, packetSequenceNumber }];
19280
+ const out = { ...sym, packetSequenceNumber };
19281
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
19282
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
19283
+ return [out];
19284
+ }
19285
+ if (!restamped) {
19286
+ const baseSsId = unstampedBaseSsId(
19287
+ this.#ssIdSeqOffsetByStream.get(streamKey),
19288
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
19289
+ sym.packetSequenceNumber
19290
+ );
19291
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
19292
+ return [sym];
18941
19293
  }
18942
- if (!restamped) return [sym];
18943
19294
  this.#overlayStats.unsequencedDropped++;
18944
19295
  return [];
18945
19296
  });
@@ -18948,6 +19299,85 @@ var MmtpRouter = class {
18948
19299
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
18949
19300
  });
18950
19301
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
19302
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
19303
+ }
19304
+ /**
19305
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
19306
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
19307
+ * from a received member of its MPU, else unstampedBaseSsId),
19308
+ * so it is fed to the base decoder as a rebuilt source
19309
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
19310
+ * fewer to solve, and its repair can recover the block's other packets.
19311
+ * A base block no repair has stated yet takes it when one does
19312
+ * (#feedPendingRebuilt).
19313
+ *
19314
+ * The overlay route already handled each packet fed (delivered it, or
19315
+ * held an untimed head for its FT=1), so a base recovery routes none of
19316
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
19317
+ * held head would wait in the parser for timing its first copy consumed.
19318
+ * Only an overlay block decoded from multicast symbols alone is fed: with
19319
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
19320
+ * that crossed no multicast socket, which the provenance counted from the
19321
+ * block's own arrivals cannot show.
19322
+ */
19323
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
19324
+ const fec = this.#fecManager;
19325
+ const owner = inst.leg.sourcePacketId;
19326
+ const name = inst.leg.sourceCatalogTrackName;
19327
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
19328
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
19329
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
19330
+ const feeds = [];
19331
+ for (const sym of symbols) {
19332
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
19333
+ const ssId = baseSsIds.get(sym);
19334
+ if (ssId === void 0) {
19335
+ this.#overlayStats.baseUndetermined++;
19336
+ continue;
19337
+ }
19338
+ routed.delete(ssId);
19339
+ routed.add(ssId);
19340
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
19341
+ feeds.push([ssId, sym.rawSymbol]);
19342
+ }
19343
+ for (const [ssId, symbolData] of feeds) {
19344
+ if (this.#classifyPacketId(owner) === null) return;
19345
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19346
+ }
19347
+ }
19348
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
19349
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
19350
+ const fec = this.#fecManager;
19351
+ if (!fec?.feedRebuiltSourceSymbol) return;
19352
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
19353
+ if (coords == null) {
19354
+ const pending = this.#stream(owner, name).overlayToBase.pending;
19355
+ pending.delete(ssId);
19356
+ pending.set(ssId, symbolData.slice());
19357
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
19358
+ return;
19359
+ }
19360
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
19361
+ if (!fed.taken) return;
19362
+ this.#overlayStats.baseFed++;
19363
+ if (fed.recovered === null) return;
19364
+ const sbn = coords.sbn;
19365
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
19366
+ fec.cancelBlockTimeout(sbn, owner, name);
19367
+ fec.countRecovery(owner, sbn, name);
19368
+ fec.markBlockCompleted(sbn, owner, name);
19369
+ });
19370
+ }
19371
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
19372
+ #feedPendingRebuilt(owner, name, block) {
19373
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
19374
+ if (pending === void 0 || pending.size === 0) return;
19375
+ for (const [ssId, symbolData] of [...pending]) {
19376
+ if (this.#classifyPacketId(owner) === null) return;
19377
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
19378
+ pending.delete(ssId);
19379
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19380
+ }
18951
19381
  }
18952
19382
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
18953
19383
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -19087,6 +19517,10 @@ var MmtpRouter = class {
19087
19517
  get unadmittedListedDropCount() {
19088
19518
  return this.#unadmittedListedDrops;
19089
19519
  }
19520
+ /** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
19521
+ get admittedSourceDatagramCount() {
19522
+ return this.#admittedSourceDatagrams;
19523
+ }
19090
19524
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
19091
19525
  get unlistedRepairSetAsideCount() {
19092
19526
  return this.#unlistedRepairSetAside;
@@ -19573,6 +20007,8 @@ var MmtpRouter = class {
19573
20007
  )) return;
19574
20008
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
19575
20009
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
20010
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
20011
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
19576
20012
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
19577
20013
  const recovered = fec.feedRepairSymbol(
19578
20014
  ownerPacketId,
@@ -19710,6 +20146,7 @@ var MmtpRouter = class {
19710
20146
  this.#droppedPreSyncVideo.delete(streamKey);
19711
20147
  this.#latestVideoSeqByStream.delete(streamKey);
19712
20148
  this.#ssIdSeqOffsetByStream.delete(streamKey);
20149
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
19713
20150
  this.#multicastRapTwinByStream.delete(streamKey);
19714
20151
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
19715
20152
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -20344,6 +20781,7 @@ var MmtpRouter = class {
20344
20781
  const parsed = this.#parser.parsePacket(packet);
20345
20782
  if (!parsed) return;
20346
20783
  const info = parsed.info;
20784
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
20347
20785
  if (source === "multicast" && !manifestAdmitted) {
20348
20786
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
20349
20787
  }
@@ -20625,7 +21063,8 @@ var MmtpRouter = class {
20625
21063
  parsed.header,
20626
21064
  info.sequenceNumber,
20627
21065
  info.rapFlag,
20628
- source
21066
+ source,
21067
+ fecPayloadId?.ssId
20629
21068
  );
20630
21069
  }
20631
21070
  }
@@ -20775,7 +21214,8 @@ var MmtpRouter = class {
20775
21214
  if (stream === void 0) {
20776
21215
  stream = {
20777
21216
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
20778
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
21217
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
21218
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
20779
21219
  };
20780
21220
  byName.set(catalogTrackName, stream);
20781
21221
  }
@@ -20890,8 +21330,16 @@ var MmtpRouter = class {
20890
21330
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
20891
21331
  this.#emitDirectVideo(packetId, data, meta, cb);
20892
21332
  }
20893
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
21333
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
20894
21334
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
21335
+ if (this.#isFecTrack(packetId)) {
21336
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
21337
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
21338
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
21339
+ this.#emitDirectVideo(packetId, data, meta, cb);
21340
+ return;
21341
+ }
21342
+ }
20895
21343
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
20896
21344
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
20897
21345
  this.#drainDirectRecoveredVideo(
@@ -21045,6 +21493,13 @@ var MmtpRouter = class {
21045
21493
  * frames by epoch only if pre-protection input ever matters.
21046
21494
  */
21047
21495
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
21496
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
21497
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
21498
+ result = {
21499
+ ...result,
21500
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
21501
+ };
21502
+ }
21048
21503
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
21049
21504
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
21050
21505
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -21109,7 +21564,10 @@ var MmtpRouter = class {
21109
21564
  ...result,
21110
21565
  symbols: result.symbols.map((sym) => {
21111
21566
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
21112
- if (wire === sym.packetSequenceNumber) return sym;
21567
+ if (wire === sym.packetSequenceNumber) {
21568
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
21569
+ return sym;
21570
+ }
21113
21571
  this.#restampedRecoveredSeqs++;
21114
21572
  this.#restampedStreams.add(streamKey);
21115
21573
  return { ...sym, packetSequenceNumber: wire };
@@ -21145,6 +21603,29 @@ var MmtpRouter = class {
21145
21603
  }
21146
21604
  );
21147
21605
  }
21606
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21607
+ ownerPacketId,
21608
+ sym.fragment.mpuSequenceNumber,
21609
+ sym.fragment.movieFragmentSequenceNumber,
21610
+ sym.fragment.sampleNumber,
21611
+ catalogTrackName
21612
+ );
21613
+ for (const sym of result.symbols) {
21614
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21615
+ this.#feedViaParser(
21616
+ sym.rawSymbol,
21617
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21618
+ void 0,
21619
+ {
21620
+ skipFecPayloadId: true,
21621
+ recovered: true,
21622
+ deliveryProvenance,
21623
+ ...fecSbnOf(sym.index),
21624
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21625
+ }
21626
+ );
21627
+ }
21628
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21148
21629
  const gated = this.#isFecTrack(ownerPacketId);
21149
21630
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21150
21631
  if (assembler) {