@blockcast/mmt-container 2.2.0-main.a88c3a0aa4f3 → 2.2.0-main.b4ce3986f2dd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +505 -177
  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 +24 -15
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.js.map +2 -2
  18. package/dist/fec-state.js.map +1 -1
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/held-layer-admission.js.map +1 -1
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -1
  26. package/dist/index.js.map +2 -2
  27. package/dist/iso-block-locator.js.map +1 -1
  28. package/dist/media-router.js.map +1 -1
  29. package/dist/mfu-reassembler.js +12 -19
  30. package/dist/mfu-reassembler.js.map +2 -2
  31. package/dist/mmtp-container-parser.d.ts +2 -0
  32. package/dist/mmtp-container-parser.d.ts.map +1 -1
  33. package/dist/mmtp-container-parser.js +16 -19
  34. package/dist/mmtp-container-parser.js.map +2 -2
  35. package/dist/mmtp-container-parser.no-modules.js +137 -34
  36. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  37. package/dist/mmtp-decode-pipeline.js +16 -19
  38. package/dist/mmtp-decode-pipeline.js.map +2 -2
  39. package/dist/mmtp-fec-session.d.ts +41 -3
  40. package/dist/mmtp-fec-session.d.ts.map +1 -1
  41. package/dist/mmtp-fec-session.js +118 -13
  42. package/dist/mmtp-fec-session.js.map +3 -3
  43. package/dist/mmtp-router.d.ts.map +1 -1
  44. package/dist/mmtp-router.js +50 -34
  45. package/dist/mmtp-router.js.map +2 -2
  46. package/dist/multicast-auth-catalog.js.map +1 -1
  47. package/dist/multicast-input.js.map +1 -1
  48. package/dist/multicast-manager.d.ts +2 -0
  49. package/dist/multicast-manager.d.ts.map +1 -1
  50. package/dist/multicast-manager.js +51 -34
  51. package/dist/multicast-manager.js.map +2 -2
  52. package/dist/multicast-transport.js +24 -15
  53. package/dist/multicast-transport.js.map +2 -2
  54. package/dist/multicast-unwrap.js +12 -19
  55. package/dist/multicast-unwrap.js.map +2 -2
  56. package/dist/no-modules.js +134 -33
  57. package/dist/no-modules.js.map +3 -3
  58. package/dist/repair-fetch.js.map +1 -1
  59. package/dist/repair-layer-subscriptions.d.ts +82 -0
  60. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  61. package/dist/repair-layer-subscriptions.js +192 -0
  62. package/dist/repair-layer-subscriptions.js.map +7 -0
  63. package/dist/signaling.js.map +2 -2
  64. package/dist/timestamp-codec.js +12 -19
  65. package/dist/timestamp-codec.js.map +2 -2
  66. package/dist/track-session.d.ts +1 -1
  67. package/dist/track-session.js.map +1 -1
  68. package/dist/wasm-mfu-reassembler.js.map +1 -1
  69. package/dist/wasm-mmtp-container-parser.js +16 -19
  70. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  71. package/package.json +8 -5
  72. package/schemas/catalog.schema.json +4 -0
  73. package/src/catalog-fec-params.ts +36 -1
  74. package/src/cmcd-producer.ts +189 -179
  75. package/src/fec-worker-setup.ts +2 -2
  76. package/src/index.ts +17 -2
  77. package/src/mmtp-container-parser.ts +12 -0
  78. package/src/mmtp-fec-session.ts +115 -15
  79. package/src/mmtp-router.ts +21 -0
  80. package/src/multicast-manager.ts +3 -0
  81. package/src/repair-layer-subscriptions.ts +261 -0
  82. 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;
@@ -8710,11 +8749,11 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8710
8749
  var AMTGatewayManager = class {
8711
8750
  subscriptions = /* @__PURE__ */ new Map();
8712
8751
  messageHandler;
8752
+ messageListenerRegistered = false;
8713
8753
  pendingPackets = /* @__PURE__ */ new Map();
8714
8754
  constructor() {
8715
8755
  this.messageHandler = (event) => {
8716
8756
  const data = event.data;
8717
- console.log(`[AMT] SW message received:`, data.type);
8718
8757
  if (data.type === "UDP_PACKET") {
8719
8758
  this.handlePacket(data);
8720
8759
  } else if (data.type === "SOCKET_ERROR") {
@@ -8723,12 +8762,22 @@ var AMTGatewayManager = class {
8723
8762
  console.log(`[AMT SW Debug] ${data.message}`);
8724
8763
  }
8725
8764
  };
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");
8765
+ }
8766
+ ensureMessageListener() {
8767
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8768
+ return;
8769
+ }
8770
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8771
+ this.messageListenerRegistered = true;
8772
+ console.log("[AMT] Registered service worker message handler");
8773
+ }
8774
+ removeMessageListener() {
8775
+ if (!this.messageListenerRegistered)
8776
+ return;
8777
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8778
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8731
8779
  }
8780
+ this.messageListenerRegistered = false;
8732
8781
  }
8733
8782
  /**
8734
8783
  * Subscribe via AMT tunnel
@@ -8738,6 +8787,7 @@ var AMTGatewayManager = class {
8738
8787
  if (config.protected) {
8739
8788
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8740
8789
  }
8790
+ this.ensureMessageListener();
8741
8791
  const keyStr = this.getKeyString(key);
8742
8792
  if (this.subscriptions.has(keyStr)) {
8743
8793
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8773,6 +8823,8 @@ var AMTGatewayManager = class {
8773
8823
  this.subscriptions.delete(keyStr);
8774
8824
  this.pendingPackets.delete(subscription.subscriptionId);
8775
8825
  }
8826
+ if (this.subscriptions.size === 0)
8827
+ this.removeMessageListener();
8776
8828
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8777
8829
  }
8778
8830
  }
@@ -8794,6 +8846,8 @@ var AMTGatewayManager = class {
8794
8846
  } finally {
8795
8847
  this.subscriptions.delete(handle);
8796
8848
  this.pendingPackets.delete(subscription.subscriptionId);
8849
+ if (this.subscriptions.size === 0)
8850
+ this.removeMessageListener();
8797
8851
  }
8798
8852
  }
8799
8853
  /**
@@ -8810,10 +8864,8 @@ var AMTGatewayManager = class {
8810
8864
  if (!subscriptionId)
8811
8865
  return;
8812
8866
  const subscription = this.subscriptions.get(subscriptionId);
8813
- if (!subscription) {
8814
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8867
+ if (!subscription)
8815
8868
  return;
8816
- }
8817
8869
  if (!subscription.handshakeComplete) {
8818
8870
  const queue = this.pendingPackets.get(subscriptionId);
8819
8871
  const packetData2 = data;
@@ -8843,19 +8895,15 @@ var AMTGatewayManager = class {
8843
8895
  if (!subscriptionId)
8844
8896
  return;
8845
8897
  const subscription = this.subscriptions.get(subscriptionId);
8846
- if (!subscription) {
8847
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8898
+ if (!subscription)
8848
8899
  return;
8849
- }
8850
8900
  subscription.config.onError(new Error(error || "Unknown error"));
8851
8901
  }
8852
8902
  /**
8853
8903
  * Cleanup on destroy
8854
8904
  */
8855
8905
  destroy() {
8856
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8857
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8858
- }
8906
+ this.removeMessageListener();
8859
8907
  const handles = Array.from(this.subscriptions.keys());
8860
8908
  for (const handle of handles) {
8861
8909
  this.unsubscribe(handle).catch((err) => {
@@ -18811,10 +18859,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18811
18859
  get reassemblerDataUnitsReturned() {
18812
18860
  return this.#reassemblerDataUnitsReturned;
18813
18861
  }
18814
- #countOutcome(token) {
18815
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18816
- this.#feedOutcomes.set(token, count);
18817
- this.#lastOutcome = token;
18862
+ #countOutcome(token2) {
18863
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
18864
+ this.#feedOutcomes.set(token2, count);
18865
+ this.#lastOutcome = token2;
18818
18866
  this.#lastOutcomeCount = count;
18819
18867
  }
18820
18868
  #countDeferredOutcome(reason) {
@@ -19342,6 +19390,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19342
19390
  }
19343
19391
  this.#emitFrame(data, meta);
19344
19392
  }
19393
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19394
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19395
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19396
+ }
19345
19397
  /**
19346
19398
  * Claim and return the authored timing for a recovered sample. Recovery does
19347
19399
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -25866,6 +25918,16 @@ var MmtpRouter = class {
25866
25918
  }
25867
25919
  );
25868
25920
  }
25921
+ result = {
25922
+ ...result,
25923
+ symbols: result.symbols.filter((sym) => sym.fragmentType !== 2 || sym.fragment.fragmentationIndicator !== 1 || sym.fragment.movieFragmentSequenceNumber === void 0 || sym.fragment.sampleNumber === void 0 || this.#parser.hasAuthoredTimingFor(
25924
+ ownerPacketId,
25925
+ sym.fragment.mpuSequenceNumber,
25926
+ sym.fragment.movieFragmentSequenceNumber,
25927
+ sym.fragment.sampleNumber,
25928
+ catalogTrackName
25929
+ ))
25930
+ };
25869
25931
  const gated = this.#isFecTrack(ownerPacketId);
25870
25932
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25871
25933
  if (assembler) {
@@ -28678,6 +28740,7 @@ var MulticastManager = class {
28678
28740
  mode: endpoint.mode,
28679
28741
  amtRelay: endpoint.amtRelay,
28680
28742
  amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
28743
+ amtDiscovery: endpoint.amtDiscovery,
28681
28744
  packetIds: endpoint.packetIds,
28682
28745
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28683
28746
  // A protected repair group's records ride in band on its port (7.2.3).
@@ -29626,7 +29689,7 @@ async function setupFecWorkerForTrack(opts) {
29626
29689
  blockProfile,
29627
29690
  k: opts.fecK,
29628
29691
  // P as this receiver joined it, not the catalog total: the worker snapshot
29629
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
29692
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29630
29693
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29631
29694
  symbolSize: opts.fecSymbolSize,
29632
29695
  interleaveDepth: interleaveDepthBlocks,
@@ -30168,24 +30231,24 @@ var MediaRouter = class {
30168
30231
  if (!this.#multicastVideoKeyed) return;
30169
30232
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30170
30233
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30171
- const token = ++this.#sourceSwitchToken;
30234
+ const token2 = ++this.#sourceSwitchToken;
30172
30235
  const maybeReady = this.#onSourceSwitch?.("multicast");
30173
30236
  if (!maybeReady || typeof maybeReady.then !== "function") {
30174
- this.#commitMulticastActivation(token);
30237
+ this.#commitMulticastActivation(token2);
30175
30238
  return;
30176
30239
  }
30177
30240
  const ready = Promise.resolve(maybeReady);
30178
- this.#pendingMulticastToken = token;
30241
+ this.#pendingMulticastToken = token2;
30179
30242
  this.#sourceSwitchReady = ready;
30180
30243
  void ready.then(
30181
- () => this.#commitMulticastActivation(token),
30182
- (err) => this.#rejectMulticastActivation(token, err)
30244
+ () => this.#commitMulticastActivation(token2),
30245
+ (err) => this.#rejectMulticastActivation(token2, err)
30183
30246
  ).finally(() => {
30184
30247
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30185
30248
  });
30186
30249
  }
30187
- #commitMulticastActivation(token) {
30188
- if (token !== this.#sourceSwitchToken) return;
30250
+ #commitMulticastActivation(token2) {
30251
+ if (token2 !== this.#sourceSwitchToken) return;
30189
30252
  this.#active = "multicast";
30190
30253
  this.#suppressed = true;
30191
30254
  this.#pendingMulticastToken = null;
@@ -30196,9 +30259,9 @@ var MediaRouter = class {
30196
30259
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30197
30260
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30198
30261
  }
30199
- #rejectMulticastActivation(token, err) {
30200
- if (token !== this.#sourceSwitchToken) return;
30201
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30262
+ #rejectMulticastActivation(token2, err) {
30263
+ if (token2 !== this.#sourceSwitchToken) return;
30264
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30202
30265
  this.#pendingMulticastVideo = null;
30203
30266
  this.#pendingMulticastAudio = null;
30204
30267
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31167,63 +31230,65 @@ var FecStarvationDetector = class {
31167
31230
 
31168
31231
  // src/cmcd-producer.ts
31169
31232
  var PLAY_MODES = ["full", "base-only", "single-track"];
31170
- function sanitize(value) {
31171
- return value.replace(/[,\r\n]/g, "");
31172
- }
31173
- function buildFecParams(stats) {
31174
- const params = {};
31175
- const cfg = stats.config;
31176
- params["x-bc-fec-algo"] = cfg.algorithm;
31177
- params["x-bc-fec-k"] = cfg.k;
31178
- params["x-bc-fec-p"] = cfg.repairCount;
31179
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
31180
- return params;
31181
- }
31182
- function buildTransportParams(snap) {
31183
- if (!snap) return {};
31184
- const params = {};
31185
- params["x-bc-transport"] = snap.transport;
31186
- if (snap.multicastGroup != null) {
31187
- params["x-bc-multicast-group"] = snap.multicastGroup;
31188
- }
31189
- if (snap.multicastSource != null) {
31190
- params["x-bc-multicast-source"] = snap.multicastSource;
31191
- }
31192
- if (snap.amtRelay != null) {
31193
- params["x-bc-amt-relay"] = snap.amtRelay;
31194
- }
31195
- return params;
31196
- }
31197
- function formatQueryString(params) {
31198
- return Object.keys(params).sort().map((key) => {
31199
- const v = params[key];
31200
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
31201
- return `${key}=${v}`;
31233
+ var DEFAULT_INTERVAL_MS = 3e4;
31234
+ var CUSTOM_PREFIX = "com.blockcast-";
31235
+ var MAX_CUSTOM_VALUE = 64;
31236
+ var TRANSPORT_TOKEN = {
31237
+ native: "ssm",
31238
+ amt: "amt",
31239
+ moq: "moq"
31240
+ };
31241
+ function sfString(value) {
31242
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31243
+ }
31244
+ function encodeCmcdRecord(record) {
31245
+ return Object.keys(record).sort().map((key) => {
31246
+ const value = record[key];
31247
+ if (typeof value === "number") return `${key}=${value}`;
31248
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31249
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31250
+ return `${key}=${value.token}`;
31202
31251
  }).join(",");
31203
31252
  }
31253
+ var token = (value) => ({ token: value });
31254
+ function putCustom(record, name, value) {
31255
+ if (value === void 0) return;
31256
+ if (typeof value === "number") value = String(value);
31257
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31258
+ record[CUSTOM_PREFIX + name] = value;
31259
+ }
31204
31260
  var CmcdProducer = class {
31205
- #onCmcdParams;
31206
31261
  #onCmcdPost;
31262
+ #intervalMs;
31263
+ #sessionId;
31264
+ #now;
31265
+ #sn = 0;
31266
+ #lastHeartbeat = null;
31207
31267
  #transport;
31208
31268
  #lastStats;
31209
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31210
31269
  #playMode = null;
31211
31270
  #deltaFailRate = null;
31212
31271
  #switchMiscount = 0;
31213
31272
  #manifestStats = null;
31214
31273
  #fecFailClosed = null;
31274
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31275
+ #lifecycle = null;
31215
31276
  constructor(options) {
31216
- this.#onCmcdParams = options.onCmcdParams;
31217
31277
  this.#onCmcdPost = options.onCmcdPost;
31278
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31279
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31280
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31281
+ }
31282
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31283
+ this.#now = options.now ?? Date.now;
31218
31284
  }
31219
31285
  /** Update the stored transport state. Called by player wiring layer. */
31220
31286
  updateTransport(snapshot) {
31221
31287
  this.#transport = snapshot;
31222
31288
  }
31223
31289
  /**
31224
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
31225
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
31226
- * (CLAUDE.md: error on bad init).
31290
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31291
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31227
31292
  */
31228
31293
  updatePlayMode(mode) {
31229
31294
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31234,9 +31299,8 @@ var CmcdProducer = class {
31234
31299
  this.#playMode = mode;
31235
31300
  }
31236
31301
  /**
31237
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
31238
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
31239
- * Omitted on single-track streams.
31302
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31303
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31240
31304
  */
31241
31305
  updateDeltaFailRate(rate) {
31242
31306
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31246,112 +31310,139 @@ var CmcdProducer = class {
31246
31310
  }
31247
31311
  this.#deltaFailRate = rate;
31248
31312
  }
31249
- /**
31250
- * Bump the cumulative switching-set misalignment counter. Emitted as
31251
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31252
- */
31313
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31253
31314
  incrementSwitchMiscount() {
31254
31315
  this.#switchMiscount += 1;
31255
31316
  }
31256
31317
  /**
31257
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
31258
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
31259
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
31260
- * stream playing without FEC is told apart from one with no loss.
31318
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31319
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31320
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31321
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31322
+ * with no loss.
31261
31323
  */
31262
31324
  updateFecFailClosed(reason) {
31325
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31263
31326
  this.#fecFailClosed = reason;
31327
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31264
31328
  }
31265
31329
  updateManifestVerification(stats) {
31266
31330
  this.#manifestStats = stats;
31267
31331
  }
31268
31332
  /**
31269
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
31270
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
31271
- * include the drift key until the next emitSwitchDrift(). Mirrors the
31272
- * existing onTransition() pattern.
31333
+ * Count a video block's resolution from the repair lifecycle: recovered in
31334
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31335
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31336
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31273
31337
  */
31338
+ onRepairLifecycle(event) {
31339
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31340
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31341
+ if (event.via === "inband") this.#lifecycle.inband++;
31342
+ else this.#lifecycle.fetch++;
31343
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31344
+ this.#lifecycle.failedKeyframe++;
31345
+ }
31346
+ }
31347
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31274
31348
  emitSwitchDrift(driftUs) {
31275
31349
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31276
31350
  throw new Error(
31277
31351
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31278
31352
  );
31279
31353
  }
31280
- const params = {};
31281
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
31282
- Object.assign(params, buildTransportParams(this.#transport));
31283
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31284
- params["x-bc-switch-drift-us"] = driftUs;
31285
- this.#emit(params);
31354
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31355
+ putCustom(record, "sdu", Math.round(driftUs));
31356
+ this.#post([record]);
31286
31357
  }
31287
- /**
31288
- * Process a FecTrackStats snapshot. Finds the video track, builds
31289
- * x-bc-* vendor keys, and fires both delivery callbacks.
31290
- */
31358
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31291
31359
  onSnapshot(data) {
31292
31360
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31293
31361
  if (!videoTrack) return;
31294
31362
  this.#lastStats = videoTrack;
31295
- const params = {
31296
- ...buildFecParams(videoTrack),
31297
- ...buildTransportParams(this.#transport)
31298
- };
31299
- if (this.#playMode !== null) {
31300
- params["x-bc-play-mode"] = this.#playMode;
31301
- }
31302
- if (this.#fecFailClosed) {
31303
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31304
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31305
- }
31306
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31307
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31308
- }
31309
- if (this.#switchMiscount > 0) {
31310
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31311
- }
31312
- if (this.#manifestStats) {
31313
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31314
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31315
- }
31316
- this.#emit(params);
31363
+ this.#heartbeat();
31317
31364
  }
31318
31365
  /**
31319
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31320
- * the caller because only the transport integration knows when state changed.
31366
+ * Take transport state for receivers that do not own FEC stats, and send the
31367
+ * heartbeat when due.
31321
31368
  */
31322
31369
  onTransportSnapshot(data) {
31323
31370
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31324
31371
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31325
31372
  }
31326
31373
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31327
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31328
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31374
+ if (data.switchMiscount !== void 0) {
31375
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31376
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31377
+ }
31378
+ this.#switchMiscount = data.switchMiscount;
31329
31379
  }
31330
31380
  this.#transport = data.transport;
31331
- const params = buildTransportParams(data.transport);
31332
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31333
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31334
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31335
- this.#emit(params);
31381
+ this.#heartbeat();
31336
31382
  }
31337
- /**
31338
- * Signal a transport transition event. Fires an immediate out-of-band
31339
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31340
- * x-bc-ct (one-shot).
31341
- */
31383
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31342
31384
  onTransition(event) {
31343
- const params = {};
31344
- if (this.#lastStats) {
31345
- Object.assign(params, buildFecParams(this.#lastStats));
31385
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31386
+ putCustom(record, "ct", token(event));
31387
+ this.#putTransport(record);
31388
+ this.#post([record]);
31389
+ }
31390
+ #base(event) {
31391
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31392
+ }
31393
+ #heartbeat() {
31394
+ const now = this.#now();
31395
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31396
+ this.#lastHeartbeat = now;
31397
+ this.#post([this.#base("t"), this.#counters()]);
31398
+ }
31399
+ #putTransport(record) {
31400
+ const snap = this.#transport;
31401
+ if (!snap) return;
31402
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31403
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31404
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31405
+ }
31406
+ putCustom(record, "ar", snap.amtRelay);
31407
+ }
31408
+ /** The `e=ce` counter report. */
31409
+ #counters() {
31410
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31411
+ const stats = this.#lastStats;
31412
+ if (stats) {
31413
+ putCustom(record, "fa", stats.config.algorithm);
31414
+ putCustom(record, "fk", stats.config.k);
31415
+ putCustom(record, "fp", stats.config.repairCount);
31416
+ putCustom(record, "pr", stats.sourceSymbols);
31417
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31418
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31419
+ putCustom(record, "fbf", stats.blocksFailed);
31420
+ }
31421
+ if (this.#lifecycle) {
31422
+ putCustom(record, "fbr", this.#lifecycle.inband);
31423
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31424
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31425
+ }
31426
+ this.#putTransport(record);
31427
+ if (this.#playMode !== null) {
31428
+ putCustom(record, "pm", token(this.#playMode));
31429
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31430
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31431
+ }
31432
+ }
31433
+ if (this.#fecFailClosed) {
31434
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31435
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31346
31436
  }
31347
- Object.assign(params, buildTransportParams(this.#transport));
31348
- params["x-bc-ct"] = event;
31349
- this.#emit(params);
31437
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31438
+ if (this.#manifestStats) {
31439
+ putCustom(record, "mv", this.#manifestStats.verified);
31440
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31441
+ }
31442
+ return record;
31350
31443
  }
31351
- /** Fire both delivery callbacks with the given params. */
31352
- #emit(params) {
31353
- this.#onCmcdParams?.(params);
31354
- this.#onCmcdPost?.(formatQueryString(params));
31444
+ #post(records) {
31445
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31355
31446
  }
31356
31447
  };
31357
31448
 
@@ -31447,18 +31538,31 @@ function resolveRepairLayers(catalog, source) {
31447
31538
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31448
31539
  };
31449
31540
  }
31541
+ function valueAt(catalog, pointer) {
31542
+ let node = catalog;
31543
+ for (const segment of pointer.split("/").slice(1)) {
31544
+ if (typeof node !== "object" || node === null) return void 0;
31545
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
31546
+ }
31547
+ return node;
31548
+ }
31549
+ function catalogTrackDeliveryMode(catalog, source) {
31550
+ const binding = bindingOf(catalog, source);
31551
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
31552
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
31553
+ }
31450
31554
  function moqtPriority2(value) {
31451
31555
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31452
31556
  }
31453
31557
  function catalogRepairTarget(catalog, source) {
31454
31558
  const binding = bindingOf(catalog, source);
31455
31559
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31456
- const layerZero = binding.layers[0];
31560
+ const layerZero2 = binding.layers[0];
31457
31561
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31458
31562
  const tracks = catalog.tracks;
31459
31563
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31460
31564
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31461
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
31565
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31462
31566
  }
31463
31567
  function numericField(value) {
31464
31568
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31490,6 +31594,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31490
31594
  repairTrackName: base.repairTrack,
31491
31595
  repairLayers: repair.baseLayers,
31492
31596
  repairOverlays: repair.overlays,
31597
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31493
31598
  ...mediaRate
31494
31599
  };
31495
31600
  }
@@ -31540,7 +31645,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31540
31645
  var PATH_BITS = { moq: 1, multicast: 2 };
31541
31646
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31542
31647
  var MmtpFecSession = class {
31648
+ /** P_0 = B_0, the held total every repair packet is checked against. */
31649
+ get repairSymbols() {
31650
+ return this.#repairSymbols;
31651
+ }
31543
31652
  #decoder;
31653
+ #repairSymbols;
31654
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
31655
+ #message;
31544
31656
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31545
31657
  #blocks = /* @__PURE__ */ new Map();
31546
31658
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31578,6 +31690,24 @@ var MmtpFecSession = class {
31578
31690
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31579
31691
  */
31580
31692
  constructor(config) {
31693
+ if (config && "alFecFlow" in config) {
31694
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
31695
+ if (!message) {
31696
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
31697
+ }
31698
+ this.#message = message;
31699
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
31700
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
31701
+ this.sourceSymbols = message.maximumK;
31702
+ this.#repairSymbols = message.maximumP;
31703
+ this.symbolSize = message.repairSymbolSize;
31704
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
31705
+ this.#repairPacketId = message.repairFlowId;
31706
+ this.#heldLayers = layerZero(message.maximumP);
31707
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31708
+ this.#decoder = buildDecoder(config.Decoder, this);
31709
+ return;
31710
+ }
31581
31711
  const fec = config?.fec;
31582
31712
  if (!fec || typeof fec !== "object") {
31583
31713
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31588,7 +31718,7 @@ var MmtpFecSession = class {
31588
31718
  }
31589
31719
  this.algorithm = fec.scheme;
31590
31720
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31591
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31721
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31592
31722
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31593
31723
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31594
31724
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31608,17 +31738,30 @@ var MmtpFecSession = class {
31608
31738
  config.heldLayers
31609
31739
  );
31610
31740
  this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31611
- if (typeof config.Decoder !== "function") {
31612
- throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
31613
- }
31614
- const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm);
31615
- try {
31616
- decoder.configure_params(this.sourceSymbols, this.symbolSize);
31617
- } catch (error) {
31618
- decoder.free();
31619
- throw error;
31620
- }
31621
- this.#decoder = decoder;
31741
+ this.#decoder = buildDecoder(config.Decoder, this);
31742
+ }
31743
+ /**
31744
+ * A later version of the AL-FEC message, catalog-less sessions only. A
31745
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
31746
+ * anchored check restarts at the new total, so a block generated under the
31747
+ * previous one stays valid before the first block carrying the new total
31748
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
31749
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
31750
+ * does. An entry with another asset, repair flow, code point, K or T, or
31751
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
31752
+ * a new session. A latched fail-closed stays latched either way.
31753
+ */
31754
+ updateAlFecFlow(flow) {
31755
+ const held = this.#message;
31756
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
31757
+ const next = alFecRaptorQFlowConfig(flow);
31758
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
31759
+ return "refused";
31760
+ }
31761
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
31762
+ this.#repairSymbols = next.maximumP;
31763
+ this.#heldLayers = layerZero(next.maximumP);
31764
+ return "raised";
31622
31765
  }
31623
31766
  /** True once release() ran. */
31624
31767
  get released() {
@@ -31713,7 +31856,7 @@ var MmtpFecSession = class {
31713
31856
  }
31714
31857
  if (admission.reject) {
31715
31858
  const verdict2 = this.#heldLayers.failsClosed(
31716
- repairTrackName,
31859
+ this.#trackFor(repairTrackName),
31717
31860
  frame.ssStart,
31718
31861
  frame.rsbLength,
31719
31862
  this.#anchorHorizon,
@@ -31728,7 +31871,7 @@ var MmtpFecSession = class {
31728
31871
  this.#repairPacketId ??= frame.packetId;
31729
31872
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31730
31873
  const verdict = this.#heldLayers.admit(
31731
- repairTrackName,
31874
+ this.#trackFor(repairTrackName),
31732
31875
  frame.ssStart,
31733
31876
  frame.rsbLength,
31734
31877
  this.#anchorHorizon,
@@ -31787,6 +31930,10 @@ var MmtpFecSession = class {
31787
31930
  decoder.free();
31788
31931
  return true;
31789
31932
  }
31933
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
31934
+ #trackFor(repairTrackName) {
31935
+ return this.#message ? void 0 : repairTrackName;
31936
+ }
31790
31937
  #drop(reason) {
31791
31938
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31792
31939
  return [];
@@ -31908,6 +32055,22 @@ var MmtpFecSession = class {
31908
32055
  }
31909
32056
  }
31910
32057
  };
32058
+ function layerZero(total) {
32059
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32060
+ }
32061
+ function buildDecoder(Decoder, session) {
32062
+ if (typeof Decoder !== "function") {
32063
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32064
+ }
32065
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32066
+ try {
32067
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32068
+ } catch (error) {
32069
+ decoder.free();
32070
+ throw error;
32071
+ }
32072
+ return decoder;
32073
+ }
31911
32074
  function requirePositiveInteger(value, path) {
31912
32075
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31913
32076
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31921,6 +32084,168 @@ function requireNumber(value, path) {
31921
32084
  return value;
31922
32085
  }
31923
32086
 
32087
+ // src/repair-layer-subscriptions.ts
32088
+ var SERIAL32_HALF2 = 2147483648;
32089
+ var RepairLayerSubscriptions = class {
32090
+ #opts;
32091
+ #windowPackets;
32092
+ #margin;
32093
+ #dropAfterWindows;
32094
+ #source;
32095
+ #layerCount = 0;
32096
+ #subs = /* @__PURE__ */ new Map();
32097
+ #disposed = false;
32098
+ #lastSeq;
32099
+ #expected = 0;
32100
+ #lost = 0;
32101
+ #wantFewer = 0;
32102
+ #lossRate = null;
32103
+ #repairPackets = 0;
32104
+ #repairBytes = 0;
32105
+ #staleDropped = 0;
32106
+ constructor(opts) {
32107
+ this.#opts = opts;
32108
+ this.#source = opts.source;
32109
+ this.#windowPackets = opts.windowPackets ?? 500;
32110
+ this.#margin = opts.margin ?? 2;
32111
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32112
+ }
32113
+ get layerCount() {
32114
+ return this.#layerCount;
32115
+ }
32116
+ /**
32117
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32118
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32119
+ * or interleaved sequences read as loss.
32120
+ */
32121
+ noteSourceSequence(seq) {
32122
+ if (this.#disposed || this.#delivery() === "stream") return;
32123
+ seq >>>= 0;
32124
+ if (this.#lastSeq === void 0) {
32125
+ this.#lastSeq = seq;
32126
+ return;
32127
+ }
32128
+ const d = serial32Distance(this.#lastSeq, seq);
32129
+ if (d === 0) return;
32130
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32131
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32132
+ this.#lastSeq = seq;
32133
+ this.#expected = 0;
32134
+ this.#lost = 0;
32135
+ return;
32136
+ }
32137
+ if (back === 0) {
32138
+ this.#expected += d;
32139
+ this.#lost += d - 1;
32140
+ this.#lastSeq = seq;
32141
+ } else if (this.#lost > 0) {
32142
+ this.#lost -= 1;
32143
+ }
32144
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32145
+ }
32146
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32147
+ switchSource(next) {
32148
+ if (this.#disposed) return;
32149
+ this.#closeAll();
32150
+ this.#source = next;
32151
+ this.#lastSeq = void 0;
32152
+ this.#expected = 0;
32153
+ this.#lost = 0;
32154
+ this.#wantFewer = 0;
32155
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32156
+ this.#reconcile();
32157
+ }
32158
+ snapshot() {
32159
+ return {
32160
+ delivery: this.#delivery(),
32161
+ layerCount: this.#layerCount,
32162
+ subscribedTracks: [...this.#subs.keys()],
32163
+ repairPackets: this.#repairPackets,
32164
+ repairBytes: this.#repairBytes,
32165
+ staleDropped: this.#staleDropped,
32166
+ lossRate: this.#lossRate
32167
+ };
32168
+ }
32169
+ dispose() {
32170
+ if (this.#disposed) return;
32171
+ this.#closeAll();
32172
+ this.#disposed = true;
32173
+ }
32174
+ #delivery() {
32175
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32176
+ }
32177
+ #layers() {
32178
+ return this.#source.repairLayers ?? [];
32179
+ }
32180
+ #closeWindow() {
32181
+ const rate = this.#lost / this.#expected;
32182
+ this.#lossRate = rate;
32183
+ this.#expected = 0;
32184
+ this.#lost = 0;
32185
+ const target = this.#targetLayers(rate);
32186
+ if (target > this.#layerCount) {
32187
+ this.#wantFewer = 0;
32188
+ this.#layerCount = target;
32189
+ } else if (target < this.#layerCount) {
32190
+ this.#wantFewer += 1;
32191
+ if (this.#wantFewer >= this.#dropAfterWindows) {
32192
+ this.#wantFewer = 0;
32193
+ this.#layerCount -= 1;
32194
+ }
32195
+ } else {
32196
+ this.#wantFewer = 0;
32197
+ }
32198
+ this.#reconcile();
32199
+ }
32200
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
32201
+ #targetLayers(rate) {
32202
+ if (rate <= 0) return 0;
32203
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
32204
+ const layers = this.#layers();
32205
+ let held = 0;
32206
+ for (let n = 0; n < layers.length; n++) {
32207
+ held += layers[n].repairSymbols;
32208
+ if (held >= needed) return n + 1;
32209
+ }
32210
+ return layers.length;
32211
+ }
32212
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
32213
+ #reconcile() {
32214
+ const want = /* @__PURE__ */ new Map();
32215
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
32216
+ want.set(layer.repairTrackName, layer.priority);
32217
+ }
32218
+ const overlay = this.#source.repairOverlays?.[0];
32219
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
32220
+ for (const [name, entry] of this.#subs) {
32221
+ if (!want.has(name)) {
32222
+ entry.sub.close();
32223
+ this.#subs.delete(name);
32224
+ }
32225
+ }
32226
+ for (const [name, priority] of want) {
32227
+ if (this.#subs.has(name)) continue;
32228
+ const entry = {};
32229
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
32230
+ this.#subs.set(name, entry);
32231
+ }
32232
+ }
32233
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
32234
+ #onPacket(name, entry, packet) {
32235
+ if (this.#disposed || this.#subs.get(name) !== entry) {
32236
+ this.#staleDropped += 1;
32237
+ return;
32238
+ }
32239
+ this.#repairPackets += 1;
32240
+ this.#repairBytes += packet.byteLength;
32241
+ this.#opts.feed(packet, name);
32242
+ }
32243
+ #closeAll() {
32244
+ for (const entry of this.#subs.values()) entry.sub.close();
32245
+ this.#subs.clear();
32246
+ }
32247
+ };
32248
+
31924
32249
  // src/repair-fetch.ts
31925
32250
  var MOQT_PRIORITY_MAX = 255;
31926
32251
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
@@ -32566,6 +32891,7 @@ export {
32566
32891
  REPAIR_LIFECYCLE_LOG_PREFIX,
32567
32892
  REPAIR_PRIORITY_MAX,
32568
32893
  REPAIR_PRIORITY_MIN,
32894
+ RepairLayerSubscriptions,
32569
32895
  SOURCE_MEDIA_PRIORITY_MAX,
32570
32896
  SampleCombineError,
32571
32897
  SampleCombiner,
@@ -32590,6 +32916,7 @@ export {
32590
32916
  catalogGroupDurationMs,
32591
32917
  catalogKeyframeCadenceMs,
32592
32918
  catalogRepairTarget,
32919
+ catalogTrackDeliveryMode,
32593
32920
  catalogTrackFecUnavailable,
32594
32921
  classifyH264Keyframe,
32595
32922
  classifyHevcKeyframe,
@@ -32610,6 +32937,7 @@ export {
32610
32937
  detectH264Keyframe,
32611
32938
  detectHevcKeyframe,
32612
32939
  driftCheck,
32940
+ encodeCmcdRecord,
32613
32941
  encodeDataSegment,
32614
32942
  encodeFecSourceInfo,
32615
32943
  encodeLocTimestamp,