@blockcast/mmt-container 2.2.0-main.da2e7cd1dd5d → 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 (46) hide show
  1. package/dist/browser-runtime.js +472 -36
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/delivery-catalog.js +2 -1
  4. package/dist/delivery-catalog.js.map +2 -2
  5. package/dist/fec-manager.d.ts +30 -0
  6. package/dist/fec-manager.d.ts.map +1 -1
  7. package/dist/fec-manager.js +196 -2
  8. package/dist/fec-manager.js.map +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +2 -2
  12. package/dist/iso-block-locator.d.ts +9 -0
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +17 -0
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/iso-bmff-timing.d.ts +10 -1
  17. package/dist/iso-bmff-timing.d.ts.map +1 -1
  18. package/dist/iso-bmff-timing.js +36 -2
  19. package/dist/iso-bmff-timing.js.map +2 -2
  20. package/dist/mmtp-container-parser.d.ts +35 -2
  21. package/dist/mmtp-container-parser.d.ts.map +1 -1
  22. package/dist/mmtp-container-parser.js +82 -9
  23. package/dist/mmtp-container-parser.js.map +2 -2
  24. package/dist/mmtp-container-parser.no-modules.js +85 -11
  25. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  26. package/dist/mmtp-decode-pipeline.js +81 -9
  27. package/dist/mmtp-decode-pipeline.js.map +2 -2
  28. package/dist/mmtp-router.d.ts +18 -1
  29. package/dist/mmtp-router.d.ts.map +1 -1
  30. package/dist/mmtp-router.js +276 -34
  31. package/dist/mmtp-router.js.map +2 -2
  32. package/dist/multicast-manager.js +472 -36
  33. package/dist/multicast-manager.js.map +2 -2
  34. package/dist/multicast-transport.js +2 -1
  35. package/dist/multicast-transport.js.map +2 -2
  36. package/dist/no-modules.js +81 -9
  37. package/dist/no-modules.js.map +2 -2
  38. package/dist/wasm-mmtp-container-parser.js +81 -9
  39. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  40. package/package.json +5 -5
  41. package/src/fec-manager.ts +251 -2
  42. package/src/index.ts +1 -0
  43. package/src/iso-block-locator.ts +19 -0
  44. package/src/iso-bmff-timing.ts +40 -1
  45. package/src/mmtp-container-parser.ts +84 -8
  46. package/src/mmtp-router.ts +346 -30
@@ -8746,6 +8746,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8746
8746
  });
8747
8747
 
8748
8748
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8749
+ var MAX_PENDING_PACKETS = 256;
8749
8750
  var AMTGatewayManager = class {
8750
8751
  subscriptions = /* @__PURE__ */ new Map();
8751
8752
  messageHandler;
@@ -8869,7 +8870,7 @@ var AMTGatewayManager = class {
8869
8870
  if (!subscription.handshakeComplete) {
8870
8871
  const queue = this.pendingPackets.get(subscriptionId);
8871
8872
  const packetData2 = data;
8872
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8873
8874
  queue.push({
8874
8875
  data: packetData2.data,
8875
8876
  remoteAddress: packetData2.remoteAddress,
@@ -12948,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12948
12949
  #retentionBlocks;
12949
12950
  #heldEvicted;
12950
12951
  #newest;
12952
+ #newestSource;
12951
12953
  #holes;
12952
12954
  static #checkedRetention(retentionBlocks) {
12953
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12985,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12985
12987
  observe(position) {
12986
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12987
12989
  }
12990
+ /**
12991
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
12992
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
12993
+ * a source cannot run ahead of the sources sent before it except by
12994
+ * reordering.
12995
+ */
12996
+ get newestSource() {
12997
+ return this.#newestSource;
12998
+ }
12999
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13000
+ noteSource(ssId) {
13001
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13002
+ this.#newestSource = ssId;
13003
+ return true;
13004
+ }
12988
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12989
13006
  get heldEvicted() {
12990
13007
  return this.#heldEvicted;
@@ -13217,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13217
13234
  this.#held.clear();
13218
13235
  this.#coverageEnd = void 0;
13219
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13220
13238
  }
13221
13239
  #claim(block) {
13222
13240
  const placed = [];
@@ -13836,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13836
13854
  var WORKER_ERROR_KIND_CAP = 32;
13837
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13838
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13839
13858
  function isMulticastSymbolDelivery(value) {
13840
13859
  return value === "datagram" || value === "ordered-relay";
13841
13860
  }
@@ -13933,6 +13952,10 @@ var FECManager = class _FECManager {
13933
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13934
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13935
13954
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
13955
+ // Source ESIs per block that another FEC instance rebuilt and fed in
13956
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
13957
+ // not network arrivals and never make a block clean.
13958
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13936
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13937
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13938
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14222,6 +14245,7 @@ var FECManager = class _FECManager {
14222
14245
  #pendingWorkerCleanupBounds;
14223
14246
  #blockSourceCounts;
14224
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14225
14249
  #blockAuthoredLengths;
14226
14250
  #ssbLengthConflictCount;
14227
14251
  #blockRepairRsIds;
@@ -15544,6 +15568,7 @@ var FECManager = class _FECManager {
15544
15568
  }
15545
15569
  resetMap(this.#blockSourceCounts);
15546
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15547
15572
  resetMap(this.#blockAuthoredLengths);
15548
15573
  resetMap(this.#blockRepairRsIds);
15549
15574
  resetMap(this.#blockRsbLengths);
@@ -15668,6 +15693,7 @@ var FECManager = class _FECManager {
15668
15693
  dropMap(this.#pendingWorkerCleanupClients);
15669
15694
  dropMap(this.#blockSourceCounts);
15670
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15671
15697
  dropMap(this.#blockAuthoredLengths);
15672
15698
  dropMap(this.#blockRepairRsIds);
15673
15699
  dropMap(this.#blockRsbLengths);
@@ -15830,6 +15856,9 @@ var FECManager = class _FECManager {
15830
15856
  this.#unindexBlockKey(key);
15831
15857
  }
15832
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15833
15862
  return true;
15834
15863
  }
15835
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16147,6 +16176,7 @@ var FECManager = class _FECManager {
16147
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16148
16177
  const evictedBefore = locator.heldEvicted;
16149
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16150
16180
  const evicted = locator.heldEvicted - evictedBefore;
16151
16181
  if (evicted > 0) {
16152
16182
  const before = this.#isoHeldEvictedCount;
@@ -16742,7 +16772,15 @@ var FECManager = class _FECManager {
16742
16772
  this.#isoSourceOversizeCount++;
16743
16773
  return null;
16744
16774
  }
16745
- const sbn = located.block.start;
16775
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16776
+ }
16777
+ /**
16778
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16779
+ * answers through onFrame (null here), or its synchronous decoder, whose
16780
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16781
+ * stated for `ssId`.
16782
+ */
16783
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16746
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16747
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16748
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16771,6 +16809,74 @@ var FECManager = class _FECManager {
16771
16809
  }
16772
16810
  return recovered;
16773
16811
  }
16812
+ /**
16813
+ * Feed a source symbol of this stream that another FEC instance over the
16814
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16815
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16816
+ * BLO-52085). It joins its block's decoder as a received source would, so
16817
+ * it reduces the erasures the block still has to solve, but it is not a
16818
+ * network arrival: no transport accounting, no classification evidence,
16819
+ * no deadline refresh, and it never makes a block clean.
16820
+ *
16821
+ * Only a block a repair stated takes one (a repair-less span has no
16822
+ * decoder block). A block that is terminal, or already holds that source,
16823
+ * does not. The symbol is the exact protected packet, unpadded, as
16824
+ * feedSourceSymbol takes it.
16825
+ *
16826
+ * `recovered` is the block the synchronous decoder completed with it; the
16827
+ * caller routes it and completes the block, as for any recovery. When
16828
+ * every source of the block is then present, received or rebuilt, and no
16829
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16830
+ * block as clean on its own side), the block is completed here as
16831
+ * recovered, with nothing left to route.
16832
+ */
16833
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16834
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16835
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16836
+ if (located === null || located.block.span) return NOT_TAKEN;
16837
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16838
+ const sbn = located.block.start;
16839
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16840
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16841
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16842
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16843
+ if (rebuilt === void 0) {
16844
+ rebuilt = /* @__PURE__ */ new Set();
16845
+ this.#blockRebuiltEsis.set(key, rebuilt);
16846
+ }
16847
+ const config = this.#trackConfigs.get(packetId);
16848
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16849
+ rebuilt.add(located.esi);
16850
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16851
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16852
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16853
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16854
+ }
16855
+ }
16856
+ return { taken: true, recovered };
16857
+ }
16858
+ /**
16859
+ * A block that holds every one of its `length` sources, received or
16860
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16861
+ * every packet of it was delivered, by its own arrival or by the instance
16862
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16863
+ */
16864
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16865
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16866
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16867
+ let present = rebuilt.size;
16868
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16869
+ if (present < length) return false;
16870
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16871
+ this.countRecovery(packetId, sbn, catalogTrackName);
16872
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16873
+ if (catalogTrackName === void 0) {
16874
+ this.onBlockComplete?.(sbn, packetId);
16875
+ } else {
16876
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16877
+ }
16878
+ return true;
16879
+ }
16774
16880
  /**
16775
16881
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16776
16882
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16973,8 +17079,75 @@ var FECManager = class _FECManager {
16973
17079
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16974
17080
  return null;
16975
17081
  }
17082
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16976
17083
  return recovered;
16977
17084
  }
17085
+ /**
17086
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17087
+ * its sources in send order, so one that moves the stream's newest source
17088
+ * position past a block leaves the sources that block still misses
17089
+ * reordered behind it at best: it shortens the wait of every block it
17090
+ * passed that only a late source can complete. MoQ is left out: its streams
17091
+ * can overtake one another, so a MoQ source shows nothing about the sources
17092
+ * sent before it.
17093
+ */
17094
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17095
+ const previous = locator.newestSource;
17096
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17097
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17098
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17099
+ this.#shortenUnrecoverableWait(
17100
+ this.#blockKey(block.start, packetId, catalogTrackName),
17101
+ block.start,
17102
+ packetId,
17103
+ catalogTrackName
17104
+ );
17105
+ }
17106
+ }
17107
+ /**
17108
+ * The wait of a block that only a late source can complete (BLO-52085): a
17109
+ * repair stated it, its received sources plus every symbol its repair group
17110
+ * carries (RSB_length) are short of K' (the repair scheduler's
17111
+ * group-cannot-complete), and a later source of the stream has arrived, so
17112
+ * a source it misses is lost or reordered behind that one. The span, which
17113
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17114
+ * the late-symbol allowance alone, the multicast wait less the span (the
17115
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17116
+ * block.
17117
+ */
17118
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17119
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17120
+ const rsbLength = this.#blockRsbLengths.get(key);
17121
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17122
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17123
+ const received = this.#blockSourceEsis.get(key);
17124
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17125
+ if (present + rsbLength >= authoredLength) return void 0;
17126
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17127
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17128
+ return void 0;
17129
+ }
17130
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17131
+ }
17132
+ /**
17133
+ * Bring an open block's wait in to its late-source wait, when it has one.
17134
+ * Never later than it is armed: only a source can complete such a block,
17135
+ * so a repair of its group (paced across the span) must not keep it open.
17136
+ */
17137
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17138
+ if (!this.#activeBlocks.has(key)) return;
17139
+ const config = this.#trackConfigs.get(packetId);
17140
+ if (config === void 0) return;
17141
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17142
+ if (waitMs === void 0) return;
17143
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17144
+ this.#armBlockTimeout(
17145
+ sbn,
17146
+ packetId,
17147
+ catalogTrackName,
17148
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17149
+ );
17150
+ }
16978
17151
  /**
16979
17152
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16980
17153
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17129,6 +17302,15 @@ var FECManager = class _FECManager {
17129
17302
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17130
17303
  return;
17131
17304
  }
17305
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17306
+ key,
17307
+ sbn,
17308
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17309
+ packetId,
17310
+ catalogTrackName
17311
+ )) {
17312
+ return;
17313
+ }
17132
17314
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17133
17315
  }
17134
17316
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17137,6 +17319,7 @@ var FECManager = class _FECManager {
17137
17319
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17138
17320
  this.#blockSourceCounts.delete(key);
17139
17321
  this.#blockSourceEsis.delete(key);
17322
+ this.#blockRebuiltEsis.delete(key);
17140
17323
  this.#blockAuthoredLengths.delete(key);
17141
17324
  this.#blockRepairRsIds.delete(key);
17142
17325
  this.#blockRsbLengths.delete(key);
@@ -17203,7 +17386,16 @@ var FECManager = class _FECManager {
17203
17386
  this.#repairDeadlineMs(trackConfig)
17204
17387
  );
17205
17388
  }
17206
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17389
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17390
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17391
+ return;
17392
+ }
17393
+ this.#armBlockTimeout(
17394
+ sbn,
17395
+ packetId,
17396
+ catalogTrackName,
17397
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17398
+ );
17207
17399
  }
17208
17400
  #failBlock(sbn, packetId, catalogTrackName) {
17209
17401
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17216,6 +17408,7 @@ var FECManager = class _FECManager {
17216
17408
  }
17217
17409
  this.#blockSourceCounts.delete(key);
17218
17410
  this.#blockSourceEsis.delete(key);
17411
+ this.#blockRebuiltEsis.delete(key);
17219
17412
  this.#blockAuthoredLengths.delete(key);
17220
17413
  this.#blockRepairRsIds.delete(key);
17221
17414
  this.#blockRsbLengths.delete(key);
@@ -17254,6 +17447,7 @@ var FECManager = class _FECManager {
17254
17447
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17255
17448
  this.#blockSourceCounts.delete(key);
17256
17449
  this.#blockSourceEsis.delete(key);
17450
+ this.#blockRebuiltEsis.delete(key);
17257
17451
  this.#blockAuthoredLengths.delete(key);
17258
17452
  this.#blockRepairRsIds.delete(key);
17259
17453
  this.#blockRsbLengths.delete(key);
@@ -17345,6 +17539,7 @@ var FECManager = class _FECManager {
17345
17539
  this.#blockKeysBySbn.clear();
17346
17540
  this.#blockSourceCounts.clear();
17347
17541
  this.#blockSourceEsis.clear();
17542
+ this.#blockRebuiltEsis.clear();
17348
17543
  this.#blockAuthoredLengths.clear();
17349
17544
  this.#blockRepairRsIds.clear();
17350
17545
  this.#blockRsbLengths.clear();
@@ -17901,9 +18096,21 @@ var IsoBmffTimingResolver = class {
17901
18096
  }
17902
18097
  #track = null;
17903
18098
  #fragments = /* @__PURE__ */ new Map();
18099
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18100
+ #firstSamples = /* @__PURE__ */ new Map();
18101
+ /**
18102
+ * First sample and sample count of the newest fragments whose every slot was
18103
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18104
+ * matches the combiner, since it retires an abandoned sample as well.
18105
+ */
18106
+ #delivered = /* @__PURE__ */ new Map();
17904
18107
  #pendingSamples = 0;
17905
18108
  #evictedSamples = 0;
17906
18109
  #lastMovieFragmentRejection = null;
18110
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18111
+ get initialized() {
18112
+ return this.#track !== null;
18113
+ }
17907
18114
  get pendingSampleCount() {
17908
18115
  return this.#pendingSamples;
17909
18116
  }
@@ -17927,7 +18134,14 @@ var IsoBmffTimingResolver = class {
17927
18134
  this.#track = timing;
17928
18135
  return true;
17929
18136
  }
17930
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18137
+ /**
18138
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18139
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18140
+ * fragment whose samples are all done is a duplicate: admitting it again
18141
+ * would charge stale timing and could displace the live MPU's. A source
18142
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18143
+ */
18144
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17931
18145
  this.#lastMovieFragmentRejection = null;
17932
18146
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17933
18147
  this.#lastMovieFragmentRejection = "invalid";
@@ -17953,6 +18167,8 @@ var IsoBmffTimingResolver = class {
17953
18167
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17954
18168
  );
17955
18169
  }
18170
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18171
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17956
18172
  let freed = 0;
17957
18173
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17958
18174
  freed += this.#evictOldestFragment();
@@ -17966,6 +18182,7 @@ var IsoBmffTimingResolver = class {
17966
18182
  return false;
17967
18183
  }
17968
18184
  this.#fragments.set(key, [...parsed.samples]);
18185
+ this.#firstSamples.set(key, parsed.samples[0]);
17969
18186
  this.#pendingSamples += parsed.samples.length;
17970
18187
  return true;
17971
18188
  }
@@ -18023,6 +18240,7 @@ var IsoBmffTimingResolver = class {
18023
18240
  let live = 0;
18024
18241
  for (const sample of samples) if (sample !== null) live++;
18025
18242
  this.#fragments.delete(oldest);
18243
+ this.#firstSamples.delete(oldest);
18026
18244
  this.#pendingSamples -= live;
18027
18245
  this.#evictedSamples += live;
18028
18246
  return live;
@@ -18038,12 +18256,23 @@ var IsoBmffTimingResolver = class {
18038
18256
  if (!samples || !samples[index]) return false;
18039
18257
  samples[index] = null;
18040
18258
  this.#pendingSamples--;
18041
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18259
+ if (samples.every((value) => value === null)) {
18260
+ this.#fragments.delete(key);
18261
+ const first = this.#firstSamples.get(key);
18262
+ this.#firstSamples.delete(key);
18263
+ this.#delivered.delete(key);
18264
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18265
+ if (this.#delivered.size > this.maxPendingSamples) {
18266
+ this.#delivered.delete(this.#delivered.keys().next().value);
18267
+ }
18268
+ }
18042
18269
  return true;
18043
18270
  }
18044
18271
  reset() {
18045
18272
  this.#track = null;
18046
18273
  this.#fragments.clear();
18274
+ this.#firstSamples.clear();
18275
+ this.#delivered.clear();
18047
18276
  this.#pendingSamples = 0;
18048
18277
  }
18049
18278
  };
@@ -18525,6 +18754,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18525
18754
  this.#reclaimedTimingSamples = 0;
18526
18755
  this.#evictedTimingSamples = 0;
18527
18756
  this.#rejectedMovieFragments = 0;
18757
+ this.#recoveredHeadsAwaitingTiming = {
18758
+ held: 0,
18759
+ expired: 0,
18760
+ discarded: 0,
18761
+ superseded: 0
18762
+ };
18528
18763
  this.#sequencers = /* @__PURE__ */ new Map();
18529
18764
  this.#fecGate = null;
18530
18765
  this.#fecSequencer = null;
@@ -18633,6 +18868,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18633
18868
  #reclaimedTimingSamples;
18634
18869
  #evictedTimingSamples;
18635
18870
  #rejectedMovieFragments;
18871
+ #recoveredHeadsAwaitingTiming;
18636
18872
  #sequencers;
18637
18873
  #fecGate;
18638
18874
  #fecSequencer;
@@ -18753,6 +18989,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18753
18989
  rejectedMovieFragments: this.#rejectedMovieFragments
18754
18990
  };
18755
18991
  }
18992
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18993
+ get recoveredHeadsAwaitingTiming() {
18994
+ let pending = 0;
18995
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18996
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18997
+ }
18998
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
18999
+ }
18756
19000
  get reorderStats() {
18757
19001
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18758
19002
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18926,7 +19170,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18926
19170
  const now = Date.now();
18927
19171
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18928
19172
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18929
- awaitingTiming.shift();
19173
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18930
19174
  }
18931
19175
  }
18932
19176
  }
@@ -19044,11 +19288,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19044
19288
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19045
19289
  return this.#drop("fragment-unexpected-fragmentation");
19046
19290
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19047
- if (!resolver2) return this.#drop("fragment-no-resolver");
19291
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19048
19292
  const pendingBefore = resolver2.pendingSampleCount;
19049
19293
  const evictedBefore = resolver2.evictedSampleCount;
19050
19294
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19051
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19295
+ const admitted = resolver2.addMovieFragment(
19296
+ mpu.mpuSequenceNumber,
19297
+ data,
19298
+ remaining,
19299
+ options?.recovered === true
19300
+ );
19052
19301
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19053
19302
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19054
19303
  if (!admitted) {
@@ -19068,7 +19317,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19068
19317
  };
19069
19318
  }
19070
19319
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19071
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19320
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19072
19321
  if (!resolver) return this.#drop("mfu-no-resolver");
19073
19322
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19074
19323
  return this.#drop("mfu-empty-payload");
@@ -19089,6 +19338,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19089
19338
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19090
19339
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19091
19340
  sampleNumber: mfuDuHeader.sampleNumber,
19341
+ sequenceNumber: info.sequenceNumber,
19092
19342
  arrivedAt: Date.now()
19093
19343
  });
19094
19344
  return this.#drop("mfu-sample-timing-missing");
@@ -19477,6 +19727,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19477
19727
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19478
19728
  this.#sequencers.clear();
19479
19729
  this.#fecSequencer?.discard();
19730
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19731
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19732
+ }
19480
19733
  this.#timingStreams.clear();
19481
19734
  this.#pendingTimingSamples = 0;
19482
19735
  this.#reliableMoqStartupGate.clear();
@@ -19495,10 +19748,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19495
19748
  return resolver;
19496
19749
  }
19497
19750
  #awaitTiming(streamKey, head) {
19498
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19499
- if (!awaitingTiming) return;
19751
+ const state = this.#timingStreams.get(streamKey);
19752
+ if (!state) return;
19753
+ if (!head.options?.recovered) {
19754
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19755
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19756
+ state.awaitingTiming = kept;
19757
+ }
19758
+ const awaitingTiming = state.awaitingTiming;
19500
19759
  awaitingTiming.push(head);
19501
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19760
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19761
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19762
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19763
+ }
19764
+ }
19765
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19766
+ #leaveAwaitingTiming(head, outcome) {
19767
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19502
19768
  }
19503
19769
  /**
19504
19770
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19526,6 +19792,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19526
19792
  const state = this.#timingStreams.get(streamKey);
19527
19793
  if (!state) return;
19528
19794
  this.#timingStreams.delete(streamKey);
19795
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19529
19796
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19530
19797
  if (this.#pendingTimingSamples < 0) {
19531
19798
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22732,6 +22999,16 @@ function keyframeOverlaySsId(header) {
22732
22999
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22733
23000
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22734
23001
  }
23002
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23003
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23004
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23005
+ }
23006
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23007
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23008
+ return headerPsn + offset >>> 0;
23009
+ }
23010
+ var OVERLAY_ROUTED_LIMIT = 1024;
23011
+ var OVERLAY_PENDING_LIMIT = 256;
22735
23012
  var OVERLAY_OFFSET_MPUS = 16;
22736
23013
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22737
23014
  function assertStreamIdentity(role, stream) {
@@ -22843,6 +23120,7 @@ var MmtpRouter = class {
22843
23120
  #provenance = /* @__PURE__ */ new Map();
22844
23121
  #onFlowProtected = null;
22845
23122
  #unadmittedListedDrops = 0;
23123
+ #admittedSourceDatagrams = 0;
22846
23124
  #unlistedRepairSetAside = 0;
22847
23125
  #assistBlocks = /* @__PURE__ */ new Map();
22848
23126
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22899,6 +23177,15 @@ var MmtpRouter = class {
22899
23177
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22900
23178
  */
22901
23179
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23180
+ /**
23181
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23182
+ * sender's own packet_sequence_number: a recovered packet's header psn
23183
+ * equal to its SS_ID minus that offset. While it equals the stream's
23184
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23185
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23186
+ * an MPU no member of which arrived).
23187
+ */
23188
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22902
23189
  /**
22903
23190
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22904
23191
  * not parsed, typically because its overlap copy was parsed first
@@ -22968,7 +23255,9 @@ var MmtpRouter = class {
22968
23255
  recovered: 0,
22969
23256
  restarts: 0,
22970
23257
  unsequencedDropped: 0,
22971
- protectedDropped: 0
23258
+ protectedDropped: 0,
23259
+ baseFed: 0,
23260
+ baseUndetermined: 0
22972
23261
  };
22973
23262
  /** Callback fired when a delta-track FEC block completes or fails. */
22974
23263
  #onDeltaBlockCompleteCb = null;
@@ -23141,7 +23430,7 @@ var MmtpRouter = class {
23141
23430
  const cb = this.#getFrameCallback(packetId);
23142
23431
  if (isVideo && !assembler) {
23143
23432
  if (frameMeta.recovered) {
23144
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23433
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23145
23434
  } else {
23146
23435
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23147
23436
  }
@@ -23531,6 +23820,10 @@ var MmtpRouter = class {
23531
23820
  get keyframeOverlayStats() {
23532
23821
  return { ...this.#overlayStats };
23533
23822
  }
23823
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23824
+ get recoveredHeadsAwaitingTiming() {
23825
+ return this.#parser.recoveredHeadsAwaitingTiming;
23826
+ }
23534
23827
  #isOverlayRepair(repairPacketId, repairTrackName) {
23535
23828
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23536
23829
  }
@@ -23581,6 +23874,11 @@ var MmtpRouter = class {
23581
23874
  */
23582
23875
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23583
23876
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23877
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23878
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23879
+ stream.overlayToBase.routed.clear();
23880
+ stream.overlayToBase.pending.clear();
23881
+ }
23584
23882
  }
23585
23883
  #resetOverlay(inst) {
23586
23884
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -23637,9 +23935,10 @@ var MmtpRouter = class {
23637
23935
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23638
23936
  * every overlay installed for its source, at that SS_ID, with the bytes the
23639
23937
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23640
- * included (7.1, 7.4.3).
23938
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23939
+ * FEC reads it.
23641
23940
  */
23642
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23941
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23643
23942
  if (this.#overlays.size === 0) return;
23644
23943
  const ssId = keyframeOverlaySsId(header);
23645
23944
  if (ssId === void 0) return;
@@ -23648,7 +23947,11 @@ var MmtpRouter = class {
23648
23947
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23649
23948
  if (identity !== null) {
23650
23949
  inst.offsets.delete(identity.mpuSequenceNumber);
23651
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23950
+ inst.offsets.set(identity.mpuSequenceNumber, {
23951
+ psnOffset: ssId - sequenceNumber >>> 0,
23952
+ overlaySsId: ssId,
23953
+ ...baseSsId === void 0 ? {} : { baseSsId }
23954
+ });
23652
23955
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23653
23956
  }
23654
23957
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23727,14 +24030,27 @@ var MmtpRouter = class {
23727
24030
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23728
24031
  const restamped = this.#restampedStreams.has(streamKey);
23729
24032
  const restored = /* @__PURE__ */ new Set();
24033
+ const baseSsIds = /* @__PURE__ */ new Map();
23730
24034
  const symbols = result.symbols.flatMap((sym) => {
23731
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23732
- if (offset !== void 0) {
23733
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24035
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24036
+ if (anchor !== void 0) {
24037
+ const overlaySsId = serial32Add(blockStart, sym.index);
24038
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23734
24039
  restored.add(packetSequenceNumber);
23735
- return [{ ...sym, packetSequenceNumber }];
24040
+ const out = { ...sym, packetSequenceNumber };
24041
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24042
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
24043
+ return [out];
24044
+ }
24045
+ if (!restamped) {
24046
+ const baseSsId = unstampedBaseSsId(
24047
+ this.#ssIdSeqOffsetByStream.get(streamKey),
24048
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24049
+ sym.packetSequenceNumber
24050
+ );
24051
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
24052
+ return [sym];
23736
24053
  }
23737
- if (!restamped) return [sym];
23738
24054
  this.#overlayStats.unsequencedDropped++;
23739
24055
  return [];
23740
24056
  });
@@ -23743,6 +24059,85 @@ var MmtpRouter = class {
23743
24059
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23744
24060
  });
23745
24061
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24062
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24063
+ }
24064
+ /**
24065
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24066
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24067
+ * from a received member of its MPU, else unstampedBaseSsId),
24068
+ * so it is fed to the base decoder as a rebuilt source
24069
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24070
+ * fewer to solve, and its repair can recover the block's other packets.
24071
+ * A base block no repair has stated yet takes it when one does
24072
+ * (#feedPendingRebuilt).
24073
+ *
24074
+ * The overlay route already handled each packet fed (delivered it, or
24075
+ * held an untimed head for its FT=1), so a base recovery routes none of
24076
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24077
+ * held head would wait in the parser for timing its first copy consumed.
24078
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24079
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24080
+ * that crossed no multicast socket, which the provenance counted from the
24081
+ * block's own arrivals cannot show.
24082
+ */
24083
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24084
+ const fec = this.#fecManager;
24085
+ const owner = inst.leg.sourcePacketId;
24086
+ const name = inst.leg.sourceCatalogTrackName;
24087
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24088
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24089
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24090
+ const feeds = [];
24091
+ for (const sym of symbols) {
24092
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24093
+ const ssId = baseSsIds.get(sym);
24094
+ if (ssId === void 0) {
24095
+ this.#overlayStats.baseUndetermined++;
24096
+ continue;
24097
+ }
24098
+ routed.delete(ssId);
24099
+ routed.add(ssId);
24100
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24101
+ feeds.push([ssId, sym.rawSymbol]);
24102
+ }
24103
+ for (const [ssId, symbolData] of feeds) {
24104
+ if (this.#classifyPacketId(owner) === null) return;
24105
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24106
+ }
24107
+ }
24108
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24109
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24110
+ const fec = this.#fecManager;
24111
+ if (!fec?.feedRebuiltSourceSymbol) return;
24112
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24113
+ if (coords == null) {
24114
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24115
+ pending.delete(ssId);
24116
+ pending.set(ssId, symbolData.slice());
24117
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24118
+ return;
24119
+ }
24120
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24121
+ if (!fed.taken) return;
24122
+ this.#overlayStats.baseFed++;
24123
+ if (fed.recovered === null) return;
24124
+ const sbn = coords.sbn;
24125
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24126
+ fec.cancelBlockTimeout(sbn, owner, name);
24127
+ fec.countRecovery(owner, sbn, name);
24128
+ fec.markBlockCompleted(sbn, owner, name);
24129
+ });
24130
+ }
24131
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24132
+ #feedPendingRebuilt(owner, name, block) {
24133
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24134
+ if (pending === void 0 || pending.size === 0) return;
24135
+ for (const [ssId, symbolData] of [...pending]) {
24136
+ if (this.#classifyPacketId(owner) === null) return;
24137
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24138
+ pending.delete(ssId);
24139
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24140
+ }
23746
24141
  }
23747
24142
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23748
24143
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23882,6 +24277,10 @@ var MmtpRouter = class {
23882
24277
  get unadmittedListedDropCount() {
23883
24278
  return this.#unadmittedListedDrops;
23884
24279
  }
24280
+ /** 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. */
24281
+ get admittedSourceDatagramCount() {
24282
+ return this.#admittedSourceDatagrams;
24283
+ }
23885
24284
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23886
24285
  get unlistedRepairSetAsideCount() {
23887
24286
  return this.#unlistedRepairSetAside;
@@ -24368,6 +24767,8 @@ var MmtpRouter = class {
24368
24767
  )) return;
24369
24768
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24370
24769
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24770
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24771
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24371
24772
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24372
24773
  const recovered = fec.feedRepairSymbol(
24373
24774
  ownerPacketId,
@@ -24505,6 +24906,7 @@ var MmtpRouter = class {
24505
24906
  this.#droppedPreSyncVideo.delete(streamKey);
24506
24907
  this.#latestVideoSeqByStream.delete(streamKey);
24507
24908
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24909
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24508
24910
  this.#multicastRapTwinByStream.delete(streamKey);
24509
24911
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24510
24912
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25139,6 +25541,7 @@ var MmtpRouter = class {
25139
25541
  const parsed = this.#parser.parsePacket(packet);
25140
25542
  if (!parsed) return;
25141
25543
  const info = parsed.info;
25544
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25142
25545
  if (source === "multicast" && !manifestAdmitted) {
25143
25546
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25144
25547
  }
@@ -25420,7 +25823,8 @@ var MmtpRouter = class {
25420
25823
  parsed.header,
25421
25824
  info.sequenceNumber,
25422
25825
  info.rapFlag,
25423
- source
25826
+ source,
25827
+ fecPayloadId?.ssId
25424
25828
  );
25425
25829
  }
25426
25830
  }
@@ -25570,7 +25974,8 @@ var MmtpRouter = class {
25570
25974
  if (stream === void 0) {
25571
25975
  stream = {
25572
25976
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25573
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
25977
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
25978
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25574
25979
  };
25575
25980
  byName.set(catalogTrackName, stream);
25576
25981
  }
@@ -25685,8 +26090,16 @@ var MmtpRouter = class {
25685
26090
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25686
26091
  this.#emitDirectVideo(packetId, data, meta, cb);
25687
26092
  }
25688
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26093
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25689
26094
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26095
+ if (this.#isFecTrack(packetId)) {
26096
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26097
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26098
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26099
+ this.#emitDirectVideo(packetId, data, meta, cb);
26100
+ return;
26101
+ }
26102
+ }
25690
26103
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25691
26104
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25692
26105
  this.#drainDirectRecoveredVideo(
@@ -25840,6 +26253,13 @@ var MmtpRouter = class {
25840
26253
  * frames by epoch only if pre-protection input ever matters.
25841
26254
  */
25842
26255
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26256
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26257
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26258
+ result = {
26259
+ ...result,
26260
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26261
+ };
26262
+ }
25843
26263
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25844
26264
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25845
26265
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25904,7 +26324,10 @@ var MmtpRouter = class {
25904
26324
  ...result,
25905
26325
  symbols: result.symbols.map((sym) => {
25906
26326
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25907
- if (wire === sym.packetSequenceNumber) return sym;
26327
+ if (wire === sym.packetSequenceNumber) {
26328
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26329
+ return sym;
26330
+ }
25908
26331
  this.#restampedRecoveredSeqs++;
25909
26332
  this.#restampedStreams.add(streamKey);
25910
26333
  return { ...sym, packetSequenceNumber: wire };
@@ -25940,16 +26363,29 @@ var MmtpRouter = class {
25940
26363
  }
25941
26364
  );
25942
26365
  }
25943
- result = {
25944
- ...result,
25945
- symbols: result.symbols.filter((sym) => sym.fragmentType !== 2 || sym.fragment.fragmentationIndicator !== 1 || sym.fragment.movieFragmentSequenceNumber === void 0 || sym.fragment.sampleNumber === void 0 || this.#parser.hasAuthoredTimingFor(
25946
- ownerPacketId,
25947
- sym.fragment.mpuSequenceNumber,
25948
- sym.fragment.movieFragmentSequenceNumber,
25949
- sym.fragment.sampleNumber,
25950
- catalogTrackName
25951
- ))
25952
- };
26366
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26367
+ ownerPacketId,
26368
+ sym.fragment.mpuSequenceNumber,
26369
+ sym.fragment.movieFragmentSequenceNumber,
26370
+ sym.fragment.sampleNumber,
26371
+ catalogTrackName
26372
+ );
26373
+ for (const sym of result.symbols) {
26374
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26375
+ this.#feedViaParser(
26376
+ sym.rawSymbol,
26377
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26378
+ void 0,
26379
+ {
26380
+ skipFecPayloadId: true,
26381
+ recovered: true,
26382
+ deliveryProvenance,
26383
+ ...fecSbnOf(sym.index),
26384
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26385
+ }
26386
+ );
26387
+ }
26388
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25953
26389
  const gated = this.#isFecTrack(ownerPacketId);
25954
26390
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25955
26391
  if (assembler) {