@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 2.2.0-main.fb5c7dfbe2de

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 (82) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1336 -302
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +70 -12
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +403 -58
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +48 -13
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +97 -25
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +79 -10
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +207 -26
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +209 -29
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +206 -26
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +34 -8
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +555 -79
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.d.ts +13 -3
  48. package/dist/multicast-manager.d.ts.map +1 -1
  49. package/dist/multicast-manager.js +980 -146
  50. package/dist/multicast-manager.js.map +2 -2
  51. package/dist/multicast-transport.js +28 -18
  52. package/dist/multicast-transport.js.map +2 -2
  53. package/dist/no-modules.js +206 -26
  54. package/dist/no-modules.js.map +2 -2
  55. package/dist/repair-fetch.d.ts +36 -8
  56. package/dist/repair-fetch.d.ts.map +1 -1
  57. package/dist/repair-fetch.js +285 -26
  58. package/dist/repair-fetch.js.map +3 -3
  59. package/dist/repair-layer-subscriptions.d.ts +6 -2
  60. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  61. package/dist/repair-layer-subscriptions.js +6 -3
  62. package/dist/repair-layer-subscriptions.js.map +2 -2
  63. package/dist/track-session.d.ts +1 -1
  64. package/dist/track-session.d.ts.map +1 -1
  65. package/dist/track-session.js +1 -0
  66. package/dist/track-session.js.map +2 -2
  67. package/dist/wasm-mmtp-container-parser.js +206 -26
  68. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  69. package/package.json +8 -5
  70. package/src/catalog-fec-params.ts +36 -1
  71. package/src/cmcd-producer.ts +189 -179
  72. package/src/fec-manager.ts +433 -32
  73. package/src/fec-worker-setup.ts +2 -2
  74. package/src/index.ts +9 -2
  75. package/src/iso-block-locator.ts +133 -31
  76. package/src/iso-bmff-timing.ts +40 -1
  77. package/src/mmtp-container-parser.ts +306 -29
  78. package/src/mmtp-router.ts +548 -38
  79. package/src/multicast-manager.ts +44 -6
  80. package/src/repair-fetch.ts +363 -49
  81. package/src/repair-layer-subscriptions.ts +13 -5
  82. package/src/track-session.ts +4 -1
@@ -1610,14 +1610,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
1610
1610
  });
1611
1611
 
1612
1612
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
1613
+ var MAX_PENDING_PACKETS = 256;
1613
1614
  var AMTGatewayManager = class {
1614
1615
  subscriptions = /* @__PURE__ */ new Map();
1615
1616
  messageHandler;
1617
+ messageListenerRegistered = false;
1616
1618
  pendingPackets = /* @__PURE__ */ new Map();
1617
1619
  constructor() {
1618
1620
  this.messageHandler = (event) => {
1619
1621
  const data = event.data;
1620
- console.log(`[AMT] SW message received:`, data.type);
1621
1622
  if (data.type === "UDP_PACKET") {
1622
1623
  this.handlePacket(data);
1623
1624
  } else if (data.type === "SOCKET_ERROR") {
@@ -1626,12 +1627,22 @@ var AMTGatewayManager = class {
1626
1627
  console.log(`[AMT SW Debug] ${data.message}`);
1627
1628
  }
1628
1629
  };
1629
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
1630
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
1631
- console.log("[AMT] Registered service worker message handler");
1632
- } else {
1633
- console.warn("[AMT] Service worker not available");
1630
+ }
1631
+ ensureMessageListener() {
1632
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
1633
+ return;
1634
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);
1644
+ }
1645
+ this.messageListenerRegistered = false;
1635
1646
  }
1636
1647
  /**
1637
1648
  * Subscribe via AMT tunnel
@@ -1641,6 +1652,7 @@ var AMTGatewayManager = class {
1641
1652
  if (config.protected) {
1642
1653
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
1643
1654
  }
1655
+ this.ensureMessageListener();
1644
1656
  const keyStr = this.getKeyString(key);
1645
1657
  if (this.subscriptions.has(keyStr)) {
1646
1658
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -1676,6 +1688,8 @@ var AMTGatewayManager = class {
1676
1688
  this.subscriptions.delete(keyStr);
1677
1689
  this.pendingPackets.delete(subscription.subscriptionId);
1678
1690
  }
1691
+ if (this.subscriptions.size === 0)
1692
+ this.removeMessageListener();
1679
1693
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
1680
1694
  }
1681
1695
  }
@@ -1697,6 +1711,8 @@ var AMTGatewayManager = class {
1697
1711
  } finally {
1698
1712
  this.subscriptions.delete(handle);
1699
1713
  this.pendingPackets.delete(subscription.subscriptionId);
1714
+ if (this.subscriptions.size === 0)
1715
+ this.removeMessageListener();
1700
1716
  }
1701
1717
  }
1702
1718
  /**
@@ -1713,14 +1729,12 @@ var AMTGatewayManager = class {
1713
1729
  if (!subscriptionId)
1714
1730
  return;
1715
1731
  const subscription = this.subscriptions.get(subscriptionId);
1716
- if (!subscription) {
1717
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
1732
+ if (!subscription)
1718
1733
  return;
1719
- }
1720
1734
  if (!subscription.handshakeComplete) {
1721
1735
  const queue = this.pendingPackets.get(subscriptionId);
1722
1736
  const packetData2 = data;
1723
- if (queue) {
1737
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
1724
1738
  queue.push({
1725
1739
  data: packetData2.data,
1726
1740
  remoteAddress: packetData2.remoteAddress,
@@ -1746,19 +1760,15 @@ var AMTGatewayManager = class {
1746
1760
  if (!subscriptionId)
1747
1761
  return;
1748
1762
  const subscription = this.subscriptions.get(subscriptionId);
1749
- if (!subscription) {
1750
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
1763
+ if (!subscription)
1751
1764
  return;
1752
- }
1753
1765
  subscription.config.onError(new Error(error || "Unknown error"));
1754
1766
  }
1755
1767
  /**
1756
1768
  * Cleanup on destroy
1757
1769
  */
1758
1770
  destroy() {
1759
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
1760
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
1761
- }
1771
+ this.removeMessageListener();
1762
1772
  const handles = Array.from(this.subscriptions.keys());
1763
1773
  for (const handle of handles) {
1764
1774
  this.unsubscribe(handle).catch((err) => {
@@ -7667,9 +7677,21 @@ var IsoBmffTimingResolver = class {
7667
7677
  }
7668
7678
  #track = null;
7669
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();
7670
7688
  #pendingSamples = 0;
7671
7689
  #evictedSamples = 0;
7672
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
+ }
7673
7695
  get pendingSampleCount() {
7674
7696
  return this.#pendingSamples;
7675
7697
  }
@@ -7693,7 +7715,14 @@ var IsoBmffTimingResolver = class {
7693
7715
  this.#track = timing;
7694
7716
  return true;
7695
7717
  }
7696
- 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) {
7697
7726
  this.#lastMovieFragmentRejection = null;
7698
7727
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
7699
7728
  this.#lastMovieFragmentRejection = "invalid";
@@ -7719,6 +7748,8 @@ var IsoBmffTimingResolver = class {
7719
7748
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
7720
7749
  );
7721
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;
7722
7753
  let freed = 0;
7723
7754
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
7724
7755
  freed += this.#evictOldestFragment();
@@ -7732,6 +7763,7 @@ var IsoBmffTimingResolver = class {
7732
7763
  return false;
7733
7764
  }
7734
7765
  this.#fragments.set(key, [...parsed.samples]);
7766
+ this.#firstSamples.set(key, parsed.samples[0]);
7735
7767
  this.#pendingSamples += parsed.samples.length;
7736
7768
  return true;
7737
7769
  }
@@ -7789,6 +7821,7 @@ var IsoBmffTimingResolver = class {
7789
7821
  let live = 0;
7790
7822
  for (const sample of samples) if (sample !== null) live++;
7791
7823
  this.#fragments.delete(oldest);
7824
+ this.#firstSamples.delete(oldest);
7792
7825
  this.#pendingSamples -= live;
7793
7826
  this.#evictedSamples += live;
7794
7827
  return live;
@@ -7804,12 +7837,23 @@ var IsoBmffTimingResolver = class {
7804
7837
  if (!samples || !samples[index]) return false;
7805
7838
  samples[index] = null;
7806
7839
  this.#pendingSamples--;
7807
- 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
+ }
7808
7850
  return true;
7809
7851
  }
7810
7852
  reset() {
7811
7853
  this.#track = null;
7812
7854
  this.#fragments.clear();
7855
+ this.#firstSamples.clear();
7856
+ this.#delivered.clear();
7813
7857
  this.#pendingSamples = 0;
7814
7858
  }
7815
7859
  };
@@ -7979,6 +8023,9 @@ function authoredGap(before, after) {
7979
8023
  function ssIdAfter(a, b) {
7980
8024
  return (a - b | 0) > 0;
7981
8025
  }
8026
+ function holdsRebuilt(stream) {
8027
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
8028
+ }
7982
8029
  var FecReleaseSequencer = class {
7983
8030
  #streams = /* @__PURE__ */ new Map();
7984
8031
  #gate;
@@ -8041,6 +8088,7 @@ var FecReleaseSequencer = class {
8041
8088
  stream.lastSbn = void 0;
8042
8089
  stream.discontinuity = false;
8043
8090
  stream.rapSsId = void 0;
8091
+ stream.sourced = false;
8044
8092
  }
8045
8093
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
8046
8094
  this.#recordStat("droppedStale");
@@ -8067,6 +8115,7 @@ var FecReleaseSequencer = class {
8067
8115
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
8068
8116
  else hi = mid;
8069
8117
  }
8118
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
8070
8119
  stream.buf.splice(lo, 0, frame);
8071
8120
  this.#drainUnlessDeferred(stream);
8072
8121
  }
@@ -8102,6 +8151,41 @@ var FecReleaseSequencer = class {
8102
8151
  }
8103
8152
  return false;
8104
8153
  }
8154
+ /**
8155
+ * These streams' sink is being replaced by one that has seen no source
8156
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
8157
+ * release to the sink being replaced the frames the unanchored path had
8158
+ * already released to it (#cursorPassed): there they waited in its queue,
8159
+ * which its replacement flushes. A stream that holds none is not drained:
8160
+ * it holds nothing the anchor kept back, and a drain would still force out
8161
+ * a frame held past the hold limit, which before the anchor waited for the
8162
+ * next push. Then treat the streams as unsourced until a source frame is
8163
+ * released.
8164
+ */
8165
+ unsource(packetId, catalogTrackName) {
8166
+ for (const stream of this.#streams.values()) {
8167
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
8168
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
8169
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
8170
+ else this.#drain(stream, true);
8171
+ }
8172
+ stream.sourced = false;
8173
+ }
8174
+ }
8175
+ }
8176
+ /**
8177
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
8178
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
8179
+ * packetId.
8180
+ */
8181
+ holdsRebuilt(packetId, catalogTrackName) {
8182
+ for (const stream of this.#streams.values()) {
8183
+ if (stream.packetId !== packetId) continue;
8184
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
8185
+ if (holdsRebuilt(stream)) return true;
8186
+ }
8187
+ return false;
8188
+ }
8105
8189
  /** Re-evaluate held gaps after FEC state changed. */
8106
8190
  poke(streamKey) {
8107
8191
  if (streamKey !== void 0) {
@@ -8193,16 +8277,32 @@ var FecReleaseSequencer = class {
8193
8277
  last: null,
8194
8278
  lastSbn: void 0,
8195
8279
  discontinuity: false,
8196
- rapSsId: void 0
8280
+ rapSsId: void 0,
8281
+ sourced: false,
8282
+ draining: 0
8197
8283
  };
8198
8284
  this.#streams.set(streamKey, stream);
8199
8285
  }
8200
8286
  return stream;
8201
8287
  }
8202
- #drain(stream) {
8288
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
8289
+ #drain(stream, asUnanchored = false) {
8290
+ stream.draining++;
8291
+ try {
8292
+ this.#drainLoop(stream, asUnanchored);
8293
+ } finally {
8294
+ stream.draining--;
8295
+ }
8296
+ }
8297
+ #drainLoop(stream, asUnanchored) {
8203
8298
  while (stream.buf.length > 0 && stream.last !== null) {
8204
8299
  const head = stream.buf[0];
8205
8300
  const last = stream.last;
8301
+ if (compareFrameOrder(head, last) <= 0) {
8302
+ stream.buf.shift();
8303
+ this.#release(stream, head, void 0);
8304
+ continue;
8305
+ }
8206
8306
  if (stream.discontinuity) {
8207
8307
  stream.discontinuity = false;
8208
8308
  stream.buf.shift();
@@ -8216,9 +8316,10 @@ var FecReleaseSequencer = class {
8216
8316
  }
8217
8317
  const headSbn = head.meta.fecSbn;
8218
8318
  const from = stream.lastSbn ?? headSbn;
8219
- const to = headSbn ?? stream.lastSbn;
8319
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
8220
8320
  let lost = [];
8221
8321
  const unanchored = from === void 0 && to === void 0;
8322
+ let unresolved = false;
8222
8323
  if (from !== void 0 && to !== void 0) {
8223
8324
  const resolved = this.#gate.resolvedLoss(
8224
8325
  stream.packetId,
@@ -8227,7 +8328,8 @@ var FecReleaseSequencer = class {
8227
8328
  Math.max(from, to),
8228
8329
  from
8229
8330
  );
8230
- if (resolved === null) {
8331
+ unresolved = resolved === null;
8332
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
8231
8333
  const heldMs = performance.now() - head.arrivedAtMs;
8232
8334
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
8233
8335
  this.#recordStat("forcedWindowFlushes");
@@ -8235,22 +8337,56 @@ var FecReleaseSequencer = class {
8235
8337
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
8236
8338
  continue;
8237
8339
  }
8238
- lost = resolved;
8340
+ lost = resolved ?? [];
8239
8341
  }
8240
8342
  stream.buf.shift();
8241
8343
  this.#release(
8242
8344
  stream,
8243
8345
  head,
8244
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
8346
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
8245
8347
  );
8246
8348
  }
8247
8349
  }
8350
+ /**
8351
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
8352
+ * source cursor holds it unanchored, and neither does a later frame behind
8353
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
8354
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
8355
+ * like each frame that arrived in front of it since (stale there). The head
8356
+ * then goes, decided as it would be unanchored, once a frame the base path
8357
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
8358
+ * marked one: the base path has passed that one, so a frame still missing
8359
+ * before it can only come from a repair. It also goes while no source frame
8360
+ * has been released, when the direct path shows a recovered frame at once.
8361
+ * And it goes at once while a staged switch decides on when frames arrive
8362
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
8363
+ * sink being replaced: unanchored, it had already reached the sink.
8364
+ * The scan also writes: the `passed` marks it sets stay on the frames,
8365
+ * and push marks a frame it inserts in front of a marked one from them.
8366
+ */
8367
+ #cursorPassed(stream, asUnanchored) {
8368
+ let at = -1;
8369
+ for (let i = 0; i < stream.buf.length; i++) {
8370
+ const frame = stream.buf[i];
8371
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
8372
+ frame.passed = true;
8373
+ if (at < 0) at = i;
8374
+ } else if (frame.passed !== true) break;
8375
+ }
8376
+ if (at < 0) return false;
8377
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
8378
+ }
8248
8379
  #release(stream, frame, decision) {
8380
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
8381
+ if (stale) this.#recordStat("droppedStale");
8249
8382
  if (decision) frame.meta.gapBefore = decision;
8250
8383
  stream.last = frame;
8251
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
8384
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
8385
+ stream.lastSbn = frame.meta.fecSbn;
8386
+ }
8387
+ if (frame.meta.recovered !== true) stream.sourced = true;
8252
8388
  this.#gate.onReleased?.(frame.data, frame.meta);
8253
- this.#emit(frame.data, frame.meta);
8389
+ if (!stale) this.#emit(frame.data, frame.meta);
8254
8390
  }
8255
8391
  };
8256
8392
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -8291,6 +8427,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8291
8427
  this.#reclaimedTimingSamples = 0;
8292
8428
  this.#evictedTimingSamples = 0;
8293
8429
  this.#rejectedMovieFragments = 0;
8430
+ this.#recoveredHeadsAwaitingTiming = {
8431
+ held: 0,
8432
+ expired: 0,
8433
+ discarded: 0,
8434
+ superseded: 0
8435
+ };
8294
8436
  this.#sequencers = /* @__PURE__ */ new Map();
8295
8437
  this.#fecGate = null;
8296
8438
  this.#fecSequencer = null;
@@ -8399,6 +8541,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8399
8541
  #reclaimedTimingSamples;
8400
8542
  #evictedTimingSamples;
8401
8543
  #rejectedMovieFragments;
8544
+ #recoveredHeadsAwaitingTiming;
8402
8545
  #sequencers;
8403
8546
  #fecGate;
8404
8547
  #fecSequencer;
@@ -8504,11 +8647,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8504
8647
  /**
8505
8648
  * Typed view of the ISO-BMFF timing-sample budget.
8506
8649
  *
8507
- * This is the acceptance signal for the budget ratchet: `pending` walking
8508
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
8509
- * and it is observable here rather than only as media silently stopping.
8510
- * Per repo policy this is a producer-owned counter -- never infer budget
8511
- * state by parsing logs.
8650
+ * Each stream admits timing against its own window of `capacity` slots
8651
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
8652
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
8653
+ * timing a stream declared and never delivered shows up there. `pending`
8654
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
8655
+ * a producer-owned counter -- never infer budget state by parsing logs.
8512
8656
  */
8513
8657
  get timingBudgetStats() {
8514
8658
  return {
@@ -8519,6 +8663,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8519
8663
  rejectedMovieFragments: this.#rejectedMovieFragments
8520
8664
  };
8521
8665
  }
8666
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
8667
+ get recoveredHeadsAwaitingTiming() {
8668
+ let pending = 0;
8669
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
8670
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
8671
+ }
8672
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
8673
+ }
8522
8674
  get reorderStats() {
8523
8675
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
8524
8676
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -8560,6 +8712,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8560
8712
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
8561
8713
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
8562
8714
  }
8715
+ /** See FecReleaseSequencer.unsource. */
8716
+ unsourceFecRelease(packetId, catalogTrackName) {
8717
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
8718
+ }
8719
+ /** See FecReleaseSequencer.holdsRebuilt. */
8720
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
8721
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
8722
+ }
8563
8723
  /** Re-evaluate held gaps after FEC block state changed. */
8564
8724
  pokeFecRelease(packetId, catalogTrackName) {
8565
8725
  if (!this.#fecSequencer) return;
@@ -8692,7 +8852,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8692
8852
  const now = Date.now();
8693
8853
  for (const { awaitingTiming } of this.#timingStreams.values()) {
8694
8854
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
8695
- awaitingTiming.shift();
8855
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
8696
8856
  }
8697
8857
  }
8698
8858
  }
@@ -8810,11 +8970,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
8810
8970
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
8811
8971
  return this.#drop("fragment-unexpected-fragmentation");
8812
8972
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
8813
- if (!resolver2) return this.#drop("fragment-no-resolver");
8973
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
8814
8974
  const pendingBefore = resolver2.pendingSampleCount;
8815
8975
  const evictedBefore = resolver2.evictedSampleCount;
8816
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
8817
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
8976
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
8977
+ const admitted = resolver2.addMovieFragment(
8978
+ mpu.mpuSequenceNumber,
8979
+ data,
8980
+ remaining,
8981
+ options?.recovered === true
8982
+ );
8818
8983
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
8819
8984
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
8820
8985
  if (!admitted) {
@@ -8834,7 +8999,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8834
8999
  };
8835
9000
  }
8836
9001
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
8837
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
9002
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
8838
9003
  if (!resolver) return this.#drop("mfu-no-resolver");
8839
9004
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
8840
9005
  return this.#drop("mfu-empty-payload");
@@ -8855,6 +9020,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8855
9020
  mpuSequenceNumber: mpu.mpuSequenceNumber,
8856
9021
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
8857
9022
  sampleNumber: mfuDuHeader.sampleNumber,
9023
+ sequenceNumber: info.sequenceNumber,
8858
9024
  arrivedAt: Date.now()
8859
9025
  });
8860
9026
  return this.#drop("mfu-sample-timing-missing");
@@ -8880,7 +9046,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
8880
9046
  info,
8881
9047
  resolver,
8882
9048
  options?.recovered === true,
8883
- options?.fecSbn
9049
+ options?.fecSbn,
9050
+ options?.fecSbnFirst
8884
9051
  );
8885
9052
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
8886
9053
  }
@@ -8973,7 +9140,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
8973
9140
  info,
8974
9141
  resolver,
8975
9142
  options?.recovered === true,
8976
- options?.fecSbn
9143
+ options?.fecSbn,
9144
+ options?.fecSbnFirst
8977
9145
  );
8978
9146
  if (outcome.record !== void 0) return outcome.record;
8979
9147
  return this.#drop(outcome.reason);
@@ -8993,7 +9161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8993
9161
  * attribution exists to keep readable. Leaving the choice at the call site
8994
9162
  * is what keeps both true.
8995
9163
  */
8996
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
9164
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
8997
9165
  const timing = resolver.sampleTiming(
8998
9166
  combined.mpuSequenceNumber,
8999
9167
  combined.movieFragmentSequenceNumber,
@@ -9010,6 +9178,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9010
9178
  );
9011
9179
  if (!meta) return { reason: "combined-meta-build-failed" };
9012
9180
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
9181
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
9013
9182
  if (!resolver.consumeSample(
9014
9183
  combined.mpuSequenceNumber,
9015
9184
  combined.movieFragmentSequenceNumber,
@@ -9178,6 +9347,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
9178
9347
  }
9179
9348
  this.#emitFrame(data, meta);
9180
9349
  }
9350
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
9351
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
9352
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
9353
+ }
9181
9354
  /**
9182
9355
  * Claim and return the authored timing for a recovered sample. Recovery does
9183
9356
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -9239,6 +9412,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
9239
9412
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
9240
9413
  this.#sequencers.clear();
9241
9414
  this.#fecSequencer?.discard();
9415
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
9416
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9417
+ }
9242
9418
  this.#timingStreams.clear();
9243
9419
  this.#pendingTimingSamples = 0;
9244
9420
  this.#reliableMoqStartupGate.clear();
@@ -9257,10 +9433,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
9257
9433
  return resolver;
9258
9434
  }
9259
9435
  #awaitTiming(streamKey, head) {
9260
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
9261
- if (!awaitingTiming) return;
9436
+ const state = this.#timingStreams.get(streamKey);
9437
+ if (!state) return;
9438
+ if (!head.options?.recovered) {
9439
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
9440
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
9441
+ state.awaitingTiming = kept;
9442
+ }
9443
+ const awaitingTiming = state.awaitingTiming;
9262
9444
  awaitingTiming.push(head);
9263
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
9445
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
9446
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
9447
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
9448
+ }
9449
+ }
9450
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
9451
+ #leaveAwaitingTiming(head, outcome) {
9452
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
9264
9453
  }
9265
9454
  /**
9266
9455
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -9288,6 +9477,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9288
9477
  const state = this.#timingStreams.get(streamKey);
9289
9478
  if (!state) return;
9290
9479
  this.#timingStreams.delete(streamKey);
9480
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9291
9481
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
9292
9482
  if (this.#pendingTimingSamples < 0) {
9293
9483
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -9828,7 +10018,18 @@ function keyframeOverlaySsId(header) {
9828
10018
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
9829
10019
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
9830
10020
  }
10021
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
10022
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
10023
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
10024
+ }
10025
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
10026
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
10027
+ return headerPsn + offset >>> 0;
10028
+ }
10029
+ var OVERLAY_ROUTED_LIMIT = 1024;
10030
+ var OVERLAY_PENDING_LIMIT = 256;
9831
10031
  var OVERLAY_OFFSET_MPUS = 16;
10032
+ var OVERLAY_REBUILT_LIMIT = 256;
9832
10033
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
9833
10034
  function assertStreamIdentity(role, stream) {
9834
10035
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -9939,6 +10140,7 @@ var MmtpRouter = class {
9939
10140
  #provenance = /* @__PURE__ */ new Map();
9940
10141
  #onFlowProtected = null;
9941
10142
  #unadmittedListedDrops = 0;
10143
+ #admittedSourceDatagrams = 0;
9942
10144
  #unlistedRepairSetAside = 0;
9943
10145
  #assistBlocks = /* @__PURE__ */ new Map();
9944
10146
  #assistFailures = /* @__PURE__ */ new Map();
@@ -9995,6 +10197,26 @@ var MmtpRouter = class {
9995
10197
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
9996
10198
  */
9997
10199
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10200
+ /**
10201
+ * Per stream, the SS_ID offset at which a base recovery last showed the
10202
+ * sender's own packet_sequence_number: a recovered packet's header psn
10203
+ * equal to its SS_ID minus that offset. While it equals the stream's
10204
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
10205
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
10206
+ * an MPU no member of which arrived).
10207
+ */
10208
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10209
+ /**
10210
+ * The same, shown by the overlay alone, before any base recovery did: a
10211
+ * packet it rebuilt whose base copy carries its protected psn
10212
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
10213
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
10214
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
10215
+ * make the base drop a late base copy of a keyframe the overlay never
10216
+ * finished, and lift a block's late-source cut, which nothing before
10217
+ * this offset did.
10218
+ */
10219
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
9998
10220
  /**
9999
10221
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
10000
10222
  * not parsed, typically because its overlap copy was parsed first
@@ -10064,7 +10286,9 @@ var MmtpRouter = class {
10064
10286
  recovered: 0,
10065
10287
  restarts: 0,
10066
10288
  unsequencedDropped: 0,
10067
- protectedDropped: 0
10289
+ protectedDropped: 0,
10290
+ baseFed: 0,
10291
+ baseUndetermined: 0
10068
10292
  };
10069
10293
  /** Callback fired when a delta-track FEC block completes or fails. */
10070
10294
  #onDeltaBlockCompleteCb = null;
@@ -10237,7 +10461,7 @@ var MmtpRouter = class {
10237
10461
  const cb = this.#getFrameCallback(packetId);
10238
10462
  if (isVideo && !assembler) {
10239
10463
  if (frameMeta.recovered) {
10240
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
10464
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
10241
10465
  } else {
10242
10466
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
10243
10467
  }
@@ -10270,13 +10494,18 @@ var MmtpRouter = class {
10270
10494
  this.#onDeltaBlockCompleteCb?.(packetId, false);
10271
10495
  }
10272
10496
  };
10273
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
10497
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
10274
10498
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
10275
10499
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
10276
10500
  this.#onDeltaBlockCompleteCb?.(packetId, true);
10277
10501
  }
10278
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10279
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10502
+ if (span === void 0) {
10503
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10504
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10505
+ } else {
10506
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
10507
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
10508
+ }
10280
10509
  };
10281
10510
  }
10282
10511
  }
@@ -10321,8 +10550,9 @@ var MmtpRouter = class {
10321
10550
  }
10322
10551
  /**
10323
10552
  * FEC-recovered source symbols whose reconstructed header named a
10324
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
10325
- * a hop re-stamped the sequence after the sender protected the packet.
10553
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
10554
+ * or, rebuilt by the overlay, than their base copy carries: a hop
10555
+ * re-stamped the sequence after the sender protected the packet.
10326
10556
  */
10327
10557
  get restampedRecoveredSequenceCount() {
10328
10558
  return this.#restampedRecoveredSeqs;
@@ -10501,6 +10731,7 @@ var MmtpRouter = class {
10501
10731
  enableMseAssembler(packetId, config, catalogTrackName) {
10502
10732
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
10503
10733
  const previous = this.#mseAssemblers.get(assemblerKey);
10734
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
10504
10735
  if (previous) {
10505
10736
  previous.flush();
10506
10737
  previous.dispose();
@@ -10627,6 +10858,10 @@ var MmtpRouter = class {
10627
10858
  get keyframeOverlayStats() {
10628
10859
  return { ...this.#overlayStats };
10629
10860
  }
10861
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
10862
+ get recoveredHeadsAwaitingTiming() {
10863
+ return this.#parser.recoveredHeadsAwaitingTiming;
10864
+ }
10630
10865
  #isOverlayRepair(repairPacketId, repairTrackName) {
10631
10866
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
10632
10867
  }
@@ -10647,7 +10882,8 @@ var MmtpRouter = class {
10647
10882
  fec,
10648
10883
  k,
10649
10884
  offsets: /* @__PURE__ */ new Map(),
10650
- moqBlocks: /* @__PURE__ */ new Set()
10885
+ moqBlocks: /* @__PURE__ */ new Set(),
10886
+ rebuilt: /* @__PURE__ */ new Map()
10651
10887
  };
10652
10888
  this.#overlays.set(repairPacketId, inst);
10653
10889
  return () => {
@@ -10677,12 +10913,18 @@ var MmtpRouter = class {
10677
10913
  */
10678
10914
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
10679
10915
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
10916
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
10917
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
10918
+ stream.overlayToBase.routed.clear();
10919
+ stream.overlayToBase.pending.clear();
10920
+ }
10680
10921
  }
10681
10922
  #resetOverlay(inst) {
10682
10923
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
10683
10924
  inst.last = void 0;
10684
10925
  inst.offsets.clear();
10685
10926
  inst.moqBlocks.clear();
10927
+ inst.rebuilt.clear();
10686
10928
  }
10687
10929
  #overlaysForSource(sourcePacketId, catalogTrackName) {
10688
10930
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -10733,18 +10975,33 @@ var MmtpRouter = class {
10733
10975
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
10734
10976
  * every overlay installed for its source, at that SS_ID, with the bytes the
10735
10977
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
10736
- * included (7.1, 7.4.3).
10737
- */
10738
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
10978
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
10979
+ * FEC reads it.
10980
+ *
10981
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
10982
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
10983
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
10984
+ * the stream unstamped at the SS_ID offset it states, for the release gate
10985
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
10986
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
10987
+ */
10988
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
10739
10989
  if (this.#overlays.size === 0) return;
10740
10990
  const ssId = keyframeOverlaySsId(header);
10741
10991
  if (ssId === void 0) return;
10992
+ let rebuiltPsn;
10742
10993
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
10743
10994
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
10995
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
10996
+ inst.rebuilt.delete(ssId);
10744
10997
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
10745
10998
  if (identity !== null) {
10746
10999
  inst.offsets.delete(identity.mpuSequenceNumber);
10747
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
11000
+ inst.offsets.set(identity.mpuSequenceNumber, {
11001
+ psnOffset: ssId - sequenceNumber >>> 0,
11002
+ overlaySsId: ssId,
11003
+ ...baseSsId === void 0 ? {} : { baseSsId }
11004
+ });
10748
11005
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
10749
11006
  }
10750
11007
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10766,6 +11023,14 @@ var MmtpRouter = class {
10766
11023
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
10767
11024
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
10768
11025
  }
11026
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
11027
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
11028
+ if (rebuiltPsn !== sequenceNumber) {
11029
+ this.#restampedRecoveredSeqs++;
11030
+ } else if (baseSsId !== void 0) {
11031
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11032
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
11033
+ }
10769
11034
  }
10770
11035
  #noteOverlayMoqBlock(inst, blockStart) {
10771
11036
  inst.moqBlocks.add(blockStart);
@@ -10799,11 +11064,16 @@ var MmtpRouter = class {
10799
11064
  }
10800
11065
  /**
10801
11066
  * Route an overlay-recovered block's packets as source media of the
10802
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
10803
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
10804
- * base re-stamp, no block advance. Each packet's wire sequence is restored
10805
- * from its keyframe's own offset; without one, a stream the base never
10806
- * re-stamped keeps the header's sequence and any other drops the packet.
11067
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
11068
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
11069
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
11070
+ * base FEC recovered does, so it waits behind base frames still in flight,
11071
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
11072
+ * longer than unanchored; one whose base SS_ID is undetermined has no
11073
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
11074
+ * advance. Each packet's wire sequence is restored from its keyframe's own
11075
+ * offset; without one, a stream the base never re-stamped keeps the
11076
+ * header's sequence and any other drops the packet.
10807
11077
  */
10808
11078
  #routeOverlayRecovered(inst, blockStart, recovered) {
10809
11079
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10823,22 +11093,139 @@ var MmtpRouter = class {
10823
11093
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
10824
11094
  const restamped = this.#restampedStreams.has(streamKey);
10825
11095
  const restored = /* @__PURE__ */ new Set();
11096
+ const baseSsIds = /* @__PURE__ */ new Map();
11097
+ const gateOnly = /* @__PURE__ */ new Set();
10826
11098
  const symbols = result.symbols.flatMap((sym) => {
10827
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10828
- if (offset !== void 0) {
10829
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
11099
+ const overlaySsId = serial32Add(blockStart, sym.index);
11100
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
11101
+ if (anchor !== void 0) {
11102
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
10830
11103
  restored.add(packetSequenceNumber);
10831
- return [{ ...sym, packetSequenceNumber }];
11104
+ const out = { ...sym, packetSequenceNumber };
11105
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
11106
+ if (baseSsId !== void 0) {
11107
+ baseSsIds.set(sym.index, baseSsId);
11108
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11109
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
11110
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
11111
+ }
11112
+ }
11113
+ return [out];
11114
+ }
11115
+ if (!restamped) {
11116
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11117
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
11118
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
11119
+ if (baseSsId !== void 0) {
11120
+ baseSsIds.set(sym.index, baseSsId);
11121
+ if (fedSsId === void 0) gateOnly.add(sym.index);
11122
+ } else {
11123
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
11124
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
11125
+ }
11126
+ return [sym];
10832
11127
  }
10833
- if (!restamped) return [sym];
10834
11128
  this.#overlayStats.unsequencedDropped++;
10835
11129
  return [];
10836
11130
  });
10837
11131
  this.#overlayStats.recovered++;
11132
+ const mpuFirst = /* @__PURE__ */ new Map();
11133
+ for (const sym of symbols) {
11134
+ const ssId = baseSsIds.get(sym.index);
11135
+ const mpu = sym.fragment.mpuSequenceNumber;
11136
+ const first = mpuFirst.get(mpu);
11137
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
11138
+ }
11139
+ const firstSsIds = /* @__PURE__ */ new Map();
11140
+ for (const sym of symbols) {
11141
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
11142
+ }
10838
11143
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
10839
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
11144
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
11145
+ baseSsIds,
11146
+ firstSsIds
10840
11147
  });
10841
11148
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
11149
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
11150
+ }
11151
+ /**
11152
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
11153
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
11154
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
11155
+ * base recovery showed; `baseSsIds` holds no other),
11156
+ * so it is fed to the base decoder as a rebuilt source
11157
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
11158
+ * fewer to solve, and its repair can recover the block's other packets.
11159
+ * A base block no repair has stated yet takes it when one does
11160
+ * (#feedPendingRebuilt).
11161
+ *
11162
+ * The overlay route already handled each packet fed (delivered it, or
11163
+ * held an untimed head for its FT=1), so a base recovery routes none of
11164
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
11165
+ * held head would wait in the parser for timing its first copy consumed.
11166
+ * Only an overlay block decoded from multicast symbols alone is fed: with
11167
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
11168
+ * that crossed no multicast socket, which the provenance counted from the
11169
+ * block's own arrivals cannot show.
11170
+ */
11171
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
11172
+ const fec = this.#fecManager;
11173
+ const owner = inst.leg.sourcePacketId;
11174
+ const name = inst.leg.sourceCatalogTrackName;
11175
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
11176
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
11177
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
11178
+ const feeds = [];
11179
+ for (const sym of symbols) {
11180
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
11181
+ const ssId = baseSsIds.get(sym.index);
11182
+ if (ssId === void 0) {
11183
+ this.#overlayStats.baseUndetermined++;
11184
+ continue;
11185
+ }
11186
+ routed.delete(ssId);
11187
+ routed.add(ssId);
11188
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
11189
+ feeds.push([ssId, sym.rawSymbol]);
11190
+ }
11191
+ for (const [ssId, symbolData] of feeds) {
11192
+ if (this.#classifyPacketId(owner) === null) return;
11193
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11194
+ }
11195
+ }
11196
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
11197
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
11198
+ const fec = this.#fecManager;
11199
+ if (!fec?.feedRebuiltSourceSymbol) return;
11200
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
11201
+ if (coords == null) {
11202
+ const pending = this.#stream(owner, name).overlayToBase.pending;
11203
+ pending.delete(ssId);
11204
+ pending.set(ssId, symbolData.slice());
11205
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
11206
+ return;
11207
+ }
11208
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
11209
+ if (!fed.taken) return;
11210
+ this.#overlayStats.baseFed++;
11211
+ if (fed.recovered === null) return;
11212
+ const sbn = coords.sbn;
11213
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
11214
+ fec.cancelBlockTimeout(sbn, owner, name);
11215
+ fec.countRecovery(owner, sbn, name);
11216
+ fec.markBlockCompleted(sbn, owner, name);
11217
+ });
11218
+ }
11219
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
11220
+ #feedPendingRebuilt(owner, name, block) {
11221
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
11222
+ if (pending === void 0 || pending.size === 0) return;
11223
+ for (const [ssId, symbolData] of [...pending]) {
11224
+ if (this.#classifyPacketId(owner) === null) return;
11225
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
11226
+ pending.delete(ssId);
11227
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11228
+ }
10842
11229
  }
10843
11230
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
10844
11231
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -10978,6 +11365,10 @@ var MmtpRouter = class {
10978
11365
  get unadmittedListedDropCount() {
10979
11366
  return this.#unadmittedListedDrops;
10980
11367
  }
11368
+ /** 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. */
11369
+ get admittedSourceDatagramCount() {
11370
+ return this.#admittedSourceDatagrams;
11371
+ }
10981
11372
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
10982
11373
  get unlistedRepairSetAsideCount() {
10983
11374
  return this.#unlistedRepairSetAside;
@@ -11464,6 +11855,8 @@ var MmtpRouter = class {
11464
11855
  )) return;
11465
11856
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
11466
11857
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
11858
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
11859
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
11467
11860
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
11468
11861
  const recovered = fec.feedRepairSymbol(
11469
11862
  ownerPacketId,
@@ -11514,7 +11907,7 @@ var MmtpRouter = class {
11514
11907
  catalogTrackName
11515
11908
  )) continue;
11516
11909
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
11517
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
11910
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
11518
11911
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
11519
11912
  }
11520
11913
  if (payload.oversized) continue;
@@ -11601,6 +11994,8 @@ var MmtpRouter = class {
11601
11994
  this.#droppedPreSyncVideo.delete(streamKey);
11602
11995
  this.#latestVideoSeqByStream.delete(streamKey);
11603
11996
  this.#ssIdSeqOffsetByStream.delete(streamKey);
11997
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
11998
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
11604
11999
  this.#multicastRapTwinByStream.delete(streamKey);
11605
12000
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
11606
12001
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -11727,6 +12122,12 @@ var MmtpRouter = class {
11727
12122
  * the outgoing stream stops. The hold is bounded by the incoming stream's
11728
12123
  * own RAP cadence, so no timer is involved.
11729
12124
  *
12125
+ * While the switch is staged, the FEC release gate lets a keyframe the
12126
+ * overlay rebuilt reach it, on either side, when the keyframe did before
12127
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
12128
+ * decides on what each side delivered, and the commit discards the gate's
12129
+ * frames of the outgoing stream.
12130
+ *
11730
12131
  * The commit is one synchronous transition inside the frame path: the
11731
12132
  * outgoing stream's already-admitted frames are released (its queued FEC
11732
12133
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -11777,6 +12178,9 @@ var MmtpRouter = class {
11777
12178
  droppedIncomingFrames: 0
11778
12179
  };
11779
12180
  this.#staged = state;
12181
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
12182
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
12183
+ }
11780
12184
  return {
11781
12185
  cancel: () => {
11782
12186
  if (this.#staged !== state) return false;
@@ -11859,7 +12263,17 @@ var MmtpRouter = class {
11859
12263
  */
11860
12264
  #routeParsedFrame(data, meta) {
11861
12265
  if (this.#classifyPacketId(meta.packetId) === null) return;
11862
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
12266
+ if (this.#staged !== null) {
12267
+ let consumed = false;
12268
+ this.#fecPokeDepth++;
12269
+ try {
12270
+ consumed = this.#interceptSwitchFrame(data, meta);
12271
+ } finally {
12272
+ this.#fecPokeDepth--;
12273
+ this.#flushPendingFecPokes();
12274
+ }
12275
+ if (consumed) return;
12276
+ }
11863
12277
  this.#deliverParsedFrame(data, meta);
11864
12278
  }
11865
12279
  #commitStreamSwitch(state, rap, reason) {
@@ -12011,12 +12425,16 @@ var MmtpRouter = class {
12011
12425
  this.#parser.deferFecRelease(work);
12012
12426
  } finally {
12013
12427
  this.#fecPokeDepth--;
12014
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
12015
- const pokes = [...this.#pendingFecPokes.values()];
12016
- this.#pendingFecPokes.clear();
12017
- for (const poke of pokes) {
12018
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
12019
- }
12428
+ this.#flushPendingFecPokes();
12429
+ }
12430
+ }
12431
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
12432
+ #flushPendingFecPokes() {
12433
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
12434
+ const pokes = [...this.#pendingFecPokes.values()];
12435
+ this.#pendingFecPokes.clear();
12436
+ for (const poke of pokes) {
12437
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
12020
12438
  }
12021
12439
  }
12022
12440
  }
@@ -12094,6 +12512,14 @@ var MmtpRouter = class {
12094
12512
  onRap: (packetId, catalogTrackName, ssId) => {
12095
12513
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
12096
12514
  },
12515
+ // A staged switch commits on when each side's frames reach it
12516
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
12517
+ // they did unanchored.
12518
+ releaseUnanchored: (packetId, catalogTrackName) => {
12519
+ const staged = this.#staged;
12520
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
12521
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
12522
+ },
12097
12523
  onReleased: (data, meta) => {
12098
12524
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
12099
12525
  const timing = meta.authoredTiming;
@@ -12235,6 +12661,7 @@ var MmtpRouter = class {
12235
12661
  const parsed = this.#parser.parsePacket(packet);
12236
12662
  if (!parsed) return;
12237
12663
  const info = parsed.info;
12664
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
12238
12665
  if (source === "multicast" && !manifestAdmitted) {
12239
12666
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
12240
12667
  }
@@ -12516,7 +12943,8 @@ var MmtpRouter = class {
12516
12943
  parsed.header,
12517
12944
  info.sequenceNumber,
12518
12945
  info.rapFlag,
12519
- source
12946
+ source,
12947
+ fecPayloadId?.ssId
12520
12948
  );
12521
12949
  }
12522
12950
  }
@@ -12542,7 +12970,8 @@ var MmtpRouter = class {
12542
12970
  source: parserOptions?.source,
12543
12971
  deliveryProvenance: parserOptions?.deliveryProvenance,
12544
12972
  catalogTrackName: parserOptions?.catalogTrackName,
12545
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
12973
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
12974
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
12546
12975
  };
12547
12976
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
12548
12977
  else this.#parser.feed(packet, feedOptions);
@@ -12666,7 +13095,8 @@ var MmtpRouter = class {
12666
13095
  if (stream === void 0) {
12667
13096
  stream = {
12668
13097
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
12669
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
13098
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
13099
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
12670
13100
  };
12671
13101
  byName.set(catalogTrackName, stream);
12672
13102
  }
@@ -12781,8 +13211,16 @@ var MmtpRouter = class {
12781
13211
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
12782
13212
  this.#emitDirectVideo(packetId, data, meta, cb);
12783
13213
  }
12784
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
13214
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
12785
13215
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
13216
+ if (this.#isFecTrack(packetId)) {
13217
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
13218
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
13219
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
13220
+ this.#emitDirectVideo(packetId, data, meta, cb);
13221
+ return;
13222
+ }
13223
+ }
12786
13224
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
12787
13225
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
12788
13226
  this.#drainDirectRecoveredVideo(
@@ -12936,6 +13374,13 @@ var MmtpRouter = class {
12936
13374
  * frames by epoch only if pre-protection input ever matters.
12937
13375
  */
12938
13376
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
13377
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
13378
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
13379
+ result = {
13380
+ ...result,
13381
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
13382
+ };
13383
+ }
12939
13384
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12940
13385
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
12941
13386
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -12992,7 +13437,12 @@ var MmtpRouter = class {
12992
13437
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
12993
13438
  if (ownerPacketId == null) return;
12994
13439
  if (this.#classifyPacketId(ownerPacketId) === null) return;
12995
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
13440
+ const fecSbnOf = (index) => {
13441
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
13442
+ const fecSbn = overlay.baseSsIds.get(index);
13443
+ const fecSbnFirst = overlay.firstSsIds.get(index);
13444
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
13445
+ };
12996
13446
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
12997
13447
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
12998
13448
  if (ssIdSeqOffset !== void 0) {
@@ -13000,7 +13450,10 @@ var MmtpRouter = class {
13000
13450
  ...result,
13001
13451
  symbols: result.symbols.map((sym) => {
13002
13452
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
13003
- if (wire === sym.packetSequenceNumber) return sym;
13453
+ if (wire === sym.packetSequenceNumber) {
13454
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
13455
+ return sym;
13456
+ }
13004
13457
  this.#restampedRecoveredSeqs++;
13005
13458
  this.#restampedStreams.add(streamKey);
13006
13459
  return { ...sym, packetSequenceNumber: wire };
@@ -13036,6 +13489,29 @@ var MmtpRouter = class {
13036
13489
  }
13037
13490
  );
13038
13491
  }
13492
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
13493
+ ownerPacketId,
13494
+ sym.fragment.mpuSequenceNumber,
13495
+ sym.fragment.movieFragmentSequenceNumber,
13496
+ sym.fragment.sampleNumber,
13497
+ catalogTrackName
13498
+ );
13499
+ for (const sym of result.symbols) {
13500
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
13501
+ this.#feedViaParser(
13502
+ sym.rawSymbol,
13503
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
13504
+ void 0,
13505
+ {
13506
+ skipFecPayloadId: true,
13507
+ recovered: true,
13508
+ deliveryProvenance,
13509
+ ...fecSbnOf(sym.index),
13510
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13511
+ }
13512
+ );
13513
+ }
13514
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
13039
13515
  const gated = this.#isFecTrack(ownerPacketId);
13040
13516
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
13041
13517
  if (assembler) {