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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1001 -215
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +222 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +58 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +105 -34
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +229 -51
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +104 -34
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +18 -1
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +323 -64
  56. package/dist/mmtp-router.js.map +2 -2
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.js +547 -72
  60. package/dist/multicast-manager.js.map +2 -2
  61. package/dist/multicast-transport.js +28 -18
  62. package/dist/multicast-transport.js.map +2 -2
  63. package/dist/multicast-unwrap.js +12 -19
  64. package/dist/multicast-unwrap.js.map +2 -2
  65. package/dist/no-modules.js +222 -48
  66. package/dist/no-modules.js.map +3 -3
  67. package/dist/repair-fetch.js.map +1 -1
  68. package/dist/repair-layer-subscriptions.d.ts +82 -0
  69. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  70. package/dist/repair-layer-subscriptions.js +192 -0
  71. package/dist/repair-layer-subscriptions.js.map +7 -0
  72. package/dist/signaling.js.map +1 -1
  73. package/dist/timestamp-codec.js +12 -19
  74. package/dist/timestamp-codec.js.map +2 -2
  75. package/dist/track-session.d.ts +1 -1
  76. package/dist/track-session.js.map +1 -1
  77. package/dist/wasm-mfu-reassembler.js.map +1 -1
  78. package/dist/wasm-mmtp-container-parser.js +104 -34
  79. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  80. package/package.json +8 -5
  81. package/schemas/catalog.schema.json +4 -0
  82. package/src/catalog-fec-params.ts +36 -1
  83. package/src/cmcd-producer.ts +189 -179
  84. package/src/fec-manager.ts +277 -6
  85. package/src/fec-worker-setup.ts +2 -2
  86. package/src/index.ts +18 -2
  87. package/src/iso-block-locator.ts +19 -0
  88. package/src/iso-bmff-timing.ts +40 -1
  89. package/src/mmtp-container-parser.ts +125 -19
  90. package/src/mmtp-fec-session.ts +115 -15
  91. package/src/mmtp-router.ts +350 -13
  92. package/src/repair-layer-subscriptions.ts +261 -0
  93. package/src/track-session.ts +1 -1
@@ -489,6 +489,8 @@ var PACKET_TYPE_SIGNALING = 2;
489
489
  var PACKET_TYPE_REPAIR = 3;
490
490
  var MMTP_SIGNALING_PACKET_ID = 0;
491
491
  var MMTP_PACKET_ID_MAX = 65535;
492
+ var FEC_CODE_REED_SOLOMON = 1;
493
+ var FEC_CODE_RAPTORQ = 3;
492
494
  function parseMMTPHeader(buffer) {
493
495
  if (buffer.length < MMTP_HEADER_SIZE) {
494
496
  throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
@@ -655,30 +657,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
655
657
  const authLength = view2.getUint16(2, false);
656
658
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
657
659
  }
658
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
660
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
659
661
  if (declaredEnd > payload.length)
660
662
  return null;
661
663
  if (options?.skipFecId) {
662
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
664
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
663
665
  }
664
- const overlay = options?.overlayDeclared === true && rapFlag;
665
666
  if (fecType === 1) {
666
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
667
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
667
668
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
668
669
  return null;
669
670
  }
670
- return { hasFecId: true, hasOverlayFecId: overlay };
671
+ return { hasFecId: true };
671
672
  }
672
- if (fecType !== 0 || overlay)
673
+ if (fecType !== 0)
673
674
  return null;
674
675
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
675
- return { hasFecId: false, hasOverlayFecId: false };
676
+ return { hasFecId: false };
676
677
  }
677
678
  return null;
678
679
  }
679
- function sourceFecTrailerSize(overlay) {
680
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
681
- }
682
680
  function extractNALUnit(mmtpPacket) {
683
681
  const extraction = extractMFUPayload(mmtpPacket);
684
682
  if (!extraction)
@@ -737,11 +735,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
737
735
  if ("rejection" in layoutResult)
738
736
  return layoutResult;
739
737
  const { layout } = layoutResult;
740
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
738
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
741
739
  if (!trailerLayout) {
742
740
  if (!options?.skipFecId && header.fecType === 1) {
743
741
  const observed = payload.length - layout.dataEnd;
744
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
742
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
745
743
  if (observed !== expected) {
746
744
  return {
747
745
  rejection: {
@@ -776,13 +774,10 @@ function extractPlannedMfu(mmtpPacket, result) {
776
774
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
777
775
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
778
776
  const data = payload.subarray(mfuOffset, dataEnd);
779
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
780
- const ssIdAt = (offset) => ({
781
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
782
- });
783
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
784
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
785
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
777
+ const fecPayloadId = trailerLayout.hasFecId ? {
778
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
779
+ } : null;
780
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
786
781
  }
787
782
  function extractTimestamp(mmtpPacket) {
788
783
  const { header } = unwrapMMTP(mmtpPacket);
@@ -903,6 +898,50 @@ var NTP_SHORT_BITS = 32;
903
898
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
904
899
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
905
900
 
901
+ // ../mmt-base/dist/signalling.js
902
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
903
+ var SSBG_MODE_1 = 1;
904
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
905
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
906
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
907
+ var REED_SOLOMON_MAX_BLOCK = 255;
908
+ function alFecRaptorQFlowConfig(flow) {
909
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
910
+ return null;
911
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
912
+ return null;
913
+ if (flow.packetIds.length !== 1)
914
+ return null;
915
+ const repair = flow.repairFlows[0];
916
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
917
+ const t = flow.repairSymbolSize;
918
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
919
+ return null;
920
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
921
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
922
+ return null;
923
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
924
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
925
+ return null;
926
+ } else {
927
+ return null;
928
+ }
929
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
930
+ return null;
931
+ return {
932
+ sourcePacketIds: [...flow.packetIds],
933
+ repairFlowId: repair.repairFlowId,
934
+ fecCodeId,
935
+ ssbgMode: flow.ssbgMode,
936
+ fecPayloadIdMode: flow.fecPayloadIdMode,
937
+ repairSymbolSize: t,
938
+ maximumK: k,
939
+ maximumP: p,
940
+ protectionWindowTime: repair.protectionWindowTime,
941
+ protectionWindowSize: repair.protectionWindowSize
942
+ };
943
+ }
944
+
906
945
  // ../mmt-base/dist/serial-number.js
907
946
  var SERIAL32_SPAN = 4294967296;
908
947
  var SERIAL32_HALF = 2147483648;
@@ -7949,8 +7988,8 @@ var MulticastSubscriptionPool = class {
7949
7988
  record.claims--;
7950
7989
  return this.acquire(gateway, config, consumer);
7951
7990
  }
7952
- const token = Symbol(key);
7953
- entry.consumers.set(token, consumer);
7991
+ const token2 = Symbol(key);
7992
+ entry.consumers.set(token2, consumer);
7954
7993
  let released = false;
7955
7994
  return {
7956
7995
  key,
@@ -7958,7 +7997,7 @@ var MulticastSubscriptionPool = class {
7958
7997
  if (released)
7959
7998
  return;
7960
7999
  released = true;
7961
- entry.consumers.delete(token);
8000
+ entry.consumers.delete(token2);
7962
8001
  record.claims--;
7963
8002
  if (record.claims > 0)
7964
8003
  return;
@@ -8586,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
8586
8625
  } catch (error) {
8587
8626
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8588
8627
  }
8589
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8628
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8590
8629
  await rx.close();
8591
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8630
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8592
8631
  }
8593
8632
  const events = new EventTarget();
8594
8633
  let state = "connected";
@@ -8707,14 +8746,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8707
8746
  });
8708
8747
 
8709
8748
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8749
+ var MAX_PENDING_PACKETS = 256;
8710
8750
  var AMTGatewayManager = class {
8711
8751
  subscriptions = /* @__PURE__ */ new Map();
8712
8752
  messageHandler;
8753
+ messageListenerRegistered = false;
8713
8754
  pendingPackets = /* @__PURE__ */ new Map();
8714
8755
  constructor() {
8715
8756
  this.messageHandler = (event) => {
8716
8757
  const data = event.data;
8717
- console.log(`[AMT] SW message received:`, data.type);
8718
8758
  if (data.type === "UDP_PACKET") {
8719
8759
  this.handlePacket(data);
8720
8760
  } else if (data.type === "SOCKET_ERROR") {
@@ -8723,12 +8763,22 @@ var AMTGatewayManager = class {
8723
8763
  console.log(`[AMT SW Debug] ${data.message}`);
8724
8764
  }
8725
8765
  };
8726
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
8727
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
8728
- console.log("[AMT] Registered service worker message handler");
8729
- } else {
8730
- console.warn("[AMT] Service worker not available");
8766
+ }
8767
+ ensureMessageListener() {
8768
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8769
+ return;
8770
+ }
8771
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8772
+ this.messageListenerRegistered = true;
8773
+ console.log("[AMT] Registered service worker message handler");
8774
+ }
8775
+ removeMessageListener() {
8776
+ if (!this.messageListenerRegistered)
8777
+ return;
8778
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8779
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8731
8780
  }
8781
+ this.messageListenerRegistered = false;
8732
8782
  }
8733
8783
  /**
8734
8784
  * Subscribe via AMT tunnel
@@ -8738,6 +8788,7 @@ var AMTGatewayManager = class {
8738
8788
  if (config.protected) {
8739
8789
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8740
8790
  }
8791
+ this.ensureMessageListener();
8741
8792
  const keyStr = this.getKeyString(key);
8742
8793
  if (this.subscriptions.has(keyStr)) {
8743
8794
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8773,6 +8824,8 @@ var AMTGatewayManager = class {
8773
8824
  this.subscriptions.delete(keyStr);
8774
8825
  this.pendingPackets.delete(subscription.subscriptionId);
8775
8826
  }
8827
+ if (this.subscriptions.size === 0)
8828
+ this.removeMessageListener();
8776
8829
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8777
8830
  }
8778
8831
  }
@@ -8794,6 +8847,8 @@ var AMTGatewayManager = class {
8794
8847
  } finally {
8795
8848
  this.subscriptions.delete(handle);
8796
8849
  this.pendingPackets.delete(subscription.subscriptionId);
8850
+ if (this.subscriptions.size === 0)
8851
+ this.removeMessageListener();
8797
8852
  }
8798
8853
  }
8799
8854
  /**
@@ -8810,14 +8865,12 @@ var AMTGatewayManager = class {
8810
8865
  if (!subscriptionId)
8811
8866
  return;
8812
8867
  const subscription = this.subscriptions.get(subscriptionId);
8813
- if (!subscription) {
8814
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8868
+ if (!subscription)
8815
8869
  return;
8816
- }
8817
8870
  if (!subscription.handshakeComplete) {
8818
8871
  const queue = this.pendingPackets.get(subscriptionId);
8819
8872
  const packetData2 = data;
8820
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8821
8874
  queue.push({
8822
8875
  data: packetData2.data,
8823
8876
  remoteAddress: packetData2.remoteAddress,
@@ -8843,19 +8896,15 @@ var AMTGatewayManager = class {
8843
8896
  if (!subscriptionId)
8844
8897
  return;
8845
8898
  const subscription = this.subscriptions.get(subscriptionId);
8846
- if (!subscription) {
8847
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8899
+ if (!subscription)
8848
8900
  return;
8849
- }
8850
8901
  subscription.config.onError(new Error(error || "Unknown error"));
8851
8902
  }
8852
8903
  /**
8853
8904
  * Cleanup on destroy
8854
8905
  */
8855
8906
  destroy() {
8856
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8857
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8858
- }
8907
+ this.removeMessageListener();
8859
8908
  const handles = Array.from(this.subscriptions.keys());
8860
8909
  for (const handle of handles) {
8861
8910
  this.unsubscribe(handle).catch((err) => {
@@ -12900,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12900
12949
  #retentionBlocks;
12901
12950
  #heldEvicted;
12902
12951
  #newest;
12952
+ #newestSource;
12903
12953
  #holes;
12904
12954
  static #checkedRetention(retentionBlocks) {
12905
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12937,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12937
12987
  observe(position) {
12938
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12939
12989
  }
12990
+ /**
12991
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
12992
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
12993
+ * a source cannot run ahead of the sources sent before it except by
12994
+ * reordering.
12995
+ */
12996
+ get newestSource() {
12997
+ return this.#newestSource;
12998
+ }
12999
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13000
+ noteSource(ssId) {
13001
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13002
+ this.#newestSource = ssId;
13003
+ return true;
13004
+ }
12940
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12941
13006
  get heldEvicted() {
12942
13007
  return this.#heldEvicted;
@@ -13169,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13169
13234
  this.#held.clear();
13170
13235
  this.#coverageEnd = void 0;
13171
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13172
13238
  }
13173
13239
  #claim(block) {
13174
13240
  const placed = [];
@@ -13788,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13788
13854
  var WORKER_ERROR_KIND_CAP = 32;
13789
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13790
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13791
13858
  function isMulticastSymbolDelivery(value) {
13792
13859
  return value === "datagram" || value === "ordered-relay";
13793
13860
  }
@@ -13885,6 +13952,10 @@ var FECManager = class _FECManager {
13885
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13886
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13887
13954
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
13955
+ // Source ESIs per block that another FEC instance rebuilt and fed in
13956
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
13957
+ // not network arrivals and never make a block clean.
13958
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13888
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13889
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13890
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14013,6 +14084,10 @@ var FECManager = class _FECManager {
14013
14084
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14014
14085
  // One classifier per physical stream, built from that stream's geometry.
14015
14086
  this.#classifiers = /* @__PURE__ */ new Map();
14087
+ // A stream without catalog timing has no classifier, so the last RAP it
14088
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14089
+ // a second RAP is released inside one MPU (BLO-52085).
14090
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14016
14091
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14017
14092
  // rungs can share a packetId with a different framerate or GOP, so the
14018
14093
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14170,6 +14245,7 @@ var FECManager = class _FECManager {
14170
14245
  #pendingWorkerCleanupBounds;
14171
14246
  #blockSourceCounts;
14172
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14173
14249
  #blockAuthoredLengths;
14174
14250
  #ssbLengthConflictCount;
14175
14251
  #blockRepairRsIds;
@@ -14212,6 +14288,7 @@ var FECManager = class _FECManager {
14212
14288
  #orderedFrontiers;
14213
14289
  #orderedPendingBlocks;
14214
14290
  #classifiers;
14291
+ #noSpanRaps;
14215
14292
  #streamGeometries;
14216
14293
  #failedBlocks;
14217
14294
  #resolvedBlockClasses;
@@ -14873,6 +14950,7 @@ var FECManager = class _FECManager {
14873
14950
  };
14874
14951
  const discontinuity = () => {
14875
14952
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14953
+ this.#noSpanRaps.delete(streamKey);
14876
14954
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14877
14955
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14878
14956
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15285,6 +15363,9 @@ var FECManager = class _FECManager {
15285
15363
  }
15286
15364
  if (policyChanged) {
15287
15365
  this.#bumpEpoch(packetId);
15366
+ for (const key of [...this.#noSpanRaps.keys()]) {
15367
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15368
+ }
15288
15369
  for (const [key, classifier] of [...this.#classifiers]) {
15289
15370
  if (!this.#belongsToPacket(key, packetId)) continue;
15290
15371
  const stream = this.#parseStreamKey(key);
@@ -15487,6 +15568,7 @@ var FECManager = class _FECManager {
15487
15568
  }
15488
15569
  resetMap(this.#blockSourceCounts);
15489
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15490
15572
  resetMap(this.#blockAuthoredLengths);
15491
15573
  resetMap(this.#blockRepairRsIds);
15492
15574
  resetMap(this.#blockRsbLengths);
@@ -15520,6 +15602,7 @@ var FECManager = class _FECManager {
15520
15602
  resetMap(this.#isoLocators);
15521
15603
  resetMap(this.#sourceAnchors);
15522
15604
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15605
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15523
15606
  this.#bumpEpoch(packetId);
15524
15607
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15525
15608
  for (const key of keys.keys()) {
@@ -15610,6 +15693,7 @@ var FECManager = class _FECManager {
15610
15693
  dropMap(this.#pendingWorkerCleanupClients);
15611
15694
  dropMap(this.#blockSourceCounts);
15612
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15613
15697
  dropMap(this.#blockAuthoredLengths);
15614
15698
  dropMap(this.#blockRepairRsIds);
15615
15699
  dropMap(this.#blockRsbLengths);
@@ -15626,6 +15710,7 @@ var FECManager = class _FECManager {
15626
15710
  dropMap(this.#isoLocators);
15627
15711
  dropMap(this.#sourceAnchors);
15628
15712
  dropMap(this.#classifiers);
15713
+ dropMap(this.#noSpanRaps);
15629
15714
  dropMap(this.#streamGeometries);
15630
15715
  dropSet(this.#pendingWorkerCleanups);
15631
15716
  dropSet(this.#completedBlocks);
@@ -15771,6 +15856,9 @@ var FECManager = class _FECManager {
15771
15856
  this.#unindexBlockKey(key);
15772
15857
  }
15773
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15774
15862
  return true;
15775
15863
  }
15776
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16088,6 +16176,7 @@ var FECManager = class _FECManager {
16088
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16089
16177
  const evictedBefore = locator.heldEvicted;
16090
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16091
16180
  const evicted = locator.heldEvicted - evictedBefore;
16092
16181
  if (evicted > 0) {
16093
16182
  const before = this.#isoHeldEvictedCount;
@@ -16256,11 +16345,14 @@ var FECManager = class _FECManager {
16256
16345
  /**
16257
16346
  * Whether an MPU boundary still bounds this stream's RAPs (see
16258
16347
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16259
- * it does not. A stream with no classifier yet has shown nothing against it.
16348
+ * it does not. A stream with no classifier yet has shown nothing against it,
16349
+ * and neither has a stream without catalog timing until it releases two
16350
+ * RAPs inside one MPU.
16260
16351
  */
16261
16352
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16262
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16263
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16353
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16354
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16355
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16264
16356
  }
16265
16357
  /** A block's class now, or frozen at its resolution. */
16266
16358
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16286,7 +16378,14 @@ var FECManager = class _FECManager {
16286
16378
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16287
16379
  */
16288
16380
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16289
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16381
+ if (!this.#trackConfigs.has(packetId)) return;
16382
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16383
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16384
+ const last = this.#noSpanRaps.get(streamKey);
16385
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16386
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16387
+ return;
16388
+ }
16290
16389
  this.#applyClassifierChanges(
16291
16390
  packetId,
16292
16391
  catalogTrackName,
@@ -16673,7 +16772,15 @@ var FECManager = class _FECManager {
16673
16772
  this.#isoSourceOversizeCount++;
16674
16773
  return null;
16675
16774
  }
16676
- const sbn = located.block.start;
16775
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16776
+ }
16777
+ /**
16778
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16779
+ * answers through onFrame (null here), or its synchronous decoder, whose
16780
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16781
+ * stated for `ssId`.
16782
+ */
16783
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16677
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16678
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16679
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16702,6 +16809,74 @@ var FECManager = class _FECManager {
16702
16809
  }
16703
16810
  return recovered;
16704
16811
  }
16812
+ /**
16813
+ * Feed a source symbol of this stream that another FEC instance over the
16814
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16815
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16816
+ * BLO-52085). It joins its block's decoder as a received source would, so
16817
+ * it reduces the erasures the block still has to solve, but it is not a
16818
+ * network arrival: no transport accounting, no classification evidence,
16819
+ * no deadline refresh, and it never makes a block clean.
16820
+ *
16821
+ * Only a block a repair stated takes one (a repair-less span has no
16822
+ * decoder block). A block that is terminal, or already holds that source,
16823
+ * does not. The symbol is the exact protected packet, unpadded, as
16824
+ * feedSourceSymbol takes it.
16825
+ *
16826
+ * `recovered` is the block the synchronous decoder completed with it; the
16827
+ * caller routes it and completes the block, as for any recovery. When
16828
+ * every source of the block is then present, received or rebuilt, and no
16829
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16830
+ * block as clean on its own side), the block is completed here as
16831
+ * recovered, with nothing left to route.
16832
+ */
16833
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16834
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16835
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16836
+ if (located === null || located.block.span) return NOT_TAKEN;
16837
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16838
+ const sbn = located.block.start;
16839
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16840
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16841
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16842
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16843
+ if (rebuilt === void 0) {
16844
+ rebuilt = /* @__PURE__ */ new Set();
16845
+ this.#blockRebuiltEsis.set(key, rebuilt);
16846
+ }
16847
+ const config = this.#trackConfigs.get(packetId);
16848
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16849
+ rebuilt.add(located.esi);
16850
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16851
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16852
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16853
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16854
+ }
16855
+ }
16856
+ return { taken: true, recovered };
16857
+ }
16858
+ /**
16859
+ * A block that holds every one of its `length` sources, received or
16860
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16861
+ * every packet of it was delivered, by its own arrival or by the instance
16862
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16863
+ */
16864
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16865
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16866
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16867
+ let present = rebuilt.size;
16868
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16869
+ if (present < length) return false;
16870
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16871
+ this.countRecovery(packetId, sbn, catalogTrackName);
16872
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16873
+ if (catalogTrackName === void 0) {
16874
+ this.onBlockComplete?.(sbn, packetId);
16875
+ } else {
16876
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16877
+ }
16878
+ return true;
16879
+ }
16705
16880
  /**
16706
16881
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16707
16882
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16904,8 +17079,75 @@ var FECManager = class _FECManager {
16904
17079
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16905
17080
  return null;
16906
17081
  }
17082
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16907
17083
  return recovered;
16908
17084
  }
17085
+ /**
17086
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17087
+ * its sources in send order, so one that moves the stream's newest source
17088
+ * position past a block leaves the sources that block still misses
17089
+ * reordered behind it at best: it shortens the wait of every block it
17090
+ * passed that only a late source can complete. MoQ is left out: its streams
17091
+ * can overtake one another, so a MoQ source shows nothing about the sources
17092
+ * sent before it.
17093
+ */
17094
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17095
+ const previous = locator.newestSource;
17096
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17097
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17098
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17099
+ this.#shortenUnrecoverableWait(
17100
+ this.#blockKey(block.start, packetId, catalogTrackName),
17101
+ block.start,
17102
+ packetId,
17103
+ catalogTrackName
17104
+ );
17105
+ }
17106
+ }
17107
+ /**
17108
+ * The wait of a block that only a late source can complete (BLO-52085): a
17109
+ * repair stated it, its received sources plus every symbol its repair group
17110
+ * carries (RSB_length) are short of K' (the repair scheduler's
17111
+ * group-cannot-complete), and a later source of the stream has arrived, so
17112
+ * a source it misses is lost or reordered behind that one. The span, which
17113
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17114
+ * the late-symbol allowance alone, the multicast wait less the span (the
17115
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17116
+ * block.
17117
+ */
17118
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17119
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17120
+ const rsbLength = this.#blockRsbLengths.get(key);
17121
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17122
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17123
+ const received = this.#blockSourceEsis.get(key);
17124
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17125
+ if (present + rsbLength >= authoredLength) return void 0;
17126
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17127
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17128
+ return void 0;
17129
+ }
17130
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17131
+ }
17132
+ /**
17133
+ * Bring an open block's wait in to its late-source wait, when it has one.
17134
+ * Never later than it is armed: only a source can complete such a block,
17135
+ * so a repair of its group (paced across the span) must not keep it open.
17136
+ */
17137
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17138
+ if (!this.#activeBlocks.has(key)) return;
17139
+ const config = this.#trackConfigs.get(packetId);
17140
+ if (config === void 0) return;
17141
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17142
+ if (waitMs === void 0) return;
17143
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17144
+ this.#armBlockTimeout(
17145
+ sbn,
17146
+ packetId,
17147
+ catalogTrackName,
17148
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17149
+ );
17150
+ }
16909
17151
  /**
16910
17152
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16911
17153
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17060,6 +17302,15 @@ var FECManager = class _FECManager {
17060
17302
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17061
17303
  return;
17062
17304
  }
17305
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17306
+ key,
17307
+ sbn,
17308
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17309
+ packetId,
17310
+ catalogTrackName
17311
+ )) {
17312
+ return;
17313
+ }
17063
17314
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17064
17315
  }
17065
17316
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17068,6 +17319,7 @@ var FECManager = class _FECManager {
17068
17319
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17069
17320
  this.#blockSourceCounts.delete(key);
17070
17321
  this.#blockSourceEsis.delete(key);
17322
+ this.#blockRebuiltEsis.delete(key);
17071
17323
  this.#blockAuthoredLengths.delete(key);
17072
17324
  this.#blockRepairRsIds.delete(key);
17073
17325
  this.#blockRsbLengths.delete(key);
@@ -17134,7 +17386,16 @@ var FECManager = class _FECManager {
17134
17386
  this.#repairDeadlineMs(trackConfig)
17135
17387
  );
17136
17388
  }
17137
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17389
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17390
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17391
+ return;
17392
+ }
17393
+ this.#armBlockTimeout(
17394
+ sbn,
17395
+ packetId,
17396
+ catalogTrackName,
17397
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17398
+ );
17138
17399
  }
17139
17400
  #failBlock(sbn, packetId, catalogTrackName) {
17140
17401
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17147,6 +17408,7 @@ var FECManager = class _FECManager {
17147
17408
  }
17148
17409
  this.#blockSourceCounts.delete(key);
17149
17410
  this.#blockSourceEsis.delete(key);
17411
+ this.#blockRebuiltEsis.delete(key);
17150
17412
  this.#blockAuthoredLengths.delete(key);
17151
17413
  this.#blockRepairRsIds.delete(key);
17152
17414
  this.#blockRsbLengths.delete(key);
@@ -17185,6 +17447,7 @@ var FECManager = class _FECManager {
17185
17447
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17186
17448
  this.#blockSourceCounts.delete(key);
17187
17449
  this.#blockSourceEsis.delete(key);
17450
+ this.#blockRebuiltEsis.delete(key);
17188
17451
  this.#blockAuthoredLengths.delete(key);
17189
17452
  this.#blockRepairRsIds.delete(key);
17190
17453
  this.#blockRsbLengths.delete(key);
@@ -17276,6 +17539,7 @@ var FECManager = class _FECManager {
17276
17539
  this.#blockKeysBySbn.clear();
17277
17540
  this.#blockSourceCounts.clear();
17278
17541
  this.#blockSourceEsis.clear();
17542
+ this.#blockRebuiltEsis.clear();
17279
17543
  this.#blockAuthoredLengths.clear();
17280
17544
  this.#blockRepairRsIds.clear();
17281
17545
  this.#blockRsbLengths.clear();
@@ -17301,6 +17565,7 @@ var FECManager = class _FECManager {
17301
17565
  this.#blockAnchorsMs.clear();
17302
17566
  this.#blockCatalogTrackNames.clear();
17303
17567
  this.#classifiers.clear();
17568
+ this.#noSpanRaps.clear();
17304
17569
  this.#streamGeometries.clear();
17305
17570
  this.#failedBlocks.clear();
17306
17571
  this.#resolvedBlockClasses.clear();
@@ -17831,9 +18096,21 @@ var IsoBmffTimingResolver = class {
17831
18096
  }
17832
18097
  #track = null;
17833
18098
  #fragments = /* @__PURE__ */ new Map();
18099
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18100
+ #firstSamples = /* @__PURE__ */ new Map();
18101
+ /**
18102
+ * First sample and sample count of the newest fragments whose every slot was
18103
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18104
+ * matches the combiner, since it retires an abandoned sample as well.
18105
+ */
18106
+ #delivered = /* @__PURE__ */ new Map();
17834
18107
  #pendingSamples = 0;
17835
18108
  #evictedSamples = 0;
17836
18109
  #lastMovieFragmentRejection = null;
18110
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18111
+ get initialized() {
18112
+ return this.#track !== null;
18113
+ }
17837
18114
  get pendingSampleCount() {
17838
18115
  return this.#pendingSamples;
17839
18116
  }
@@ -17857,7 +18134,14 @@ var IsoBmffTimingResolver = class {
17857
18134
  this.#track = timing;
17858
18135
  return true;
17859
18136
  }
17860
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18137
+ /**
18138
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18139
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18140
+ * fragment whose samples are all done is a duplicate: admitting it again
18141
+ * would charge stale timing and could displace the live MPU's. A source
18142
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18143
+ */
18144
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17861
18145
  this.#lastMovieFragmentRejection = null;
17862
18146
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17863
18147
  this.#lastMovieFragmentRejection = "invalid";
@@ -17883,6 +18167,8 @@ var IsoBmffTimingResolver = class {
17883
18167
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17884
18168
  );
17885
18169
  }
18170
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18171
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17886
18172
  let freed = 0;
17887
18173
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17888
18174
  freed += this.#evictOldestFragment();
@@ -17896,6 +18182,7 @@ var IsoBmffTimingResolver = class {
17896
18182
  return false;
17897
18183
  }
17898
18184
  this.#fragments.set(key, [...parsed.samples]);
18185
+ this.#firstSamples.set(key, parsed.samples[0]);
17899
18186
  this.#pendingSamples += parsed.samples.length;
17900
18187
  return true;
17901
18188
  }
@@ -17953,6 +18240,7 @@ var IsoBmffTimingResolver = class {
17953
18240
  let live = 0;
17954
18241
  for (const sample of samples) if (sample !== null) live++;
17955
18242
  this.#fragments.delete(oldest);
18243
+ this.#firstSamples.delete(oldest);
17956
18244
  this.#pendingSamples -= live;
17957
18245
  this.#evictedSamples += live;
17958
18246
  return live;
@@ -17968,12 +18256,23 @@ var IsoBmffTimingResolver = class {
17968
18256
  if (!samples || !samples[index]) return false;
17969
18257
  samples[index] = null;
17970
18258
  this.#pendingSamples--;
17971
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18259
+ if (samples.every((value) => value === null)) {
18260
+ this.#fragments.delete(key);
18261
+ const first = this.#firstSamples.get(key);
18262
+ this.#firstSamples.delete(key);
18263
+ this.#delivered.delete(key);
18264
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18265
+ if (this.#delivered.size > this.maxPendingSamples) {
18266
+ this.#delivered.delete(this.#delivered.keys().next().value);
18267
+ }
18268
+ }
17972
18269
  return true;
17973
18270
  }
17974
18271
  reset() {
17975
18272
  this.#track = null;
17976
18273
  this.#fragments.clear();
18274
+ this.#firstSamples.clear();
18275
+ this.#delivered.clear();
17977
18276
  this.#pendingSamples = 0;
17978
18277
  }
17979
18278
  };
@@ -18455,6 +18754,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18455
18754
  this.#reclaimedTimingSamples = 0;
18456
18755
  this.#evictedTimingSamples = 0;
18457
18756
  this.#rejectedMovieFragments = 0;
18757
+ this.#recoveredHeadsAwaitingTiming = {
18758
+ held: 0,
18759
+ expired: 0,
18760
+ discarded: 0,
18761
+ superseded: 0
18762
+ };
18458
18763
  this.#sequencers = /* @__PURE__ */ new Map();
18459
18764
  this.#fecGate = null;
18460
18765
  this.#fecSequencer = null;
@@ -18563,6 +18868,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18563
18868
  #reclaimedTimingSamples;
18564
18869
  #evictedTimingSamples;
18565
18870
  #rejectedMovieFragments;
18871
+ #recoveredHeadsAwaitingTiming;
18566
18872
  #sequencers;
18567
18873
  #fecGate;
18568
18874
  #fecSequencer;
@@ -18668,11 +18974,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18668
18974
  /**
18669
18975
  * Typed view of the ISO-BMFF timing-sample budget.
18670
18976
  *
18671
- * This is the acceptance signal for the budget ratchet: `pending` walking
18672
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18673
- * and it is observable here rather than only as media silently stopping.
18674
- * Per repo policy this is a producer-owned counter -- never infer budget
18675
- * state by parsing logs.
18977
+ * Each stream admits timing against its own window of `capacity` slots
18978
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
18979
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
18980
+ * timing a stream declared and never delivered shows up there. `pending`
18981
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
18982
+ * a producer-owned counter -- never infer budget state by parsing logs.
18676
18983
  */
18677
18984
  get timingBudgetStats() {
18678
18985
  return {
@@ -18683,6 +18990,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18683
18990
  rejectedMovieFragments: this.#rejectedMovieFragments
18684
18991
  };
18685
18992
  }
18993
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18994
+ get recoveredHeadsAwaitingTiming() {
18995
+ let pending = 0;
18996
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18997
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18998
+ }
18999
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
19000
+ }
18686
19001
  get reorderStats() {
18687
19002
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18688
19003
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18811,10 +19126,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18811
19126
  get reassemblerDataUnitsReturned() {
18812
19127
  return this.#reassemblerDataUnitsReturned;
18813
19128
  }
18814
- #countOutcome(token) {
18815
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18816
- this.#feedOutcomes.set(token, count);
18817
- this.#lastOutcome = token;
19129
+ #countOutcome(token2) {
19130
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19131
+ this.#feedOutcomes.set(token2, count);
19132
+ this.#lastOutcome = token2;
18818
19133
  this.#lastOutcomeCount = count;
18819
19134
  }
18820
19135
  #countDeferredOutcome(reason) {
@@ -18856,7 +19171,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18856
19171
  const now = Date.now();
18857
19172
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18858
19173
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18859
- awaitingTiming.shift();
19174
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18860
19175
  }
18861
19176
  }
18862
19177
  }
@@ -18974,11 +19289,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
18974
19289
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
18975
19290
  return this.#drop("fragment-unexpected-fragmentation");
18976
19291
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
18977
- if (!resolver2) return this.#drop("fragment-no-resolver");
19292
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
18978
19293
  const pendingBefore = resolver2.pendingSampleCount;
18979
19294
  const evictedBefore = resolver2.evictedSampleCount;
18980
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
18981
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19295
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19296
+ const admitted = resolver2.addMovieFragment(
19297
+ mpu.mpuSequenceNumber,
19298
+ data,
19299
+ remaining,
19300
+ options?.recovered === true
19301
+ );
18982
19302
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
18983
19303
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
18984
19304
  if (!admitted) {
@@ -18998,7 +19318,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18998
19318
  };
18999
19319
  }
19000
19320
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19001
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19321
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19002
19322
  if (!resolver) return this.#drop("mfu-no-resolver");
19003
19323
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19004
19324
  return this.#drop("mfu-empty-payload");
@@ -19019,6 +19339,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19019
19339
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19020
19340
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19021
19341
  sampleNumber: mfuDuHeader.sampleNumber,
19342
+ sequenceNumber: info.sequenceNumber,
19022
19343
  arrivedAt: Date.now()
19023
19344
  });
19024
19345
  return this.#drop("mfu-sample-timing-missing");
@@ -19342,6 +19663,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19342
19663
  }
19343
19664
  this.#emitFrame(data, meta);
19344
19665
  }
19666
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19667
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19668
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19669
+ }
19345
19670
  /**
19346
19671
  * Claim and return the authored timing for a recovered sample. Recovery does
19347
19672
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19403,6 +19728,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19403
19728
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19404
19729
  this.#sequencers.clear();
19405
19730
  this.#fecSequencer?.discard();
19731
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19732
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19733
+ }
19406
19734
  this.#timingStreams.clear();
19407
19735
  this.#pendingTimingSamples = 0;
19408
19736
  this.#reliableMoqStartupGate.clear();
@@ -19421,10 +19749,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19421
19749
  return resolver;
19422
19750
  }
19423
19751
  #awaitTiming(streamKey, head) {
19424
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19425
- if (!awaitingTiming) return;
19752
+ const state = this.#timingStreams.get(streamKey);
19753
+ if (!state) return;
19754
+ if (!head.options?.recovered) {
19755
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19756
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19757
+ state.awaitingTiming = kept;
19758
+ }
19759
+ const awaitingTiming = state.awaitingTiming;
19426
19760
  awaitingTiming.push(head);
19427
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19761
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19762
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19763
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19764
+ }
19765
+ }
19766
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19767
+ #leaveAwaitingTiming(head, outcome) {
19768
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19428
19769
  }
19429
19770
  /**
19430
19771
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19452,6 +19793,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19452
19793
  const state = this.#timingStreams.get(streamKey);
19453
19794
  if (!state) return;
19454
19795
  this.#timingStreams.delete(streamKey);
19796
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19455
19797
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19456
19798
  if (this.#pendingTimingSamples < 0) {
19457
19799
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22658,6 +23000,16 @@ function keyframeOverlaySsId(header) {
22658
23000
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22659
23001
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22660
23002
  }
23003
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23004
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23005
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23006
+ }
23007
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23008
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23009
+ return headerPsn + offset >>> 0;
23010
+ }
23011
+ var OVERLAY_ROUTED_LIMIT = 1024;
23012
+ var OVERLAY_PENDING_LIMIT = 256;
22661
23013
  var OVERLAY_OFFSET_MPUS = 16;
22662
23014
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22663
23015
  function assertStreamIdentity(role, stream) {
@@ -22769,6 +23121,7 @@ var MmtpRouter = class {
22769
23121
  #provenance = /* @__PURE__ */ new Map();
22770
23122
  #onFlowProtected = null;
22771
23123
  #unadmittedListedDrops = 0;
23124
+ #admittedSourceDatagrams = 0;
22772
23125
  #unlistedRepairSetAside = 0;
22773
23126
  #assistBlocks = /* @__PURE__ */ new Map();
22774
23127
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22825,6 +23178,15 @@ var MmtpRouter = class {
22825
23178
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22826
23179
  */
22827
23180
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23181
+ /**
23182
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23183
+ * sender's own packet_sequence_number: a recovered packet's header psn
23184
+ * equal to its SS_ID minus that offset. While it equals the stream's
23185
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23186
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23187
+ * an MPU no member of which arrived).
23188
+ */
23189
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22828
23190
  /**
22829
23191
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22830
23192
  * not parsed, typically because its overlap copy was parsed first
@@ -22894,7 +23256,9 @@ var MmtpRouter = class {
22894
23256
  recovered: 0,
22895
23257
  restarts: 0,
22896
23258
  unsequencedDropped: 0,
22897
- protectedDropped: 0
23259
+ protectedDropped: 0,
23260
+ baseFed: 0,
23261
+ baseUndetermined: 0
22898
23262
  };
22899
23263
  /** Callback fired when a delta-track FEC block completes or fails. */
22900
23264
  #onDeltaBlockCompleteCb = null;
@@ -23067,7 +23431,7 @@ var MmtpRouter = class {
23067
23431
  const cb = this.#getFrameCallback(packetId);
23068
23432
  if (isVideo && !assembler) {
23069
23433
  if (frameMeta.recovered) {
23070
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23434
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23071
23435
  } else {
23072
23436
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23073
23437
  }
@@ -23457,6 +23821,10 @@ var MmtpRouter = class {
23457
23821
  get keyframeOverlayStats() {
23458
23822
  return { ...this.#overlayStats };
23459
23823
  }
23824
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23825
+ get recoveredHeadsAwaitingTiming() {
23826
+ return this.#parser.recoveredHeadsAwaitingTiming;
23827
+ }
23460
23828
  #isOverlayRepair(repairPacketId, repairTrackName) {
23461
23829
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23462
23830
  }
@@ -23507,6 +23875,11 @@ var MmtpRouter = class {
23507
23875
  */
23508
23876
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23509
23877
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23878
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23879
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23880
+ stream.overlayToBase.routed.clear();
23881
+ stream.overlayToBase.pending.clear();
23882
+ }
23510
23883
  }
23511
23884
  #resetOverlay(inst) {
23512
23885
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -23563,9 +23936,10 @@ var MmtpRouter = class {
23563
23936
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23564
23937
  * every overlay installed for its source, at that SS_ID, with the bytes the
23565
23938
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23566
- * included (7.1, 7.4.3).
23939
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23940
+ * FEC reads it.
23567
23941
  */
23568
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23942
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23569
23943
  if (this.#overlays.size === 0) return;
23570
23944
  const ssId = keyframeOverlaySsId(header);
23571
23945
  if (ssId === void 0) return;
@@ -23574,7 +23948,11 @@ var MmtpRouter = class {
23574
23948
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23575
23949
  if (identity !== null) {
23576
23950
  inst.offsets.delete(identity.mpuSequenceNumber);
23577
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23951
+ inst.offsets.set(identity.mpuSequenceNumber, {
23952
+ psnOffset: ssId - sequenceNumber >>> 0,
23953
+ overlaySsId: ssId,
23954
+ ...baseSsId === void 0 ? {} : { baseSsId }
23955
+ });
23578
23956
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23579
23957
  }
23580
23958
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23653,14 +24031,27 @@ var MmtpRouter = class {
23653
24031
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23654
24032
  const restamped = this.#restampedStreams.has(streamKey);
23655
24033
  const restored = /* @__PURE__ */ new Set();
24034
+ const baseSsIds = /* @__PURE__ */ new Map();
23656
24035
  const symbols = result.symbols.flatMap((sym) => {
23657
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23658
- if (offset !== void 0) {
23659
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24036
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24037
+ if (anchor !== void 0) {
24038
+ const overlaySsId = serial32Add(blockStart, sym.index);
24039
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23660
24040
  restored.add(packetSequenceNumber);
23661
- return [{ ...sym, packetSequenceNumber }];
24041
+ const out = { ...sym, packetSequenceNumber };
24042
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24043
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
24044
+ return [out];
24045
+ }
24046
+ if (!restamped) {
24047
+ const baseSsId = unstampedBaseSsId(
24048
+ this.#ssIdSeqOffsetByStream.get(streamKey),
24049
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24050
+ sym.packetSequenceNumber
24051
+ );
24052
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
24053
+ return [sym];
23662
24054
  }
23663
- if (!restamped) return [sym];
23664
24055
  this.#overlayStats.unsequencedDropped++;
23665
24056
  return [];
23666
24057
  });
@@ -23669,6 +24060,85 @@ var MmtpRouter = class {
23669
24060
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23670
24061
  });
23671
24062
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24063
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24064
+ }
24065
+ /**
24066
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24067
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24068
+ * from a received member of its MPU, else unstampedBaseSsId),
24069
+ * so it is fed to the base decoder as a rebuilt source
24070
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24071
+ * fewer to solve, and its repair can recover the block's other packets.
24072
+ * A base block no repair has stated yet takes it when one does
24073
+ * (#feedPendingRebuilt).
24074
+ *
24075
+ * The overlay route already handled each packet fed (delivered it, or
24076
+ * held an untimed head for its FT=1), so a base recovery routes none of
24077
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24078
+ * held head would wait in the parser for timing its first copy consumed.
24079
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24080
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24081
+ * that crossed no multicast socket, which the provenance counted from the
24082
+ * block's own arrivals cannot show.
24083
+ */
24084
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24085
+ const fec = this.#fecManager;
24086
+ const owner = inst.leg.sourcePacketId;
24087
+ const name = inst.leg.sourceCatalogTrackName;
24088
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24089
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24090
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24091
+ const feeds = [];
24092
+ for (const sym of symbols) {
24093
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24094
+ const ssId = baseSsIds.get(sym);
24095
+ if (ssId === void 0) {
24096
+ this.#overlayStats.baseUndetermined++;
24097
+ continue;
24098
+ }
24099
+ routed.delete(ssId);
24100
+ routed.add(ssId);
24101
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24102
+ feeds.push([ssId, sym.rawSymbol]);
24103
+ }
24104
+ for (const [ssId, symbolData] of feeds) {
24105
+ if (this.#classifyPacketId(owner) === null) return;
24106
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24107
+ }
24108
+ }
24109
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24110
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24111
+ const fec = this.#fecManager;
24112
+ if (!fec?.feedRebuiltSourceSymbol) return;
24113
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24114
+ if (coords == null) {
24115
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24116
+ pending.delete(ssId);
24117
+ pending.set(ssId, symbolData.slice());
24118
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24119
+ return;
24120
+ }
24121
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24122
+ if (!fed.taken) return;
24123
+ this.#overlayStats.baseFed++;
24124
+ if (fed.recovered === null) return;
24125
+ const sbn = coords.sbn;
24126
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24127
+ fec.cancelBlockTimeout(sbn, owner, name);
24128
+ fec.countRecovery(owner, sbn, name);
24129
+ fec.markBlockCompleted(sbn, owner, name);
24130
+ });
24131
+ }
24132
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24133
+ #feedPendingRebuilt(owner, name, block) {
24134
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24135
+ if (pending === void 0 || pending.size === 0) return;
24136
+ for (const [ssId, symbolData] of [...pending]) {
24137
+ if (this.#classifyPacketId(owner) === null) return;
24138
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24139
+ pending.delete(ssId);
24140
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24141
+ }
23672
24142
  }
23673
24143
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23674
24144
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23808,6 +24278,10 @@ var MmtpRouter = class {
23808
24278
  get unadmittedListedDropCount() {
23809
24279
  return this.#unadmittedListedDrops;
23810
24280
  }
24281
+ /** 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. */
24282
+ get admittedSourceDatagramCount() {
24283
+ return this.#admittedSourceDatagrams;
24284
+ }
23811
24285
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23812
24286
  get unlistedRepairSetAsideCount() {
23813
24287
  return this.#unlistedRepairSetAside;
@@ -24294,6 +24768,8 @@ var MmtpRouter = class {
24294
24768
  )) return;
24295
24769
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24296
24770
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24771
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24772
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24297
24773
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24298
24774
  const recovered = fec.feedRepairSymbol(
24299
24775
  ownerPacketId,
@@ -24431,6 +24907,7 @@ var MmtpRouter = class {
24431
24907
  this.#droppedPreSyncVideo.delete(streamKey);
24432
24908
  this.#latestVideoSeqByStream.delete(streamKey);
24433
24909
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24910
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24434
24911
  this.#multicastRapTwinByStream.delete(streamKey);
24435
24912
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24436
24913
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25065,6 +25542,7 @@ var MmtpRouter = class {
25065
25542
  const parsed = this.#parser.parsePacket(packet);
25066
25543
  if (!parsed) return;
25067
25544
  const info = parsed.info;
25545
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25068
25546
  if (source === "multicast" && !manifestAdmitted) {
25069
25547
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25070
25548
  }
@@ -25346,7 +25824,8 @@ var MmtpRouter = class {
25346
25824
  parsed.header,
25347
25825
  info.sequenceNumber,
25348
25826
  info.rapFlag,
25349
- source
25827
+ source,
25828
+ fecPayloadId?.ssId
25350
25829
  );
25351
25830
  }
25352
25831
  }
@@ -25496,7 +25975,8 @@ var MmtpRouter = class {
25496
25975
  if (stream === void 0) {
25497
25976
  stream = {
25498
25977
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25499
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
25978
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
25979
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25500
25980
  };
25501
25981
  byName.set(catalogTrackName, stream);
25502
25982
  }
@@ -25611,8 +26091,16 @@ var MmtpRouter = class {
25611
26091
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25612
26092
  this.#emitDirectVideo(packetId, data, meta, cb);
25613
26093
  }
25614
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26094
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25615
26095
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26096
+ if (this.#isFecTrack(packetId)) {
26097
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26098
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26099
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26100
+ this.#emitDirectVideo(packetId, data, meta, cb);
26101
+ return;
26102
+ }
26103
+ }
25616
26104
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25617
26105
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25618
26106
  this.#drainDirectRecoveredVideo(
@@ -25766,6 +26254,13 @@ var MmtpRouter = class {
25766
26254
  * frames by epoch only if pre-protection input ever matters.
25767
26255
  */
25768
26256
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26257
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26258
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26259
+ result = {
26260
+ ...result,
26261
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26262
+ };
26263
+ }
25769
26264
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25770
26265
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25771
26266
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25830,7 +26325,10 @@ var MmtpRouter = class {
25830
26325
  ...result,
25831
26326
  symbols: result.symbols.map((sym) => {
25832
26327
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25833
- if (wire === sym.packetSequenceNumber) return sym;
26328
+ if (wire === sym.packetSequenceNumber) {
26329
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26330
+ return sym;
26331
+ }
25834
26332
  this.#restampedRecoveredSeqs++;
25835
26333
  this.#restampedStreams.add(streamKey);
25836
26334
  return { ...sym, packetSequenceNumber: wire };
@@ -25866,6 +26364,29 @@ var MmtpRouter = class {
25866
26364
  }
25867
26365
  );
25868
26366
  }
26367
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26368
+ ownerPacketId,
26369
+ sym.fragment.mpuSequenceNumber,
26370
+ sym.fragment.movieFragmentSequenceNumber,
26371
+ sym.fragment.sampleNumber,
26372
+ catalogTrackName
26373
+ );
26374
+ for (const sym of result.symbols) {
26375
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26376
+ this.#feedViaParser(
26377
+ sym.rawSymbol,
26378
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26379
+ void 0,
26380
+ {
26381
+ skipFecPayloadId: true,
26382
+ recovered: true,
26383
+ deliveryProvenance,
26384
+ ...fecSbnOf(sym.index),
26385
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26386
+ }
26387
+ );
26388
+ }
26389
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25869
26390
  const gated = this.#isFecTrack(ownerPacketId);
25870
26391
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25871
26392
  if (assembler) {
@@ -29627,7 +30148,7 @@ async function setupFecWorkerForTrack(opts) {
29627
30148
  blockProfile,
29628
30149
  k: opts.fecK,
29629
30150
  // P as this receiver joined it, not the catalog total: the worker snapshot
29630
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
30151
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29631
30152
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29632
30153
  symbolSize: opts.fecSymbolSize,
29633
30154
  interleaveDepth: interleaveDepthBlocks,
@@ -30169,24 +30690,24 @@ var MediaRouter = class {
30169
30690
  if (!this.#multicastVideoKeyed) return;
30170
30691
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30171
30692
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30172
- const token = ++this.#sourceSwitchToken;
30693
+ const token2 = ++this.#sourceSwitchToken;
30173
30694
  const maybeReady = this.#onSourceSwitch?.("multicast");
30174
30695
  if (!maybeReady || typeof maybeReady.then !== "function") {
30175
- this.#commitMulticastActivation(token);
30696
+ this.#commitMulticastActivation(token2);
30176
30697
  return;
30177
30698
  }
30178
30699
  const ready = Promise.resolve(maybeReady);
30179
- this.#pendingMulticastToken = token;
30700
+ this.#pendingMulticastToken = token2;
30180
30701
  this.#sourceSwitchReady = ready;
30181
30702
  void ready.then(
30182
- () => this.#commitMulticastActivation(token),
30183
- (err) => this.#rejectMulticastActivation(token, err)
30703
+ () => this.#commitMulticastActivation(token2),
30704
+ (err) => this.#rejectMulticastActivation(token2, err)
30184
30705
  ).finally(() => {
30185
30706
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30186
30707
  });
30187
30708
  }
30188
- #commitMulticastActivation(token) {
30189
- if (token !== this.#sourceSwitchToken) return;
30709
+ #commitMulticastActivation(token2) {
30710
+ if (token2 !== this.#sourceSwitchToken) return;
30190
30711
  this.#active = "multicast";
30191
30712
  this.#suppressed = true;
30192
30713
  this.#pendingMulticastToken = null;
@@ -30197,9 +30718,9 @@ var MediaRouter = class {
30197
30718
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30198
30719
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30199
30720
  }
30200
- #rejectMulticastActivation(token, err) {
30201
- if (token !== this.#sourceSwitchToken) return;
30202
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30721
+ #rejectMulticastActivation(token2, err) {
30722
+ if (token2 !== this.#sourceSwitchToken) return;
30723
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30203
30724
  this.#pendingMulticastVideo = null;
30204
30725
  this.#pendingMulticastAudio = null;
30205
30726
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31168,63 +31689,65 @@ var FecStarvationDetector = class {
31168
31689
 
31169
31690
  // src/cmcd-producer.ts
31170
31691
  var PLAY_MODES = ["full", "base-only", "single-track"];
31171
- function sanitize(value) {
31172
- return value.replace(/[,\r\n]/g, "");
31173
- }
31174
- function buildFecParams(stats) {
31175
- const params = {};
31176
- const cfg = stats.config;
31177
- params["x-bc-fec-algo"] = cfg.algorithm;
31178
- params["x-bc-fec-k"] = cfg.k;
31179
- params["x-bc-fec-p"] = cfg.repairCount;
31180
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
31181
- return params;
31182
- }
31183
- function buildTransportParams(snap) {
31184
- if (!snap) return {};
31185
- const params = {};
31186
- params["x-bc-transport"] = snap.transport;
31187
- if (snap.multicastGroup != null) {
31188
- params["x-bc-multicast-group"] = snap.multicastGroup;
31189
- }
31190
- if (snap.multicastSource != null) {
31191
- params["x-bc-multicast-source"] = snap.multicastSource;
31192
- }
31193
- if (snap.amtRelay != null) {
31194
- params["x-bc-amt-relay"] = snap.amtRelay;
31195
- }
31196
- return params;
31197
- }
31198
- function formatQueryString(params) {
31199
- return Object.keys(params).sort().map((key) => {
31200
- const v = params[key];
31201
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
31202
- return `${key}=${v}`;
31692
+ var DEFAULT_INTERVAL_MS = 3e4;
31693
+ var CUSTOM_PREFIX = "com.blockcast-";
31694
+ var MAX_CUSTOM_VALUE = 64;
31695
+ var TRANSPORT_TOKEN = {
31696
+ native: "ssm",
31697
+ amt: "amt",
31698
+ moq: "moq"
31699
+ };
31700
+ function sfString(value) {
31701
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31702
+ }
31703
+ function encodeCmcdRecord(record) {
31704
+ return Object.keys(record).sort().map((key) => {
31705
+ const value = record[key];
31706
+ if (typeof value === "number") return `${key}=${value}`;
31707
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31708
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31709
+ return `${key}=${value.token}`;
31203
31710
  }).join(",");
31204
31711
  }
31712
+ var token = (value) => ({ token: value });
31713
+ function putCustom(record, name, value) {
31714
+ if (value === void 0) return;
31715
+ if (typeof value === "number") value = String(value);
31716
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31717
+ record[CUSTOM_PREFIX + name] = value;
31718
+ }
31205
31719
  var CmcdProducer = class {
31206
- #onCmcdParams;
31207
31720
  #onCmcdPost;
31721
+ #intervalMs;
31722
+ #sessionId;
31723
+ #now;
31724
+ #sn = 0;
31725
+ #lastHeartbeat = null;
31208
31726
  #transport;
31209
31727
  #lastStats;
31210
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31211
31728
  #playMode = null;
31212
31729
  #deltaFailRate = null;
31213
31730
  #switchMiscount = 0;
31214
31731
  #manifestStats = null;
31215
31732
  #fecFailClosed = null;
31733
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31734
+ #lifecycle = null;
31216
31735
  constructor(options) {
31217
- this.#onCmcdParams = options.onCmcdParams;
31218
31736
  this.#onCmcdPost = options.onCmcdPost;
31737
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31738
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31739
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31740
+ }
31741
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31742
+ this.#now = options.now ?? Date.now;
31219
31743
  }
31220
31744
  /** Update the stored transport state. Called by player wiring layer. */
31221
31745
  updateTransport(snapshot) {
31222
31746
  this.#transport = snapshot;
31223
31747
  }
31224
31748
  /**
31225
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
31226
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
31227
- * (CLAUDE.md: error on bad init).
31749
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31750
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31228
31751
  */
31229
31752
  updatePlayMode(mode) {
31230
31753
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31235,9 +31758,8 @@ var CmcdProducer = class {
31235
31758
  this.#playMode = mode;
31236
31759
  }
31237
31760
  /**
31238
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
31239
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
31240
- * Omitted on single-track streams.
31761
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31762
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31241
31763
  */
31242
31764
  updateDeltaFailRate(rate) {
31243
31765
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31247,112 +31769,139 @@ var CmcdProducer = class {
31247
31769
  }
31248
31770
  this.#deltaFailRate = rate;
31249
31771
  }
31250
- /**
31251
- * Bump the cumulative switching-set misalignment counter. Emitted as
31252
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31253
- */
31772
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31254
31773
  incrementSwitchMiscount() {
31255
31774
  this.#switchMiscount += 1;
31256
31775
  }
31257
31776
  /**
31258
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
31259
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
31260
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
31261
- * stream playing without FEC is told apart from one with no loss.
31777
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31778
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31779
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31780
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31781
+ * with no loss.
31262
31782
  */
31263
31783
  updateFecFailClosed(reason) {
31784
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31264
31785
  this.#fecFailClosed = reason;
31786
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31265
31787
  }
31266
31788
  updateManifestVerification(stats) {
31267
31789
  this.#manifestStats = stats;
31268
31790
  }
31269
31791
  /**
31270
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
31271
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
31272
- * include the drift key until the next emitSwitchDrift(). Mirrors the
31273
- * existing onTransition() pattern.
31792
+ * Count a video block's resolution from the repair lifecycle: recovered in
31793
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31794
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31795
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31274
31796
  */
31797
+ onRepairLifecycle(event) {
31798
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31799
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31800
+ if (event.via === "inband") this.#lifecycle.inband++;
31801
+ else this.#lifecycle.fetch++;
31802
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31803
+ this.#lifecycle.failedKeyframe++;
31804
+ }
31805
+ }
31806
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31275
31807
  emitSwitchDrift(driftUs) {
31276
31808
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31277
31809
  throw new Error(
31278
31810
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31279
31811
  );
31280
31812
  }
31281
- const params = {};
31282
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
31283
- Object.assign(params, buildTransportParams(this.#transport));
31284
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31285
- params["x-bc-switch-drift-us"] = driftUs;
31286
- this.#emit(params);
31813
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31814
+ putCustom(record, "sdu", Math.round(driftUs));
31815
+ this.#post([record]);
31287
31816
  }
31288
- /**
31289
- * Process a FecTrackStats snapshot. Finds the video track, builds
31290
- * x-bc-* vendor keys, and fires both delivery callbacks.
31291
- */
31817
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31292
31818
  onSnapshot(data) {
31293
31819
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31294
31820
  if (!videoTrack) return;
31295
31821
  this.#lastStats = videoTrack;
31296
- const params = {
31297
- ...buildFecParams(videoTrack),
31298
- ...buildTransportParams(this.#transport)
31299
- };
31300
- if (this.#playMode !== null) {
31301
- params["x-bc-play-mode"] = this.#playMode;
31302
- }
31303
- if (this.#fecFailClosed) {
31304
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31305
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31306
- }
31307
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31308
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31309
- }
31310
- if (this.#switchMiscount > 0) {
31311
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31312
- }
31313
- if (this.#manifestStats) {
31314
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31315
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31316
- }
31317
- this.#emit(params);
31822
+ this.#heartbeat();
31318
31823
  }
31319
31824
  /**
31320
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31321
- * the caller because only the transport integration knows when state changed.
31825
+ * Take transport state for receivers that do not own FEC stats, and send the
31826
+ * heartbeat when due.
31322
31827
  */
31323
31828
  onTransportSnapshot(data) {
31324
31829
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31325
31830
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31326
31831
  }
31327
31832
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31328
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31329
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31833
+ if (data.switchMiscount !== void 0) {
31834
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31835
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31836
+ }
31837
+ this.#switchMiscount = data.switchMiscount;
31330
31838
  }
31331
31839
  this.#transport = data.transport;
31332
- const params = buildTransportParams(data.transport);
31333
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31334
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31335
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31336
- this.#emit(params);
31840
+ this.#heartbeat();
31337
31841
  }
31338
- /**
31339
- * Signal a transport transition event. Fires an immediate out-of-band
31340
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31341
- * x-bc-ct (one-shot).
31342
- */
31842
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31343
31843
  onTransition(event) {
31344
- const params = {};
31345
- if (this.#lastStats) {
31346
- Object.assign(params, buildFecParams(this.#lastStats));
31844
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31845
+ putCustom(record, "ct", token(event));
31846
+ this.#putTransport(record);
31847
+ this.#post([record]);
31848
+ }
31849
+ #base(event) {
31850
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31851
+ }
31852
+ #heartbeat() {
31853
+ const now = this.#now();
31854
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31855
+ this.#lastHeartbeat = now;
31856
+ this.#post([this.#base("t"), this.#counters()]);
31857
+ }
31858
+ #putTransport(record) {
31859
+ const snap = this.#transport;
31860
+ if (!snap) return;
31861
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31862
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31863
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31864
+ }
31865
+ putCustom(record, "ar", snap.amtRelay);
31866
+ }
31867
+ /** The `e=ce` counter report. */
31868
+ #counters() {
31869
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31870
+ const stats = this.#lastStats;
31871
+ if (stats) {
31872
+ putCustom(record, "fa", stats.config.algorithm);
31873
+ putCustom(record, "fk", stats.config.k);
31874
+ putCustom(record, "fp", stats.config.repairCount);
31875
+ putCustom(record, "pr", stats.sourceSymbols);
31876
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31877
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31878
+ putCustom(record, "fbf", stats.blocksFailed);
31879
+ }
31880
+ if (this.#lifecycle) {
31881
+ putCustom(record, "fbr", this.#lifecycle.inband);
31882
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31883
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31884
+ }
31885
+ this.#putTransport(record);
31886
+ if (this.#playMode !== null) {
31887
+ putCustom(record, "pm", token(this.#playMode));
31888
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31889
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31890
+ }
31891
+ }
31892
+ if (this.#fecFailClosed) {
31893
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31894
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31347
31895
  }
31348
- Object.assign(params, buildTransportParams(this.#transport));
31349
- params["x-bc-ct"] = event;
31350
- this.#emit(params);
31896
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31897
+ if (this.#manifestStats) {
31898
+ putCustom(record, "mv", this.#manifestStats.verified);
31899
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31900
+ }
31901
+ return record;
31351
31902
  }
31352
- /** Fire both delivery callbacks with the given params. */
31353
- #emit(params) {
31354
- this.#onCmcdParams?.(params);
31355
- this.#onCmcdPost?.(formatQueryString(params));
31903
+ #post(records) {
31904
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31356
31905
  }
31357
31906
  };
31358
31907
 
@@ -31448,18 +31997,31 @@ function resolveRepairLayers(catalog, source) {
31448
31997
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31449
31998
  };
31450
31999
  }
32000
+ function valueAt(catalog, pointer) {
32001
+ let node = catalog;
32002
+ for (const segment of pointer.split("/").slice(1)) {
32003
+ if (typeof node !== "object" || node === null) return void 0;
32004
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
32005
+ }
32006
+ return node;
32007
+ }
32008
+ function catalogTrackDeliveryMode(catalog, source) {
32009
+ const binding = bindingOf(catalog, source);
32010
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
32011
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
32012
+ }
31451
32013
  function moqtPriority2(value) {
31452
32014
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31453
32015
  }
31454
32016
  function catalogRepairTarget(catalog, source) {
31455
32017
  const binding = bindingOf(catalog, source);
31456
32018
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31457
- const layerZero = binding.layers[0];
32019
+ const layerZero2 = binding.layers[0];
31458
32020
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31459
32021
  const tracks = catalog.tracks;
31460
32022
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31461
32023
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31462
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
32024
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31463
32025
  }
31464
32026
  function numericField(value) {
31465
32027
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31491,6 +32053,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31491
32053
  repairTrackName: base.repairTrack,
31492
32054
  repairLayers: repair.baseLayers,
31493
32055
  repairOverlays: repair.overlays,
32056
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31494
32057
  ...mediaRate
31495
32058
  };
31496
32059
  }
@@ -31541,7 +32104,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31541
32104
  var PATH_BITS = { moq: 1, multicast: 2 };
31542
32105
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31543
32106
  var MmtpFecSession = class {
32107
+ /** P_0 = B_0, the held total every repair packet is checked against. */
32108
+ get repairSymbols() {
32109
+ return this.#repairSymbols;
32110
+ }
31544
32111
  #decoder;
32112
+ #repairSymbols;
32113
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
32114
+ #message;
31545
32115
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31546
32116
  #blocks = /* @__PURE__ */ new Map();
31547
32117
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31579,6 +32149,24 @@ var MmtpFecSession = class {
31579
32149
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31580
32150
  */
31581
32151
  constructor(config) {
32152
+ if (config && "alFecFlow" in config) {
32153
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
32154
+ if (!message) {
32155
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
32156
+ }
32157
+ this.#message = message;
32158
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
32159
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
32160
+ this.sourceSymbols = message.maximumK;
32161
+ this.#repairSymbols = message.maximumP;
32162
+ this.symbolSize = message.repairSymbolSize;
32163
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
32164
+ this.#repairPacketId = message.repairFlowId;
32165
+ this.#heldLayers = layerZero(message.maximumP);
32166
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
32167
+ this.#decoder = buildDecoder(config.Decoder, this);
32168
+ return;
32169
+ }
31582
32170
  const fec = config?.fec;
31583
32171
  if (!fec || typeof fec !== "object") {
31584
32172
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31589,7 +32177,7 @@ var MmtpFecSession = class {
31589
32177
  }
31590
32178
  this.algorithm = fec.scheme;
31591
32179
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31592
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
32180
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31593
32181
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31594
32182
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31595
32183
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31609,17 +32197,30 @@ var MmtpFecSession = class {
31609
32197
  config.heldLayers
31610
32198
  );
31611
32199
  this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31612
- if (typeof config.Decoder !== "function") {
31613
- throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
31614
- }
31615
- const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm);
31616
- try {
31617
- decoder.configure_params(this.sourceSymbols, this.symbolSize);
31618
- } catch (error) {
31619
- decoder.free();
31620
- throw error;
31621
- }
31622
- this.#decoder = decoder;
32200
+ this.#decoder = buildDecoder(config.Decoder, this);
32201
+ }
32202
+ /**
32203
+ * A later version of the AL-FEC message, catalog-less sessions only. A
32204
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
32205
+ * anchored check restarts at the new total, so a block generated under the
32206
+ * previous one stays valid before the first block carrying the new total
32207
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
32208
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
32209
+ * does. An entry with another asset, repair flow, code point, K or T, or
32210
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
32211
+ * a new session. A latched fail-closed stays latched either way.
32212
+ */
32213
+ updateAlFecFlow(flow) {
32214
+ const held = this.#message;
32215
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
32216
+ const next = alFecRaptorQFlowConfig(flow);
32217
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
32218
+ return "refused";
32219
+ }
32220
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
32221
+ this.#repairSymbols = next.maximumP;
32222
+ this.#heldLayers = layerZero(next.maximumP);
32223
+ return "raised";
31623
32224
  }
31624
32225
  /** True once release() ran. */
31625
32226
  get released() {
@@ -31714,7 +32315,7 @@ var MmtpFecSession = class {
31714
32315
  }
31715
32316
  if (admission.reject) {
31716
32317
  const verdict2 = this.#heldLayers.failsClosed(
31717
- repairTrackName,
32318
+ this.#trackFor(repairTrackName),
31718
32319
  frame.ssStart,
31719
32320
  frame.rsbLength,
31720
32321
  this.#anchorHorizon,
@@ -31729,7 +32330,7 @@ var MmtpFecSession = class {
31729
32330
  this.#repairPacketId ??= frame.packetId;
31730
32331
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31731
32332
  const verdict = this.#heldLayers.admit(
31732
- repairTrackName,
32333
+ this.#trackFor(repairTrackName),
31733
32334
  frame.ssStart,
31734
32335
  frame.rsbLength,
31735
32336
  this.#anchorHorizon,
@@ -31788,6 +32389,10 @@ var MmtpFecSession = class {
31788
32389
  decoder.free();
31789
32390
  return true;
31790
32391
  }
32392
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
32393
+ #trackFor(repairTrackName) {
32394
+ return this.#message ? void 0 : repairTrackName;
32395
+ }
31791
32396
  #drop(reason) {
31792
32397
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31793
32398
  return [];
@@ -31909,6 +32514,22 @@ var MmtpFecSession = class {
31909
32514
  }
31910
32515
  }
31911
32516
  };
32517
+ function layerZero(total) {
32518
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32519
+ }
32520
+ function buildDecoder(Decoder, session) {
32521
+ if (typeof Decoder !== "function") {
32522
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32523
+ }
32524
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32525
+ try {
32526
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32527
+ } catch (error) {
32528
+ decoder.free();
32529
+ throw error;
32530
+ }
32531
+ return decoder;
32532
+ }
31912
32533
  function requirePositiveInteger(value, path) {
31913
32534
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31914
32535
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31922,6 +32543,168 @@ function requireNumber(value, path) {
31922
32543
  return value;
31923
32544
  }
31924
32545
 
32546
+ // src/repair-layer-subscriptions.ts
32547
+ var SERIAL32_HALF2 = 2147483648;
32548
+ var RepairLayerSubscriptions = class {
32549
+ #opts;
32550
+ #windowPackets;
32551
+ #margin;
32552
+ #dropAfterWindows;
32553
+ #source;
32554
+ #layerCount = 0;
32555
+ #subs = /* @__PURE__ */ new Map();
32556
+ #disposed = false;
32557
+ #lastSeq;
32558
+ #expected = 0;
32559
+ #lost = 0;
32560
+ #wantFewer = 0;
32561
+ #lossRate = null;
32562
+ #repairPackets = 0;
32563
+ #repairBytes = 0;
32564
+ #staleDropped = 0;
32565
+ constructor(opts) {
32566
+ this.#opts = opts;
32567
+ this.#source = opts.source;
32568
+ this.#windowPackets = opts.windowPackets ?? 500;
32569
+ this.#margin = opts.margin ?? 2;
32570
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32571
+ }
32572
+ get layerCount() {
32573
+ return this.#layerCount;
32574
+ }
32575
+ /**
32576
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32577
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32578
+ * or interleaved sequences read as loss.
32579
+ */
32580
+ noteSourceSequence(seq) {
32581
+ if (this.#disposed || this.#delivery() === "stream") return;
32582
+ seq >>>= 0;
32583
+ if (this.#lastSeq === void 0) {
32584
+ this.#lastSeq = seq;
32585
+ return;
32586
+ }
32587
+ const d = serial32Distance(this.#lastSeq, seq);
32588
+ if (d === 0) return;
32589
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32590
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32591
+ this.#lastSeq = seq;
32592
+ this.#expected = 0;
32593
+ this.#lost = 0;
32594
+ return;
32595
+ }
32596
+ if (back === 0) {
32597
+ this.#expected += d;
32598
+ this.#lost += d - 1;
32599
+ this.#lastSeq = seq;
32600
+ } else if (this.#lost > 0) {
32601
+ this.#lost -= 1;
32602
+ }
32603
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32604
+ }
32605
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32606
+ switchSource(next) {
32607
+ if (this.#disposed) return;
32608
+ this.#closeAll();
32609
+ this.#source = next;
32610
+ this.#lastSeq = void 0;
32611
+ this.#expected = 0;
32612
+ this.#lost = 0;
32613
+ this.#wantFewer = 0;
32614
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32615
+ this.#reconcile();
32616
+ }
32617
+ snapshot() {
32618
+ return {
32619
+ delivery: this.#delivery(),
32620
+ layerCount: this.#layerCount,
32621
+ subscribedTracks: [...this.#subs.keys()],
32622
+ repairPackets: this.#repairPackets,
32623
+ repairBytes: this.#repairBytes,
32624
+ staleDropped: this.#staleDropped,
32625
+ lossRate: this.#lossRate
32626
+ };
32627
+ }
32628
+ dispose() {
32629
+ if (this.#disposed) return;
32630
+ this.#closeAll();
32631
+ this.#disposed = true;
32632
+ }
32633
+ #delivery() {
32634
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32635
+ }
32636
+ #layers() {
32637
+ return this.#source.repairLayers ?? [];
32638
+ }
32639
+ #closeWindow() {
32640
+ const rate = this.#lost / this.#expected;
32641
+ this.#lossRate = rate;
32642
+ this.#expected = 0;
32643
+ this.#lost = 0;
32644
+ const target = this.#targetLayers(rate);
32645
+ if (target > this.#layerCount) {
32646
+ this.#wantFewer = 0;
32647
+ this.#layerCount = target;
32648
+ } else if (target < this.#layerCount) {
32649
+ this.#wantFewer += 1;
32650
+ if (this.#wantFewer >= this.#dropAfterWindows) {
32651
+ this.#wantFewer = 0;
32652
+ this.#layerCount -= 1;
32653
+ }
32654
+ } else {
32655
+ this.#wantFewer = 0;
32656
+ }
32657
+ this.#reconcile();
32658
+ }
32659
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
32660
+ #targetLayers(rate) {
32661
+ if (rate <= 0) return 0;
32662
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
32663
+ const layers = this.#layers();
32664
+ let held = 0;
32665
+ for (let n = 0; n < layers.length; n++) {
32666
+ held += layers[n].repairSymbols;
32667
+ if (held >= needed) return n + 1;
32668
+ }
32669
+ return layers.length;
32670
+ }
32671
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
32672
+ #reconcile() {
32673
+ const want = /* @__PURE__ */ new Map();
32674
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
32675
+ want.set(layer.repairTrackName, layer.priority);
32676
+ }
32677
+ const overlay = this.#source.repairOverlays?.[0];
32678
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
32679
+ for (const [name, entry] of this.#subs) {
32680
+ if (!want.has(name)) {
32681
+ entry.sub.close();
32682
+ this.#subs.delete(name);
32683
+ }
32684
+ }
32685
+ for (const [name, priority] of want) {
32686
+ if (this.#subs.has(name)) continue;
32687
+ const entry = {};
32688
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
32689
+ this.#subs.set(name, entry);
32690
+ }
32691
+ }
32692
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
32693
+ #onPacket(name, entry, packet) {
32694
+ if (this.#disposed || this.#subs.get(name) !== entry) {
32695
+ this.#staleDropped += 1;
32696
+ return;
32697
+ }
32698
+ this.#repairPackets += 1;
32699
+ this.#repairBytes += packet.byteLength;
32700
+ this.#opts.feed(packet, name);
32701
+ }
32702
+ #closeAll() {
32703
+ for (const entry of this.#subs.values()) entry.sub.close();
32704
+ this.#subs.clear();
32705
+ }
32706
+ };
32707
+
31925
32708
  // src/repair-fetch.ts
31926
32709
  var MOQT_PRIORITY_MAX = 255;
31927
32710
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
@@ -32567,6 +33350,7 @@ export {
32567
33350
  REPAIR_LIFECYCLE_LOG_PREFIX,
32568
33351
  REPAIR_PRIORITY_MAX,
32569
33352
  REPAIR_PRIORITY_MIN,
33353
+ RepairLayerSubscriptions,
32570
33354
  SOURCE_MEDIA_PRIORITY_MAX,
32571
33355
  SampleCombineError,
32572
33356
  SampleCombiner,
@@ -32591,6 +33375,7 @@ export {
32591
33375
  catalogGroupDurationMs,
32592
33376
  catalogKeyframeCadenceMs,
32593
33377
  catalogRepairTarget,
33378
+ catalogTrackDeliveryMode,
32594
33379
  catalogTrackFecUnavailable,
32595
33380
  classifyH264Keyframe,
32596
33381
  classifyHevcKeyframe,
@@ -32611,6 +33396,7 @@ export {
32611
33396
  detectH264Keyframe,
32612
33397
  detectHevcKeyframe,
32613
33398
  driftCheck,
33399
+ encodeCmcdRecord,
32614
33400
  encodeDataSegment,
32615
33401
  encodeFecSourceInfo,
32616
33402
  encodeLocTimestamp,