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

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 (77) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +491 -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.js +12 -19
  32. package/dist/mmtp-container-parser.js.map +2 -2
  33. package/dist/mmtp-container-parser.no-modules.js +133 -34
  34. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  35. package/dist/mmtp-decode-pipeline.js +12 -19
  36. package/dist/mmtp-decode-pipeline.js.map +2 -2
  37. package/dist/mmtp-fec-session.d.ts +41 -3
  38. package/dist/mmtp-fec-session.d.ts.map +1 -1
  39. package/dist/mmtp-fec-session.js +118 -13
  40. package/dist/mmtp-fec-session.js.map +3 -3
  41. package/dist/mmtp-router.js +36 -34
  42. package/dist/mmtp-router.js.map +2 -2
  43. package/dist/multicast-auth-catalog.js.map +1 -1
  44. package/dist/multicast-input.js.map +1 -1
  45. package/dist/multicast-manager.d.ts +2 -0
  46. package/dist/multicast-manager.d.ts.map +1 -1
  47. package/dist/multicast-manager.js +37 -34
  48. package/dist/multicast-manager.js.map +2 -2
  49. package/dist/multicast-transport.js +24 -15
  50. package/dist/multicast-transport.js.map +2 -2
  51. package/dist/multicast-unwrap.js +12 -19
  52. package/dist/multicast-unwrap.js.map +2 -2
  53. package/dist/no-modules.js +130 -33
  54. package/dist/no-modules.js.map +3 -3
  55. package/dist/repair-fetch.js.map +1 -1
  56. package/dist/repair-layer-subscriptions.d.ts +82 -0
  57. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  58. package/dist/repair-layer-subscriptions.js +192 -0
  59. package/dist/repair-layer-subscriptions.js.map +7 -0
  60. package/dist/signaling.js.map +2 -2
  61. package/dist/timestamp-codec.js +12 -19
  62. package/dist/timestamp-codec.js.map +2 -2
  63. package/dist/track-session.d.ts +1 -1
  64. package/dist/track-session.js.map +1 -1
  65. package/dist/wasm-mfu-reassembler.js.map +1 -1
  66. package/dist/wasm-mmtp-container-parser.js +12 -19
  67. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  68. package/package.json +8 -5
  69. package/schemas/catalog.schema.json +4 -0
  70. package/src/catalog-fec-params.ts +36 -1
  71. package/src/cmcd-producer.ts +189 -179
  72. package/src/fec-worker-setup.ts +2 -2
  73. package/src/index.ts +17 -2
  74. package/src/mmtp-fec-session.ts +115 -15
  75. package/src/multicast-manager.ts +3 -0
  76. package/src/repair-layer-subscriptions.ts +261 -0
  77. 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) {
@@ -28678,6 +28726,7 @@ var MulticastManager = class {
28678
28726
  mode: endpoint.mode,
28679
28727
  amtRelay: endpoint.amtRelay,
28680
28728
  amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : void 0),
28729
+ amtDiscovery: endpoint.amtDiscovery,
28681
28730
  packetIds: endpoint.packetIds,
28682
28731
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28683
28732
  // A protected repair group's records ride in band on its port (7.2.3).
@@ -29626,7 +29675,7 @@ async function setupFecWorkerForTrack(opts) {
29626
29675
  blockProfile,
29627
29676
  k: opts.fecK,
29628
29677
  // 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.
29678
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29630
29679
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29631
29680
  symbolSize: opts.fecSymbolSize,
29632
29681
  interleaveDepth: interleaveDepthBlocks,
@@ -30168,24 +30217,24 @@ var MediaRouter = class {
30168
30217
  if (!this.#multicastVideoKeyed) return;
30169
30218
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30170
30219
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30171
- const token = ++this.#sourceSwitchToken;
30220
+ const token2 = ++this.#sourceSwitchToken;
30172
30221
  const maybeReady = this.#onSourceSwitch?.("multicast");
30173
30222
  if (!maybeReady || typeof maybeReady.then !== "function") {
30174
- this.#commitMulticastActivation(token);
30223
+ this.#commitMulticastActivation(token2);
30175
30224
  return;
30176
30225
  }
30177
30226
  const ready = Promise.resolve(maybeReady);
30178
- this.#pendingMulticastToken = token;
30227
+ this.#pendingMulticastToken = token2;
30179
30228
  this.#sourceSwitchReady = ready;
30180
30229
  void ready.then(
30181
- () => this.#commitMulticastActivation(token),
30182
- (err) => this.#rejectMulticastActivation(token, err)
30230
+ () => this.#commitMulticastActivation(token2),
30231
+ (err) => this.#rejectMulticastActivation(token2, err)
30183
30232
  ).finally(() => {
30184
30233
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30185
30234
  });
30186
30235
  }
30187
- #commitMulticastActivation(token) {
30188
- if (token !== this.#sourceSwitchToken) return;
30236
+ #commitMulticastActivation(token2) {
30237
+ if (token2 !== this.#sourceSwitchToken) return;
30189
30238
  this.#active = "multicast";
30190
30239
  this.#suppressed = true;
30191
30240
  this.#pendingMulticastToken = null;
@@ -30196,9 +30245,9 @@ var MediaRouter = class {
30196
30245
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30197
30246
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30198
30247
  }
30199
- #rejectMulticastActivation(token, err) {
30200
- if (token !== this.#sourceSwitchToken) return;
30201
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30248
+ #rejectMulticastActivation(token2, err) {
30249
+ if (token2 !== this.#sourceSwitchToken) return;
30250
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30202
30251
  this.#pendingMulticastVideo = null;
30203
30252
  this.#pendingMulticastAudio = null;
30204
30253
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31167,63 +31216,65 @@ var FecStarvationDetector = class {
31167
31216
 
31168
31217
  // src/cmcd-producer.ts
31169
31218
  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}`;
31219
+ var DEFAULT_INTERVAL_MS = 3e4;
31220
+ var CUSTOM_PREFIX = "com.blockcast-";
31221
+ var MAX_CUSTOM_VALUE = 64;
31222
+ var TRANSPORT_TOKEN = {
31223
+ native: "ssm",
31224
+ amt: "amt",
31225
+ moq: "moq"
31226
+ };
31227
+ function sfString(value) {
31228
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31229
+ }
31230
+ function encodeCmcdRecord(record) {
31231
+ return Object.keys(record).sort().map((key) => {
31232
+ const value = record[key];
31233
+ if (typeof value === "number") return `${key}=${value}`;
31234
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31235
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31236
+ return `${key}=${value.token}`;
31202
31237
  }).join(",");
31203
31238
  }
31239
+ var token = (value) => ({ token: value });
31240
+ function putCustom(record, name, value) {
31241
+ if (value === void 0) return;
31242
+ if (typeof value === "number") value = String(value);
31243
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31244
+ record[CUSTOM_PREFIX + name] = value;
31245
+ }
31204
31246
  var CmcdProducer = class {
31205
- #onCmcdParams;
31206
31247
  #onCmcdPost;
31248
+ #intervalMs;
31249
+ #sessionId;
31250
+ #now;
31251
+ #sn = 0;
31252
+ #lastHeartbeat = null;
31207
31253
  #transport;
31208
31254
  #lastStats;
31209
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31210
31255
  #playMode = null;
31211
31256
  #deltaFailRate = null;
31212
31257
  #switchMiscount = 0;
31213
31258
  #manifestStats = null;
31214
31259
  #fecFailClosed = null;
31260
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31261
+ #lifecycle = null;
31215
31262
  constructor(options) {
31216
- this.#onCmcdParams = options.onCmcdParams;
31217
31263
  this.#onCmcdPost = options.onCmcdPost;
31264
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31265
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31266
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31267
+ }
31268
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31269
+ this.#now = options.now ?? Date.now;
31218
31270
  }
31219
31271
  /** Update the stored transport state. Called by player wiring layer. */
31220
31272
  updateTransport(snapshot) {
31221
31273
  this.#transport = snapshot;
31222
31274
  }
31223
31275
  /**
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).
31276
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31277
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31227
31278
  */
31228
31279
  updatePlayMode(mode) {
31229
31280
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31234,9 +31285,8 @@ var CmcdProducer = class {
31234
31285
  this.#playMode = mode;
31235
31286
  }
31236
31287
  /**
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.
31288
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31289
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31240
31290
  */
31241
31291
  updateDeltaFailRate(rate) {
31242
31292
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31246,112 +31296,139 @@ var CmcdProducer = class {
31246
31296
  }
31247
31297
  this.#deltaFailRate = rate;
31248
31298
  }
31249
- /**
31250
- * Bump the cumulative switching-set misalignment counter. Emitted as
31251
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31252
- */
31299
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31253
31300
  incrementSwitchMiscount() {
31254
31301
  this.#switchMiscount += 1;
31255
31302
  }
31256
31303
  /**
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.
31304
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31305
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31306
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31307
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31308
+ * with no loss.
31261
31309
  */
31262
31310
  updateFecFailClosed(reason) {
31311
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31263
31312
  this.#fecFailClosed = reason;
31313
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31264
31314
  }
31265
31315
  updateManifestVerification(stats) {
31266
31316
  this.#manifestStats = stats;
31267
31317
  }
31268
31318
  /**
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.
31319
+ * Count a video block's resolution from the repair lifecycle: recovered in
31320
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31321
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31322
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31273
31323
  */
31324
+ onRepairLifecycle(event) {
31325
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31326
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31327
+ if (event.via === "inband") this.#lifecycle.inband++;
31328
+ else this.#lifecycle.fetch++;
31329
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31330
+ this.#lifecycle.failedKeyframe++;
31331
+ }
31332
+ }
31333
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31274
31334
  emitSwitchDrift(driftUs) {
31275
31335
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31276
31336
  throw new Error(
31277
31337
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31278
31338
  );
31279
31339
  }
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);
31340
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31341
+ putCustom(record, "sdu", Math.round(driftUs));
31342
+ this.#post([record]);
31286
31343
  }
31287
- /**
31288
- * Process a FecTrackStats snapshot. Finds the video track, builds
31289
- * x-bc-* vendor keys, and fires both delivery callbacks.
31290
- */
31344
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31291
31345
  onSnapshot(data) {
31292
31346
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31293
31347
  if (!videoTrack) return;
31294
31348
  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);
31349
+ this.#heartbeat();
31317
31350
  }
31318
31351
  /**
31319
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31320
- * the caller because only the transport integration knows when state changed.
31352
+ * Take transport state for receivers that do not own FEC stats, and send the
31353
+ * heartbeat when due.
31321
31354
  */
31322
31355
  onTransportSnapshot(data) {
31323
31356
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31324
31357
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31325
31358
  }
31326
31359
  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");
31360
+ if (data.switchMiscount !== void 0) {
31361
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31362
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31363
+ }
31364
+ this.#switchMiscount = data.switchMiscount;
31329
31365
  }
31330
31366
  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);
31367
+ this.#heartbeat();
31336
31368
  }
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
- */
31369
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31342
31370
  onTransition(event) {
31343
- const params = {};
31344
- if (this.#lastStats) {
31345
- Object.assign(params, buildFecParams(this.#lastStats));
31371
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31372
+ putCustom(record, "ct", token(event));
31373
+ this.#putTransport(record);
31374
+ this.#post([record]);
31375
+ }
31376
+ #base(event) {
31377
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31378
+ }
31379
+ #heartbeat() {
31380
+ const now = this.#now();
31381
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31382
+ this.#lastHeartbeat = now;
31383
+ this.#post([this.#base("t"), this.#counters()]);
31384
+ }
31385
+ #putTransport(record) {
31386
+ const snap = this.#transport;
31387
+ if (!snap) return;
31388
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31389
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31390
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31391
+ }
31392
+ putCustom(record, "ar", snap.amtRelay);
31393
+ }
31394
+ /** The `e=ce` counter report. */
31395
+ #counters() {
31396
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31397
+ const stats = this.#lastStats;
31398
+ if (stats) {
31399
+ putCustom(record, "fa", stats.config.algorithm);
31400
+ putCustom(record, "fk", stats.config.k);
31401
+ putCustom(record, "fp", stats.config.repairCount);
31402
+ putCustom(record, "pr", stats.sourceSymbols);
31403
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31404
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31405
+ putCustom(record, "fbf", stats.blocksFailed);
31406
+ }
31407
+ if (this.#lifecycle) {
31408
+ putCustom(record, "fbr", this.#lifecycle.inband);
31409
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31410
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31411
+ }
31412
+ this.#putTransport(record);
31413
+ if (this.#playMode !== null) {
31414
+ putCustom(record, "pm", token(this.#playMode));
31415
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31416
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31417
+ }
31418
+ }
31419
+ if (this.#fecFailClosed) {
31420
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31421
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31422
+ }
31423
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31424
+ if (this.#manifestStats) {
31425
+ putCustom(record, "mv", this.#manifestStats.verified);
31426
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31346
31427
  }
31347
- Object.assign(params, buildTransportParams(this.#transport));
31348
- params["x-bc-ct"] = event;
31349
- this.#emit(params);
31428
+ return record;
31350
31429
  }
31351
- /** Fire both delivery callbacks with the given params. */
31352
- #emit(params) {
31353
- this.#onCmcdParams?.(params);
31354
- this.#onCmcdPost?.(formatQueryString(params));
31430
+ #post(records) {
31431
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31355
31432
  }
31356
31433
  };
31357
31434
 
@@ -31447,18 +31524,31 @@ function resolveRepairLayers(catalog, source) {
31447
31524
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31448
31525
  };
31449
31526
  }
31527
+ function valueAt(catalog, pointer) {
31528
+ let node = catalog;
31529
+ for (const segment of pointer.split("/").slice(1)) {
31530
+ if (typeof node !== "object" || node === null) return void 0;
31531
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
31532
+ }
31533
+ return node;
31534
+ }
31535
+ function catalogTrackDeliveryMode(catalog, source) {
31536
+ const binding = bindingOf(catalog, source);
31537
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
31538
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
31539
+ }
31450
31540
  function moqtPriority2(value) {
31451
31541
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31452
31542
  }
31453
31543
  function catalogRepairTarget(catalog, source) {
31454
31544
  const binding = bindingOf(catalog, source);
31455
31545
  if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
31456
- const layerZero = binding.layers[0];
31546
+ const layerZero2 = binding.layers[0];
31457
31547
  const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
31458
31548
  const tracks = catalog.tracks;
31459
31549
  const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : void 0;
31460
31550
  const sourcePriority = entry ? moqtPriority2(entry["priority"]) : null;
31461
- return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
31551
+ return { repairTrack: layerZero2.repairTrackName, sourcePriority, repairPriority: layerZero2.priority };
31462
31552
  }
31463
31553
  function numericField(value) {
31464
31554
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
@@ -31490,6 +31580,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31490
31580
  repairTrackName: base.repairTrack,
31491
31581
  repairLayers: repair.baseLayers,
31492
31582
  repairOverlays: repair.overlays,
31583
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31493
31584
  ...mediaRate
31494
31585
  };
31495
31586
  }
@@ -31540,7 +31631,14 @@ function resolveCatalogTrackFec(catalog, tracks) {
31540
31631
  var PATH_BITS = { moq: 1, multicast: 2 };
31541
31632
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
31542
31633
  var MmtpFecSession = class {
31634
+ /** P_0 = B_0, the held total every repair packet is checked against. */
31635
+ get repairSymbols() {
31636
+ return this.#repairSymbols;
31637
+ }
31543
31638
  #decoder;
31639
+ #repairSymbols;
31640
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
31641
+ #message;
31544
31642
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
31545
31643
  #blocks = /* @__PURE__ */ new Map();
31546
31644
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -31578,6 +31676,24 @@ var MmtpFecSession = class {
31578
31676
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
31579
31677
  */
31580
31678
  constructor(config) {
31679
+ if (config && "alFecFlow" in config) {
31680
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
31681
+ if (!message) {
31682
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
31683
+ }
31684
+ this.#message = message;
31685
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
31686
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
31687
+ this.sourceSymbols = message.maximumK;
31688
+ this.#repairSymbols = message.maximumP;
31689
+ this.symbolSize = message.repairSymbolSize;
31690
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
31691
+ this.#repairPacketId = message.repairFlowId;
31692
+ this.#heldLayers = layerZero(message.maximumP);
31693
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
31694
+ this.#decoder = buildDecoder(config.Decoder, this);
31695
+ return;
31696
+ }
31581
31697
  const fec = config?.fec;
31582
31698
  if (!fec || typeof fec !== "object") {
31583
31699
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -31588,7 +31704,7 @@ var MmtpFecSession = class {
31588
31704
  }
31589
31705
  this.algorithm = fec.scheme;
31590
31706
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
31591
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31707
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
31592
31708
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
31593
31709
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
31594
31710
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -31608,17 +31724,30 @@ var MmtpFecSession = class {
31608
31724
  config.heldLayers
31609
31725
  );
31610
31726
  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;
31727
+ this.#decoder = buildDecoder(config.Decoder, this);
31728
+ }
31729
+ /**
31730
+ * A later version of the AL-FEC message, catalog-less sessions only. A
31731
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
31732
+ * anchored check restarts at the new total, so a block generated under the
31733
+ * previous one stays valid before the first block carrying the new total
31734
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
31735
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
31736
+ * does. An entry with another asset, repair flow, code point, K or T, or
31737
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
31738
+ * a new session. A latched fail-closed stays latched either way.
31739
+ */
31740
+ updateAlFecFlow(flow) {
31741
+ const held = this.#message;
31742
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
31743
+ const next = alFecRaptorQFlowConfig(flow);
31744
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
31745
+ return "refused";
31746
+ }
31747
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
31748
+ this.#repairSymbols = next.maximumP;
31749
+ this.#heldLayers = layerZero(next.maximumP);
31750
+ return "raised";
31622
31751
  }
31623
31752
  /** True once release() ran. */
31624
31753
  get released() {
@@ -31713,7 +31842,7 @@ var MmtpFecSession = class {
31713
31842
  }
31714
31843
  if (admission.reject) {
31715
31844
  const verdict2 = this.#heldLayers.failsClosed(
31716
- repairTrackName,
31845
+ this.#trackFor(repairTrackName),
31717
31846
  frame.ssStart,
31718
31847
  frame.rsbLength,
31719
31848
  this.#anchorHorizon,
@@ -31728,7 +31857,7 @@ var MmtpFecSession = class {
31728
31857
  this.#repairPacketId ??= frame.packetId;
31729
31858
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
31730
31859
  const verdict = this.#heldLayers.admit(
31731
- repairTrackName,
31860
+ this.#trackFor(repairTrackName),
31732
31861
  frame.ssStart,
31733
31862
  frame.rsbLength,
31734
31863
  this.#anchorHorizon,
@@ -31787,6 +31916,10 @@ var MmtpFecSession = class {
31787
31916
  decoder.free();
31788
31917
  return true;
31789
31918
  }
31919
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
31920
+ #trackFor(repairTrackName) {
31921
+ return this.#message ? void 0 : repairTrackName;
31922
+ }
31790
31923
  #drop(reason) {
31791
31924
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
31792
31925
  return [];
@@ -31908,6 +32041,22 @@ var MmtpFecSession = class {
31908
32041
  }
31909
32042
  }
31910
32043
  };
32044
+ function layerZero(total) {
32045
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
32046
+ }
32047
+ function buildDecoder(Decoder, session) {
32048
+ if (typeof Decoder !== "function") {
32049
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
32050
+ }
32051
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
32052
+ try {
32053
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
32054
+ } catch (error) {
32055
+ decoder.free();
32056
+ throw error;
32057
+ }
32058
+ return decoder;
32059
+ }
31911
32060
  function requirePositiveInteger(value, path) {
31912
32061
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
31913
32062
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);
@@ -31921,6 +32070,168 @@ function requireNumber(value, path) {
31921
32070
  return value;
31922
32071
  }
31923
32072
 
32073
+ // src/repair-layer-subscriptions.ts
32074
+ var SERIAL32_HALF2 = 2147483648;
32075
+ var RepairLayerSubscriptions = class {
32076
+ #opts;
32077
+ #windowPackets;
32078
+ #margin;
32079
+ #dropAfterWindows;
32080
+ #source;
32081
+ #layerCount = 0;
32082
+ #subs = /* @__PURE__ */ new Map();
32083
+ #disposed = false;
32084
+ #lastSeq;
32085
+ #expected = 0;
32086
+ #lost = 0;
32087
+ #wantFewer = 0;
32088
+ #lossRate = null;
32089
+ #repairPackets = 0;
32090
+ #repairBytes = 0;
32091
+ #staleDropped = 0;
32092
+ constructor(opts) {
32093
+ this.#opts = opts;
32094
+ this.#source = opts.source;
32095
+ this.#windowPackets = opts.windowPackets ?? 500;
32096
+ this.#margin = opts.margin ?? 2;
32097
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
32098
+ }
32099
+ get layerCount() {
32100
+ return this.#layerCount;
32101
+ }
32102
+ /**
32103
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
32104
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
32105
+ * or interleaved sequences read as loss.
32106
+ */
32107
+ noteSourceSequence(seq) {
32108
+ if (this.#disposed || this.#delivery() === "stream") return;
32109
+ seq >>>= 0;
32110
+ if (this.#lastSeq === void 0) {
32111
+ this.#lastSeq = seq;
32112
+ return;
32113
+ }
32114
+ const d = serial32Distance(this.#lastSeq, seq);
32115
+ if (d === 0) return;
32116
+ const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
32117
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
32118
+ this.#lastSeq = seq;
32119
+ this.#expected = 0;
32120
+ this.#lost = 0;
32121
+ return;
32122
+ }
32123
+ if (back === 0) {
32124
+ this.#expected += d;
32125
+ this.#lost += d - 1;
32126
+ this.#lastSeq = seq;
32127
+ } else if (this.#lost > 0) {
32128
+ this.#lost -= 1;
32129
+ }
32130
+ if (this.#expected >= this.#windowPackets) this.#closeWindow();
32131
+ }
32132
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
32133
+ switchSource(next) {
32134
+ if (this.#disposed) return;
32135
+ this.#closeAll();
32136
+ this.#source = next;
32137
+ this.#lastSeq = void 0;
32138
+ this.#expected = 0;
32139
+ this.#lost = 0;
32140
+ this.#wantFewer = 0;
32141
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32142
+ this.#reconcile();
32143
+ }
32144
+ snapshot() {
32145
+ return {
32146
+ delivery: this.#delivery(),
32147
+ layerCount: this.#layerCount,
32148
+ subscribedTracks: [...this.#subs.keys()],
32149
+ repairPackets: this.#repairPackets,
32150
+ repairBytes: this.#repairBytes,
32151
+ staleDropped: this.#staleDropped,
32152
+ lossRate: this.#lossRate
32153
+ };
32154
+ }
32155
+ dispose() {
32156
+ if (this.#disposed) return;
32157
+ this.#closeAll();
32158
+ this.#disposed = true;
32159
+ }
32160
+ #delivery() {
32161
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32162
+ }
32163
+ #layers() {
32164
+ return this.#source.repairLayers ?? [];
32165
+ }
32166
+ #closeWindow() {
32167
+ const rate = this.#lost / this.#expected;
32168
+ this.#lossRate = rate;
32169
+ this.#expected = 0;
32170
+ this.#lost = 0;
32171
+ const target = this.#targetLayers(rate);
32172
+ if (target > this.#layerCount) {
32173
+ this.#wantFewer = 0;
32174
+ this.#layerCount = target;
32175
+ } else if (target < this.#layerCount) {
32176
+ this.#wantFewer += 1;
32177
+ if (this.#wantFewer >= this.#dropAfterWindows) {
32178
+ this.#wantFewer = 0;
32179
+ this.#layerCount -= 1;
32180
+ }
32181
+ } else {
32182
+ this.#wantFewer = 0;
32183
+ }
32184
+ this.#reconcile();
32185
+ }
32186
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
32187
+ #targetLayers(rate) {
32188
+ if (rate <= 0) return 0;
32189
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
32190
+ const layers = this.#layers();
32191
+ let held = 0;
32192
+ for (let n = 0; n < layers.length; n++) {
32193
+ held += layers[n].repairSymbols;
32194
+ if (held >= needed) return n + 1;
32195
+ }
32196
+ return layers.length;
32197
+ }
32198
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
32199
+ #reconcile() {
32200
+ const want = /* @__PURE__ */ new Map();
32201
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
32202
+ want.set(layer.repairTrackName, layer.priority);
32203
+ }
32204
+ const overlay = this.#source.repairOverlays?.[0];
32205
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
32206
+ for (const [name, entry] of this.#subs) {
32207
+ if (!want.has(name)) {
32208
+ entry.sub.close();
32209
+ this.#subs.delete(name);
32210
+ }
32211
+ }
32212
+ for (const [name, priority] of want) {
32213
+ if (this.#subs.has(name)) continue;
32214
+ const entry = {};
32215
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
32216
+ this.#subs.set(name, entry);
32217
+ }
32218
+ }
32219
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
32220
+ #onPacket(name, entry, packet) {
32221
+ if (this.#disposed || this.#subs.get(name) !== entry) {
32222
+ this.#staleDropped += 1;
32223
+ return;
32224
+ }
32225
+ this.#repairPackets += 1;
32226
+ this.#repairBytes += packet.byteLength;
32227
+ this.#opts.feed(packet, name);
32228
+ }
32229
+ #closeAll() {
32230
+ for (const entry of this.#subs.values()) entry.sub.close();
32231
+ this.#subs.clear();
32232
+ }
32233
+ };
32234
+
31924
32235
  // src/repair-fetch.ts
31925
32236
  var MOQT_PRIORITY_MAX = 255;
31926
32237
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
@@ -32566,6 +32877,7 @@ export {
32566
32877
  REPAIR_LIFECYCLE_LOG_PREFIX,
32567
32878
  REPAIR_PRIORITY_MAX,
32568
32879
  REPAIR_PRIORITY_MIN,
32880
+ RepairLayerSubscriptions,
32569
32881
  SOURCE_MEDIA_PRIORITY_MAX,
32570
32882
  SampleCombineError,
32571
32883
  SampleCombiner,
@@ -32590,6 +32902,7 @@ export {
32590
32902
  catalogGroupDurationMs,
32591
32903
  catalogKeyframeCadenceMs,
32592
32904
  catalogRepairTarget,
32905
+ catalogTrackDeliveryMode,
32593
32906
  catalogTrackFecUnavailable,
32594
32907
  classifyH264Keyframe,
32595
32908
  classifyHevcKeyframe,
@@ -32610,6 +32923,7 @@ export {
32610
32923
  detectH264Keyframe,
32611
32924
  detectHevcKeyframe,
32612
32925
  driftCheck,
32926
+ encodeCmcdRecord,
32613
32927
  encodeDataSegment,
32614
32928
  encodeFecSourceInfo,
32615
32929
  encodeLocTimestamp,