@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.75b39a2cdddf

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 (93) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1001 -215
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +222 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +58 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +105 -34
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +229 -51
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +104 -34
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +18 -1
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +323 -64
  56. package/dist/mmtp-router.js.map +2 -2
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.js +547 -72
  60. package/dist/multicast-manager.js.map +2 -2
  61. package/dist/multicast-transport.js +28 -18
  62. package/dist/multicast-transport.js.map +2 -2
  63. package/dist/multicast-unwrap.js +12 -19
  64. package/dist/multicast-unwrap.js.map +2 -2
  65. package/dist/no-modules.js +222 -48
  66. package/dist/no-modules.js.map +3 -3
  67. package/dist/repair-fetch.js.map +1 -1
  68. package/dist/repair-layer-subscriptions.d.ts +82 -0
  69. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  70. package/dist/repair-layer-subscriptions.js +192 -0
  71. package/dist/repair-layer-subscriptions.js.map +7 -0
  72. package/dist/signaling.js.map +1 -1
  73. package/dist/timestamp-codec.js +12 -19
  74. package/dist/timestamp-codec.js.map +2 -2
  75. package/dist/track-session.d.ts +1 -1
  76. package/dist/track-session.js.map +1 -1
  77. package/dist/wasm-mfu-reassembler.js.map +1 -1
  78. package/dist/wasm-mmtp-container-parser.js +104 -34
  79. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  80. package/package.json +8 -5
  81. package/schemas/catalog.schema.json +4 -0
  82. package/src/catalog-fec-params.ts +36 -1
  83. package/src/cmcd-producer.ts +189 -179
  84. package/src/fec-manager.ts +277 -6
  85. package/src/fec-worker-setup.ts +2 -2
  86. package/src/index.ts +18 -2
  87. package/src/iso-block-locator.ts +19 -0
  88. package/src/iso-bmff-timing.ts +40 -1
  89. package/src/mmtp-container-parser.ts +125 -19
  90. package/src/mmtp-fec-session.ts +115 -15
  91. package/src/mmtp-router.ts +350 -13
  92. package/src/repair-layer-subscriptions.ts +261 -0
  93. package/src/track-session.ts +1 -1
@@ -149,30 +149,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
149
149
  const authLength = view2.getUint16(2, false);
150
150
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
151
151
  }
152
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
152
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
153
153
  if (declaredEnd > payload.length)
154
154
  return null;
155
155
  if (options?.skipFecId) {
156
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
156
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
157
157
  }
158
- const overlay = options?.overlayDeclared === true && rapFlag;
159
158
  if (fecType === 1) {
160
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
159
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
161
160
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
162
161
  return null;
163
162
  }
164
- return { hasFecId: true, hasOverlayFecId: overlay };
163
+ return { hasFecId: true };
165
164
  }
166
- if (fecType !== 0 || overlay)
165
+ if (fecType !== 0)
167
166
  return null;
168
167
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
169
- return { hasFecId: false, hasOverlayFecId: false };
168
+ return { hasFecId: false };
170
169
  }
171
170
  return null;
172
171
  }
173
- function sourceFecTrailerSize(overlay) {
174
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
175
- }
176
172
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
177
173
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
178
174
  return {
@@ -225,11 +221,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
225
221
  if ("rejection" in layoutResult)
226
222
  return layoutResult;
227
223
  const { layout } = layoutResult;
228
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
224
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
229
225
  if (!trailerLayout) {
230
226
  if (!options?.skipFecId && header.fecType === 1) {
231
227
  const observed = payload.length - layout.dataEnd;
232
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
228
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
233
229
  if (observed !== expected) {
234
230
  return {
235
231
  rejection: {
@@ -261,13 +257,10 @@ function extractPlannedMfu(mmtpPacket, result) {
261
257
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
262
258
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
263
259
  const data = payload.subarray(mfuOffset, dataEnd);
264
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
265
- const ssIdAt = (offset) => ({
266
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
267
- });
268
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
269
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
270
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
260
+ const fecPayloadId = trailerLayout.hasFecId ? {
261
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
262
+ } : null;
263
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
271
264
  }
272
265
  function sourceFecPayloadIdOf(mmtpPacket, header) {
273
266
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -1617,14 +1610,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
1617
1610
  });
1618
1611
 
1619
1612
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
1613
+ var MAX_PENDING_PACKETS = 256;
1620
1614
  var AMTGatewayManager = class {
1621
1615
  subscriptions = /* @__PURE__ */ new Map();
1622
1616
  messageHandler;
1617
+ messageListenerRegistered = false;
1623
1618
  pendingPackets = /* @__PURE__ */ new Map();
1624
1619
  constructor() {
1625
1620
  this.messageHandler = (event) => {
1626
1621
  const data = event.data;
1627
- console.log(`[AMT] SW message received:`, data.type);
1628
1622
  if (data.type === "UDP_PACKET") {
1629
1623
  this.handlePacket(data);
1630
1624
  } else if (data.type === "SOCKET_ERROR") {
@@ -1633,12 +1627,22 @@ var AMTGatewayManager = class {
1633
1627
  console.log(`[AMT SW Debug] ${data.message}`);
1634
1628
  }
1635
1629
  };
1636
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
1637
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
1638
- console.log("[AMT] Registered service worker message handler");
1639
- } else {
1640
- console.warn("[AMT] Service worker not available");
1630
+ }
1631
+ ensureMessageListener() {
1632
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
1633
+ return;
1634
+ }
1635
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
1636
+ this.messageListenerRegistered = true;
1637
+ console.log("[AMT] Registered service worker message handler");
1638
+ }
1639
+ removeMessageListener() {
1640
+ if (!this.messageListenerRegistered)
1641
+ return;
1642
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
1643
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
1641
1644
  }
1645
+ this.messageListenerRegistered = false;
1642
1646
  }
1643
1647
  /**
1644
1648
  * Subscribe via AMT tunnel
@@ -1648,6 +1652,7 @@ var AMTGatewayManager = class {
1648
1652
  if (config.protected) {
1649
1653
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
1650
1654
  }
1655
+ this.ensureMessageListener();
1651
1656
  const keyStr = this.getKeyString(key);
1652
1657
  if (this.subscriptions.has(keyStr)) {
1653
1658
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -1683,6 +1688,8 @@ var AMTGatewayManager = class {
1683
1688
  this.subscriptions.delete(keyStr);
1684
1689
  this.pendingPackets.delete(subscription.subscriptionId);
1685
1690
  }
1691
+ if (this.subscriptions.size === 0)
1692
+ this.removeMessageListener();
1686
1693
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
1687
1694
  }
1688
1695
  }
@@ -1704,6 +1711,8 @@ var AMTGatewayManager = class {
1704
1711
  } finally {
1705
1712
  this.subscriptions.delete(handle);
1706
1713
  this.pendingPackets.delete(subscription.subscriptionId);
1714
+ if (this.subscriptions.size === 0)
1715
+ this.removeMessageListener();
1707
1716
  }
1708
1717
  }
1709
1718
  /**
@@ -1720,14 +1729,12 @@ var AMTGatewayManager = class {
1720
1729
  if (!subscriptionId)
1721
1730
  return;
1722
1731
  const subscription = this.subscriptions.get(subscriptionId);
1723
- if (!subscription) {
1724
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
1732
+ if (!subscription)
1725
1733
  return;
1726
- }
1727
1734
  if (!subscription.handshakeComplete) {
1728
1735
  const queue = this.pendingPackets.get(subscriptionId);
1729
1736
  const packetData2 = data;
1730
- if (queue) {
1737
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
1731
1738
  queue.push({
1732
1739
  data: packetData2.data,
1733
1740
  remoteAddress: packetData2.remoteAddress,
@@ -1753,19 +1760,15 @@ var AMTGatewayManager = class {
1753
1760
  if (!subscriptionId)
1754
1761
  return;
1755
1762
  const subscription = this.subscriptions.get(subscriptionId);
1756
- if (!subscription) {
1757
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
1763
+ if (!subscription)
1758
1764
  return;
1759
- }
1760
1765
  subscription.config.onError(new Error(error || "Unknown error"));
1761
1766
  }
1762
1767
  /**
1763
1768
  * Cleanup on destroy
1764
1769
  */
1765
1770
  destroy() {
1766
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
1767
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
1768
- }
1771
+ this.removeMessageListener();
1769
1772
  const handles = Array.from(this.subscriptions.keys());
1770
1773
  for (const handle of handles) {
1771
1774
  this.unsubscribe(handle).catch((err) => {
@@ -7674,9 +7677,21 @@ var IsoBmffTimingResolver = class {
7674
7677
  }
7675
7678
  #track = null;
7676
7679
  #fragments = /* @__PURE__ */ new Map();
7680
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
7681
+ #firstSamples = /* @__PURE__ */ new Map();
7682
+ /**
7683
+ * First sample and sample count of the newest fragments whose every slot was
7684
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
7685
+ * matches the combiner, since it retires an abandoned sample as well.
7686
+ */
7687
+ #delivered = /* @__PURE__ */ new Map();
7677
7688
  #pendingSamples = 0;
7678
7689
  #evictedSamples = 0;
7679
7690
  #lastMovieFragmentRejection = null;
7691
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
7692
+ get initialized() {
7693
+ return this.#track !== null;
7694
+ }
7680
7695
  get pendingSampleCount() {
7681
7696
  return this.#pendingSamples;
7682
7697
  }
@@ -7700,7 +7715,14 @@ var IsoBmffTimingResolver = class {
7700
7715
  this.#track = timing;
7701
7716
  return true;
7702
7717
  }
7703
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
7718
+ /**
7719
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
7720
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
7721
+ * fragment whose samples are all done is a duplicate: admitting it again
7722
+ * would charge stale timing and could displace the live MPU's. A source
7723
+ * copy is still admitted again (a rendition switch re-arms timing that way).
7724
+ */
7725
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
7704
7726
  this.#lastMovieFragmentRejection = null;
7705
7727
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
7706
7728
  this.#lastMovieFragmentRejection = "invalid";
@@ -7726,6 +7748,8 @@ var IsoBmffTimingResolver = class {
7726
7748
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
7727
7749
  );
7728
7750
  }
7751
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
7752
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
7729
7753
  let freed = 0;
7730
7754
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
7731
7755
  freed += this.#evictOldestFragment();
@@ -7739,6 +7763,7 @@ var IsoBmffTimingResolver = class {
7739
7763
  return false;
7740
7764
  }
7741
7765
  this.#fragments.set(key, [...parsed.samples]);
7766
+ this.#firstSamples.set(key, parsed.samples[0]);
7742
7767
  this.#pendingSamples += parsed.samples.length;
7743
7768
  return true;
7744
7769
  }
@@ -7796,6 +7821,7 @@ var IsoBmffTimingResolver = class {
7796
7821
  let live = 0;
7797
7822
  for (const sample of samples) if (sample !== null) live++;
7798
7823
  this.#fragments.delete(oldest);
7824
+ this.#firstSamples.delete(oldest);
7799
7825
  this.#pendingSamples -= live;
7800
7826
  this.#evictedSamples += live;
7801
7827
  return live;
@@ -7811,12 +7837,23 @@ var IsoBmffTimingResolver = class {
7811
7837
  if (!samples || !samples[index]) return false;
7812
7838
  samples[index] = null;
7813
7839
  this.#pendingSamples--;
7814
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
7840
+ if (samples.every((value) => value === null)) {
7841
+ this.#fragments.delete(key);
7842
+ const first = this.#firstSamples.get(key);
7843
+ this.#firstSamples.delete(key);
7844
+ this.#delivered.delete(key);
7845
+ this.#delivered.set(key, { first, sampleCount: samples.length });
7846
+ if (this.#delivered.size > this.maxPendingSamples) {
7847
+ this.#delivered.delete(this.#delivered.keys().next().value);
7848
+ }
7849
+ }
7815
7850
  return true;
7816
7851
  }
7817
7852
  reset() {
7818
7853
  this.#track = null;
7819
7854
  this.#fragments.clear();
7855
+ this.#firstSamples.clear();
7856
+ this.#delivered.clear();
7820
7857
  this.#pendingSamples = 0;
7821
7858
  }
7822
7859
  };
@@ -8298,6 +8335,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8298
8335
  this.#reclaimedTimingSamples = 0;
8299
8336
  this.#evictedTimingSamples = 0;
8300
8337
  this.#rejectedMovieFragments = 0;
8338
+ this.#recoveredHeadsAwaitingTiming = {
8339
+ held: 0,
8340
+ expired: 0,
8341
+ discarded: 0,
8342
+ superseded: 0
8343
+ };
8301
8344
  this.#sequencers = /* @__PURE__ */ new Map();
8302
8345
  this.#fecGate = null;
8303
8346
  this.#fecSequencer = null;
@@ -8406,6 +8449,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8406
8449
  #reclaimedTimingSamples;
8407
8450
  #evictedTimingSamples;
8408
8451
  #rejectedMovieFragments;
8452
+ #recoveredHeadsAwaitingTiming;
8409
8453
  #sequencers;
8410
8454
  #fecGate;
8411
8455
  #fecSequencer;
@@ -8511,11 +8555,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8511
8555
  /**
8512
8556
  * Typed view of the ISO-BMFF timing-sample budget.
8513
8557
  *
8514
- * This is the acceptance signal for the budget ratchet: `pending` walking
8515
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
8516
- * and it is observable here rather than only as media silently stopping.
8517
- * Per repo policy this is a producer-owned counter -- never infer budget
8518
- * state by parsing logs.
8558
+ * Each stream admits timing against its own window of `capacity` slots
8559
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
8560
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
8561
+ * timing a stream declared and never delivered shows up there. `pending`
8562
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
8563
+ * a producer-owned counter -- never infer budget state by parsing logs.
8519
8564
  */
8520
8565
  get timingBudgetStats() {
8521
8566
  return {
@@ -8526,6 +8571,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8526
8571
  rejectedMovieFragments: this.#rejectedMovieFragments
8527
8572
  };
8528
8573
  }
8574
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
8575
+ get recoveredHeadsAwaitingTiming() {
8576
+ let pending = 0;
8577
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
8578
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
8579
+ }
8580
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
8581
+ }
8529
8582
  get reorderStats() {
8530
8583
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
8531
8584
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -8699,7 +8752,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8699
8752
  const now = Date.now();
8700
8753
  for (const { awaitingTiming } of this.#timingStreams.values()) {
8701
8754
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
8702
- awaitingTiming.shift();
8755
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
8703
8756
  }
8704
8757
  }
8705
8758
  }
@@ -8817,11 +8870,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
8817
8870
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
8818
8871
  return this.#drop("fragment-unexpected-fragmentation");
8819
8872
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
8820
- if (!resolver2) return this.#drop("fragment-no-resolver");
8873
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
8821
8874
  const pendingBefore = resolver2.pendingSampleCount;
8822
8875
  const evictedBefore = resolver2.evictedSampleCount;
8823
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
8824
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
8876
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
8877
+ const admitted = resolver2.addMovieFragment(
8878
+ mpu.mpuSequenceNumber,
8879
+ data,
8880
+ remaining,
8881
+ options?.recovered === true
8882
+ );
8825
8883
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
8826
8884
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
8827
8885
  if (!admitted) {
@@ -8841,7 +8899,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8841
8899
  };
8842
8900
  }
8843
8901
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
8844
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
8902
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
8845
8903
  if (!resolver) return this.#drop("mfu-no-resolver");
8846
8904
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
8847
8905
  return this.#drop("mfu-empty-payload");
@@ -8862,6 +8920,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8862
8920
  mpuSequenceNumber: mpu.mpuSequenceNumber,
8863
8921
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
8864
8922
  sampleNumber: mfuDuHeader.sampleNumber,
8923
+ sequenceNumber: info.sequenceNumber,
8865
8924
  arrivedAt: Date.now()
8866
8925
  });
8867
8926
  return this.#drop("mfu-sample-timing-missing");
@@ -9185,6 +9244,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
9185
9244
  }
9186
9245
  this.#emitFrame(data, meta);
9187
9246
  }
9247
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
9248
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
9249
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
9250
+ }
9188
9251
  /**
9189
9252
  * Claim and return the authored timing for a recovered sample. Recovery does
9190
9253
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -9246,6 +9309,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
9246
9309
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
9247
9310
  this.#sequencers.clear();
9248
9311
  this.#fecSequencer?.discard();
9312
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
9313
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9314
+ }
9249
9315
  this.#timingStreams.clear();
9250
9316
  this.#pendingTimingSamples = 0;
9251
9317
  this.#reliableMoqStartupGate.clear();
@@ -9264,10 +9330,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
9264
9330
  return resolver;
9265
9331
  }
9266
9332
  #awaitTiming(streamKey, head) {
9267
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
9268
- if (!awaitingTiming) return;
9333
+ const state = this.#timingStreams.get(streamKey);
9334
+ if (!state) return;
9335
+ if (!head.options?.recovered) {
9336
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
9337
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
9338
+ state.awaitingTiming = kept;
9339
+ }
9340
+ const awaitingTiming = state.awaitingTiming;
9269
9341
  awaitingTiming.push(head);
9270
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
9342
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
9343
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
9344
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
9345
+ }
9346
+ }
9347
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
9348
+ #leaveAwaitingTiming(head, outcome) {
9349
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
9271
9350
  }
9272
9351
  /**
9273
9352
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -9295,6 +9374,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9295
9374
  const state = this.#timingStreams.get(streamKey);
9296
9375
  if (!state) return;
9297
9376
  this.#timingStreams.delete(streamKey);
9377
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9298
9378
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
9299
9379
  if (this.#pendingTimingSamples < 0) {
9300
9380
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -9835,6 +9915,16 @@ function keyframeOverlaySsId(header) {
9835
9915
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
9836
9916
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
9837
9917
  }
9918
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
9919
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
9920
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
9921
+ }
9922
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
9923
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
9924
+ return headerPsn + offset >>> 0;
9925
+ }
9926
+ var OVERLAY_ROUTED_LIMIT = 1024;
9927
+ var OVERLAY_PENDING_LIMIT = 256;
9838
9928
  var OVERLAY_OFFSET_MPUS = 16;
9839
9929
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
9840
9930
  function assertStreamIdentity(role, stream) {
@@ -9946,6 +10036,7 @@ var MmtpRouter = class {
9946
10036
  #provenance = /* @__PURE__ */ new Map();
9947
10037
  #onFlowProtected = null;
9948
10038
  #unadmittedListedDrops = 0;
10039
+ #admittedSourceDatagrams = 0;
9949
10040
  #unlistedRepairSetAside = 0;
9950
10041
  #assistBlocks = /* @__PURE__ */ new Map();
9951
10042
  #assistFailures = /* @__PURE__ */ new Map();
@@ -10002,6 +10093,15 @@ var MmtpRouter = class {
10002
10093
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
10003
10094
  */
10004
10095
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10096
+ /**
10097
+ * Per stream, the SS_ID offset at which a base recovery last showed the
10098
+ * sender's own packet_sequence_number: a recovered packet's header psn
10099
+ * equal to its SS_ID minus that offset. While it equals the stream's
10100
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
10101
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
10102
+ * an MPU no member of which arrived).
10103
+ */
10104
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10005
10105
  /**
10006
10106
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
10007
10107
  * not parsed, typically because its overlap copy was parsed first
@@ -10071,7 +10171,9 @@ var MmtpRouter = class {
10071
10171
  recovered: 0,
10072
10172
  restarts: 0,
10073
10173
  unsequencedDropped: 0,
10074
- protectedDropped: 0
10174
+ protectedDropped: 0,
10175
+ baseFed: 0,
10176
+ baseUndetermined: 0
10075
10177
  };
10076
10178
  /** Callback fired when a delta-track FEC block completes or fails. */
10077
10179
  #onDeltaBlockCompleteCb = null;
@@ -10244,7 +10346,7 @@ var MmtpRouter = class {
10244
10346
  const cb = this.#getFrameCallback(packetId);
10245
10347
  if (isVideo && !assembler) {
10246
10348
  if (frameMeta.recovered) {
10247
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
10349
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
10248
10350
  } else {
10249
10351
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
10250
10352
  }
@@ -10634,6 +10736,10 @@ var MmtpRouter = class {
10634
10736
  get keyframeOverlayStats() {
10635
10737
  return { ...this.#overlayStats };
10636
10738
  }
10739
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
10740
+ get recoveredHeadsAwaitingTiming() {
10741
+ return this.#parser.recoveredHeadsAwaitingTiming;
10742
+ }
10637
10743
  #isOverlayRepair(repairPacketId, repairTrackName) {
10638
10744
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
10639
10745
  }
@@ -10684,6 +10790,11 @@ var MmtpRouter = class {
10684
10790
  */
10685
10791
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
10686
10792
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
10793
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
10794
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
10795
+ stream.overlayToBase.routed.clear();
10796
+ stream.overlayToBase.pending.clear();
10797
+ }
10687
10798
  }
10688
10799
  #resetOverlay(inst) {
10689
10800
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -10740,9 +10851,10 @@ var MmtpRouter = class {
10740
10851
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
10741
10852
  * every overlay installed for its source, at that SS_ID, with the bytes the
10742
10853
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
10743
- * included (7.1, 7.4.3).
10854
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
10855
+ * FEC reads it.
10744
10856
  */
10745
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
10857
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
10746
10858
  if (this.#overlays.size === 0) return;
10747
10859
  const ssId = keyframeOverlaySsId(header);
10748
10860
  if (ssId === void 0) return;
@@ -10751,7 +10863,11 @@ var MmtpRouter = class {
10751
10863
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
10752
10864
  if (identity !== null) {
10753
10865
  inst.offsets.delete(identity.mpuSequenceNumber);
10754
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
10866
+ inst.offsets.set(identity.mpuSequenceNumber, {
10867
+ psnOffset: ssId - sequenceNumber >>> 0,
10868
+ overlaySsId: ssId,
10869
+ ...baseSsId === void 0 ? {} : { baseSsId }
10870
+ });
10755
10871
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
10756
10872
  }
10757
10873
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10830,14 +10946,27 @@ var MmtpRouter = class {
10830
10946
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
10831
10947
  const restamped = this.#restampedStreams.has(streamKey);
10832
10948
  const restored = /* @__PURE__ */ new Set();
10949
+ const baseSsIds = /* @__PURE__ */ new Map();
10833
10950
  const symbols = result.symbols.flatMap((sym) => {
10834
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10835
- if (offset !== void 0) {
10836
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
10951
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10952
+ if (anchor !== void 0) {
10953
+ const overlaySsId = serial32Add(blockStart, sym.index);
10954
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
10837
10955
  restored.add(packetSequenceNumber);
10838
- return [{ ...sym, packetSequenceNumber }];
10956
+ const out = { ...sym, packetSequenceNumber };
10957
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
10958
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
10959
+ return [out];
10960
+ }
10961
+ if (!restamped) {
10962
+ const baseSsId = unstampedBaseSsId(
10963
+ this.#ssIdSeqOffsetByStream.get(streamKey),
10964
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
10965
+ sym.packetSequenceNumber
10966
+ );
10967
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
10968
+ return [sym];
10839
10969
  }
10840
- if (!restamped) return [sym];
10841
10970
  this.#overlayStats.unsequencedDropped++;
10842
10971
  return [];
10843
10972
  });
@@ -10846,6 +10975,85 @@ var MmtpRouter = class {
10846
10975
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
10847
10976
  });
10848
10977
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
10978
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
10979
+ }
10980
+ /**
10981
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
10982
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
10983
+ * from a received member of its MPU, else unstampedBaseSsId),
10984
+ * so it is fed to the base decoder as a rebuilt source
10985
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
10986
+ * fewer to solve, and its repair can recover the block's other packets.
10987
+ * A base block no repair has stated yet takes it when one does
10988
+ * (#feedPendingRebuilt).
10989
+ *
10990
+ * The overlay route already handled each packet fed (delivered it, or
10991
+ * held an untimed head for its FT=1), so a base recovery routes none of
10992
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
10993
+ * held head would wait in the parser for timing its first copy consumed.
10994
+ * Only an overlay block decoded from multicast symbols alone is fed: with
10995
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
10996
+ * that crossed no multicast socket, which the provenance counted from the
10997
+ * block's own arrivals cannot show.
10998
+ */
10999
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
11000
+ const fec = this.#fecManager;
11001
+ const owner = inst.leg.sourcePacketId;
11002
+ const name = inst.leg.sourceCatalogTrackName;
11003
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
11004
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
11005
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
11006
+ const feeds = [];
11007
+ for (const sym of symbols) {
11008
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
11009
+ const ssId = baseSsIds.get(sym);
11010
+ if (ssId === void 0) {
11011
+ this.#overlayStats.baseUndetermined++;
11012
+ continue;
11013
+ }
11014
+ routed.delete(ssId);
11015
+ routed.add(ssId);
11016
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
11017
+ feeds.push([ssId, sym.rawSymbol]);
11018
+ }
11019
+ for (const [ssId, symbolData] of feeds) {
11020
+ if (this.#classifyPacketId(owner) === null) return;
11021
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11022
+ }
11023
+ }
11024
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
11025
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
11026
+ const fec = this.#fecManager;
11027
+ if (!fec?.feedRebuiltSourceSymbol) return;
11028
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
11029
+ if (coords == null) {
11030
+ const pending = this.#stream(owner, name).overlayToBase.pending;
11031
+ pending.delete(ssId);
11032
+ pending.set(ssId, symbolData.slice());
11033
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
11034
+ return;
11035
+ }
11036
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
11037
+ if (!fed.taken) return;
11038
+ this.#overlayStats.baseFed++;
11039
+ if (fed.recovered === null) return;
11040
+ const sbn = coords.sbn;
11041
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
11042
+ fec.cancelBlockTimeout(sbn, owner, name);
11043
+ fec.countRecovery(owner, sbn, name);
11044
+ fec.markBlockCompleted(sbn, owner, name);
11045
+ });
11046
+ }
11047
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
11048
+ #feedPendingRebuilt(owner, name, block) {
11049
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
11050
+ if (pending === void 0 || pending.size === 0) return;
11051
+ for (const [ssId, symbolData] of [...pending]) {
11052
+ if (this.#classifyPacketId(owner) === null) return;
11053
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
11054
+ pending.delete(ssId);
11055
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11056
+ }
10849
11057
  }
10850
11058
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
10851
11059
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -10985,6 +11193,10 @@ var MmtpRouter = class {
10985
11193
  get unadmittedListedDropCount() {
10986
11194
  return this.#unadmittedListedDrops;
10987
11195
  }
11196
+ /** 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. */
11197
+ get admittedSourceDatagramCount() {
11198
+ return this.#admittedSourceDatagrams;
11199
+ }
10988
11200
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
10989
11201
  get unlistedRepairSetAsideCount() {
10990
11202
  return this.#unlistedRepairSetAside;
@@ -11471,6 +11683,8 @@ var MmtpRouter = class {
11471
11683
  )) return;
11472
11684
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
11473
11685
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
11686
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
11687
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
11474
11688
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
11475
11689
  const recovered = fec.feedRepairSymbol(
11476
11690
  ownerPacketId,
@@ -11608,6 +11822,7 @@ var MmtpRouter = class {
11608
11822
  this.#droppedPreSyncVideo.delete(streamKey);
11609
11823
  this.#latestVideoSeqByStream.delete(streamKey);
11610
11824
  this.#ssIdSeqOffsetByStream.delete(streamKey);
11825
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
11611
11826
  this.#multicastRapTwinByStream.delete(streamKey);
11612
11827
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
11613
11828
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -12242,6 +12457,7 @@ var MmtpRouter = class {
12242
12457
  const parsed = this.#parser.parsePacket(packet);
12243
12458
  if (!parsed) return;
12244
12459
  const info = parsed.info;
12460
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
12245
12461
  if (source === "multicast" && !manifestAdmitted) {
12246
12462
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
12247
12463
  }
@@ -12523,7 +12739,8 @@ var MmtpRouter = class {
12523
12739
  parsed.header,
12524
12740
  info.sequenceNumber,
12525
12741
  info.rapFlag,
12526
- source
12742
+ source,
12743
+ fecPayloadId?.ssId
12527
12744
  );
12528
12745
  }
12529
12746
  }
@@ -12673,7 +12890,8 @@ var MmtpRouter = class {
12673
12890
  if (stream === void 0) {
12674
12891
  stream = {
12675
12892
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
12676
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
12893
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
12894
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
12677
12895
  };
12678
12896
  byName.set(catalogTrackName, stream);
12679
12897
  }
@@ -12788,8 +13006,16 @@ var MmtpRouter = class {
12788
13006
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
12789
13007
  this.#emitDirectVideo(packetId, data, meta, cb);
12790
13008
  }
12791
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
13009
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
12792
13010
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
13011
+ if (this.#isFecTrack(packetId)) {
13012
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
13013
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
13014
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
13015
+ this.#emitDirectVideo(packetId, data, meta, cb);
13016
+ return;
13017
+ }
13018
+ }
12793
13019
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
12794
13020
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
12795
13021
  this.#drainDirectRecoveredVideo(
@@ -12943,6 +13169,13 @@ var MmtpRouter = class {
12943
13169
  * frames by epoch only if pre-protection input ever matters.
12944
13170
  */
12945
13171
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
13172
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
13173
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
13174
+ result = {
13175
+ ...result,
13176
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
13177
+ };
13178
+ }
12946
13179
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12947
13180
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
12948
13181
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -13007,7 +13240,10 @@ var MmtpRouter = class {
13007
13240
  ...result,
13008
13241
  symbols: result.symbols.map((sym) => {
13009
13242
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
13010
- if (wire === sym.packetSequenceNumber) return sym;
13243
+ if (wire === sym.packetSequenceNumber) {
13244
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
13245
+ return sym;
13246
+ }
13011
13247
  this.#restampedRecoveredSeqs++;
13012
13248
  this.#restampedStreams.add(streamKey);
13013
13249
  return { ...sym, packetSequenceNumber: wire };
@@ -13043,6 +13279,29 @@ var MmtpRouter = class {
13043
13279
  }
13044
13280
  );
13045
13281
  }
13282
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
13283
+ ownerPacketId,
13284
+ sym.fragment.mpuSequenceNumber,
13285
+ sym.fragment.movieFragmentSequenceNumber,
13286
+ sym.fragment.sampleNumber,
13287
+ catalogTrackName
13288
+ );
13289
+ for (const sym of result.symbols) {
13290
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
13291
+ this.#feedViaParser(
13292
+ sym.rawSymbol,
13293
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
13294
+ void 0,
13295
+ {
13296
+ skipFecPayloadId: true,
13297
+ recovered: true,
13298
+ deliveryProvenance,
13299
+ ...fecSbnOf(sym.index),
13300
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13301
+ }
13302
+ );
13303
+ }
13304
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
13046
13305
  const gated = this.#isFecTrack(ownerPacketId);
13047
13306
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
13048
13307
  if (assembler) {