@blockcast/mmt-container 2.2.0-main.4fb5899c44e6 → 2.2.0-main.66f097ed5157

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 (44) hide show
  1. package/dist/browser-runtime.js +595 -61
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/fec-manager.d.ts +30 -0
  4. package/dist/fec-manager.d.ts.map +1 -1
  5. package/dist/fec-manager.js +196 -2
  6. package/dist/fec-manager.js.map +2 -2
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js.map +2 -2
  10. package/dist/iso-block-locator.d.ts +9 -0
  11. package/dist/iso-block-locator.d.ts.map +1 -1
  12. package/dist/iso-block-locator.js +17 -0
  13. package/dist/iso-block-locator.js.map +2 -2
  14. package/dist/iso-bmff-timing.d.ts +10 -1
  15. package/dist/iso-bmff-timing.d.ts.map +1 -1
  16. package/dist/iso-bmff-timing.js +36 -2
  17. package/dist/iso-bmff-timing.js.map +2 -2
  18. package/dist/mmtp-container-parser.d.ts +75 -10
  19. package/dist/mmtp-container-parser.d.ts.map +1 -1
  20. package/dist/mmtp-container-parser.js +157 -25
  21. package/dist/mmtp-container-parser.js.map +2 -2
  22. package/dist/mmtp-container-parser.no-modules.js +159 -28
  23. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  24. package/dist/mmtp-decode-pipeline.js +156 -25
  25. package/dist/mmtp-decode-pipeline.js.map +2 -2
  26. package/dist/mmtp-router.d.ts +24 -1
  27. package/dist/mmtp-router.d.ts.map +1 -1
  28. package/dist/mmtp-router.js +397 -57
  29. package/dist/mmtp-router.js.map +2 -2
  30. package/dist/multicast-manager.js +595 -61
  31. package/dist/multicast-manager.js.map +2 -2
  32. package/dist/multicast-transport.js +2 -2
  33. package/dist/multicast-transport.js.map +2 -2
  34. package/dist/no-modules.js +156 -25
  35. package/dist/no-modules.js.map +2 -2
  36. package/dist/wasm-mmtp-container-parser.js +156 -25
  37. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  38. package/package.json +5 -5
  39. package/src/fec-manager.ts +251 -2
  40. package/src/index.ts +1 -0
  41. package/src/iso-block-locator.ts +19 -0
  42. package/src/iso-bmff-timing.ts +40 -1
  43. package/src/mmtp-container-parser.ts +222 -28
  44. package/src/mmtp-router.ts +413 -39
@@ -8625,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
8625
8625
  } catch (error) {
8626
8626
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8627
8627
  }
8628
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8628
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8629
8629
  await rx.close();
8630
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8630
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8631
8631
  }
8632
8632
  const events = new EventTarget();
8633
8633
  let state = "connected";
@@ -12949,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12949
12949
  #retentionBlocks;
12950
12950
  #heldEvicted;
12951
12951
  #newest;
12952
+ #newestSource;
12952
12953
  #holes;
12953
12954
  static #checkedRetention(retentionBlocks) {
12954
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12986,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12986
12987
  observe(position) {
12987
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12988
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
+ }
12989
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12990
13006
  get heldEvicted() {
12991
13007
  return this.#heldEvicted;
@@ -13218,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13218
13234
  this.#held.clear();
13219
13235
  this.#coverageEnd = void 0;
13220
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13221
13238
  }
13222
13239
  #claim(block) {
13223
13240
  const placed = [];
@@ -13837,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13837
13854
  var WORKER_ERROR_KIND_CAP = 32;
13838
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13839
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13840
13858
  function isMulticastSymbolDelivery(value) {
13841
13859
  return value === "datagram" || value === "ordered-relay";
13842
13860
  }
@@ -13934,6 +13952,10 @@ var FECManager = class _FECManager {
13934
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13935
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13936
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();
13937
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13938
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13939
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14223,6 +14245,7 @@ var FECManager = class _FECManager {
14223
14245
  #pendingWorkerCleanupBounds;
14224
14246
  #blockSourceCounts;
14225
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14226
14249
  #blockAuthoredLengths;
14227
14250
  #ssbLengthConflictCount;
14228
14251
  #blockRepairRsIds;
@@ -15545,6 +15568,7 @@ var FECManager = class _FECManager {
15545
15568
  }
15546
15569
  resetMap(this.#blockSourceCounts);
15547
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15548
15572
  resetMap(this.#blockAuthoredLengths);
15549
15573
  resetMap(this.#blockRepairRsIds);
15550
15574
  resetMap(this.#blockRsbLengths);
@@ -15669,6 +15693,7 @@ var FECManager = class _FECManager {
15669
15693
  dropMap(this.#pendingWorkerCleanupClients);
15670
15694
  dropMap(this.#blockSourceCounts);
15671
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15672
15697
  dropMap(this.#blockAuthoredLengths);
15673
15698
  dropMap(this.#blockRepairRsIds);
15674
15699
  dropMap(this.#blockRsbLengths);
@@ -15831,6 +15856,9 @@ var FECManager = class _FECManager {
15831
15856
  this.#unindexBlockKey(key);
15832
15857
  }
15833
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15834
15862
  return true;
15835
15863
  }
15836
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16148,6 +16176,7 @@ var FECManager = class _FECManager {
16148
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16149
16177
  const evictedBefore = locator.heldEvicted;
16150
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16151
16180
  const evicted = locator.heldEvicted - evictedBefore;
16152
16181
  if (evicted > 0) {
16153
16182
  const before = this.#isoHeldEvictedCount;
@@ -16743,7 +16772,15 @@ var FECManager = class _FECManager {
16743
16772
  this.#isoSourceOversizeCount++;
16744
16773
  return null;
16745
16774
  }
16746
- 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) {
16747
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16748
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16749
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16772,6 +16809,74 @@ var FECManager = class _FECManager {
16772
16809
  }
16773
16810
  return recovered;
16774
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
+ }
16775
16880
  /**
16776
16881
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16777
16882
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16974,8 +17079,75 @@ var FECManager = class _FECManager {
16974
17079
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16975
17080
  return null;
16976
17081
  }
17082
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16977
17083
  return recovered;
16978
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
+ }
16979
17151
  /**
16980
17152
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16981
17153
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17130,6 +17302,15 @@ var FECManager = class _FECManager {
17130
17302
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17131
17303
  return;
17132
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
+ }
17133
17314
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17134
17315
  }
17135
17316
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17138,6 +17319,7 @@ var FECManager = class _FECManager {
17138
17319
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17139
17320
  this.#blockSourceCounts.delete(key);
17140
17321
  this.#blockSourceEsis.delete(key);
17322
+ this.#blockRebuiltEsis.delete(key);
17141
17323
  this.#blockAuthoredLengths.delete(key);
17142
17324
  this.#blockRepairRsIds.delete(key);
17143
17325
  this.#blockRsbLengths.delete(key);
@@ -17204,7 +17386,16 @@ var FECManager = class _FECManager {
17204
17386
  this.#repairDeadlineMs(trackConfig)
17205
17387
  );
17206
17388
  }
17207
- 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
+ );
17208
17399
  }
17209
17400
  #failBlock(sbn, packetId, catalogTrackName) {
17210
17401
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17217,6 +17408,7 @@ var FECManager = class _FECManager {
17217
17408
  }
17218
17409
  this.#blockSourceCounts.delete(key);
17219
17410
  this.#blockSourceEsis.delete(key);
17411
+ this.#blockRebuiltEsis.delete(key);
17220
17412
  this.#blockAuthoredLengths.delete(key);
17221
17413
  this.#blockRepairRsIds.delete(key);
17222
17414
  this.#blockRsbLengths.delete(key);
@@ -17255,6 +17447,7 @@ var FECManager = class _FECManager {
17255
17447
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17256
17448
  this.#blockSourceCounts.delete(key);
17257
17449
  this.#blockSourceEsis.delete(key);
17450
+ this.#blockRebuiltEsis.delete(key);
17258
17451
  this.#blockAuthoredLengths.delete(key);
17259
17452
  this.#blockRepairRsIds.delete(key);
17260
17453
  this.#blockRsbLengths.delete(key);
@@ -17346,6 +17539,7 @@ var FECManager = class _FECManager {
17346
17539
  this.#blockKeysBySbn.clear();
17347
17540
  this.#blockSourceCounts.clear();
17348
17541
  this.#blockSourceEsis.clear();
17542
+ this.#blockRebuiltEsis.clear();
17349
17543
  this.#blockAuthoredLengths.clear();
17350
17544
  this.#blockRepairRsIds.clear();
17351
17545
  this.#blockRsbLengths.clear();
@@ -17902,9 +18096,21 @@ var IsoBmffTimingResolver = class {
17902
18096
  }
17903
18097
  #track = null;
17904
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();
17905
18107
  #pendingSamples = 0;
17906
18108
  #evictedSamples = 0;
17907
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
+ }
17908
18114
  get pendingSampleCount() {
17909
18115
  return this.#pendingSamples;
17910
18116
  }
@@ -17928,7 +18134,14 @@ var IsoBmffTimingResolver = class {
17928
18134
  this.#track = timing;
17929
18135
  return true;
17930
18136
  }
17931
- 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) {
17932
18145
  this.#lastMovieFragmentRejection = null;
17933
18146
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17934
18147
  this.#lastMovieFragmentRejection = "invalid";
@@ -17954,6 +18167,8 @@ var IsoBmffTimingResolver = class {
17954
18167
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17955
18168
  );
17956
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;
17957
18172
  let freed = 0;
17958
18173
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17959
18174
  freed += this.#evictOldestFragment();
@@ -17967,6 +18182,7 @@ var IsoBmffTimingResolver = class {
17967
18182
  return false;
17968
18183
  }
17969
18184
  this.#fragments.set(key, [...parsed.samples]);
18185
+ this.#firstSamples.set(key, parsed.samples[0]);
17970
18186
  this.#pendingSamples += parsed.samples.length;
17971
18187
  return true;
17972
18188
  }
@@ -18024,6 +18240,7 @@ var IsoBmffTimingResolver = class {
18024
18240
  let live = 0;
18025
18241
  for (const sample of samples) if (sample !== null) live++;
18026
18242
  this.#fragments.delete(oldest);
18243
+ this.#firstSamples.delete(oldest);
18027
18244
  this.#pendingSamples -= live;
18028
18245
  this.#evictedSamples += live;
18029
18246
  return live;
@@ -18039,12 +18256,23 @@ var IsoBmffTimingResolver = class {
18039
18256
  if (!samples || !samples[index]) return false;
18040
18257
  samples[index] = null;
18041
18258
  this.#pendingSamples--;
18042
- 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
+ }
18043
18269
  return true;
18044
18270
  }
18045
18271
  reset() {
18046
18272
  this.#track = null;
18047
18273
  this.#fragments.clear();
18274
+ this.#firstSamples.clear();
18275
+ this.#delivered.clear();
18048
18276
  this.#pendingSamples = 0;
18049
18277
  }
18050
18278
  };
@@ -18276,6 +18504,7 @@ var FecReleaseSequencer = class {
18276
18504
  stream.lastSbn = void 0;
18277
18505
  stream.discontinuity = false;
18278
18506
  stream.rapSsId = void 0;
18507
+ stream.sourced = false;
18279
18508
  }
18280
18509
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18281
18510
  this.#recordStat("droppedStale");
@@ -18302,6 +18531,7 @@ var FecReleaseSequencer = class {
18302
18531
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18303
18532
  else hi = mid;
18304
18533
  }
18534
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18305
18535
  stream.buf.splice(lo, 0, frame);
18306
18536
  this.#drainUnlessDeferred(stream);
18307
18537
  }
@@ -18337,6 +18567,21 @@ var FecReleaseSequencer = class {
18337
18567
  }
18338
18568
  return false;
18339
18569
  }
18570
+ /**
18571
+ * These streams' sink is being replaced by one that has seen no source
18572
+ * frame yet. First release, to the sink being replaced, the frames the
18573
+ * unanchored path had already released to it (#cursorPassed): there they
18574
+ * waited in its queue, which its replacement flushes. Then treat the
18575
+ * streams as unsourced until a source frame is released.
18576
+ */
18577
+ unsource(packetId, catalogTrackName) {
18578
+ for (const stream of this.#streams.values()) {
18579
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18580
+ this.#drain(stream, true);
18581
+ stream.sourced = false;
18582
+ }
18583
+ }
18584
+ }
18340
18585
  /** Re-evaluate held gaps after FEC state changed. */
18341
18586
  poke(streamKey) {
18342
18587
  if (streamKey !== void 0) {
@@ -18428,13 +18673,15 @@ var FecReleaseSequencer = class {
18428
18673
  last: null,
18429
18674
  lastSbn: void 0,
18430
18675
  discontinuity: false,
18431
- rapSsId: void 0
18676
+ rapSsId: void 0,
18677
+ sourced: false
18432
18678
  };
18433
18679
  this.#streams.set(streamKey, stream);
18434
18680
  }
18435
18681
  return stream;
18436
18682
  }
18437
- #drain(stream) {
18683
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18684
+ #drain(stream, asUnanchored = false) {
18438
18685
  while (stream.buf.length > 0 && stream.last !== null) {
18439
18686
  const head = stream.buf[0];
18440
18687
  const last = stream.last;
@@ -18451,9 +18698,10 @@ var FecReleaseSequencer = class {
18451
18698
  }
18452
18699
  const headSbn = head.meta.fecSbn;
18453
18700
  const from = stream.lastSbn ?? headSbn;
18454
- const to = headSbn ?? stream.lastSbn;
18701
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18455
18702
  let lost = [];
18456
18703
  const unanchored = from === void 0 && to === void 0;
18704
+ let unresolved = false;
18457
18705
  if (from !== void 0 && to !== void 0) {
18458
18706
  const resolved = this.#gate.resolvedLoss(
18459
18707
  stream.packetId,
@@ -18462,7 +18710,8 @@ var FecReleaseSequencer = class {
18462
18710
  Math.max(from, to),
18463
18711
  from
18464
18712
  );
18465
- if (resolved === null) {
18713
+ unresolved = resolved === null;
18714
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18466
18715
  const heldMs = performance.now() - head.arrivedAtMs;
18467
18716
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18468
18717
  this.#recordStat("forcedWindowFlushes");
@@ -18470,20 +18719,50 @@ var FecReleaseSequencer = class {
18470
18719
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18471
18720
  continue;
18472
18721
  }
18473
- lost = resolved;
18722
+ lost = resolved ?? [];
18474
18723
  }
18475
18724
  stream.buf.shift();
18476
18725
  this.#release(
18477
18726
  stream,
18478
18727
  head,
18479
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18728
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18480
18729
  );
18481
18730
  }
18482
18731
  }
18732
+ /**
18733
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
18734
+ * source cursor holds it unanchored, and neither does a later frame behind
18735
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
18736
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
18737
+ * like each frame that arrived in front of it since (stale there). The head
18738
+ * then goes, decided as it would be unanchored, once a frame the base path
18739
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
18740
+ * marked one: the base path has passed that one, so a frame still missing
18741
+ * before it can only come from a repair. It also goes while no source frame
18742
+ * has been released, when the direct path shows a recovered frame at once.
18743
+ * And it goes at once while a staged switch decides on when frames arrive
18744
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18745
+ * sink being replaced: unanchored, it had already reached the sink.
18746
+ */
18747
+ #cursorPassed(stream, asUnanchored) {
18748
+ let at2 = -1;
18749
+ for (let i = 0; i < stream.buf.length; i++) {
18750
+ const frame = stream.buf[i];
18751
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
18752
+ frame.passed = true;
18753
+ if (at2 < 0) at2 = i;
18754
+ } else if (frame.passed !== true) break;
18755
+ }
18756
+ if (at2 < 0) return false;
18757
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
18758
+ }
18483
18759
  #release(stream, frame, decision) {
18484
18760
  if (decision) frame.meta.gapBefore = decision;
18485
18761
  stream.last = frame;
18486
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
18762
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
18763
+ stream.lastSbn = frame.meta.fecSbn;
18764
+ }
18765
+ if (frame.meta.recovered !== true) stream.sourced = true;
18487
18766
  this.#gate.onReleased?.(frame.data, frame.meta);
18488
18767
  this.#emit(frame.data, frame.meta);
18489
18768
  }
@@ -18526,6 +18805,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18526
18805
  this.#reclaimedTimingSamples = 0;
18527
18806
  this.#evictedTimingSamples = 0;
18528
18807
  this.#rejectedMovieFragments = 0;
18808
+ this.#recoveredHeadsAwaitingTiming = {
18809
+ held: 0,
18810
+ expired: 0,
18811
+ discarded: 0,
18812
+ superseded: 0
18813
+ };
18529
18814
  this.#sequencers = /* @__PURE__ */ new Map();
18530
18815
  this.#fecGate = null;
18531
18816
  this.#fecSequencer = null;
@@ -18634,6 +18919,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18634
18919
  #reclaimedTimingSamples;
18635
18920
  #evictedTimingSamples;
18636
18921
  #rejectedMovieFragments;
18922
+ #recoveredHeadsAwaitingTiming;
18637
18923
  #sequencers;
18638
18924
  #fecGate;
18639
18925
  #fecSequencer;
@@ -18739,11 +19025,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18739
19025
  /**
18740
19026
  * Typed view of the ISO-BMFF timing-sample budget.
18741
19027
  *
18742
- * This is the acceptance signal for the budget ratchet: `pending` walking
18743
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18744
- * and it is observable here rather than only as media silently stopping.
18745
- * Per repo policy this is a producer-owned counter -- never infer budget
18746
- * state by parsing logs.
19028
+ * Each stream admits timing against its own window of `capacity` slots
19029
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
19030
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
19031
+ * timing a stream declared and never delivered shows up there. `pending`
19032
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
19033
+ * a producer-owned counter -- never infer budget state by parsing logs.
18747
19034
  */
18748
19035
  get timingBudgetStats() {
18749
19036
  return {
@@ -18754,6 +19041,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18754
19041
  rejectedMovieFragments: this.#rejectedMovieFragments
18755
19042
  };
18756
19043
  }
19044
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
19045
+ get recoveredHeadsAwaitingTiming() {
19046
+ let pending = 0;
19047
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19048
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
19049
+ }
19050
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
19051
+ }
18757
19052
  get reorderStats() {
18758
19053
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18759
19054
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18795,6 +19090,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18795
19090
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18796
19091
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18797
19092
  }
19093
+ /** See FecReleaseSequencer.unsource. */
19094
+ unsourceFecRelease(packetId, catalogTrackName) {
19095
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
19096
+ }
18798
19097
  /** Re-evaluate held gaps after FEC block state changed. */
18799
19098
  pokeFecRelease(packetId, catalogTrackName) {
18800
19099
  if (!this.#fecSequencer) return;
@@ -18927,7 +19226,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18927
19226
  const now = Date.now();
18928
19227
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18929
19228
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18930
- awaitingTiming.shift();
19229
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18931
19230
  }
18932
19231
  }
18933
19232
  }
@@ -19045,11 +19344,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19045
19344
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19046
19345
  return this.#drop("fragment-unexpected-fragmentation");
19047
19346
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19048
- if (!resolver2) return this.#drop("fragment-no-resolver");
19347
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19049
19348
  const pendingBefore = resolver2.pendingSampleCount;
19050
19349
  const evictedBefore = resolver2.evictedSampleCount;
19051
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19052
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19350
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19351
+ const admitted = resolver2.addMovieFragment(
19352
+ mpu.mpuSequenceNumber,
19353
+ data,
19354
+ remaining,
19355
+ options?.recovered === true
19356
+ );
19053
19357
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19054
19358
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19055
19359
  if (!admitted) {
@@ -19069,7 +19373,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19069
19373
  };
19070
19374
  }
19071
19375
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19072
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19376
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19073
19377
  if (!resolver) return this.#drop("mfu-no-resolver");
19074
19378
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19075
19379
  return this.#drop("mfu-empty-payload");
@@ -19090,6 +19394,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19090
19394
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19091
19395
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19092
19396
  sampleNumber: mfuDuHeader.sampleNumber,
19397
+ sequenceNumber: info.sequenceNumber,
19093
19398
  arrivedAt: Date.now()
19094
19399
  });
19095
19400
  return this.#drop("mfu-sample-timing-missing");
@@ -19115,7 +19420,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19115
19420
  info,
19116
19421
  resolver,
19117
19422
  options?.recovered === true,
19118
- options?.fecSbn
19423
+ options?.fecSbn,
19424
+ options?.fecSbnFirst
19119
19425
  );
19120
19426
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19121
19427
  }
@@ -19208,7 +19514,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19208
19514
  info,
19209
19515
  resolver,
19210
19516
  options?.recovered === true,
19211
- options?.fecSbn
19517
+ options?.fecSbn,
19518
+ options?.fecSbnFirst
19212
19519
  );
19213
19520
  if (outcome.record !== void 0) return outcome.record;
19214
19521
  return this.#drop(outcome.reason);
@@ -19228,7 +19535,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19228
19535
  * attribution exists to keep readable. Leaving the choice at the call site
19229
19536
  * is what keeps both true.
19230
19537
  */
19231
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19538
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19232
19539
  const timing = resolver.sampleTiming(
19233
19540
  combined.mpuSequenceNumber,
19234
19541
  combined.movieFragmentSequenceNumber,
@@ -19245,6 +19552,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19245
19552
  );
19246
19553
  if (!meta) return { reason: "combined-meta-build-failed" };
19247
19554
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19555
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19248
19556
  if (!resolver.consumeSample(
19249
19557
  combined.mpuSequenceNumber,
19250
19558
  combined.movieFragmentSequenceNumber,
@@ -19478,6 +19786,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19478
19786
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19479
19787
  this.#sequencers.clear();
19480
19788
  this.#fecSequencer?.discard();
19789
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19790
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19791
+ }
19481
19792
  this.#timingStreams.clear();
19482
19793
  this.#pendingTimingSamples = 0;
19483
19794
  this.#reliableMoqStartupGate.clear();
@@ -19496,10 +19807,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19496
19807
  return resolver;
19497
19808
  }
19498
19809
  #awaitTiming(streamKey, head) {
19499
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19500
- if (!awaitingTiming) return;
19810
+ const state = this.#timingStreams.get(streamKey);
19811
+ if (!state) return;
19812
+ if (!head.options?.recovered) {
19813
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19814
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19815
+ state.awaitingTiming = kept;
19816
+ }
19817
+ const awaitingTiming = state.awaitingTiming;
19501
19818
  awaitingTiming.push(head);
19502
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19819
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19820
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19821
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19822
+ }
19823
+ }
19824
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19825
+ #leaveAwaitingTiming(head, outcome) {
19826
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19503
19827
  }
19504
19828
  /**
19505
19829
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19527,6 +19851,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19527
19851
  const state = this.#timingStreams.get(streamKey);
19528
19852
  if (!state) return;
19529
19853
  this.#timingStreams.delete(streamKey);
19854
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19530
19855
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19531
19856
  if (this.#pendingTimingSamples < 0) {
19532
19857
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22733,6 +23058,16 @@ function keyframeOverlaySsId(header) {
22733
23058
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22734
23059
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22735
23060
  }
23061
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23062
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23063
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23064
+ }
23065
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23066
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23067
+ return headerPsn + offset >>> 0;
23068
+ }
23069
+ var OVERLAY_ROUTED_LIMIT = 1024;
23070
+ var OVERLAY_PENDING_LIMIT = 256;
22736
23071
  var OVERLAY_OFFSET_MPUS = 16;
22737
23072
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22738
23073
  function assertStreamIdentity(role, stream) {
@@ -22844,6 +23179,7 @@ var MmtpRouter = class {
22844
23179
  #provenance = /* @__PURE__ */ new Map();
22845
23180
  #onFlowProtected = null;
22846
23181
  #unadmittedListedDrops = 0;
23182
+ #admittedSourceDatagrams = 0;
22847
23183
  #unlistedRepairSetAside = 0;
22848
23184
  #assistBlocks = /* @__PURE__ */ new Map();
22849
23185
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22900,6 +23236,15 @@ var MmtpRouter = class {
22900
23236
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22901
23237
  */
22902
23238
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23239
+ /**
23240
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23241
+ * sender's own packet_sequence_number: a recovered packet's header psn
23242
+ * equal to its SS_ID minus that offset. While it equals the stream's
23243
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23244
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23245
+ * an MPU no member of which arrived).
23246
+ */
23247
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22903
23248
  /**
22904
23249
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22905
23250
  * not parsed, typically because its overlap copy was parsed first
@@ -22969,7 +23314,9 @@ var MmtpRouter = class {
22969
23314
  recovered: 0,
22970
23315
  restarts: 0,
22971
23316
  unsequencedDropped: 0,
22972
- protectedDropped: 0
23317
+ protectedDropped: 0,
23318
+ baseFed: 0,
23319
+ baseUndetermined: 0
22973
23320
  };
22974
23321
  /** Callback fired when a delta-track FEC block completes or fails. */
22975
23322
  #onDeltaBlockCompleteCb = null;
@@ -23142,7 +23489,7 @@ var MmtpRouter = class {
23142
23489
  const cb = this.#getFrameCallback(packetId);
23143
23490
  if (isVideo && !assembler) {
23144
23491
  if (frameMeta.recovered) {
23145
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23492
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23146
23493
  } else {
23147
23494
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23148
23495
  }
@@ -23406,6 +23753,7 @@ var MmtpRouter = class {
23406
23753
  enableMseAssembler(packetId, config, catalogTrackName) {
23407
23754
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23408
23755
  const previous = this.#mseAssemblers.get(assemblerKey);
23756
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23409
23757
  if (previous) {
23410
23758
  previous.flush();
23411
23759
  previous.dispose();
@@ -23532,6 +23880,10 @@ var MmtpRouter = class {
23532
23880
  get keyframeOverlayStats() {
23533
23881
  return { ...this.#overlayStats };
23534
23882
  }
23883
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23884
+ get recoveredHeadsAwaitingTiming() {
23885
+ return this.#parser.recoveredHeadsAwaitingTiming;
23886
+ }
23535
23887
  #isOverlayRepair(repairPacketId, repairTrackName) {
23536
23888
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23537
23889
  }
@@ -23582,6 +23934,11 @@ var MmtpRouter = class {
23582
23934
  */
23583
23935
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23584
23936
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23937
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23938
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23939
+ stream.overlayToBase.routed.clear();
23940
+ stream.overlayToBase.pending.clear();
23941
+ }
23585
23942
  }
23586
23943
  #resetOverlay(inst) {
23587
23944
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -23638,9 +23995,10 @@ var MmtpRouter = class {
23638
23995
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23639
23996
  * every overlay installed for its source, at that SS_ID, with the bytes the
23640
23997
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23641
- * included (7.1, 7.4.3).
23998
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23999
+ * FEC reads it.
23642
24000
  */
23643
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24001
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23644
24002
  if (this.#overlays.size === 0) return;
23645
24003
  const ssId = keyframeOverlaySsId(header);
23646
24004
  if (ssId === void 0) return;
@@ -23649,7 +24007,11 @@ var MmtpRouter = class {
23649
24007
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23650
24008
  if (identity !== null) {
23651
24009
  inst.offsets.delete(identity.mpuSequenceNumber);
23652
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
24010
+ inst.offsets.set(identity.mpuSequenceNumber, {
24011
+ psnOffset: ssId - sequenceNumber >>> 0,
24012
+ overlaySsId: ssId,
24013
+ ...baseSsId === void 0 ? {} : { baseSsId }
24014
+ });
23653
24015
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23654
24016
  }
23655
24017
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23704,11 +24066,16 @@ var MmtpRouter = class {
23704
24066
  }
23705
24067
  /**
23706
24068
  * Route an overlay-recovered block's packets as source media of the
23707
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23708
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23709
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23710
- * from its keyframe's own offset; without one, a stream the base never
23711
- * re-stamped keeps the header's sequence and any other drops the packet.
24069
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
24070
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
24071
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
24072
+ * base FEC recovered does, so it waits behind base frames still in flight,
24073
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
24074
+ * longer than unanchored; one whose base SS_ID is undetermined has no
24075
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
24076
+ * advance. Each packet's wire sequence is restored from its keyframe's own
24077
+ * offset; without one, a stream the base never re-stamped keeps the
24078
+ * header's sequence and any other drops the packet.
23712
24079
  */
23713
24080
  #routeOverlayRecovered(inst, blockStart, recovered) {
23714
24081
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23728,22 +24095,127 @@ var MmtpRouter = class {
23728
24095
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23729
24096
  const restamped = this.#restampedStreams.has(streamKey);
23730
24097
  const restored = /* @__PURE__ */ new Set();
24098
+ const baseSsIds = /* @__PURE__ */ new Map();
23731
24099
  const symbols = result.symbols.flatMap((sym) => {
23732
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23733
- if (offset !== void 0) {
23734
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24100
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24101
+ if (anchor !== void 0) {
24102
+ const overlaySsId = serial32Add(blockStart, sym.index);
24103
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23735
24104
  restored.add(packetSequenceNumber);
23736
- return [{ ...sym, packetSequenceNumber }];
24105
+ const out = { ...sym, packetSequenceNumber };
24106
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24107
+ if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
24108
+ return [out];
24109
+ }
24110
+ if (!restamped) {
24111
+ const baseSsId = unstampedBaseSsId(
24112
+ this.#ssIdSeqOffsetByStream.get(streamKey),
24113
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24114
+ sym.packetSequenceNumber
24115
+ );
24116
+ if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
24117
+ return [sym];
23737
24118
  }
23738
- if (!restamped) return [sym];
23739
24119
  this.#overlayStats.unsequencedDropped++;
23740
24120
  return [];
23741
24121
  });
23742
24122
  this.#overlayStats.recovered++;
24123
+ const mpuFirst = /* @__PURE__ */ new Map();
24124
+ for (const sym of symbols) {
24125
+ const ssId = baseSsIds.get(sym.index);
24126
+ const mpu = sym.fragment.mpuSequenceNumber;
24127
+ const first = mpuFirst.get(mpu);
24128
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
24129
+ }
24130
+ const firstSsIds = /* @__PURE__ */ new Map();
24131
+ for (const sym of symbols) {
24132
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
24133
+ }
23743
24134
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23744
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24135
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24136
+ baseSsIds,
24137
+ firstSsIds
23745
24138
  });
23746
24139
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24140
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24141
+ }
24142
+ /**
24143
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24144
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24145
+ * from a received member of its MPU, else unstampedBaseSsId),
24146
+ * so it is fed to the base decoder as a rebuilt source
24147
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24148
+ * fewer to solve, and its repair can recover the block's other packets.
24149
+ * A base block no repair has stated yet takes it when one does
24150
+ * (#feedPendingRebuilt).
24151
+ *
24152
+ * The overlay route already handled each packet fed (delivered it, or
24153
+ * held an untimed head for its FT=1), so a base recovery routes none of
24154
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24155
+ * held head would wait in the parser for timing its first copy consumed.
24156
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24157
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24158
+ * that crossed no multicast socket, which the provenance counted from the
24159
+ * block's own arrivals cannot show.
24160
+ */
24161
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24162
+ const fec = this.#fecManager;
24163
+ const owner = inst.leg.sourcePacketId;
24164
+ const name = inst.leg.sourceCatalogTrackName;
24165
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24166
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24167
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24168
+ const feeds = [];
24169
+ for (const sym of symbols) {
24170
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24171
+ const ssId = baseSsIds.get(sym.index);
24172
+ if (ssId === void 0) {
24173
+ this.#overlayStats.baseUndetermined++;
24174
+ continue;
24175
+ }
24176
+ routed.delete(ssId);
24177
+ routed.add(ssId);
24178
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24179
+ feeds.push([ssId, sym.rawSymbol]);
24180
+ }
24181
+ for (const [ssId, symbolData] of feeds) {
24182
+ if (this.#classifyPacketId(owner) === null) return;
24183
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24184
+ }
24185
+ }
24186
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24187
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24188
+ const fec = this.#fecManager;
24189
+ if (!fec?.feedRebuiltSourceSymbol) return;
24190
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24191
+ if (coords == null) {
24192
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24193
+ pending.delete(ssId);
24194
+ pending.set(ssId, symbolData.slice());
24195
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24196
+ return;
24197
+ }
24198
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24199
+ if (!fed.taken) return;
24200
+ this.#overlayStats.baseFed++;
24201
+ if (fed.recovered === null) return;
24202
+ const sbn = coords.sbn;
24203
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24204
+ fec.cancelBlockTimeout(sbn, owner, name);
24205
+ fec.countRecovery(owner, sbn, name);
24206
+ fec.markBlockCompleted(sbn, owner, name);
24207
+ });
24208
+ }
24209
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24210
+ #feedPendingRebuilt(owner, name, block) {
24211
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24212
+ if (pending === void 0 || pending.size === 0) return;
24213
+ for (const [ssId, symbolData] of [...pending]) {
24214
+ if (this.#classifyPacketId(owner) === null) return;
24215
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24216
+ pending.delete(ssId);
24217
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24218
+ }
23747
24219
  }
23748
24220
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23749
24221
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23883,6 +24355,10 @@ var MmtpRouter = class {
23883
24355
  get unadmittedListedDropCount() {
23884
24356
  return this.#unadmittedListedDrops;
23885
24357
  }
24358
+ /** 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. */
24359
+ get admittedSourceDatagramCount() {
24360
+ return this.#admittedSourceDatagrams;
24361
+ }
23886
24362
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23887
24363
  get unlistedRepairSetAsideCount() {
23888
24364
  return this.#unlistedRepairSetAside;
@@ -24369,6 +24845,8 @@ var MmtpRouter = class {
24369
24845
  )) return;
24370
24846
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24371
24847
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24848
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24849
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24372
24850
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24373
24851
  const recovered = fec.feedRepairSymbol(
24374
24852
  ownerPacketId,
@@ -24506,6 +24984,7 @@ var MmtpRouter = class {
24506
24984
  this.#droppedPreSyncVideo.delete(streamKey);
24507
24985
  this.#latestVideoSeqByStream.delete(streamKey);
24508
24986
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24987
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24509
24988
  this.#multicastRapTwinByStream.delete(streamKey);
24510
24989
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24511
24990
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24632,6 +25111,12 @@ var MmtpRouter = class {
24632
25111
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24633
25112
  * own RAP cadence, so no timer is involved.
24634
25113
  *
25114
+ * While the switch is staged, the FEC release gate lets a keyframe the
25115
+ * overlay rebuilt reach it, on either side, when the keyframe did before
25116
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
25117
+ * decides on what each side delivered, and the commit discards the gate's
25118
+ * frames of the outgoing stream.
25119
+ *
24635
25120
  * The commit is one synchronous transition inside the frame path: the
24636
25121
  * outgoing stream's already-admitted frames are released (its queued FEC
24637
25122
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24682,6 +25167,7 @@ var MmtpRouter = class {
24682
25167
  droppedIncomingFrames: 0
24683
25168
  };
24684
25169
  this.#staged = state;
25170
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
24685
25171
  return {
24686
25172
  cancel: () => {
24687
25173
  if (this.#staged !== state) return false;
@@ -24999,6 +25485,14 @@ var MmtpRouter = class {
24999
25485
  onRap: (packetId, catalogTrackName, ssId) => {
25000
25486
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25001
25487
  },
25488
+ // A staged switch commits on when each side's frames reach it
25489
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
25490
+ // they did unanchored.
25491
+ releaseUnanchored: (packetId, catalogTrackName) => {
25492
+ const staged = this.#staged;
25493
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
25494
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
25495
+ },
25002
25496
  onReleased: (data, meta) => {
25003
25497
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25004
25498
  const timing = meta.authoredTiming;
@@ -25140,6 +25634,7 @@ var MmtpRouter = class {
25140
25634
  const parsed = this.#parser.parsePacket(packet);
25141
25635
  if (!parsed) return;
25142
25636
  const info = parsed.info;
25637
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25143
25638
  if (source === "multicast" && !manifestAdmitted) {
25144
25639
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25145
25640
  }
@@ -25421,7 +25916,8 @@ var MmtpRouter = class {
25421
25916
  parsed.header,
25422
25917
  info.sequenceNumber,
25423
25918
  info.rapFlag,
25424
- source
25919
+ source,
25920
+ fecPayloadId?.ssId
25425
25921
  );
25426
25922
  }
25427
25923
  }
@@ -25447,7 +25943,8 @@ var MmtpRouter = class {
25447
25943
  source: parserOptions?.source,
25448
25944
  deliveryProvenance: parserOptions?.deliveryProvenance,
25449
25945
  catalogTrackName: parserOptions?.catalogTrackName,
25450
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
25946
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
25947
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25451
25948
  };
25452
25949
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25453
25950
  else this.#parser.feed(packet, feedOptions);
@@ -25571,7 +26068,8 @@ var MmtpRouter = class {
25571
26068
  if (stream === void 0) {
25572
26069
  stream = {
25573
26070
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25574
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26071
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26072
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25575
26073
  };
25576
26074
  byName.set(catalogTrackName, stream);
25577
26075
  }
@@ -25686,8 +26184,16 @@ var MmtpRouter = class {
25686
26184
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25687
26185
  this.#emitDirectVideo(packetId, data, meta, cb);
25688
26186
  }
25689
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26187
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25690
26188
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26189
+ if (this.#isFecTrack(packetId)) {
26190
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26191
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26192
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26193
+ this.#emitDirectVideo(packetId, data, meta, cb);
26194
+ return;
26195
+ }
26196
+ }
25691
26197
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25692
26198
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25693
26199
  this.#drainDirectRecoveredVideo(
@@ -25841,6 +26347,13 @@ var MmtpRouter = class {
25841
26347
  * frames by epoch only if pre-protection input ever matters.
25842
26348
  */
25843
26349
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26350
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26351
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26352
+ result = {
26353
+ ...result,
26354
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26355
+ };
26356
+ }
25844
26357
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25845
26358
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25846
26359
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25897,7 +26410,12 @@ var MmtpRouter = class {
25897
26410
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25898
26411
  if (ownerPacketId == null) return;
25899
26412
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25900
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26413
+ const fecSbnOf = (index) => {
26414
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26415
+ const fecSbn = overlay.baseSsIds.get(index);
26416
+ const fecSbnFirst = overlay.firstSsIds.get(index);
26417
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
26418
+ };
25901
26419
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25902
26420
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25903
26421
  if (ssIdSeqOffset !== void 0) {
@@ -25905,7 +26423,10 @@ var MmtpRouter = class {
25905
26423
  ...result,
25906
26424
  symbols: result.symbols.map((sym) => {
25907
26425
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25908
- if (wire === sym.packetSequenceNumber) return sym;
26426
+ if (wire === sym.packetSequenceNumber) {
26427
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26428
+ return sym;
26429
+ }
25909
26430
  this.#restampedRecoveredSeqs++;
25910
26431
  this.#restampedStreams.add(streamKey);
25911
26432
  return { ...sym, packetSequenceNumber: wire };
@@ -25941,16 +26462,29 @@ var MmtpRouter = class {
25941
26462
  }
25942
26463
  );
25943
26464
  }
25944
- result = {
25945
- ...result,
25946
- 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(
25947
- ownerPacketId,
25948
- sym.fragment.mpuSequenceNumber,
25949
- sym.fragment.movieFragmentSequenceNumber,
25950
- sym.fragment.sampleNumber,
25951
- catalogTrackName
25952
- ))
25953
- };
26465
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26466
+ ownerPacketId,
26467
+ sym.fragment.mpuSequenceNumber,
26468
+ sym.fragment.movieFragmentSequenceNumber,
26469
+ sym.fragment.sampleNumber,
26470
+ catalogTrackName
26471
+ );
26472
+ for (const sym of result.symbols) {
26473
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26474
+ this.#feedViaParser(
26475
+ sym.rawSymbol,
26476
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26477
+ void 0,
26478
+ {
26479
+ skipFecPayloadId: true,
26480
+ recovered: true,
26481
+ deliveryProvenance,
26482
+ ...fecSbnOf(sym.index),
26483
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26484
+ }
26485
+ );
26486
+ }
26487
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25954
26488
  const gated = this.#isFecTrack(ownerPacketId);
25955
26489
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25956
26490
  if (assembler) {