@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00

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 (105) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1949 -374
  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 +65 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +70 -12
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +403 -58
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +6 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +48 -13
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +97 -25
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +79 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +219 -45
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +342 -63
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +218 -45
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +43 -9
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +2006 -95
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +28 -5
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +16116 -15005
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +336 -59
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.d.ts +36 -8
  72. package/dist/repair-fetch.d.ts.map +1 -1
  73. package/dist/repair-fetch.js +285 -26
  74. package/dist/repair-fetch.js.map +3 -3
  75. package/dist/repair-layer-subscriptions.d.ts +82 -0
  76. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  77. package/dist/repair-layer-subscriptions.js +192 -0
  78. package/dist/repair-layer-subscriptions.js.map +7 -0
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/timestamp-codec.js +12 -19
  81. package/dist/timestamp-codec.js.map +2 -2
  82. package/dist/track-session.d.ts +1 -1
  83. package/dist/track-session.d.ts.map +1 -1
  84. package/dist/track-session.js +1 -0
  85. package/dist/track-session.js.map +2 -2
  86. package/dist/wasm-mfu-reassembler.js.map +1 -1
  87. package/dist/wasm-mmtp-container-parser.js +218 -45
  88. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  89. package/package.json +8 -5
  90. package/schemas/catalog.schema.json +4 -0
  91. package/src/catalog-fec-params.ts +36 -1
  92. package/src/cmcd-producer.ts +194 -179
  93. package/src/fec-manager.ts +433 -32
  94. package/src/fec-worker-setup.ts +2 -2
  95. package/src/index.ts +20 -2
  96. package/src/iso-block-locator.ts +133 -31
  97. package/src/iso-bmff-timing.ts +40 -1
  98. package/src/mmtp-container-parser.ts +306 -29
  99. package/src/mmtp-fec-session.ts +115 -15
  100. package/src/mmtp-router.ts +629 -54
  101. package/src/multicast-manager.ts +70 -11
  102. package/src/multicast-transport.ts +44 -20
  103. package/src/repair-fetch.ts +363 -49
  104. package/src/repair-layer-subscriptions.ts +261 -0
  105. package/src/track-session.ts +4 -1
@@ -10,6 +10,8 @@ var PACKET_TYPE_MPU = 0;
10
10
  var PACKET_TYPE_REPAIR = 3;
11
11
  var MMTP_SIGNALING_PACKET_ID = 0;
12
12
  var MMTP_PACKET_ID_MAX = 65535;
13
+ var FEC_CODE_REED_SOLOMON = 1;
14
+ var FEC_CODE_RAPTORQ = 3;
13
15
  function parseMMTPHeader(buffer) {
14
16
  if (buffer.length < MMTP_HEADER_SIZE) {
15
17
  throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
@@ -150,30 +152,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
150
152
  const authLength = view2.getUint16(2, false);
151
153
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
152
154
  }
153
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
155
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
154
156
  if (declaredEnd > payload.length)
155
157
  return null;
156
158
  if (options?.skipFecId) {
157
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
159
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
158
160
  }
159
- const overlay = options?.overlayDeclared === true && rapFlag;
160
161
  if (fecType === 1) {
161
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
162
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
162
163
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
163
164
  return null;
164
165
  }
165
- return { hasFecId: true, hasOverlayFecId: overlay };
166
+ return { hasFecId: true };
166
167
  }
167
- if (fecType !== 0 || overlay)
168
+ if (fecType !== 0)
168
169
  return null;
169
170
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
170
- return { hasFecId: false, hasOverlayFecId: false };
171
+ return { hasFecId: false };
171
172
  }
172
173
  return null;
173
174
  }
174
- function sourceFecTrailerSize(overlay) {
175
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
176
- }
177
175
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
178
176
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
179
177
  return {
@@ -226,11 +224,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
226
224
  if ("rejection" in layoutResult)
227
225
  return layoutResult;
228
226
  const { layout } = layoutResult;
229
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
227
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
230
228
  if (!trailerLayout) {
231
229
  if (!options?.skipFecId && header.fecType === 1) {
232
230
  const observed = payload.length - layout.dataEnd;
233
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
231
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
234
232
  if (observed !== expected) {
235
233
  return {
236
234
  rejection: {
@@ -262,13 +260,10 @@ function extractPlannedMfu(mmtpPacket, result) {
262
260
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
263
261
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
264
262
  const data = payload.subarray(mfuOffset, dataEnd);
265
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
266
- const ssIdAt = (offset) => ({
267
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
268
- });
269
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
270
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
271
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
263
+ const fecPayloadId = trailerLayout.hasFecId ? {
264
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
265
+ } : null;
266
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
272
267
  }
273
268
  function sourceFecPayloadIdOf(mmtpPacket, header) {
274
269
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -360,6 +355,50 @@ var NTP_SHORT_BITS = 32;
360
355
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
361
356
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
362
357
 
358
+ // ../mmt-base/dist/signalling.js
359
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
360
+ var SSBG_MODE_1 = 1;
361
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
362
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
363
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
364
+ var REED_SOLOMON_MAX_BLOCK = 255;
365
+ function alFecRaptorQFlowConfig(flow) {
366
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
367
+ return null;
368
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
369
+ return null;
370
+ if (flow.packetIds.length !== 1)
371
+ return null;
372
+ const repair = flow.repairFlows[0];
373
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
374
+ const t = flow.repairSymbolSize;
375
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
376
+ return null;
377
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
378
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
379
+ return null;
380
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
381
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
382
+ return null;
383
+ } else {
384
+ return null;
385
+ }
386
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
387
+ return null;
388
+ return {
389
+ sourcePacketIds: [...flow.packetIds],
390
+ repairFlowId: repair.repairFlowId,
391
+ fecCodeId,
392
+ ssbgMode: flow.ssbgMode,
393
+ fecPayloadIdMode: flow.fecPayloadIdMode,
394
+ repairSymbolSize: t,
395
+ maximumK: k,
396
+ maximumP: p,
397
+ protectionWindowTime: repair.protectionWindowTime,
398
+ protectionWindowSize: repair.protectionWindowSize
399
+ };
400
+ }
401
+
363
402
  // ../mmt-base/dist/serial-number.js
364
403
  var SERIAL32_SPAN = 4294967296;
365
404
  var SERIAL32_HALF = 2147483648;
@@ -5529,9 +5568,21 @@ var IsoBmffTimingResolver = class {
5529
5568
  }
5530
5569
  #track = null;
5531
5570
  #fragments = /* @__PURE__ */ new Map();
5571
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
5572
+ #firstSamples = /* @__PURE__ */ new Map();
5573
+ /**
5574
+ * First sample and sample count of the newest fragments whose every slot was
5575
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
5576
+ * matches the combiner, since it retires an abandoned sample as well.
5577
+ */
5578
+ #delivered = /* @__PURE__ */ new Map();
5532
5579
  #pendingSamples = 0;
5533
5580
  #evictedSamples = 0;
5534
5581
  #lastMovieFragmentRejection = null;
5582
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
5583
+ get initialized() {
5584
+ return this.#track !== null;
5585
+ }
5535
5586
  get pendingSampleCount() {
5536
5587
  return this.#pendingSamples;
5537
5588
  }
@@ -5555,7 +5606,14 @@ var IsoBmffTimingResolver = class {
5555
5606
  this.#track = timing;
5556
5607
  return true;
5557
5608
  }
5558
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
5609
+ /**
5610
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
5611
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
5612
+ * fragment whose samples are all done is a duplicate: admitting it again
5613
+ * would charge stale timing and could displace the live MPU's. A source
5614
+ * copy is still admitted again (a rendition switch re-arms timing that way).
5615
+ */
5616
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
5559
5617
  this.#lastMovieFragmentRejection = null;
5560
5618
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
5561
5619
  this.#lastMovieFragmentRejection = "invalid";
@@ -5581,6 +5639,8 @@ var IsoBmffTimingResolver = class {
5581
5639
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
5582
5640
  );
5583
5641
  }
5642
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
5643
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
5584
5644
  let freed = 0;
5585
5645
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
5586
5646
  freed += this.#evictOldestFragment();
@@ -5594,6 +5654,7 @@ var IsoBmffTimingResolver = class {
5594
5654
  return false;
5595
5655
  }
5596
5656
  this.#fragments.set(key, [...parsed.samples]);
5657
+ this.#firstSamples.set(key, parsed.samples[0]);
5597
5658
  this.#pendingSamples += parsed.samples.length;
5598
5659
  return true;
5599
5660
  }
@@ -5651,6 +5712,7 @@ var IsoBmffTimingResolver = class {
5651
5712
  let live = 0;
5652
5713
  for (const sample of samples) if (sample !== null) live++;
5653
5714
  this.#fragments.delete(oldest);
5715
+ this.#firstSamples.delete(oldest);
5654
5716
  this.#pendingSamples -= live;
5655
5717
  this.#evictedSamples += live;
5656
5718
  return live;
@@ -5666,12 +5728,23 @@ var IsoBmffTimingResolver = class {
5666
5728
  if (!samples || !samples[index]) return false;
5667
5729
  samples[index] = null;
5668
5730
  this.#pendingSamples--;
5669
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
5731
+ if (samples.every((value) => value === null)) {
5732
+ this.#fragments.delete(key);
5733
+ const first = this.#firstSamples.get(key);
5734
+ this.#firstSamples.delete(key);
5735
+ this.#delivered.delete(key);
5736
+ this.#delivered.set(key, { first, sampleCount: samples.length });
5737
+ if (this.#delivered.size > this.maxPendingSamples) {
5738
+ this.#delivered.delete(this.#delivered.keys().next().value);
5739
+ }
5740
+ }
5670
5741
  return true;
5671
5742
  }
5672
5743
  reset() {
5673
5744
  this.#track = null;
5674
5745
  this.#fragments.clear();
5746
+ this.#firstSamples.clear();
5747
+ this.#delivered.clear();
5675
5748
  this.#pendingSamples = 0;
5676
5749
  }
5677
5750
  };
@@ -5841,6 +5914,9 @@ function authoredGap(before, after) {
5841
5914
  function ssIdAfter(a, b) {
5842
5915
  return (a - b | 0) > 0;
5843
5916
  }
5917
+ function holdsRebuilt(stream) {
5918
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
5919
+ }
5844
5920
  var FecReleaseSequencer = class {
5845
5921
  #streams = /* @__PURE__ */ new Map();
5846
5922
  #gate;
@@ -5903,6 +5979,7 @@ var FecReleaseSequencer = class {
5903
5979
  stream.lastSbn = void 0;
5904
5980
  stream.discontinuity = false;
5905
5981
  stream.rapSsId = void 0;
5982
+ stream.sourced = false;
5906
5983
  }
5907
5984
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
5908
5985
  this.#recordStat("droppedStale");
@@ -5929,6 +6006,7 @@ var FecReleaseSequencer = class {
5929
6006
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
5930
6007
  else hi = mid;
5931
6008
  }
6009
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
5932
6010
  stream.buf.splice(lo, 0, frame);
5933
6011
  this.#drainUnlessDeferred(stream);
5934
6012
  }
@@ -5964,6 +6042,41 @@ var FecReleaseSequencer = class {
5964
6042
  }
5965
6043
  return false;
5966
6044
  }
6045
+ /**
6046
+ * These streams' sink is being replaced by one that has seen no source
6047
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
6048
+ * release to the sink being replaced the frames the unanchored path had
6049
+ * already released to it (#cursorPassed): there they waited in its queue,
6050
+ * which its replacement flushes. A stream that holds none is not drained:
6051
+ * it holds nothing the anchor kept back, and a drain would still force out
6052
+ * a frame held past the hold limit, which before the anchor waited for the
6053
+ * next push. Then treat the streams as unsourced until a source frame is
6054
+ * released.
6055
+ */
6056
+ unsource(packetId, catalogTrackName) {
6057
+ for (const stream of this.#streams.values()) {
6058
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
6059
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
6060
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
6061
+ else this.#drain(stream, true);
6062
+ }
6063
+ stream.sourced = false;
6064
+ }
6065
+ }
6066
+ }
6067
+ /**
6068
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
6069
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
6070
+ * packetId.
6071
+ */
6072
+ holdsRebuilt(packetId, catalogTrackName) {
6073
+ for (const stream of this.#streams.values()) {
6074
+ if (stream.packetId !== packetId) continue;
6075
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
6076
+ if (holdsRebuilt(stream)) return true;
6077
+ }
6078
+ return false;
6079
+ }
5967
6080
  /** Re-evaluate held gaps after FEC state changed. */
5968
6081
  poke(streamKey) {
5969
6082
  if (streamKey !== void 0) {
@@ -6055,16 +6168,32 @@ var FecReleaseSequencer = class {
6055
6168
  last: null,
6056
6169
  lastSbn: void 0,
6057
6170
  discontinuity: false,
6058
- rapSsId: void 0
6171
+ rapSsId: void 0,
6172
+ sourced: false,
6173
+ draining: 0
6059
6174
  };
6060
6175
  this.#streams.set(streamKey, stream);
6061
6176
  }
6062
6177
  return stream;
6063
6178
  }
6064
- #drain(stream) {
6179
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
6180
+ #drain(stream, asUnanchored = false) {
6181
+ stream.draining++;
6182
+ try {
6183
+ this.#drainLoop(stream, asUnanchored);
6184
+ } finally {
6185
+ stream.draining--;
6186
+ }
6187
+ }
6188
+ #drainLoop(stream, asUnanchored) {
6065
6189
  while (stream.buf.length > 0 && stream.last !== null) {
6066
6190
  const head = stream.buf[0];
6067
6191
  const last = stream.last;
6192
+ if (compareFrameOrder(head, last) <= 0) {
6193
+ stream.buf.shift();
6194
+ this.#release(stream, head, void 0);
6195
+ continue;
6196
+ }
6068
6197
  if (stream.discontinuity) {
6069
6198
  stream.discontinuity = false;
6070
6199
  stream.buf.shift();
@@ -6078,9 +6207,10 @@ var FecReleaseSequencer = class {
6078
6207
  }
6079
6208
  const headSbn = head.meta.fecSbn;
6080
6209
  const from = stream.lastSbn ?? headSbn;
6081
- const to = headSbn ?? stream.lastSbn;
6210
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
6082
6211
  let lost = [];
6083
6212
  const unanchored = from === void 0 && to === void 0;
6213
+ let unresolved = false;
6084
6214
  if (from !== void 0 && to !== void 0) {
6085
6215
  const resolved = this.#gate.resolvedLoss(
6086
6216
  stream.packetId,
@@ -6089,7 +6219,8 @@ var FecReleaseSequencer = class {
6089
6219
  Math.max(from, to),
6090
6220
  from
6091
6221
  );
6092
- if (resolved === null) {
6222
+ unresolved = resolved === null;
6223
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
6093
6224
  const heldMs = performance.now() - head.arrivedAtMs;
6094
6225
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
6095
6226
  this.#recordStat("forcedWindowFlushes");
@@ -6097,22 +6228,56 @@ var FecReleaseSequencer = class {
6097
6228
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
6098
6229
  continue;
6099
6230
  }
6100
- lost = resolved;
6231
+ lost = resolved ?? [];
6101
6232
  }
6102
6233
  stream.buf.shift();
6103
6234
  this.#release(
6104
6235
  stream,
6105
6236
  head,
6106
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
6237
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
6107
6238
  );
6108
6239
  }
6109
6240
  }
6241
+ /**
6242
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
6243
+ * source cursor holds it unanchored, and neither does a later frame behind
6244
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
6245
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
6246
+ * like each frame that arrived in front of it since (stale there). The head
6247
+ * then goes, decided as it would be unanchored, once a frame the base path
6248
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
6249
+ * marked one: the base path has passed that one, so a frame still missing
6250
+ * before it can only come from a repair. It also goes while no source frame
6251
+ * has been released, when the direct path shows a recovered frame at once.
6252
+ * And it goes at once while a staged switch decides on when frames arrive
6253
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
6254
+ * sink being replaced: unanchored, it had already reached the sink.
6255
+ * The scan also writes: the `passed` marks it sets stay on the frames,
6256
+ * and push marks a frame it inserts in front of a marked one from them.
6257
+ */
6258
+ #cursorPassed(stream, asUnanchored) {
6259
+ let at = -1;
6260
+ for (let i = 0; i < stream.buf.length; i++) {
6261
+ const frame = stream.buf[i];
6262
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
6263
+ frame.passed = true;
6264
+ if (at < 0) at = i;
6265
+ } else if (frame.passed !== true) break;
6266
+ }
6267
+ if (at < 0) return false;
6268
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
6269
+ }
6110
6270
  #release(stream, frame, decision) {
6271
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
6272
+ if (stale) this.#recordStat("droppedStale");
6111
6273
  if (decision) frame.meta.gapBefore = decision;
6112
6274
  stream.last = frame;
6113
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
6275
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
6276
+ stream.lastSbn = frame.meta.fecSbn;
6277
+ }
6278
+ if (frame.meta.recovered !== true) stream.sourced = true;
6114
6279
  this.#gate.onReleased?.(frame.data, frame.meta);
6115
- this.#emit(frame.data, frame.meta);
6280
+ if (!stale) this.#emit(frame.data, frame.meta);
6116
6281
  }
6117
6282
  };
6118
6283
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -6153,6 +6318,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
6153
6318
  this.#reclaimedTimingSamples = 0;
6154
6319
  this.#evictedTimingSamples = 0;
6155
6320
  this.#rejectedMovieFragments = 0;
6321
+ this.#recoveredHeadsAwaitingTiming = {
6322
+ held: 0,
6323
+ expired: 0,
6324
+ discarded: 0,
6325
+ superseded: 0
6326
+ };
6156
6327
  this.#sequencers = /* @__PURE__ */ new Map();
6157
6328
  this.#fecGate = null;
6158
6329
  this.#fecSequencer = null;
@@ -6261,6 +6432,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6261
6432
  #reclaimedTimingSamples;
6262
6433
  #evictedTimingSamples;
6263
6434
  #rejectedMovieFragments;
6435
+ #recoveredHeadsAwaitingTiming;
6264
6436
  #sequencers;
6265
6437
  #fecGate;
6266
6438
  #fecSequencer;
@@ -6366,11 +6538,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
6366
6538
  /**
6367
6539
  * Typed view of the ISO-BMFF timing-sample budget.
6368
6540
  *
6369
- * This is the acceptance signal for the budget ratchet: `pending` walking
6370
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
6371
- * and it is observable here rather than only as media silently stopping.
6372
- * Per repo policy this is a producer-owned counter -- never infer budget
6373
- * state by parsing logs.
6541
+ * Each stream admits timing against its own window of `capacity` slots
6542
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
6543
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
6544
+ * timing a stream declared and never delivered shows up there. `pending`
6545
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
6546
+ * a producer-owned counter -- never infer budget state by parsing logs.
6374
6547
  */
6375
6548
  get timingBudgetStats() {
6376
6549
  return {
@@ -6381,6 +6554,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
6381
6554
  rejectedMovieFragments: this.#rejectedMovieFragments
6382
6555
  };
6383
6556
  }
6557
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
6558
+ get recoveredHeadsAwaitingTiming() {
6559
+ let pending = 0;
6560
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
6561
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
6562
+ }
6563
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
6564
+ }
6384
6565
  get reorderStats() {
6385
6566
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
6386
6567
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -6422,6 +6603,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
6422
6603
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
6423
6604
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
6424
6605
  }
6606
+ /** See FecReleaseSequencer.unsource. */
6607
+ unsourceFecRelease(packetId, catalogTrackName) {
6608
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
6609
+ }
6610
+ /** See FecReleaseSequencer.holdsRebuilt. */
6611
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
6612
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
6613
+ }
6425
6614
  /** Re-evaluate held gaps after FEC block state changed. */
6426
6615
  pokeFecRelease(packetId, catalogTrackName) {
6427
6616
  if (!this.#fecSequencer) return;
@@ -6554,7 +6743,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6554
6743
  const now = Date.now();
6555
6744
  for (const { awaitingTiming } of this.#timingStreams.values()) {
6556
6745
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
6557
- awaitingTiming.shift();
6746
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
6558
6747
  }
6559
6748
  }
6560
6749
  }
@@ -6672,11 +6861,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
6672
6861
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
6673
6862
  return this.#drop("fragment-unexpected-fragmentation");
6674
6863
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
6675
- if (!resolver2) return this.#drop("fragment-no-resolver");
6864
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
6676
6865
  const pendingBefore = resolver2.pendingSampleCount;
6677
6866
  const evictedBefore = resolver2.evictedSampleCount;
6678
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
6679
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
6867
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
6868
+ const admitted = resolver2.addMovieFragment(
6869
+ mpu.mpuSequenceNumber,
6870
+ data,
6871
+ remaining,
6872
+ options?.recovered === true
6873
+ );
6680
6874
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
6681
6875
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
6682
6876
  if (!admitted) {
@@ -6696,7 +6890,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6696
6890
  };
6697
6891
  }
6698
6892
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
6699
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
6893
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
6700
6894
  if (!resolver) return this.#drop("mfu-no-resolver");
6701
6895
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
6702
6896
  return this.#drop("mfu-empty-payload");
@@ -6717,6 +6911,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6717
6911
  mpuSequenceNumber: mpu.mpuSequenceNumber,
6718
6912
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
6719
6913
  sampleNumber: mfuDuHeader.sampleNumber,
6914
+ sequenceNumber: info.sequenceNumber,
6720
6915
  arrivedAt: Date.now()
6721
6916
  });
6722
6917
  return this.#drop("mfu-sample-timing-missing");
@@ -6742,7 +6937,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
6742
6937
  info,
6743
6938
  resolver,
6744
6939
  options?.recovered === true,
6745
- options?.fecSbn
6940
+ options?.fecSbn,
6941
+ options?.fecSbnFirst
6746
6942
  );
6747
6943
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
6748
6944
  }
@@ -6835,7 +7031,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
6835
7031
  info,
6836
7032
  resolver,
6837
7033
  options?.recovered === true,
6838
- options?.fecSbn
7034
+ options?.fecSbn,
7035
+ options?.fecSbnFirst
6839
7036
  );
6840
7037
  if (outcome.record !== void 0) return outcome.record;
6841
7038
  return this.#drop(outcome.reason);
@@ -6855,7 +7052,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6855
7052
  * attribution exists to keep readable. Leaving the choice at the call site
6856
7053
  * is what keeps both true.
6857
7054
  */
6858
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
7055
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
6859
7056
  const timing = resolver.sampleTiming(
6860
7057
  combined.mpuSequenceNumber,
6861
7058
  combined.movieFragmentSequenceNumber,
@@ -6872,6 +7069,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6872
7069
  );
6873
7070
  if (!meta) return { reason: "combined-meta-build-failed" };
6874
7071
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
7072
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
6875
7073
  if (!resolver.consumeSample(
6876
7074
  combined.mpuSequenceNumber,
6877
7075
  combined.movieFragmentSequenceNumber,
@@ -7040,6 +7238,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
7040
7238
  }
7041
7239
  this.#emitFrame(data, meta);
7042
7240
  }
7241
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
7242
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
7243
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
7244
+ }
7043
7245
  /**
7044
7246
  * Claim and return the authored timing for a recovered sample. Recovery does
7045
7247
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -7101,6 +7303,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
7101
7303
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
7102
7304
  this.#sequencers.clear();
7103
7305
  this.#fecSequencer?.discard();
7306
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
7307
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7308
+ }
7104
7309
  this.#timingStreams.clear();
7105
7310
  this.#pendingTimingSamples = 0;
7106
7311
  this.#reliableMoqStartupGate.clear();
@@ -7119,10 +7324,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
7119
7324
  return resolver;
7120
7325
  }
7121
7326
  #awaitTiming(streamKey, head) {
7122
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
7123
- if (!awaitingTiming) return;
7327
+ const state = this.#timingStreams.get(streamKey);
7328
+ if (!state) return;
7329
+ if (!head.options?.recovered) {
7330
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
7331
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
7332
+ state.awaitingTiming = kept;
7333
+ }
7334
+ const awaitingTiming = state.awaitingTiming;
7124
7335
  awaitingTiming.push(head);
7125
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
7336
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
7337
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
7338
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
7339
+ }
7340
+ }
7341
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
7342
+ #leaveAwaitingTiming(head, outcome) {
7343
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
7126
7344
  }
7127
7345
  /**
7128
7346
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -7150,6 +7368,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
7150
7368
  const state = this.#timingStreams.get(streamKey);
7151
7369
  if (!state) return;
7152
7370
  this.#timingStreams.delete(streamKey);
7371
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7153
7372
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
7154
7373
  if (this.#pendingTimingSamples < 0) {
7155
7374
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -8071,7 +8290,14 @@ var HeldLayerAdmission = class {
8071
8290
  var PATH_BITS = { moq: 1, multicast: 2 };
8072
8291
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
8073
8292
  var MmtpFecSession = class {
8293
+ /** P_0 = B_0, the held total every repair packet is checked against. */
8294
+ get repairSymbols() {
8295
+ return this.#repairSymbols;
8296
+ }
8074
8297
  #decoder;
8298
+ #repairSymbols;
8299
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
8300
+ #message;
8075
8301
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
8076
8302
  #blocks = /* @__PURE__ */ new Map();
8077
8303
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -8109,6 +8335,24 @@ var MmtpFecSession = class {
8109
8335
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
8110
8336
  */
8111
8337
  constructor(config) {
8338
+ if (config && "alFecFlow" in config) {
8339
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
8340
+ if (!message) {
8341
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
8342
+ }
8343
+ this.#message = message;
8344
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
8345
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
8346
+ this.sourceSymbols = message.maximumK;
8347
+ this.#repairSymbols = message.maximumP;
8348
+ this.symbolSize = message.repairSymbolSize;
8349
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
8350
+ this.#repairPacketId = message.repairFlowId;
8351
+ this.#heldLayers = layerZero(message.maximumP);
8352
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
8353
+ this.#decoder = buildDecoder(config.Decoder, this);
8354
+ return;
8355
+ }
8112
8356
  const fec = config?.fec;
8113
8357
  if (!fec || typeof fec !== "object") {
8114
8358
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -8119,7 +8363,7 @@ var MmtpFecSession = class {
8119
8363
  }
8120
8364
  this.algorithm = fec.scheme;
8121
8365
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
8122
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
8366
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
8123
8367
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
8124
8368
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
8125
8369
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -8139,17 +8383,30 @@ var MmtpFecSession = class {
8139
8383
  config.heldLayers
8140
8384
  );
8141
8385
  this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
8142
- if (typeof config.Decoder !== "function") {
8143
- throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
8144
- }
8145
- const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm);
8146
- try {
8147
- decoder.configure_params(this.sourceSymbols, this.symbolSize);
8148
- } catch (error) {
8149
- decoder.free();
8150
- throw error;
8151
- }
8152
- this.#decoder = decoder;
8386
+ this.#decoder = buildDecoder(config.Decoder, this);
8387
+ }
8388
+ /**
8389
+ * A later version of the AL-FEC message, catalog-less sessions only. A
8390
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
8391
+ * anchored check restarts at the new total, so a block generated under the
8392
+ * previous one stays valid before the first block carrying the new total
8393
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
8394
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
8395
+ * does. An entry with another asset, repair flow, code point, K or T, or
8396
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
8397
+ * a new session. A latched fail-closed stays latched either way.
8398
+ */
8399
+ updateAlFecFlow(flow) {
8400
+ const held = this.#message;
8401
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
8402
+ const next = alFecRaptorQFlowConfig(flow);
8403
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
8404
+ return "refused";
8405
+ }
8406
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
8407
+ this.#repairSymbols = next.maximumP;
8408
+ this.#heldLayers = layerZero(next.maximumP);
8409
+ return "raised";
8153
8410
  }
8154
8411
  /** True once release() ran. */
8155
8412
  get released() {
@@ -8244,7 +8501,7 @@ var MmtpFecSession = class {
8244
8501
  }
8245
8502
  if (admission.reject) {
8246
8503
  const verdict2 = this.#heldLayers.failsClosed(
8247
- repairTrackName,
8504
+ this.#trackFor(repairTrackName),
8248
8505
  frame.ssStart,
8249
8506
  frame.rsbLength,
8250
8507
  this.#anchorHorizon,
@@ -8259,7 +8516,7 @@ var MmtpFecSession = class {
8259
8516
  this.#repairPacketId ??= frame.packetId;
8260
8517
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
8261
8518
  const verdict = this.#heldLayers.admit(
8262
- repairTrackName,
8519
+ this.#trackFor(repairTrackName),
8263
8520
  frame.ssStart,
8264
8521
  frame.rsbLength,
8265
8522
  this.#anchorHorizon,
@@ -8318,6 +8575,10 @@ var MmtpFecSession = class {
8318
8575
  decoder.free();
8319
8576
  return true;
8320
8577
  }
8578
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
8579
+ #trackFor(repairTrackName) {
8580
+ return this.#message ? void 0 : repairTrackName;
8581
+ }
8321
8582
  #drop(reason) {
8322
8583
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
8323
8584
  return [];
@@ -8439,6 +8700,22 @@ var MmtpFecSession = class {
8439
8700
  }
8440
8701
  }
8441
8702
  };
8703
+ function layerZero(total) {
8704
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
8705
+ }
8706
+ function buildDecoder(Decoder, session) {
8707
+ if (typeof Decoder !== "function") {
8708
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
8709
+ }
8710
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
8711
+ try {
8712
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
8713
+ } catch (error) {
8714
+ decoder.free();
8715
+ throw error;
8716
+ }
8717
+ return decoder;
8718
+ }
8442
8719
  function requirePositiveInteger(value, path) {
8443
8720
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
8444
8721
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);