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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +703 -175
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +33 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +222 -6
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +9 -0
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +17 -0
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +37 -2
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +86 -9
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +89 -11
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +85 -9
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +18 -1
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +304 -39
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.js +526 -45
  48. package/dist/multicast-manager.js.map +2 -2
  49. package/dist/multicast-transport.js +26 -16
  50. package/dist/multicast-transport.js.map +2 -2
  51. package/dist/no-modules.js +85 -9
  52. package/dist/no-modules.js.map +2 -2
  53. package/dist/repair-layer-subscriptions.d.ts +6 -2
  54. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  55. package/dist/repair-layer-subscriptions.js +6 -3
  56. package/dist/repair-layer-subscriptions.js.map +2 -2
  57. package/dist/track-session.d.ts +1 -1
  58. package/dist/track-session.js.map +1 -1
  59. package/dist/wasm-mmtp-container-parser.js +85 -9
  60. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  61. package/package.json +8 -5
  62. package/schemas/catalog.schema.json +4 -0
  63. package/src/catalog-fec-params.ts +36 -1
  64. package/src/cmcd-producer.ts +189 -179
  65. package/src/fec-manager.ts +277 -6
  66. package/src/fec-worker-setup.ts +2 -2
  67. package/src/index.ts +7 -2
  68. package/src/iso-block-locator.ts +19 -0
  69. package/src/iso-bmff-timing.ts +40 -1
  70. package/src/mmtp-container-parser.ts +96 -8
  71. package/src/mmtp-router.ts +350 -13
  72. package/src/repair-layer-subscriptions.ts +13 -5
  73. package/src/track-session.ts +1 -1
@@ -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
  };
@@ -8291,6 +8335,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8291
8335
  this.#reclaimedTimingSamples = 0;
8292
8336
  this.#evictedTimingSamples = 0;
8293
8337
  this.#rejectedMovieFragments = 0;
8338
+ this.#recoveredHeadsAwaitingTiming = {
8339
+ held: 0,
8340
+ expired: 0,
8341
+ discarded: 0,
8342
+ superseded: 0
8343
+ };
8294
8344
  this.#sequencers = /* @__PURE__ */ new Map();
8295
8345
  this.#fecGate = null;
8296
8346
  this.#fecSequencer = null;
@@ -8399,6 +8449,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8399
8449
  #reclaimedTimingSamples;
8400
8450
  #evictedTimingSamples;
8401
8451
  #rejectedMovieFragments;
8452
+ #recoveredHeadsAwaitingTiming;
8402
8453
  #sequencers;
8403
8454
  #fecGate;
8404
8455
  #fecSequencer;
@@ -8519,6 +8570,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8519
8570
  rejectedMovieFragments: this.#rejectedMovieFragments
8520
8571
  };
8521
8572
  }
8573
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
8574
+ get recoveredHeadsAwaitingTiming() {
8575
+ let pending = 0;
8576
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
8577
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
8578
+ }
8579
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
8580
+ }
8522
8581
  get reorderStats() {
8523
8582
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
8524
8583
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -8692,7 +8751,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8692
8751
  const now = Date.now();
8693
8752
  for (const { awaitingTiming } of this.#timingStreams.values()) {
8694
8753
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
8695
- awaitingTiming.shift();
8754
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
8696
8755
  }
8697
8756
  }
8698
8757
  }
@@ -8810,11 +8869,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
8810
8869
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
8811
8870
  return this.#drop("fragment-unexpected-fragmentation");
8812
8871
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
8813
- if (!resolver2) return this.#drop("fragment-no-resolver");
8872
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
8814
8873
  const pendingBefore = resolver2.pendingSampleCount;
8815
8874
  const evictedBefore = resolver2.evictedSampleCount;
8816
8875
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
8817
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
8876
+ const admitted = resolver2.addMovieFragment(
8877
+ mpu.mpuSequenceNumber,
8878
+ data,
8879
+ remaining,
8880
+ options?.recovered === true
8881
+ );
8818
8882
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
8819
8883
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
8820
8884
  if (!admitted) {
@@ -8834,7 +8898,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8834
8898
  };
8835
8899
  }
8836
8900
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
8837
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
8901
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
8838
8902
  if (!resolver) return this.#drop("mfu-no-resolver");
8839
8903
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
8840
8904
  return this.#drop("mfu-empty-payload");
@@ -8855,6 +8919,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8855
8919
  mpuSequenceNumber: mpu.mpuSequenceNumber,
8856
8920
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
8857
8921
  sampleNumber: mfuDuHeader.sampleNumber,
8922
+ sequenceNumber: info.sequenceNumber,
8858
8923
  arrivedAt: Date.now()
8859
8924
  });
8860
8925
  return this.#drop("mfu-sample-timing-missing");
@@ -9178,6 +9243,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
9178
9243
  }
9179
9244
  this.#emitFrame(data, meta);
9180
9245
  }
9246
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
9247
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
9248
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
9249
+ }
9181
9250
  /**
9182
9251
  * Claim and return the authored timing for a recovered sample. Recovery does
9183
9252
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -9239,6 +9308,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
9239
9308
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
9240
9309
  this.#sequencers.clear();
9241
9310
  this.#fecSequencer?.discard();
9311
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
9312
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9313
+ }
9242
9314
  this.#timingStreams.clear();
9243
9315
  this.#pendingTimingSamples = 0;
9244
9316
  this.#reliableMoqStartupGate.clear();
@@ -9257,10 +9329,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
9257
9329
  return resolver;
9258
9330
  }
9259
9331
  #awaitTiming(streamKey, head) {
9260
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
9261
- if (!awaitingTiming) return;
9332
+ const state = this.#timingStreams.get(streamKey);
9333
+ if (!state) return;
9334
+ if (!head.options?.recovered) {
9335
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
9336
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
9337
+ state.awaitingTiming = kept;
9338
+ }
9339
+ const awaitingTiming = state.awaitingTiming;
9262
9340
  awaitingTiming.push(head);
9263
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
9341
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
9342
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
9343
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
9344
+ }
9345
+ }
9346
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
9347
+ #leaveAwaitingTiming(head, outcome) {
9348
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
9264
9349
  }
9265
9350
  /**
9266
9351
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -9288,6 +9373,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9288
9373
  const state = this.#timingStreams.get(streamKey);
9289
9374
  if (!state) return;
9290
9375
  this.#timingStreams.delete(streamKey);
9376
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9291
9377
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
9292
9378
  if (this.#pendingTimingSamples < 0) {
9293
9379
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -9828,6 +9914,16 @@ function keyframeOverlaySsId(header) {
9828
9914
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
9829
9915
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
9830
9916
  }
9917
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
9918
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
9919
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
9920
+ }
9921
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
9922
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
9923
+ return headerPsn + offset >>> 0;
9924
+ }
9925
+ var OVERLAY_ROUTED_LIMIT = 1024;
9926
+ var OVERLAY_PENDING_LIMIT = 256;
9831
9927
  var OVERLAY_OFFSET_MPUS = 16;
9832
9928
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
9833
9929
  function assertStreamIdentity(role, stream) {
@@ -9939,6 +10035,7 @@ var MmtpRouter = class {
9939
10035
  #provenance = /* @__PURE__ */ new Map();
9940
10036
  #onFlowProtected = null;
9941
10037
  #unadmittedListedDrops = 0;
10038
+ #admittedSourceDatagrams = 0;
9942
10039
  #unlistedRepairSetAside = 0;
9943
10040
  #assistBlocks = /* @__PURE__ */ new Map();
9944
10041
  #assistFailures = /* @__PURE__ */ new Map();
@@ -9995,6 +10092,15 @@ var MmtpRouter = class {
9995
10092
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
9996
10093
  */
9997
10094
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10095
+ /**
10096
+ * Per stream, the SS_ID offset at which a base recovery last showed the
10097
+ * sender's own packet_sequence_number: a recovered packet's header psn
10098
+ * equal to its SS_ID minus that offset. While it equals the stream's
10099
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
10100
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
10101
+ * an MPU no member of which arrived).
10102
+ */
10103
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
9998
10104
  /**
9999
10105
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
10000
10106
  * not parsed, typically because its overlap copy was parsed first
@@ -10064,7 +10170,9 @@ var MmtpRouter = class {
10064
10170
  recovered: 0,
10065
10171
  restarts: 0,
10066
10172
  unsequencedDropped: 0,
10067
- protectedDropped: 0
10173
+ protectedDropped: 0,
10174
+ baseFed: 0,
10175
+ baseUndetermined: 0
10068
10176
  };
10069
10177
  /** Callback fired when a delta-track FEC block completes or fails. */
10070
10178
  #onDeltaBlockCompleteCb = null;
@@ -10237,7 +10345,7 @@ var MmtpRouter = class {
10237
10345
  const cb = this.#getFrameCallback(packetId);
10238
10346
  if (isVideo && !assembler) {
10239
10347
  if (frameMeta.recovered) {
10240
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
10348
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
10241
10349
  } else {
10242
10350
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
10243
10351
  }
@@ -10627,6 +10735,10 @@ var MmtpRouter = class {
10627
10735
  get keyframeOverlayStats() {
10628
10736
  return { ...this.#overlayStats };
10629
10737
  }
10738
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
10739
+ get recoveredHeadsAwaitingTiming() {
10740
+ return this.#parser.recoveredHeadsAwaitingTiming;
10741
+ }
10630
10742
  #isOverlayRepair(repairPacketId, repairTrackName) {
10631
10743
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
10632
10744
  }
@@ -10677,6 +10789,11 @@ var MmtpRouter = class {
10677
10789
  */
10678
10790
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
10679
10791
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
10792
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
10793
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
10794
+ stream.overlayToBase.routed.clear();
10795
+ stream.overlayToBase.pending.clear();
10796
+ }
10680
10797
  }
10681
10798
  #resetOverlay(inst) {
10682
10799
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -10733,9 +10850,10 @@ var MmtpRouter = class {
10733
10850
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
10734
10851
  * every overlay installed for its source, at that SS_ID, with the bytes the
10735
10852
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
10736
- * included (7.1, 7.4.3).
10853
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
10854
+ * FEC reads it.
10737
10855
  */
10738
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
10856
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
10739
10857
  if (this.#overlays.size === 0) return;
10740
10858
  const ssId = keyframeOverlaySsId(header);
10741
10859
  if (ssId === void 0) return;
@@ -10744,7 +10862,11 @@ var MmtpRouter = class {
10744
10862
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
10745
10863
  if (identity !== null) {
10746
10864
  inst.offsets.delete(identity.mpuSequenceNumber);
10747
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
10865
+ inst.offsets.set(identity.mpuSequenceNumber, {
10866
+ psnOffset: ssId - sequenceNumber >>> 0,
10867
+ overlaySsId: ssId,
10868
+ ...baseSsId === void 0 ? {} : { baseSsId }
10869
+ });
10748
10870
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
10749
10871
  }
10750
10872
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10823,14 +10945,27 @@ var MmtpRouter = class {
10823
10945
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
10824
10946
  const restamped = this.#restampedStreams.has(streamKey);
10825
10947
  const restored = /* @__PURE__ */ new Set();
10948
+ const baseSsIds = /* @__PURE__ */ new Map();
10826
10949
  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;
10950
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10951
+ if (anchor !== void 0) {
10952
+ const overlaySsId = serial32Add(blockStart, sym.index);
10953
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
10830
10954
  restored.add(packetSequenceNumber);
10831
- return [{ ...sym, packetSequenceNumber }];
10955
+ const out = { ...sym, packetSequenceNumber };
10956
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
10957
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
10958
+ return [out];
10959
+ }
10960
+ if (!restamped) {
10961
+ const baseSsId = unstampedBaseSsId(
10962
+ this.#ssIdSeqOffsetByStream.get(streamKey),
10963
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
10964
+ sym.packetSequenceNumber
10965
+ );
10966
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
10967
+ return [sym];
10832
10968
  }
10833
- if (!restamped) return [sym];
10834
10969
  this.#overlayStats.unsequencedDropped++;
10835
10970
  return [];
10836
10971
  });
@@ -10839,6 +10974,85 @@ var MmtpRouter = class {
10839
10974
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
10840
10975
  });
10841
10976
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
10977
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
10978
+ }
10979
+ /**
10980
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
10981
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
10982
+ * from a received member of its MPU, else unstampedBaseSsId),
10983
+ * so it is fed to the base decoder as a rebuilt source
10984
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
10985
+ * fewer to solve, and its repair can recover the block's other packets.
10986
+ * A base block no repair has stated yet takes it when one does
10987
+ * (#feedPendingRebuilt).
10988
+ *
10989
+ * The overlay route already handled each packet fed (delivered it, or
10990
+ * held an untimed head for its FT=1), so a base recovery routes none of
10991
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
10992
+ * held head would wait in the parser for timing its first copy consumed.
10993
+ * Only an overlay block decoded from multicast symbols alone is fed: with
10994
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
10995
+ * that crossed no multicast socket, which the provenance counted from the
10996
+ * block's own arrivals cannot show.
10997
+ */
10998
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
10999
+ const fec = this.#fecManager;
11000
+ const owner = inst.leg.sourcePacketId;
11001
+ const name = inst.leg.sourceCatalogTrackName;
11002
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
11003
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
11004
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
11005
+ const feeds = [];
11006
+ for (const sym of symbols) {
11007
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
11008
+ const ssId = baseSsIds.get(sym);
11009
+ if (ssId === void 0) {
11010
+ this.#overlayStats.baseUndetermined++;
11011
+ continue;
11012
+ }
11013
+ routed.delete(ssId);
11014
+ routed.add(ssId);
11015
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
11016
+ feeds.push([ssId, sym.rawSymbol]);
11017
+ }
11018
+ for (const [ssId, symbolData] of feeds) {
11019
+ if (this.#classifyPacketId(owner) === null) return;
11020
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11021
+ }
11022
+ }
11023
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
11024
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
11025
+ const fec = this.#fecManager;
11026
+ if (!fec?.feedRebuiltSourceSymbol) return;
11027
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
11028
+ if (coords == null) {
11029
+ const pending = this.#stream(owner, name).overlayToBase.pending;
11030
+ pending.delete(ssId);
11031
+ pending.set(ssId, symbolData.slice());
11032
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
11033
+ return;
11034
+ }
11035
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
11036
+ if (!fed.taken) return;
11037
+ this.#overlayStats.baseFed++;
11038
+ if (fed.recovered === null) return;
11039
+ const sbn = coords.sbn;
11040
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
11041
+ fec.cancelBlockTimeout(sbn, owner, name);
11042
+ fec.countRecovery(owner, sbn, name);
11043
+ fec.markBlockCompleted(sbn, owner, name);
11044
+ });
11045
+ }
11046
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
11047
+ #feedPendingRebuilt(owner, name, block) {
11048
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
11049
+ if (pending === void 0 || pending.size === 0) return;
11050
+ for (const [ssId, symbolData] of [...pending]) {
11051
+ if (this.#classifyPacketId(owner) === null) return;
11052
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
11053
+ pending.delete(ssId);
11054
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11055
+ }
10842
11056
  }
10843
11057
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
10844
11058
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -10978,6 +11192,10 @@ var MmtpRouter = class {
10978
11192
  get unadmittedListedDropCount() {
10979
11193
  return this.#unadmittedListedDrops;
10980
11194
  }
11195
+ /** 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. */
11196
+ get admittedSourceDatagramCount() {
11197
+ return this.#admittedSourceDatagrams;
11198
+ }
10981
11199
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
10982
11200
  get unlistedRepairSetAsideCount() {
10983
11201
  return this.#unlistedRepairSetAside;
@@ -11464,6 +11682,8 @@ var MmtpRouter = class {
11464
11682
  )) return;
11465
11683
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
11466
11684
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
11685
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
11686
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
11467
11687
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
11468
11688
  const recovered = fec.feedRepairSymbol(
11469
11689
  ownerPacketId,
@@ -11601,6 +11821,7 @@ var MmtpRouter = class {
11601
11821
  this.#droppedPreSyncVideo.delete(streamKey);
11602
11822
  this.#latestVideoSeqByStream.delete(streamKey);
11603
11823
  this.#ssIdSeqOffsetByStream.delete(streamKey);
11824
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
11604
11825
  this.#multicastRapTwinByStream.delete(streamKey);
11605
11826
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
11606
11827
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -12235,6 +12456,7 @@ var MmtpRouter = class {
12235
12456
  const parsed = this.#parser.parsePacket(packet);
12236
12457
  if (!parsed) return;
12237
12458
  const info = parsed.info;
12459
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
12238
12460
  if (source === "multicast" && !manifestAdmitted) {
12239
12461
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
12240
12462
  }
@@ -12516,7 +12738,8 @@ var MmtpRouter = class {
12516
12738
  parsed.header,
12517
12739
  info.sequenceNumber,
12518
12740
  info.rapFlag,
12519
- source
12741
+ source,
12742
+ fecPayloadId?.ssId
12520
12743
  );
12521
12744
  }
12522
12745
  }
@@ -12666,7 +12889,8 @@ var MmtpRouter = class {
12666
12889
  if (stream === void 0) {
12667
12890
  stream = {
12668
12891
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
12669
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
12892
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
12893
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
12670
12894
  };
12671
12895
  byName.set(catalogTrackName, stream);
12672
12896
  }
@@ -12781,8 +13005,16 @@ var MmtpRouter = class {
12781
13005
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
12782
13006
  this.#emitDirectVideo(packetId, data, meta, cb);
12783
13007
  }
12784
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
13008
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
12785
13009
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
13010
+ if (this.#isFecTrack(packetId)) {
13011
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
13012
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
13013
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
13014
+ this.#emitDirectVideo(packetId, data, meta, cb);
13015
+ return;
13016
+ }
13017
+ }
12786
13018
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
12787
13019
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
12788
13020
  this.#drainDirectRecoveredVideo(
@@ -12936,6 +13168,13 @@ var MmtpRouter = class {
12936
13168
  * frames by epoch only if pre-protection input ever matters.
12937
13169
  */
12938
13170
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
13171
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
13172
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
13173
+ result = {
13174
+ ...result,
13175
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
13176
+ };
13177
+ }
12939
13178
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12940
13179
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
12941
13180
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -13000,7 +13239,10 @@ var MmtpRouter = class {
13000
13239
  ...result,
13001
13240
  symbols: result.symbols.map((sym) => {
13002
13241
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
13003
- if (wire === sym.packetSequenceNumber) return sym;
13242
+ if (wire === sym.packetSequenceNumber) {
13243
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
13244
+ return sym;
13245
+ }
13004
13246
  this.#restampedRecoveredSeqs++;
13005
13247
  this.#restampedStreams.add(streamKey);
13006
13248
  return { ...sym, packetSequenceNumber: wire };
@@ -13036,6 +13278,29 @@ var MmtpRouter = class {
13036
13278
  }
13037
13279
  );
13038
13280
  }
13281
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
13282
+ ownerPacketId,
13283
+ sym.fragment.mpuSequenceNumber,
13284
+ sym.fragment.movieFragmentSequenceNumber,
13285
+ sym.fragment.sampleNumber,
13286
+ catalogTrackName
13287
+ );
13288
+ for (const sym of result.symbols) {
13289
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
13290
+ this.#feedViaParser(
13291
+ sym.rawSymbol,
13292
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
13293
+ void 0,
13294
+ {
13295
+ skipFecPayloadId: true,
13296
+ recovered: true,
13297
+ deliveryProvenance,
13298
+ ...fecSbnOf(sym.index),
13299
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13300
+ }
13301
+ );
13302
+ }
13303
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
13039
13304
  const gated = this.#isFecTrack(ownerPacketId);
13040
13305
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
13041
13306
  if (assembler) {