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

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 (89) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +630 -191
  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 +3 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +26 -4
  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.js.map +1 -1
  31. package/dist/iso-bmff-timing.d.ts +10 -1
  32. package/dist/iso-bmff-timing.d.ts.map +1 -1
  33. package/dist/iso-bmff-timing.js +36 -2
  34. package/dist/iso-bmff-timing.js.map +2 -2
  35. package/dist/media-router.js.map +1 -1
  36. package/dist/mfu-reassembler.js +12 -19
  37. package/dist/mfu-reassembler.js.map +2 -2
  38. package/dist/mmtp-container-parser.d.ts +35 -2
  39. package/dist/mmtp-container-parser.d.ts.map +1 -1
  40. package/dist/mmtp-container-parser.js +97 -28
  41. package/dist/mmtp-container-parser.js.map +2 -2
  42. package/dist/mmtp-container-parser.no-modules.js +222 -45
  43. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  44. package/dist/mmtp-decode-pipeline.js +97 -28
  45. package/dist/mmtp-decode-pipeline.js.map +2 -2
  46. package/dist/mmtp-fec-session.d.ts +41 -3
  47. package/dist/mmtp-fec-session.d.ts.map +1 -1
  48. package/dist/mmtp-fec-session.js +118 -13
  49. package/dist/mmtp-fec-session.js.map +3 -3
  50. package/dist/mmtp-router.d.ts +6 -0
  51. package/dist/mmtp-router.d.ts.map +1 -1
  52. package/dist/mmtp-router.js +150 -44
  53. package/dist/mmtp-router.js.map +2 -2
  54. package/dist/multicast-auth-catalog.js.map +1 -1
  55. package/dist/multicast-input.js.map +1 -1
  56. package/dist/multicast-manager.js +176 -48
  57. package/dist/multicast-manager.js.map +2 -2
  58. package/dist/multicast-transport.js +26 -16
  59. package/dist/multicast-transport.js.map +2 -2
  60. package/dist/multicast-unwrap.js +12 -19
  61. package/dist/multicast-unwrap.js.map +2 -2
  62. package/dist/no-modules.js +215 -42
  63. package/dist/no-modules.js.map +3 -3
  64. package/dist/repair-fetch.js.map +1 -1
  65. package/dist/repair-layer-subscriptions.d.ts +82 -0
  66. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  67. package/dist/repair-layer-subscriptions.js +192 -0
  68. package/dist/repair-layer-subscriptions.js.map +7 -0
  69. package/dist/signaling.js.map +1 -1
  70. package/dist/timestamp-codec.js +12 -19
  71. package/dist/timestamp-codec.js.map +2 -2
  72. package/dist/track-session.d.ts +1 -1
  73. package/dist/track-session.js.map +1 -1
  74. package/dist/wasm-mfu-reassembler.js.map +1 -1
  75. package/dist/wasm-mmtp-container-parser.js +97 -28
  76. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  77. package/package.json +8 -5
  78. package/schemas/catalog.schema.json +4 -0
  79. package/src/catalog-fec-params.ts +36 -1
  80. package/src/cmcd-producer.ts +189 -179
  81. package/src/fec-manager.ts +26 -4
  82. package/src/fec-worker-setup.ts +2 -2
  83. package/src/index.ts +18 -2
  84. package/src/iso-bmff-timing.ts +40 -1
  85. package/src/mmtp-container-parser.ts +92 -7
  86. package/src/mmtp-fec-session.ts +115 -15
  87. package/src/mmtp-router.ts +46 -0
  88. package/src/repair-layer-subscriptions.ts +261 -0
  89. 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;
@@ -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;
8731
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);
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) => {
@@ -14013,6 +14062,10 @@ var FECManager = class _FECManager {
14013
14062
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14014
14063
  // One classifier per physical stream, built from that stream's geometry.
14015
14064
  this.#classifiers = /* @__PURE__ */ new Map();
14065
+ // A stream without catalog timing has no classifier, so the last RAP it
14066
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14067
+ // a second RAP is released inside one MPU (BLO-52085).
14068
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14016
14069
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14017
14070
  // rungs can share a packetId with a different framerate or GOP, so the
14018
14071
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14212,6 +14265,7 @@ var FECManager = class _FECManager {
14212
14265
  #orderedFrontiers;
14213
14266
  #orderedPendingBlocks;
14214
14267
  #classifiers;
14268
+ #noSpanRaps;
14215
14269
  #streamGeometries;
14216
14270
  #failedBlocks;
14217
14271
  #resolvedBlockClasses;
@@ -14873,6 +14927,7 @@ var FECManager = class _FECManager {
14873
14927
  };
14874
14928
  const discontinuity = () => {
14875
14929
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14930
+ this.#noSpanRaps.delete(streamKey);
14876
14931
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14877
14932
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14878
14933
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15285,6 +15340,9 @@ var FECManager = class _FECManager {
15285
15340
  }
15286
15341
  if (policyChanged) {
15287
15342
  this.#bumpEpoch(packetId);
15343
+ for (const key of [...this.#noSpanRaps.keys()]) {
15344
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15345
+ }
15288
15346
  for (const [key, classifier] of [...this.#classifiers]) {
15289
15347
  if (!this.#belongsToPacket(key, packetId)) continue;
15290
15348
  const stream = this.#parseStreamKey(key);
@@ -15520,6 +15578,7 @@ var FECManager = class _FECManager {
15520
15578
  resetMap(this.#isoLocators);
15521
15579
  resetMap(this.#sourceAnchors);
15522
15580
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15581
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15523
15582
  this.#bumpEpoch(packetId);
15524
15583
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15525
15584
  for (const key of keys.keys()) {
@@ -15626,6 +15685,7 @@ var FECManager = class _FECManager {
15626
15685
  dropMap(this.#isoLocators);
15627
15686
  dropMap(this.#sourceAnchors);
15628
15687
  dropMap(this.#classifiers);
15688
+ dropMap(this.#noSpanRaps);
15629
15689
  dropMap(this.#streamGeometries);
15630
15690
  dropSet(this.#pendingWorkerCleanups);
15631
15691
  dropSet(this.#completedBlocks);
@@ -16256,11 +16316,14 @@ var FECManager = class _FECManager {
16256
16316
  /**
16257
16317
  * Whether an MPU boundary still bounds this stream's RAPs (see
16258
16318
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16259
- * it does not. A stream with no classifier yet has shown nothing against it.
16319
+ * it does not. A stream with no classifier yet has shown nothing against it,
16320
+ * and neither has a stream without catalog timing until it releases two
16321
+ * RAPs inside one MPU.
16260
16322
  */
16261
16323
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16262
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16263
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16324
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16325
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16326
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16264
16327
  }
16265
16328
  /** A block's class now, or frozen at its resolution. */
16266
16329
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16286,7 +16349,14 @@ var FECManager = class _FECManager {
16286
16349
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16287
16350
  */
16288
16351
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16289
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16352
+ if (!this.#trackConfigs.has(packetId)) return;
16353
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16354
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16355
+ const last = this.#noSpanRaps.get(streamKey);
16356
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16357
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16358
+ return;
16359
+ }
16290
16360
  this.#applyClassifierChanges(
16291
16361
  packetId,
16292
16362
  catalogTrackName,
@@ -17301,6 +17371,7 @@ var FECManager = class _FECManager {
17301
17371
  this.#blockAnchorsMs.clear();
17302
17372
  this.#blockCatalogTrackNames.clear();
17303
17373
  this.#classifiers.clear();
17374
+ this.#noSpanRaps.clear();
17304
17375
  this.#streamGeometries.clear();
17305
17376
  this.#failedBlocks.clear();
17306
17377
  this.#resolvedBlockClasses.clear();
@@ -17831,9 +17902,21 @@ var IsoBmffTimingResolver = class {
17831
17902
  }
17832
17903
  #track = null;
17833
17904
  #fragments = /* @__PURE__ */ new Map();
17905
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
17906
+ #firstSamples = /* @__PURE__ */ new Map();
17907
+ /**
17908
+ * First sample and sample count of the newest fragments whose every slot was
17909
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
17910
+ * matches the combiner, since it retires an abandoned sample as well.
17911
+ */
17912
+ #delivered = /* @__PURE__ */ new Map();
17834
17913
  #pendingSamples = 0;
17835
17914
  #evictedSamples = 0;
17836
17915
  #lastMovieFragmentRejection = null;
17916
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
17917
+ get initialized() {
17918
+ return this.#track !== null;
17919
+ }
17837
17920
  get pendingSampleCount() {
17838
17921
  return this.#pendingSamples;
17839
17922
  }
@@ -17857,7 +17940,14 @@ var IsoBmffTimingResolver = class {
17857
17940
  this.#track = timing;
17858
17941
  return true;
17859
17942
  }
17860
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
17943
+ /**
17944
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
17945
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
17946
+ * fragment whose samples are all done is a duplicate: admitting it again
17947
+ * would charge stale timing and could displace the live MPU's. A source
17948
+ * copy is still admitted again (a rendition switch re-arms timing that way).
17949
+ */
17950
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17861
17951
  this.#lastMovieFragmentRejection = null;
17862
17952
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17863
17953
  this.#lastMovieFragmentRejection = "invalid";
@@ -17883,6 +17973,8 @@ var IsoBmffTimingResolver = class {
17883
17973
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17884
17974
  );
17885
17975
  }
17976
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
17977
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17886
17978
  let freed = 0;
17887
17979
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17888
17980
  freed += this.#evictOldestFragment();
@@ -17896,6 +17988,7 @@ var IsoBmffTimingResolver = class {
17896
17988
  return false;
17897
17989
  }
17898
17990
  this.#fragments.set(key, [...parsed.samples]);
17991
+ this.#firstSamples.set(key, parsed.samples[0]);
17899
17992
  this.#pendingSamples += parsed.samples.length;
17900
17993
  return true;
17901
17994
  }
@@ -17953,6 +18046,7 @@ var IsoBmffTimingResolver = class {
17953
18046
  let live = 0;
17954
18047
  for (const sample of samples) if (sample !== null) live++;
17955
18048
  this.#fragments.delete(oldest);
18049
+ this.#firstSamples.delete(oldest);
17956
18050
  this.#pendingSamples -= live;
17957
18051
  this.#evictedSamples += live;
17958
18052
  return live;
@@ -17968,12 +18062,23 @@ var IsoBmffTimingResolver = class {
17968
18062
  if (!samples || !samples[index]) return false;
17969
18063
  samples[index] = null;
17970
18064
  this.#pendingSamples--;
17971
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18065
+ if (samples.every((value) => value === null)) {
18066
+ this.#fragments.delete(key);
18067
+ const first = this.#firstSamples.get(key);
18068
+ this.#firstSamples.delete(key);
18069
+ this.#delivered.delete(key);
18070
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18071
+ if (this.#delivered.size > this.maxPendingSamples) {
18072
+ this.#delivered.delete(this.#delivered.keys().next().value);
18073
+ }
18074
+ }
17972
18075
  return true;
17973
18076
  }
17974
18077
  reset() {
17975
18078
  this.#track = null;
17976
18079
  this.#fragments.clear();
18080
+ this.#firstSamples.clear();
18081
+ this.#delivered.clear();
17977
18082
  this.#pendingSamples = 0;
17978
18083
  }
17979
18084
  };
@@ -18455,6 +18560,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18455
18560
  this.#reclaimedTimingSamples = 0;
18456
18561
  this.#evictedTimingSamples = 0;
18457
18562
  this.#rejectedMovieFragments = 0;
18563
+ this.#recoveredHeadsAwaitingTiming = {
18564
+ held: 0,
18565
+ expired: 0,
18566
+ discarded: 0,
18567
+ superseded: 0
18568
+ };
18458
18569
  this.#sequencers = /* @__PURE__ */ new Map();
18459
18570
  this.#fecGate = null;
18460
18571
  this.#fecSequencer = null;
@@ -18563,6 +18674,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18563
18674
  #reclaimedTimingSamples;
18564
18675
  #evictedTimingSamples;
18565
18676
  #rejectedMovieFragments;
18677
+ #recoveredHeadsAwaitingTiming;
18566
18678
  #sequencers;
18567
18679
  #fecGate;
18568
18680
  #fecSequencer;
@@ -18683,6 +18795,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18683
18795
  rejectedMovieFragments: this.#rejectedMovieFragments
18684
18796
  };
18685
18797
  }
18798
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18799
+ get recoveredHeadsAwaitingTiming() {
18800
+ let pending = 0;
18801
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18802
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18803
+ }
18804
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
18805
+ }
18686
18806
  get reorderStats() {
18687
18807
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18688
18808
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18811,10 +18931,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18811
18931
  get reassemblerDataUnitsReturned() {
18812
18932
  return this.#reassemblerDataUnitsReturned;
18813
18933
  }
18814
- #countOutcome(token) {
18815
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18816
- this.#feedOutcomes.set(token, count);
18817
- this.#lastOutcome = token;
18934
+ #countOutcome(token2) {
18935
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
18936
+ this.#feedOutcomes.set(token2, count);
18937
+ this.#lastOutcome = token2;
18818
18938
  this.#lastOutcomeCount = count;
18819
18939
  }
18820
18940
  #countDeferredOutcome(reason) {
@@ -18856,7 +18976,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18856
18976
  const now = Date.now();
18857
18977
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18858
18978
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18859
- awaitingTiming.shift();
18979
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18860
18980
  }
18861
18981
  }
18862
18982
  }
@@ -18974,11 +19094,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
18974
19094
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
18975
19095
  return this.#drop("fragment-unexpected-fragmentation");
18976
19096
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
18977
- if (!resolver2) return this.#drop("fragment-no-resolver");
19097
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
18978
19098
  const pendingBefore = resolver2.pendingSampleCount;
18979
19099
  const evictedBefore = resolver2.evictedSampleCount;
18980
19100
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
18981
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19101
+ const admitted = resolver2.addMovieFragment(
19102
+ mpu.mpuSequenceNumber,
19103
+ data,
19104
+ remaining,
19105
+ options?.recovered === true
19106
+ );
18982
19107
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
18983
19108
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
18984
19109
  if (!admitted) {
@@ -18998,7 +19123,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18998
19123
  };
18999
19124
  }
19000
19125
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19001
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19126
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19002
19127
  if (!resolver) return this.#drop("mfu-no-resolver");
19003
19128
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19004
19129
  return this.#drop("mfu-empty-payload");
@@ -19019,6 +19144,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19019
19144
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19020
19145
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19021
19146
  sampleNumber: mfuDuHeader.sampleNumber,
19147
+ sequenceNumber: info.sequenceNumber,
19022
19148
  arrivedAt: Date.now()
19023
19149
  });
19024
19150
  return this.#drop("mfu-sample-timing-missing");
@@ -19342,6 +19468,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19342
19468
  }
19343
19469
  this.#emitFrame(data, meta);
19344
19470
  }
19471
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19472
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19473
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19474
+ }
19345
19475
  /**
19346
19476
  * Claim and return the authored timing for a recovered sample. Recovery does
19347
19477
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19403,6 +19533,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19403
19533
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19404
19534
  this.#sequencers.clear();
19405
19535
  this.#fecSequencer?.discard();
19536
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19537
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19538
+ }
19406
19539
  this.#timingStreams.clear();
19407
19540
  this.#pendingTimingSamples = 0;
19408
19541
  this.#reliableMoqStartupGate.clear();
@@ -19421,10 +19554,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19421
19554
  return resolver;
19422
19555
  }
19423
19556
  #awaitTiming(streamKey, head) {
19424
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19425
- if (!awaitingTiming) return;
19557
+ const state = this.#timingStreams.get(streamKey);
19558
+ if (!state) return;
19559
+ if (!head.options?.recovered) {
19560
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19561
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19562
+ state.awaitingTiming = kept;
19563
+ }
19564
+ const awaitingTiming = state.awaitingTiming;
19426
19565
  awaitingTiming.push(head);
19427
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19566
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19567
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19568
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19569
+ }
19570
+ }
19571
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19572
+ #leaveAwaitingTiming(head, outcome) {
19573
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19428
19574
  }
19429
19575
  /**
19430
19576
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19452,6 +19598,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19452
19598
  const state = this.#timingStreams.get(streamKey);
19453
19599
  if (!state) return;
19454
19600
  this.#timingStreams.delete(streamKey);
19601
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19455
19602
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19456
19603
  if (this.#pendingTimingSamples < 0) {
19457
19604
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -23457,6 +23604,10 @@ var MmtpRouter = class {
23457
23604
  get keyframeOverlayStats() {
23458
23605
  return { ...this.#overlayStats };
23459
23606
  }
23607
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23608
+ get recoveredHeadsAwaitingTiming() {
23609
+ return this.#parser.recoveredHeadsAwaitingTiming;
23610
+ }
23460
23611
  #isOverlayRepair(repairPacketId, repairTrackName) {
23461
23612
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23462
23613
  }
@@ -25866,6 +26017,29 @@ var MmtpRouter = class {
25866
26017
  }
25867
26018
  );
25868
26019
  }
26020
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26021
+ ownerPacketId,
26022
+ sym.fragment.mpuSequenceNumber,
26023
+ sym.fragment.movieFragmentSequenceNumber,
26024
+ sym.fragment.sampleNumber,
26025
+ catalogTrackName
26026
+ );
26027
+ for (const sym of result.symbols) {
26028
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26029
+ this.#feedViaParser(
26030
+ sym.rawSymbol,
26031
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26032
+ void 0,
26033
+ {
26034
+ skipFecPayloadId: true,
26035
+ recovered: true,
26036
+ deliveryProvenance,
26037
+ ...fecSbnOf(sym.index),
26038
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26039
+ }
26040
+ );
26041
+ }
26042
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25869
26043
  const gated = this.#isFecTrack(ownerPacketId);
25870
26044
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25871
26045
  if (assembler) {
@@ -29627,7 +29801,7 @@ async function setupFecWorkerForTrack(opts) {
29627
29801
  blockProfile,
29628
29802
  k: opts.fecK,
29629
29803
  // 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.
29804
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29631
29805
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29632
29806
  symbolSize: opts.fecSymbolSize,
29633
29807
  interleaveDepth: interleaveDepthBlocks,
@@ -30169,24 +30343,24 @@ var MediaRouter = class {
30169
30343
  if (!this.#multicastVideoKeyed) return;
30170
30344
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30171
30345
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30172
- const token = ++this.#sourceSwitchToken;
30346
+ const token2 = ++this.#sourceSwitchToken;
30173
30347
  const maybeReady = this.#onSourceSwitch?.("multicast");
30174
30348
  if (!maybeReady || typeof maybeReady.then !== "function") {
30175
- this.#commitMulticastActivation(token);
30349
+ this.#commitMulticastActivation(token2);
30176
30350
  return;
30177
30351
  }
30178
30352
  const ready = Promise.resolve(maybeReady);
30179
- this.#pendingMulticastToken = token;
30353
+ this.#pendingMulticastToken = token2;
30180
30354
  this.#sourceSwitchReady = ready;
30181
30355
  void ready.then(
30182
- () => this.#commitMulticastActivation(token),
30183
- (err) => this.#rejectMulticastActivation(token, err)
30356
+ () => this.#commitMulticastActivation(token2),
30357
+ (err) => this.#rejectMulticastActivation(token2, err)
30184
30358
  ).finally(() => {
30185
30359
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30186
30360
  });
30187
30361
  }
30188
- #commitMulticastActivation(token) {
30189
- if (token !== this.#sourceSwitchToken) return;
30362
+ #commitMulticastActivation(token2) {
30363
+ if (token2 !== this.#sourceSwitchToken) return;
30190
30364
  this.#active = "multicast";
30191
30365
  this.#suppressed = true;
30192
30366
  this.#pendingMulticastToken = null;
@@ -30197,9 +30371,9 @@ var MediaRouter = class {
30197
30371
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30198
30372
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30199
30373
  }
30200
- #rejectMulticastActivation(token, err) {
30201
- if (token !== this.#sourceSwitchToken) return;
30202
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30374
+ #rejectMulticastActivation(token2, err) {
30375
+ if (token2 !== this.#sourceSwitchToken) return;
30376
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30203
30377
  this.#pendingMulticastVideo = null;
30204
30378
  this.#pendingMulticastAudio = null;
30205
30379
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31168,63 +31342,65 @@ var FecStarvationDetector = class {
31168
31342
 
31169
31343
  // src/cmcd-producer.ts
31170
31344
  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}`;
31345
+ var DEFAULT_INTERVAL_MS = 3e4;
31346
+ var CUSTOM_PREFIX = "com.blockcast-";
31347
+ var MAX_CUSTOM_VALUE = 64;
31348
+ var TRANSPORT_TOKEN = {
31349
+ native: "ssm",
31350
+ amt: "amt",
31351
+ moq: "moq"
31352
+ };
31353
+ function sfString(value) {
31354
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31355
+ }
31356
+ function encodeCmcdRecord(record) {
31357
+ return Object.keys(record).sort().map((key) => {
31358
+ const value = record[key];
31359
+ if (typeof value === "number") return `${key}=${value}`;
31360
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31361
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31362
+ return `${key}=${value.token}`;
31203
31363
  }).join(",");
31204
31364
  }
31365
+ var token = (value) => ({ token: value });
31366
+ function putCustom(record, name, value) {
31367
+ if (value === void 0) return;
31368
+ if (typeof value === "number") value = String(value);
31369
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31370
+ record[CUSTOM_PREFIX + name] = value;
31371
+ }
31205
31372
  var CmcdProducer = class {
31206
- #onCmcdParams;
31207
31373
  #onCmcdPost;
31374
+ #intervalMs;
31375
+ #sessionId;
31376
+ #now;
31377
+ #sn = 0;
31378
+ #lastHeartbeat = null;
31208
31379
  #transport;
31209
31380
  #lastStats;
31210
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31211
31381
  #playMode = null;
31212
31382
  #deltaFailRate = null;
31213
31383
  #switchMiscount = 0;
31214
31384
  #manifestStats = null;
31215
31385
  #fecFailClosed = null;
31386
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31387
+ #lifecycle = null;
31216
31388
  constructor(options) {
31217
- this.#onCmcdParams = options.onCmcdParams;
31218
31389
  this.#onCmcdPost = options.onCmcdPost;
31390
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31391
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31392
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31393
+ }
31394
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31395
+ this.#now = options.now ?? Date.now;
31219
31396
  }
31220
31397
  /** Update the stored transport state. Called by player wiring layer. */
31221
31398
  updateTransport(snapshot) {
31222
31399
  this.#transport = snapshot;
31223
31400
  }
31224
31401
  /**
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).
31402
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31403
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31228
31404
  */
31229
31405
  updatePlayMode(mode) {
31230
31406
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31235,9 +31411,8 @@ var CmcdProducer = class {
31235
31411
  this.#playMode = mode;
31236
31412
  }
31237
31413
  /**
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.
31414
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31415
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31241
31416
  */
31242
31417
  updateDeltaFailRate(rate) {
31243
31418
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31247,112 +31422,139 @@ var CmcdProducer = class {
31247
31422
  }
31248
31423
  this.#deltaFailRate = rate;
31249
31424
  }
31250
- /**
31251
- * Bump the cumulative switching-set misalignment counter. Emitted as
31252
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31253
- */
31425
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31254
31426
  incrementSwitchMiscount() {
31255
31427
  this.#switchMiscount += 1;
31256
31428
  }
31257
31429
  /**
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.
31430
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31431
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31432
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31433
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31434
+ * with no loss.
31262
31435
  */
31263
31436
  updateFecFailClosed(reason) {
31437
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31264
31438
  this.#fecFailClosed = reason;
31439
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31265
31440
  }
31266
31441
  updateManifestVerification(stats) {
31267
31442
  this.#manifestStats = stats;
31268
31443
  }
31269
31444
  /**
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.
31445
+ * Count a video block's resolution from the repair lifecycle: recovered in
31446
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31447
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31448
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31274
31449
  */
31450
+ onRepairLifecycle(event) {
31451
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31452
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31453
+ if (event.via === "inband") this.#lifecycle.inband++;
31454
+ else this.#lifecycle.fetch++;
31455
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31456
+ this.#lifecycle.failedKeyframe++;
31457
+ }
31458
+ }
31459
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31275
31460
  emitSwitchDrift(driftUs) {
31276
31461
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31277
31462
  throw new Error(
31278
31463
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31279
31464
  );
31280
31465
  }
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);
31466
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31467
+ putCustom(record, "sdu", Math.round(driftUs));
31468
+ this.#post([record]);
31287
31469
  }
31288
- /**
31289
- * Process a FecTrackStats snapshot. Finds the video track, builds
31290
- * x-bc-* vendor keys, and fires both delivery callbacks.
31291
- */
31470
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31292
31471
  onSnapshot(data) {
31293
31472
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31294
31473
  if (!videoTrack) return;
31295
31474
  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);
31475
+ this.#heartbeat();
31318
31476
  }
31319
31477
  /**
31320
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31321
- * the caller because only the transport integration knows when state changed.
31478
+ * Take transport state for receivers that do not own FEC stats, and send the
31479
+ * heartbeat when due.
31322
31480
  */
31323
31481
  onTransportSnapshot(data) {
31324
31482
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31325
31483
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31326
31484
  }
31327
31485
  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");
31486
+ if (data.switchMiscount !== void 0) {
31487
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31488
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31489
+ }
31490
+ this.#switchMiscount = data.switchMiscount;
31330
31491
  }
31331
31492
  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);
31493
+ this.#heartbeat();
31337
31494
  }
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
- */
31495
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31343
31496
  onTransition(event) {
31344
- const params = {};
31345
- if (this.#lastStats) {
31346
- Object.assign(params, buildFecParams(this.#lastStats));
31497
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31498
+ putCustom(record, "ct", token(event));
31499
+ this.#putTransport(record);
31500
+ this.#post([record]);
31501
+ }
31502
+ #base(event) {
31503
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31504
+ }
31505
+ #heartbeat() {
31506
+ const now = this.#now();
31507
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31508
+ this.#lastHeartbeat = now;
31509
+ this.#post([this.#base("t"), this.#counters()]);
31510
+ }
31511
+ #putTransport(record) {
31512
+ const snap = this.#transport;
31513
+ if (!snap) return;
31514
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31515
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31516
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31517
+ }
31518
+ putCustom(record, "ar", snap.amtRelay);
31519
+ }
31520
+ /** The `e=ce` counter report. */
31521
+ #counters() {
31522
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31523
+ const stats = this.#lastStats;
31524
+ if (stats) {
31525
+ putCustom(record, "fa", stats.config.algorithm);
31526
+ putCustom(record, "fk", stats.config.k);
31527
+ putCustom(record, "fp", stats.config.repairCount);
31528
+ putCustom(record, "pr", stats.sourceSymbols);
31529
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31530
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31531
+ putCustom(record, "fbf", stats.blocksFailed);
31532
+ }
31533
+ if (this.#lifecycle) {
31534
+ putCustom(record, "fbr", this.#lifecycle.inband);
31535
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31536
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31537
+ }
31538
+ this.#putTransport(record);
31539
+ if (this.#playMode !== null) {
31540
+ putCustom(record, "pm", token(this.#playMode));
31541
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31542
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31543
+ }
31347
31544
  }
31348
- Object.assign(params, buildTransportParams(this.#transport));
31349
- params["x-bc-ct"] = event;
31350
- this.#emit(params);
31545
+ if (this.#fecFailClosed) {
31546
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31547
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31548
+ }
31549
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31550
+ if (this.#manifestStats) {
31551
+ putCustom(record, "mv", this.#manifestStats.verified);
31552
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31553
+ }
31554
+ return record;
31351
31555
  }
31352
- /** Fire both delivery callbacks with the given params. */
31353
- #emit(params) {
31354
- this.#onCmcdParams?.(params);
31355
- this.#onCmcdPost?.(formatQueryString(params));
31556
+ #post(records) {
31557
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31356
31558
  }
31357
31559
  };
31358
31560
 
@@ -31448,18 +31650,31 @@ function resolveRepairLayers(catalog, source) {
31448
31650
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31449
31651
  };
31450
31652
  }
31653
+ function valueAt(catalog, pointer) {
31654
+ let node = catalog;
31655
+ for (const segment of pointer.split("/").slice(1)) {
31656
+ if (typeof node !== "object" || node === null) return void 0;
31657
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
31658
+ }
31659
+ return node;
31660
+ }
31661
+ function catalogTrackDeliveryMode(catalog, source) {
31662
+ const binding = bindingOf(catalog, source);
31663
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
31664
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
31665
+ }
31451
31666
  function moqtPriority2(value) {
31452
31667
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31453
31668
  }
31454
31669
  function catalogRepairTarget(catalog, source) {
31455
31670
  const binding = bindingOf(catalog, source);
31456
31671
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31457
- const layerZero = binding.layers[0];
31672
+ const layerZero2 = binding.layers[0];
31458
31673
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31459
31674
  const tracks = catalog.tracks;
31460
31675
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31461
31676
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31462
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
31677
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31463
31678
  }
31464
31679
  function numericField(value) {
31465
31680
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31491,6 +31706,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31491
31706
  repairTrackName: base.repairTrack,
31492
31707
  repairLayers: repair.baseLayers,
31493
31708
  repairOverlays: repair.overlays,
31709
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31494
31710
  ...mediaRate
31495
31711
  };
31496
31712
  }
@@ -31541,7 +31757,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31541
31757
  var PATH_BITS = { moq: 1, multicast: 2 };
31542
31758
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31543
31759
  var MmtpFecSession = class {
31760
+ /** P_0 = B_0, the held total every repair packet is checked against. */
31761
+ get repairSymbols() {
31762
+ return this.#repairSymbols;
31763
+ }
31544
31764
  #decoder;
31765
+ #repairSymbols;
31766
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
31767
+ #message;
31545
31768
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31546
31769
  #blocks = /* @__PURE__ */ new Map();
31547
31770
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31579,6 +31802,24 @@ var MmtpFecSession = class {
31579
31802
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31580
31803
  */
31581
31804
  constructor(config) {
31805
+ if (config && "alFecFlow" in config) {
31806
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
31807
+ if (!message) {
31808
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
31809
+ }
31810
+ this.#message = message;
31811
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
31812
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
31813
+ this.sourceSymbols = message.maximumK;
31814
+ this.#repairSymbols = message.maximumP;
31815
+ this.symbolSize = message.repairSymbolSize;
31816
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
31817
+ this.#repairPacketId = message.repairFlowId;
31818
+ this.#heldLayers = layerZero(message.maximumP);
31819
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31820
+ this.#decoder = buildDecoder(config.Decoder, this);
31821
+ return;
31822
+ }
31582
31823
  const fec = config?.fec;
31583
31824
  if (!fec || typeof fec !== "object") {
31584
31825
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31589,7 +31830,7 @@ var MmtpFecSession = class {
31589
31830
  }
31590
31831
  this.algorithm = fec.scheme;
31591
31832
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31592
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31833
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31593
31834
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31594
31835
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31595
31836
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31609,17 +31850,30 @@ var MmtpFecSession = class {
31609
31850
  config.heldLayers
31610
31851
  );
31611
31852
  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;
31853
+ this.#decoder = buildDecoder(config.Decoder, this);
31854
+ }
31855
+ /**
31856
+ * A later version of the AL-FEC message, catalog-less sessions only. A
31857
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
31858
+ * anchored check restarts at the new total, so a block generated under the
31859
+ * previous one stays valid before the first block carrying the new total
31860
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
31861
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
31862
+ * does. An entry with another asset, repair flow, code point, K or T, or
31863
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
31864
+ * a new session. A latched fail-closed stays latched either way.
31865
+ */
31866
+ updateAlFecFlow(flow) {
31867
+ const held = this.#message;
31868
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
31869
+ const next = alFecRaptorQFlowConfig(flow);
31870
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
31871
+ return "refused";
31872
+ }
31873
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
31874
+ this.#repairSymbols = next.maximumP;
31875
+ this.#heldLayers = layerZero(next.maximumP);
31876
+ return "raised";
31623
31877
  }
31624
31878
  /** True once release() ran. */
31625
31879
  get released() {
@@ -31714,7 +31968,7 @@ var MmtpFecSession = class {
31714
31968
  }
31715
31969
  if (admission.reject) {
31716
31970
  const verdict2 = this.#heldLayers.failsClosed(
31717
- repairTrackName,
31971
+ this.#trackFor(repairTrackName),
31718
31972
  frame.ssStart,
31719
31973
  frame.rsbLength,
31720
31974
  this.#anchorHorizon,
@@ -31729,7 +31983,7 @@ var MmtpFecSession = class {
31729
31983
  this.#repairPacketId ??= frame.packetId;
31730
31984
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31731
31985
  const verdict = this.#heldLayers.admit(
31732
- repairTrackName,
31986
+ this.#trackFor(repairTrackName),
31733
31987
  frame.ssStart,
31734
31988
  frame.rsbLength,
31735
31989
  this.#anchorHorizon,
@@ -31788,6 +32042,10 @@ var MmtpFecSession = class {
31788
32042
  decoder.free();
31789
32043
  return true;
31790
32044
  }
32045
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
32046
+ #trackFor(repairTrackName) {
32047
+ return this.#message ? void 0 : repairTrackName;
32048
+ }
31791
32049
  #drop(reason) {
31792
32050
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31793
32051
  return [];
@@ -31909,6 +32167,22 @@ var MmtpFecSession = class {
31909
32167
  }
31910
32168
  }
31911
32169
  };
32170
+ function layerZero(total) {
32171
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32172
+ }
32173
+ function buildDecoder(Decoder, session) {
32174
+ if (typeof Decoder !== "function") {
32175
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32176
+ }
32177
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32178
+ try {
32179
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32180
+ } catch (error) {
32181
+ decoder.free();
32182
+ throw error;
32183
+ }
32184
+ return decoder;
32185
+ }
31912
32186
  function requirePositiveInteger(value, path) {
31913
32187
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31914
32188
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31922,6 +32196,168 @@ function requireNumber(value, path) {
31922
32196
  return value;
31923
32197
  }
31924
32198
 
32199
+ // src/repair-layer-subscriptions.ts
32200
+ var SERIAL32_HALF2 = 2147483648;
32201
+ var RepairLayerSubscriptions = class {
32202
+ #opts;
32203
+ #windowPackets;
32204
+ #margin;
32205
+ #dropAfterWindows;
32206
+ #source;
32207
+ #layerCount = 0;
32208
+ #subs = /* @__PURE__ */ new Map();
32209
+ #disposed = false;
32210
+ #lastSeq;
32211
+ #expected = 0;
32212
+ #lost = 0;
32213
+ #wantFewer = 0;
32214
+ #lossRate = null;
32215
+ #repairPackets = 0;
32216
+ #repairBytes = 0;
32217
+ #staleDropped = 0;
32218
+ constructor(opts) {
32219
+ this.#opts = opts;
32220
+ this.#source = opts.source;
32221
+ this.#windowPackets = opts.windowPackets ?? 500;
32222
+ this.#margin = opts.margin ?? 2;
32223
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32224
+ }
32225
+ get layerCount() {
32226
+ return this.#layerCount;
32227
+ }
32228
+ /**
32229
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32230
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32231
+ * or interleaved sequences read as loss.
32232
+ */
32233
+ noteSourceSequence(seq) {
32234
+ if (this.#disposed || this.#delivery() === "stream") return;
32235
+ seq >>>= 0;
32236
+ if (this.#lastSeq === void 0) {
32237
+ this.#lastSeq = seq;
32238
+ return;
32239
+ }
32240
+ const d = serial32Distance(this.#lastSeq, seq);
32241
+ if (d === 0) return;
32242
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32243
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32244
+ this.#lastSeq = seq;
32245
+ this.#expected = 0;
32246
+ this.#lost = 0;
32247
+ return;
32248
+ }
32249
+ if (back === 0) {
32250
+ this.#expected += d;
32251
+ this.#lost += d - 1;
32252
+ this.#lastSeq = seq;
32253
+ } else if (this.#lost > 0) {
32254
+ this.#lost -= 1;
32255
+ }
32256
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32257
+ }
32258
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32259
+ switchSource(next) {
32260
+ if (this.#disposed) return;
32261
+ this.#closeAll();
32262
+ this.#source = next;
32263
+ this.#lastSeq = void 0;
32264
+ this.#expected = 0;
32265
+ this.#lost = 0;
32266
+ this.#wantFewer = 0;
32267
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32268
+ this.#reconcile();
32269
+ }
32270
+ snapshot() {
32271
+ return {
32272
+ delivery: this.#delivery(),
32273
+ layerCount: this.#layerCount,
32274
+ subscribedTracks: [...this.#subs.keys()],
32275
+ repairPackets: this.#repairPackets,
32276
+ repairBytes: this.#repairBytes,
32277
+ staleDropped: this.#staleDropped,
32278
+ lossRate: this.#lossRate
32279
+ };
32280
+ }
32281
+ dispose() {
32282
+ if (this.#disposed) return;
32283
+ this.#closeAll();
32284
+ this.#disposed = true;
32285
+ }
32286
+ #delivery() {
32287
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32288
+ }
32289
+ #layers() {
32290
+ return this.#source.repairLayers ?? [];
32291
+ }
32292
+ #closeWindow() {
32293
+ const rate = this.#lost / this.#expected;
32294
+ this.#lossRate = rate;
32295
+ this.#expected = 0;
32296
+ this.#lost = 0;
32297
+ const target = this.#targetLayers(rate);
32298
+ if (target > this.#layerCount) {
32299
+ this.#wantFewer = 0;
32300
+ this.#layerCount = target;
32301
+ } else if (target < this.#layerCount) {
32302
+ this.#wantFewer += 1;
32303
+ if (this.#wantFewer >= this.#dropAfterWindows) {
32304
+ this.#wantFewer = 0;
32305
+ this.#layerCount -= 1;
32306
+ }
32307
+ } else {
32308
+ this.#wantFewer = 0;
32309
+ }
32310
+ this.#reconcile();
32311
+ }
32312
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
32313
+ #targetLayers(rate) {
32314
+ if (rate <= 0) return 0;
32315
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
32316
+ const layers = this.#layers();
32317
+ let held = 0;
32318
+ for (let n = 0; n < layers.length; n++) {
32319
+ held += layers[n].repairSymbols;
32320
+ if (held >= needed) return n + 1;
32321
+ }
32322
+ return layers.length;
32323
+ }
32324
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
32325
+ #reconcile() {
32326
+ const want = /* @__PURE__ */ new Map();
32327
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
32328
+ want.set(layer.repairTrackName, layer.priority);
32329
+ }
32330
+ const overlay = this.#source.repairOverlays?.[0];
32331
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
32332
+ for (const [name, entry] of this.#subs) {
32333
+ if (!want.has(name)) {
32334
+ entry.sub.close();
32335
+ this.#subs.delete(name);
32336
+ }
32337
+ }
32338
+ for (const [name, priority] of want) {
32339
+ if (this.#subs.has(name)) continue;
32340
+ const entry = {};
32341
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
32342
+ this.#subs.set(name, entry);
32343
+ }
32344
+ }
32345
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
32346
+ #onPacket(name, entry, packet) {
32347
+ if (this.#disposed || this.#subs.get(name) !== entry) {
32348
+ this.#staleDropped += 1;
32349
+ return;
32350
+ }
32351
+ this.#repairPackets += 1;
32352
+ this.#repairBytes += packet.byteLength;
32353
+ this.#opts.feed(packet, name);
32354
+ }
32355
+ #closeAll() {
32356
+ for (const entry of this.#subs.values()) entry.sub.close();
32357
+ this.#subs.clear();
32358
+ }
32359
+ };
32360
+
31925
32361
  // src/repair-fetch.ts
31926
32362
  var MOQT_PRIORITY_MAX = 255;
31927
32363
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
@@ -32567,6 +33003,7 @@ export {
32567
33003
  REPAIR_LIFECYCLE_LOG_PREFIX,
32568
33004
  REPAIR_PRIORITY_MAX,
32569
33005
  REPAIR_PRIORITY_MIN,
33006
+ RepairLayerSubscriptions,
32570
33007
  SOURCE_MEDIA_PRIORITY_MAX,
32571
33008
  SampleCombineError,
32572
33009
  SampleCombiner,
@@ -32591,6 +33028,7 @@ export {
32591
33028
  catalogGroupDurationMs,
32592
33029
  catalogKeyframeCadenceMs,
32593
33030
  catalogRepairTarget,
33031
+ catalogTrackDeliveryMode,
32594
33032
  catalogTrackFecUnavailable,
32595
33033
  classifyH264Keyframe,
32596
33034
  classifyHevcKeyframe,
@@ -32611,6 +33049,7 @@ export {
32611
33049
  detectH264Keyframe,
32612
33050
  detectHevcKeyframe,
32613
33051
  driftCheck,
33052
+ encodeCmcdRecord,
32614
33053
  encodeDataSegment,
32615
33054
  encodeFecSourceInfo,
32616
33055
  encodeLocTimestamp,