@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
@@ -73,6 +73,8 @@ var BlockcastMmt = (() => {
73
73
  var PACKET_TYPE_REPAIR = 3;
74
74
  var MMTP_SIGNALING_PACKET_ID = 0;
75
75
  var MMTP_PACKET_ID_MAX = 65535;
76
+ var FEC_CODE_REED_SOLOMON = 1;
77
+ var FEC_CODE_RAPTORQ = 3;
76
78
  function parseMMTPHeader(buffer) {
77
79
  if (buffer.length < MMTP_HEADER_SIZE) {
78
80
  throw new Error(`MMTP packet too small: ${buffer.length} bytes`);
@@ -213,30 +215,26 @@ var BlockcastMmt = (() => {
213
215
  const authLength = view2.getUint16(2, false);
214
216
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
215
217
  }
216
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
218
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
217
219
  if (declaredEnd > payload.length)
218
220
  return null;
219
221
  if (options?.skipFecId) {
220
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
222
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
221
223
  }
222
- const overlay = options?.overlayDeclared === true && rapFlag;
223
224
  if (fecType === 1) {
224
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
225
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
225
226
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
226
227
  return null;
227
228
  }
228
- return { hasFecId: true, hasOverlayFecId: overlay };
229
+ return { hasFecId: true };
229
230
  }
230
- if (fecType !== 0 || overlay)
231
+ if (fecType !== 0)
231
232
  return null;
232
233
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
233
- return { hasFecId: false, hasOverlayFecId: false };
234
+ return { hasFecId: false };
234
235
  }
235
236
  return null;
236
237
  }
237
- function sourceFecTrailerSize(overlay) {
238
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
239
- }
240
238
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
241
239
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
242
240
  return {
@@ -289,11 +287,11 @@ var BlockcastMmt = (() => {
289
287
  if ("rejection" in layoutResult)
290
288
  return layoutResult;
291
289
  const { layout } = layoutResult;
292
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
290
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
293
291
  if (!trailerLayout) {
294
292
  if (!options?.skipFecId && header.fecType === 1) {
295
293
  const observed = payload.length - layout.dataEnd;
296
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
294
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
297
295
  if (observed !== expected) {
298
296
  return {
299
297
  rejection: {
@@ -325,13 +323,10 @@ var BlockcastMmt = (() => {
325
323
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
326
324
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
327
325
  const data = payload.subarray(mfuOffset, dataEnd);
328
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
329
- const ssIdAt = (offset) => ({
330
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
331
- });
332
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
333
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
334
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
326
+ const fecPayloadId = trailerLayout.hasFecId ? {
327
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
328
+ } : null;
329
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
335
330
  }
336
331
  function sourceFecPayloadIdOf(mmtpPacket, header) {
337
332
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -423,6 +418,50 @@ var BlockcastMmt = (() => {
423
418
  var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
424
419
  var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
425
420
 
421
+ // ../mmt-base/dist/signalling.js
422
+ var FEC_CODING_STRUCTURE_ONE_STAGE = 1;
423
+ var SSBG_MODE_1 = 1;
424
+ var RAPTORQ_MAX_SOURCE_SYMBOLS = 56403;
425
+ var REPAIR_SYMBOL_ID_MAX = 16777215;
426
+ var MIN_REPAIR_SYMBOL_SIZE = 14;
427
+ var REED_SOLOMON_MAX_BLOCK = 255;
428
+ function alFecRaptorQFlowConfig(flow) {
429
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1)
430
+ return null;
431
+ if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0)
432
+ return null;
433
+ if (flow.packetIds.length !== 1)
434
+ return null;
435
+ const repair = flow.repairFlows[0];
436
+ const { fecCodeId, maximumK: k, maximumP: p } = repair;
437
+ const t = flow.repairSymbolSize;
438
+ if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX)
439
+ return null;
440
+ if (fecCodeId === FEC_CODE_RAPTORQ) {
441
+ if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS)
442
+ return null;
443
+ } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
444
+ if (k + p > REED_SOLOMON_MAX_BLOCK)
445
+ return null;
446
+ } else {
447
+ return null;
448
+ }
449
+ if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0])
450
+ return null;
451
+ return {
452
+ sourcePacketIds: [...flow.packetIds],
453
+ repairFlowId: repair.repairFlowId,
454
+ fecCodeId,
455
+ ssbgMode: flow.ssbgMode,
456
+ fecPayloadIdMode: flow.fecPayloadIdMode,
457
+ repairSymbolSize: t,
458
+ maximumK: k,
459
+ maximumP: p,
460
+ protectionWindowTime: repair.protectionWindowTime,
461
+ protectionWindowSize: repair.protectionWindowSize
462
+ };
463
+ }
464
+
426
465
  // ../mmt-base/dist/serial-number.js
427
466
  var SERIAL32_SPAN = 4294967296;
428
467
  var SERIAL32_HALF = 2147483648;
@@ -5594,13 +5633,21 @@ var BlockcastMmt = (() => {
5594
5633
  function sameSample(a, b) {
5595
5634
  return a.decodeTime === b.decodeTime && a.presentationTime === b.presentationTime && a.duration === b.duration && a.timescale === b.timescale && a.size === b.size && a.bytePosition === b.bytePosition;
5596
5635
  }
5597
- var _track, _fragments, _pendingSamples, _evictedSamples, _lastMovieFragmentRejection, _IsoBmffTimingResolver_instances, evictOldestFragment_fn, clearSlot_fn;
5636
+ var _track, _fragments, _firstSamples, _delivered, _pendingSamples, _evictedSamples, _lastMovieFragmentRejection, _IsoBmffTimingResolver_instances, evictOldestFragment_fn, clearSlot_fn;
5598
5637
  var IsoBmffTimingResolver = class {
5599
5638
  constructor(maxPendingSamples) {
5600
5639
  this.maxPendingSamples = maxPendingSamples;
5601
5640
  __privateAdd(this, _IsoBmffTimingResolver_instances);
5602
5641
  __privateAdd(this, _track, null);
5603
5642
  __privateAdd(this, _fragments, /* @__PURE__ */ new Map());
5643
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
5644
+ __privateAdd(this, _firstSamples, /* @__PURE__ */ new Map());
5645
+ /**
5646
+ * First sample and sample count of the newest fragments whose every slot was
5647
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
5648
+ * matches the combiner, since it retires an abandoned sample as well.
5649
+ */
5650
+ __privateAdd(this, _delivered, /* @__PURE__ */ new Map());
5604
5651
  __privateAdd(this, _pendingSamples, 0);
5605
5652
  __privateAdd(this, _evictedSamples, 0);
5606
5653
  __privateAdd(this, _lastMovieFragmentRejection, null);
@@ -5608,6 +5655,10 @@ var BlockcastMmt = (() => {
5608
5655
  throw new Error("[IsoBmffTimingResolver] maxPendingSamples must be a positive integer");
5609
5656
  }
5610
5657
  }
5658
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
5659
+ get initialized() {
5660
+ return __privateGet(this, _track) !== null;
5661
+ }
5611
5662
  get pendingSampleCount() {
5612
5663
  return __privateGet(this, _pendingSamples);
5613
5664
  }
@@ -5631,7 +5682,14 @@ var BlockcastMmt = (() => {
5631
5682
  __privateSet(this, _track, timing);
5632
5683
  return true;
5633
5684
  }
5634
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
5685
+ /**
5686
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
5687
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
5688
+ * fragment whose samples are all done is a duplicate: admitting it again
5689
+ * would charge stale timing and could displace the live MPU's. A source
5690
+ * copy is still admitted again (a rendition switch re-arms timing that way).
5691
+ */
5692
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
5635
5693
  __privateSet(this, _lastMovieFragmentRejection, null);
5636
5694
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
5637
5695
  __privateSet(this, _lastMovieFragmentRejection, "invalid");
@@ -5657,6 +5715,8 @@ var BlockcastMmt = (() => {
5657
5715
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
5658
5716
  );
5659
5717
  }
5718
+ const delivered = recovered ? __privateGet(this, _delivered).get(key) : void 0;
5719
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
5660
5720
  let freed = 0;
5661
5721
  while (__privateGet(this, _fragments).size > 0 && (__privateGet(this, _pendingSamples) + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
5662
5722
  freed += __privateMethod(this, _IsoBmffTimingResolver_instances, evictOldestFragment_fn).call(this);
@@ -5670,6 +5730,7 @@ var BlockcastMmt = (() => {
5670
5730
  return false;
5671
5731
  }
5672
5732
  __privateGet(this, _fragments).set(key, [...parsed.samples]);
5733
+ __privateGet(this, _firstSamples).set(key, parsed.samples[0]);
5673
5734
  __privateSet(this, _pendingSamples, __privateGet(this, _pendingSamples) + parsed.samples.length);
5674
5735
  return true;
5675
5736
  }
@@ -5716,11 +5777,15 @@ var BlockcastMmt = (() => {
5716
5777
  reset() {
5717
5778
  __privateSet(this, _track, null);
5718
5779
  __privateGet(this, _fragments).clear();
5780
+ __privateGet(this, _firstSamples).clear();
5781
+ __privateGet(this, _delivered).clear();
5719
5782
  __privateSet(this, _pendingSamples, 0);
5720
5783
  }
5721
5784
  };
5722
5785
  _track = new WeakMap();
5723
5786
  _fragments = new WeakMap();
5787
+ _firstSamples = new WeakMap();
5788
+ _delivered = new WeakMap();
5724
5789
  _pendingSamples = new WeakMap();
5725
5790
  _evictedSamples = new WeakMap();
5726
5791
  _lastMovieFragmentRejection = new WeakMap();
@@ -5739,6 +5804,7 @@ var BlockcastMmt = (() => {
5739
5804
  let live = 0;
5740
5805
  for (const sample of samples) if (sample !== null) live++;
5741
5806
  __privateGet(this, _fragments).delete(oldest);
5807
+ __privateGet(this, _firstSamples).delete(oldest);
5742
5808
  __privateSet(this, _pendingSamples, __privateGet(this, _pendingSamples) - live);
5743
5809
  __privateSet(this, _evictedSamples, __privateGet(this, _evictedSamples) + live);
5744
5810
  return live;
@@ -5754,7 +5820,16 @@ var BlockcastMmt = (() => {
5754
5820
  if (!samples || !samples[index]) return false;
5755
5821
  samples[index] = null;
5756
5822
  __privateWrapper(this, _pendingSamples)._--;
5757
- if (samples.every((value) => value === null)) __privateGet(this, _fragments).delete(key);
5823
+ if (samples.every((value) => value === null)) {
5824
+ __privateGet(this, _fragments).delete(key);
5825
+ const first = __privateGet(this, _firstSamples).get(key);
5826
+ __privateGet(this, _firstSamples).delete(key);
5827
+ __privateGet(this, _delivered).delete(key);
5828
+ __privateGet(this, _delivered).set(key, { first, sampleCount: samples.length });
5829
+ if (__privateGet(this, _delivered).size > this.maxPendingSamples) {
5830
+ __privateGet(this, _delivered).delete(__privateGet(this, _delivered).keys().next().value);
5831
+ }
5832
+ }
5758
5833
  return true;
5759
5834
  };
5760
5835
  function fragmentKey(mpuSequenceNumber, movieFragmentSequenceNumber) {
@@ -5930,7 +6005,10 @@ var BlockcastMmt = (() => {
5930
6005
  function ssIdAfter(a, b) {
5931
6006
  return (a - b | 0) > 0;
5932
6007
  }
5933
- var _streams2, _gate, _emit2, _recordStat2, _deferDepth, _deferredStreams, _FecReleaseSequencer_instances, drainUnlessDeferred_fn, streamFor_fn2, drain_fn, release_fn;
6008
+ function holdsRebuilt(stream) {
6009
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
6010
+ }
6011
+ var _streams2, _gate, _emit2, _recordStat2, _deferDepth, _deferredStreams, _FecReleaseSequencer_instances, drainUnlessDeferred_fn, streamFor_fn2, drain_fn, drainLoop_fn, cursorPassed_fn, release_fn;
5934
6012
  var FecReleaseSequencer = class {
5935
6013
  constructor(gate, emit, recordStat) {
5936
6014
  __privateAdd(this, _FecReleaseSequencer_instances);
@@ -5990,6 +6068,7 @@ var BlockcastMmt = (() => {
5990
6068
  stream.lastSbn = void 0;
5991
6069
  stream.discontinuity = false;
5992
6070
  stream.rapSsId = void 0;
6071
+ stream.sourced = false;
5993
6072
  }
5994
6073
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
5995
6074
  __privateGet(this, _recordStat2).call(this, "droppedStale");
@@ -6016,6 +6095,7 @@ var BlockcastMmt = (() => {
6016
6095
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
6017
6096
  else hi = mid;
6018
6097
  }
6098
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
6019
6099
  stream.buf.splice(lo, 0, frame);
6020
6100
  __privateMethod(this, _FecReleaseSequencer_instances, drainUnlessDeferred_fn).call(this, stream);
6021
6101
  }
@@ -6051,6 +6131,41 @@ var BlockcastMmt = (() => {
6051
6131
  }
6052
6132
  return false;
6053
6133
  }
6134
+ /**
6135
+ * These streams' sink is being replaced by one that has seen no source
6136
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
6137
+ * release to the sink being replaced the frames the unanchored path had
6138
+ * already released to it (#cursorPassed): there they waited in its queue,
6139
+ * which its replacement flushes. A stream that holds none is not drained:
6140
+ * it holds nothing the anchor kept back, and a drain would still force out
6141
+ * a frame held past the hold limit, which before the anchor waited for the
6142
+ * next push. Then treat the streams as unsourced until a source frame is
6143
+ * released.
6144
+ */
6145
+ unsource(packetId, catalogTrackName) {
6146
+ for (const stream of __privateGet(this, _streams2).values()) {
6147
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
6148
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
6149
+ if (__privateGet(this, _deferDepth) > 0) __privateGet(this, _deferredStreams).add(stream);
6150
+ else __privateMethod(this, _FecReleaseSequencer_instances, drain_fn).call(this, stream, true);
6151
+ }
6152
+ stream.sourced = false;
6153
+ }
6154
+ }
6155
+ }
6156
+ /**
6157
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
6158
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
6159
+ * packetId.
6160
+ */
6161
+ holdsRebuilt(packetId, catalogTrackName) {
6162
+ for (const stream of __privateGet(this, _streams2).values()) {
6163
+ if (stream.packetId !== packetId) continue;
6164
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
6165
+ if (holdsRebuilt(stream)) return true;
6166
+ }
6167
+ return false;
6168
+ }
6054
6169
  /** Re-evaluate held gaps after FEC state changed. */
6055
6170
  poke(streamKey) {
6056
6171
  if (streamKey !== void 0) {
@@ -6154,16 +6269,32 @@ var BlockcastMmt = (() => {
6154
6269
  last: null,
6155
6270
  lastSbn: void 0,
6156
6271
  discontinuity: false,
6157
- rapSsId: void 0
6272
+ rapSsId: void 0,
6273
+ sourced: false,
6274
+ draining: 0
6158
6275
  };
6159
6276
  __privateGet(this, _streams2).set(streamKey, stream);
6160
6277
  }
6161
6278
  return stream;
6162
6279
  };
6163
- drain_fn = function(stream) {
6280
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
6281
+ drain_fn = function(stream, asUnanchored = false) {
6282
+ stream.draining++;
6283
+ try {
6284
+ __privateMethod(this, _FecReleaseSequencer_instances, drainLoop_fn).call(this, stream, asUnanchored);
6285
+ } finally {
6286
+ stream.draining--;
6287
+ }
6288
+ };
6289
+ drainLoop_fn = function(stream, asUnanchored) {
6164
6290
  while (stream.buf.length > 0 && stream.last !== null) {
6165
6291
  const head = stream.buf[0];
6166
6292
  const last = stream.last;
6293
+ if (compareFrameOrder(head, last) <= 0) {
6294
+ stream.buf.shift();
6295
+ __privateMethod(this, _FecReleaseSequencer_instances, release_fn).call(this, stream, head, void 0);
6296
+ continue;
6297
+ }
6167
6298
  if (stream.discontinuity) {
6168
6299
  stream.discontinuity = false;
6169
6300
  stream.buf.shift();
@@ -6177,9 +6308,10 @@ var BlockcastMmt = (() => {
6177
6308
  }
6178
6309
  const headSbn = head.meta.fecSbn;
6179
6310
  const from = stream.lastSbn ?? headSbn;
6180
- const to = headSbn ?? stream.lastSbn;
6311
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
6181
6312
  let lost = [];
6182
6313
  const unanchored = from === void 0 && to === void 0;
6314
+ let unresolved = false;
6183
6315
  if (from !== void 0 && to !== void 0) {
6184
6316
  const resolved = __privateGet(this, _gate).resolvedLoss(
6185
6317
  stream.packetId,
@@ -6188,7 +6320,8 @@ var BlockcastMmt = (() => {
6188
6320
  Math.max(from, to),
6189
6321
  from
6190
6322
  );
6191
- if (resolved === null) {
6323
+ unresolved = resolved === null;
6324
+ if (unresolved && !__privateMethod(this, _FecReleaseSequencer_instances, cursorPassed_fn).call(this, stream, asUnanchored)) {
6192
6325
  const heldMs = performance.now() - head.arrivedAtMs;
6193
6326
  if (heldMs <= __privateGet(this, _gate).holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
6194
6327
  __privateGet(this, _recordStat2).call(this, "forcedWindowFlushes");
@@ -6196,18 +6329,52 @@ var BlockcastMmt = (() => {
6196
6329
  __privateMethod(this, _FecReleaseSequencer_instances, release_fn).call(this, stream, head, __privateGet(this, _gate).decide(last, head, [], true));
6197
6330
  continue;
6198
6331
  }
6199
- lost = resolved;
6332
+ lost = resolved ?? [];
6200
6333
  }
6201
6334
  stream.buf.shift();
6202
- __privateMethod(this, _FecReleaseSequencer_instances, release_fn).call(this, stream, head, lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? __privateGet(this, _gate).decide(last, head, lost, false) : void 0);
6335
+ __privateMethod(this, _FecReleaseSequencer_instances, release_fn).call(this, stream, head, lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? __privateGet(this, _gate).decide(last, head, lost, false) : void 0);
6203
6336
  }
6204
6337
  };
6338
+ /**
6339
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
6340
+ * source cursor holds it unanchored, and neither does a later frame behind
6341
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
6342
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
6343
+ * like each frame that arrived in front of it since (stale there). The head
6344
+ * then goes, decided as it would be unanchored, once a frame the base path
6345
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
6346
+ * marked one: the base path has passed that one, so a frame still missing
6347
+ * before it can only come from a repair. It also goes while no source frame
6348
+ * has been released, when the direct path shows a recovered frame at once.
6349
+ * And it goes at once while a staged switch decides on when frames arrive
6350
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
6351
+ * sink being replaced: unanchored, it had already reached the sink.
6352
+ * The scan also writes: the `passed` marks it sets stay on the frames,
6353
+ * and push marks a frame it inserts in front of a marked one from them.
6354
+ */
6355
+ cursorPassed_fn = function(stream, asUnanchored) {
6356
+ let at = -1;
6357
+ for (let i = 0; i < stream.buf.length; i++) {
6358
+ const frame = stream.buf[i];
6359
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
6360
+ frame.passed = true;
6361
+ if (at < 0) at = i;
6362
+ } else if (frame.passed !== true) break;
6363
+ }
6364
+ if (at < 0) return false;
6365
+ return asUnanchored || !stream.sourced || __privateGet(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));
6366
+ };
6205
6367
  release_fn = function(stream, frame, decision) {
6368
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
6369
+ if (stale) __privateGet(this, _recordStat2).call(this, "droppedStale");
6206
6370
  if (decision) frame.meta.gapBefore = decision;
6207
6371
  stream.last = frame;
6208
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
6372
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
6373
+ stream.lastSbn = frame.meta.fecSbn;
6374
+ }
6375
+ if (frame.meta.recovered !== true) stream.sourced = true;
6209
6376
  __privateGet(this, _gate).onReleased?.(frame.data, frame.meta);
6210
- __privateGet(this, _emit2).call(this, frame.data, frame.meta);
6377
+ if (!stale) __privateGet(this, _emit2).call(this, frame.data, frame.meta);
6211
6378
  };
6212
6379
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
6213
6380
  function isFmp4Box(data) {
@@ -6216,7 +6383,7 @@ var BlockcastMmt = (() => {
6216
6383
  return FMPU_BOX_TYPES.has(type);
6217
6384
  }
6218
6385
  var MAX_AWAITING_TIMING_HEADS = 8;
6219
- var _tracks, _defaultTrack, _reassembler, _sampleCombiners, _sampleCombinerOptions, _retiredSampleCombinerStats, _timingStreams, _maxPendingTimingSamples, _pendingTimingSamples, _reclaimedTimingSamples, _evictedTimingSamples, _rejectedMovieFragments, _sequencers, _fecGate, _fecSequencer, _reorderWindow, _reassemblyPolicy, _frameCb, _initCb, _sourceSymbolCb, _trackFrameCbs, _reorderStats, _reliableMoqStartupGate, _disposed, _replayingAwaitingTiming, _fedObjects, _reassemblerFragmentsFed, _reassemblerDataUnitsReturned, _feedOutcomes, _deferredFeedOutcomes, _lastOutcome, _lastOutcomeCount, _MmtpContainerParser_instances, streamKey_fn, sampleCombinerFor_fn, releaseTimingSlot_fn, dropSampleCombiner_fn, countOutcome_fn, countDeferredOutcome_fn, drop_fn, feedCounted_fn, feed_fn, emitCombinedSample_fn, passReliableMoqStartupGate_fn, timingResolver_fn, awaitTiming_fn, replayAwaitingTiming_fn, dropTimingStream_fn, validateReassemblyPolicy_fn, buildFrameMeta_fn, sequencerFor_fn, flushSequencers_fn, emitFrame_fn2, _MmtpContainerParser_static, scanForKeyframe_fn, scanLengthPrefixedHevcForKeyframe_fn, scanAnnexBForKeyframe_fn;
6386
+ var _tracks, _defaultTrack, _reassembler, _sampleCombiners, _sampleCombinerOptions, _retiredSampleCombinerStats, _timingStreams, _maxPendingTimingSamples, _pendingTimingSamples, _reclaimedTimingSamples, _evictedTimingSamples, _rejectedMovieFragments, _recoveredHeadsAwaitingTiming, _sequencers, _fecGate, _fecSequencer, _reorderWindow, _reassemblyPolicy, _frameCb, _initCb, _sourceSymbolCb, _trackFrameCbs, _reorderStats, _reliableMoqStartupGate, _disposed, _replayingAwaitingTiming, _fedObjects, _reassemblerFragmentsFed, _reassemblerDataUnitsReturned, _feedOutcomes, _deferredFeedOutcomes, _lastOutcome, _lastOutcomeCount, _MmtpContainerParser_instances, streamKey_fn, sampleCombinerFor_fn, releaseTimingSlot_fn, dropSampleCombiner_fn, countOutcome_fn, countDeferredOutcome_fn, drop_fn, feedCounted_fn, feed_fn, emitCombinedSample_fn, passReliableMoqStartupGate_fn, timingResolver_fn, awaitTiming_fn, leaveAwaitingTiming_fn, replayAwaitingTiming_fn, dropTimingStream_fn, validateReassemblyPolicy_fn, buildFrameMeta_fn, sequencerFor_fn, flushSequencers_fn, emitFrame_fn2, _MmtpContainerParser_static, scanForKeyframe_fn, scanLengthPrefixedHevcForKeyframe_fn, scanAnnexBForKeyframe_fn;
6220
6387
  var _MmtpContainerParser = class _MmtpContainerParser {
6221
6388
  constructor(config) {
6222
6389
  __privateAdd(this, _MmtpContainerParser_instances);
@@ -6252,6 +6419,12 @@ var BlockcastMmt = (() => {
6252
6419
  __privateAdd(this, _reclaimedTimingSamples, 0);
6253
6420
  __privateAdd(this, _evictedTimingSamples, 0);
6254
6421
  __privateAdd(this, _rejectedMovieFragments, 0);
6422
+ __privateAdd(this, _recoveredHeadsAwaitingTiming, {
6423
+ held: 0,
6424
+ expired: 0,
6425
+ discarded: 0,
6426
+ superseded: 0
6427
+ });
6255
6428
  __privateAdd(this, _sequencers, /* @__PURE__ */ new Map());
6256
6429
  __privateAdd(this, _fecGate, null);
6257
6430
  __privateAdd(this, _fecSequencer, null);
@@ -6379,11 +6552,12 @@ var BlockcastMmt = (() => {
6379
6552
  /**
6380
6553
  * Typed view of the ISO-BMFF timing-sample budget.
6381
6554
  *
6382
- * This is the acceptance signal for the budget ratchet: `pending` walking
6383
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
6384
- * and it is observable here rather than only as media silently stopping.
6385
- * Per repo policy this is a producer-owned counter -- never infer budget
6386
- * state by parsing logs.
6555
+ * Each stream admits timing against its own window of `capacity` slots
6556
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
6557
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
6558
+ * timing a stream declared and never delivered shows up there. `pending`
6559
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
6560
+ * a producer-owned counter -- never infer budget state by parsing logs.
6387
6561
  */
6388
6562
  get timingBudgetStats() {
6389
6563
  return {
@@ -6394,6 +6568,14 @@ var BlockcastMmt = (() => {
6394
6568
  rejectedMovieFragments: __privateGet(this, _rejectedMovieFragments)
6395
6569
  };
6396
6570
  }
6571
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
6572
+ get recoveredHeadsAwaitingTiming() {
6573
+ let pending = 0;
6574
+ for (const { awaitingTiming } of __privateGet(this, _timingStreams).values()) {
6575
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
6576
+ }
6577
+ return { ...__privateGet(this, _recoveredHeadsAwaitingTiming), pending };
6578
+ }
6397
6579
  get reorderStats() {
6398
6580
  let bufferedFrames = __privateGet(this, _fecSequencer)?.buffered ?? 0;
6399
6581
  for (const { sequencer } of __privateGet(this, _sequencers).values()) bufferedFrames += sequencer.buffered;
@@ -6435,6 +6617,14 @@ var BlockcastMmt = (() => {
6435
6617
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
6436
6618
  return __privateGet(this, _fecSequencer)?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
6437
6619
  }
6620
+ /** See FecReleaseSequencer.unsource. */
6621
+ unsourceFecRelease(packetId, catalogTrackName) {
6622
+ __privateGet(this, _fecSequencer)?.unsource(packetId, catalogTrackName);
6623
+ }
6624
+ /** See FecReleaseSequencer.holdsRebuilt. */
6625
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
6626
+ return __privateGet(this, _fecSequencer)?.holdsRebuilt(packetId, catalogTrackName) ?? false;
6627
+ }
6438
6628
  /** Re-evaluate held gaps after FEC block state changed. */
6439
6629
  pokeFecRelease(packetId, catalogTrackName) {
6440
6630
  if (!__privateGet(this, _fecSequencer)) return;
@@ -6545,7 +6735,7 @@ var BlockcastMmt = (() => {
6545
6735
  const now = Date.now();
6546
6736
  for (const { awaitingTiming } of __privateGet(this, _timingStreams).values()) {
6547
6737
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
6548
- awaitingTiming.shift();
6738
+ __privateMethod(this, _MmtpContainerParser_instances, leaveAwaitingTiming_fn).call(this, awaitingTiming.shift(), "expired");
6549
6739
  }
6550
6740
  }
6551
6741
  }
@@ -6750,6 +6940,10 @@ var BlockcastMmt = (() => {
6750
6940
  }
6751
6941
  __privateMethod(this, _MmtpContainerParser_instances, emitFrame_fn2).call(this, data, meta);
6752
6942
  }
6943
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
6944
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
6945
+ return __privateMethod(this, _MmtpContainerParser_instances, timingResolver_fn).call(this, __privateMethod(this, _MmtpContainerParser_instances, streamKey_fn).call(this, packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
6946
+ }
6753
6947
  /**
6754
6948
  * Claim and return the authored timing for a recovered sample. Recovery does
6755
6949
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -6794,6 +6988,9 @@ var BlockcastMmt = (() => {
6794
6988
  for (const { sequencer } of __privateGet(this, _sequencers).values()) sequencer.discard();
6795
6989
  __privateGet(this, _sequencers).clear();
6796
6990
  __privateGet(this, _fecSequencer)?.discard();
6991
+ for (const { awaitingTiming } of __privateGet(this, _timingStreams).values()) {
6992
+ for (const head of awaitingTiming) __privateMethod(this, _MmtpContainerParser_instances, leaveAwaitingTiming_fn).call(this, head, "discarded");
6993
+ }
6797
6994
  __privateGet(this, _timingStreams).clear();
6798
6995
  __privateSet(this, _pendingTimingSamples, 0);
6799
6996
  __privateGet(this, _reliableMoqStartupGate).clear();
@@ -6822,6 +7019,7 @@ var BlockcastMmt = (() => {
6822
7019
  _reclaimedTimingSamples = new WeakMap();
6823
7020
  _evictedTimingSamples = new WeakMap();
6824
7021
  _rejectedMovieFragments = new WeakMap();
7022
+ _recoveredHeadsAwaitingTiming = new WeakMap();
6825
7023
  _sequencers = new WeakMap();
6826
7024
  _fecGate = new WeakMap();
6827
7025
  _fecSequencer = new WeakMap();
@@ -6980,11 +7178,16 @@ var BlockcastMmt = (() => {
6980
7178
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
6981
7179
  return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "fragment-unexpected-fragmentation");
6982
7180
  const resolver2 = __privateMethod(this, _MmtpContainerParser_instances, timingResolver_fn).call(this, streamKey, info.packetId, false);
6983
- if (!resolver2) return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "fragment-no-resolver");
7181
+ if (!resolver2?.initialized) return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "fragment-no-resolver");
6984
7182
  const pendingBefore = resolver2.pendingSampleCount;
6985
7183
  const evictedBefore = resolver2.evictedSampleCount;
6986
- const remaining = __privateGet(this, _maxPendingTimingSamples) - __privateGet(this, _pendingTimingSamples);
6987
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
7184
+ const remaining = __privateGet(this, _maxPendingTimingSamples) - resolver2.pendingSampleCount;
7185
+ const admitted = resolver2.addMovieFragment(
7186
+ mpu.mpuSequenceNumber,
7187
+ data,
7188
+ remaining,
7189
+ options?.recovered === true
7190
+ );
6988
7191
  __privateSet(this, _pendingTimingSamples, __privateGet(this, _pendingTimingSamples) + (resolver2.pendingSampleCount - pendingBefore));
6989
7192
  __privateSet(this, _evictedTimingSamples, __privateGet(this, _evictedTimingSamples) + (resolver2.evictedSampleCount - evictedBefore));
6990
7193
  if (!admitted) {
@@ -7004,7 +7207,7 @@ var BlockcastMmt = (() => {
7004
7207
  };
7005
7208
  }
7006
7209
  if (fragmentType !== 2) return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "unknown-fragment-type");
7007
- const resolver = __privateMethod(this, _MmtpContainerParser_instances, timingResolver_fn).call(this, streamKey, info.packetId, false);
7210
+ const resolver = __privateMethod(this, _MmtpContainerParser_instances, timingResolver_fn).call(this, streamKey, info.packetId, true);
7008
7211
  if (!resolver) return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "mfu-no-resolver");
7009
7212
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
7010
7213
  return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "mfu-empty-payload");
@@ -7025,6 +7228,7 @@ var BlockcastMmt = (() => {
7025
7228
  mpuSequenceNumber: mpu.mpuSequenceNumber,
7026
7229
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
7027
7230
  sampleNumber: mfuDuHeader.sampleNumber,
7231
+ sequenceNumber: info.sequenceNumber,
7028
7232
  arrivedAt: Date.now()
7029
7233
  });
7030
7234
  return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, "mfu-sample-timing-missing");
@@ -7045,7 +7249,7 @@ var BlockcastMmt = (() => {
7045
7249
  headTiming.size
7046
7250
  );
7047
7251
  if (readyOnRegister) {
7048
- const deferred = __privateMethod(this, _MmtpContainerParser_instances, emitCombinedSample_fn).call(this, readyOnRegister, info, resolver, options?.recovered === true, options?.fecSbn);
7252
+ const deferred = __privateMethod(this, _MmtpContainerParser_instances, emitCombinedSample_fn).call(this, readyOnRegister, info, resolver, options?.recovered === true, options?.fecSbn, options?.fecSbnFirst);
7049
7253
  if (deferred.record === void 0) __privateMethod(this, _MmtpContainerParser_instances, countDeferredOutcome_fn).call(this, deferred.reason);
7050
7254
  }
7051
7255
  }
@@ -7132,7 +7336,7 @@ var BlockcastMmt = (() => {
7132
7336
  fragmentCounter: mpu.fragmentCounter
7133
7337
  };
7134
7338
  }
7135
- const outcome = __privateMethod(this, _MmtpContainerParser_instances, emitCombinedSample_fn).call(this, combined, info, resolver, options?.recovered === true, options?.fecSbn);
7339
+ const outcome = __privateMethod(this, _MmtpContainerParser_instances, emitCombinedSample_fn).call(this, combined, info, resolver, options?.recovered === true, options?.fecSbn, options?.fecSbnFirst);
7136
7340
  if (outcome.record !== void 0) return outcome.record;
7137
7341
  return __privateMethod(this, _MmtpContainerParser_instances, drop_fn).call(this, outcome.reason);
7138
7342
  };
@@ -7151,7 +7355,7 @@ var BlockcastMmt = (() => {
7151
7355
  * attribution exists to keep readable. Leaving the choice at the call site
7152
7356
  * is what keeps both true.
7153
7357
  */
7154
- emitCombinedSample_fn = function(combined, info, resolver, recovered, fecSbn) {
7358
+ emitCombinedSample_fn = function(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
7155
7359
  const timing = resolver.sampleTiming(
7156
7360
  combined.mpuSequenceNumber,
7157
7361
  combined.movieFragmentSequenceNumber,
@@ -7162,6 +7366,7 @@ var BlockcastMmt = (() => {
7162
7366
  const meta = __privateMethod(this, _MmtpContainerParser_instances, buildFrameMeta_fn).call(this, combined, info.packetId, info.sequenceNumber, timing, recovered);
7163
7367
  if (!meta) return { reason: "combined-meta-build-failed" };
7164
7368
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
7369
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
7165
7370
  if (!resolver.consumeSample(
7166
7371
  combined.mpuSequenceNumber,
7167
7372
  combined.movieFragmentSequenceNumber,
@@ -7206,10 +7411,23 @@ var BlockcastMmt = (() => {
7206
7411
  return resolver;
7207
7412
  };
7208
7413
  awaitTiming_fn = function(streamKey, head) {
7209
- const awaitingTiming = __privateGet(this, _timingStreams).get(streamKey)?.awaitingTiming;
7210
- if (!awaitingTiming) return;
7414
+ const state = __privateGet(this, _timingStreams).get(streamKey);
7415
+ if (!state) return;
7416
+ if (!head.options?.recovered) {
7417
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
7418
+ __privateGet(this, _recoveredHeadsAwaitingTiming).superseded += state.awaitingTiming.length - kept.length;
7419
+ state.awaitingTiming = kept;
7420
+ }
7421
+ const awaitingTiming = state.awaitingTiming;
7211
7422
  awaitingTiming.push(head);
7212
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
7423
+ if (head.options?.recovered) __privateGet(this, _recoveredHeadsAwaitingTiming).held++;
7424
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
7425
+ __privateMethod(this, _MmtpContainerParser_instances, leaveAwaitingTiming_fn).call(this, awaitingTiming.shift(), "expired");
7426
+ }
7427
+ };
7428
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
7429
+ leaveAwaitingTiming_fn = function(head, outcome) {
7430
+ if (head.options?.recovered) __privateGet(this, _recoveredHeadsAwaitingTiming)[outcome]++;
7213
7431
  };
7214
7432
  /**
7215
7433
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -7237,6 +7455,7 @@ var BlockcastMmt = (() => {
7237
7455
  const state = __privateGet(this, _timingStreams).get(streamKey);
7238
7456
  if (!state) return;
7239
7457
  __privateGet(this, _timingStreams).delete(streamKey);
7458
+ for (const head of state.awaitingTiming) __privateMethod(this, _MmtpContainerParser_instances, leaveAwaitingTiming_fn).call(this, head, "discarded");
7240
7459
  __privateSet(this, _pendingTimingSamples, __privateGet(this, _pendingTimingSamples) - state.resolver.pendingSampleCount);
7241
7460
  if (__privateGet(this, _pendingTimingSamples) < 0) {
7242
7461
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -8217,7 +8436,7 @@ var BlockcastMmt = (() => {
8217
8436
  // src/mmtp-fec-session.ts
8218
8437
  var PATH_BITS = { moq: 1, multicast: 2 };
8219
8438
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
8220
- var _decoder, _blocks, _settled, _have, _newest, _prunedAt, _repairPacketId, _newestSource, _heldLayers, _anchorHorizon, _stats3, _MmtpFecSession_instances, drop_fn2, noteNewest_fn, blockStartOf_fn, countHeld_fn, noteMixed_fn, behindHorizon_fn, settle_fn, settleClean_fn, settleDecoded_fn, retireBehindHorizon_fn;
8439
+ var _decoder, _repairSymbols, _message, _blocks, _settled, _have, _newest, _prunedAt, _repairPacketId, _newestSource, _heldLayers, _anchorHorizon, _stats3, _MmtpFecSession_instances, trackFor_fn, drop_fn2, noteNewest_fn, blockStartOf_fn, countHeld_fn, noteMixed_fn, behindHorizon_fn, settle_fn, settleClean_fn, settleDecoded_fn, retireBehindHorizon_fn;
8221
8440
  var MmtpFecSession = class {
8222
8441
  /**
8223
8442
  * @throws when the instance lacks or misstates a field the session needs:
@@ -8227,6 +8446,9 @@ var BlockcastMmt = (() => {
8227
8446
  constructor(config) {
8228
8447
  __privateAdd(this, _MmtpFecSession_instances);
8229
8448
  __privateAdd(this, _decoder);
8449
+ __privateAdd(this, _repairSymbols);
8450
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
8451
+ __privateAdd(this, _message);
8230
8452
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
8231
8453
  __privateAdd(this, _blocks, /* @__PURE__ */ new Map());
8232
8454
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -8258,6 +8480,24 @@ var BlockcastMmt = (() => {
8258
8480
  mixedPath: { clean: 0, recovered: 0, failed: 0, released: 0 },
8259
8481
  failClosed: null
8260
8482
  });
8483
+ if (config && "alFecFlow" in config) {
8484
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
8485
+ if (!message) {
8486
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
8487
+ }
8488
+ __privateSet(this, _message, message);
8489
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
8490
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
8491
+ this.sourceSymbols = message.maximumK;
8492
+ __privateSet(this, _repairSymbols, message.maximumP);
8493
+ this.symbolSize = message.repairSymbolSize;
8494
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
8495
+ __privateSet(this, _repairPacketId, message.repairFlowId);
8496
+ __privateSet(this, _heldLayers, layerZero(message.maximumP));
8497
+ __privateSet(this, _anchorHorizon, heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols));
8498
+ __privateSet(this, _decoder, buildDecoder(config.Decoder, this));
8499
+ return;
8500
+ }
8261
8501
  const fec = config?.fec;
8262
8502
  if (!fec || typeof fec !== "object") {
8263
8503
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -8268,7 +8508,7 @@ var BlockcastMmt = (() => {
8268
8508
  }
8269
8509
  this.algorithm = fec.scheme;
8270
8510
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
8271
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
8511
+ __privateSet(this, _repairSymbols, requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols"));
8272
8512
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
8273
8513
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
8274
8514
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -8288,17 +8528,34 @@ var BlockcastMmt = (() => {
8288
8528
  config.heldLayers
8289
8529
  ));
8290
8530
  __privateSet(this, _anchorHorizon, heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols));
8291
- if (typeof config.Decoder !== "function") {
8292
- throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
8293
- }
8294
- const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm);
8295
- try {
8296
- decoder.configure_params(this.sourceSymbols, this.symbolSize);
8297
- } catch (error) {
8298
- decoder.free();
8299
- throw error;
8300
- }
8301
- __privateSet(this, _decoder, decoder);
8531
+ __privateSet(this, _decoder, buildDecoder(config.Decoder, this));
8532
+ }
8533
+ /** P_0 = B_0, the held total every repair packet is checked against. */
8534
+ get repairSymbols() {
8535
+ return __privateGet(this, _repairSymbols);
8536
+ }
8537
+ /**
8538
+ * A later version of the AL-FEC message, catalog-less sessions only. A
8539
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
8540
+ * anchored check restarts at the new total, so a block generated under the
8541
+ * previous one stays valid before the first block carrying the new total
8542
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
8543
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
8544
+ * does. An entry with another asset, repair flow, code point, K or T, or
8545
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
8546
+ * a new session. A latched fail-closed stays latched either way.
8547
+ */
8548
+ updateAlFecFlow(flow) {
8549
+ const held = __privateGet(this, _message);
8550
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
8551
+ const next = alFecRaptorQFlowConfig(flow);
8552
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
8553
+ return "refused";
8554
+ }
8555
+ if (next.maximumP <= __privateGet(this, _repairSymbols)) return "unchanged";
8556
+ __privateSet(this, _repairSymbols, next.maximumP);
8557
+ __privateSet(this, _heldLayers, layerZero(next.maximumP));
8558
+ return "raised";
8302
8559
  }
8303
8560
  /** True once release() ran. */
8304
8561
  get released() {
@@ -8393,7 +8650,7 @@ var BlockcastMmt = (() => {
8393
8650
  }
8394
8651
  if (admission.reject) {
8395
8652
  const verdict2 = __privateGet(this, _heldLayers).failsClosed(
8396
- repairTrackName,
8653
+ __privateMethod(this, _MmtpFecSession_instances, trackFor_fn).call(this, repairTrackName),
8397
8654
  frame.ssStart,
8398
8655
  frame.rsbLength,
8399
8656
  __privateGet(this, _anchorHorizon),
@@ -8408,7 +8665,7 @@ var BlockcastMmt = (() => {
8408
8665
  __privateGet(this, _repairPacketId) ?? __privateSet(this, _repairPacketId, frame.packetId);
8409
8666
  if (frame.ssbLength > this.sourceSymbols) return __privateMethod(this, _MmtpFecSession_instances, drop_fn2).call(this, "repair:ssb-length");
8410
8667
  const verdict = __privateGet(this, _heldLayers).admit(
8411
- repairTrackName,
8668
+ __privateMethod(this, _MmtpFecSession_instances, trackFor_fn).call(this, repairTrackName),
8412
8669
  frame.ssStart,
8413
8670
  frame.rsbLength,
8414
8671
  __privateGet(this, _anchorHorizon),
@@ -8469,6 +8726,8 @@ var BlockcastMmt = (() => {
8469
8726
  }
8470
8727
  };
8471
8728
  _decoder = new WeakMap();
8729
+ _repairSymbols = new WeakMap();
8730
+ _message = new WeakMap();
8472
8731
  _blocks = new WeakMap();
8473
8732
  _settled = new WeakMap();
8474
8733
  _have = new WeakMap();
@@ -8480,6 +8739,10 @@ var BlockcastMmt = (() => {
8480
8739
  _anchorHorizon = new WeakMap();
8481
8740
  _stats3 = new WeakMap();
8482
8741
  _MmtpFecSession_instances = new WeakSet();
8742
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
8743
+ trackFor_fn = function(repairTrackName) {
8744
+ return __privateGet(this, _message) ? void 0 : repairTrackName;
8745
+ };
8483
8746
  drop_fn2 = function(reason) {
8484
8747
  __privateGet(this, _stats3).dropped[reason] = (__privateGet(this, _stats3).dropped[reason] ?? 0) + 1;
8485
8748
  return [];
@@ -8600,6 +8863,22 @@ var BlockcastMmt = (() => {
8600
8863
  if (__privateMethod(this, _MmtpFecSession_instances, behindHorizon_fn).call(this, start)) __privateGet(this, _settled).delete(start);
8601
8864
  }
8602
8865
  };
8866
+ function layerZero(total) {
8867
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
8868
+ }
8869
+ function buildDecoder(Decoder, session) {
8870
+ if (typeof Decoder !== "function") {
8871
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
8872
+ }
8873
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
8874
+ try {
8875
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
8876
+ } catch (error) {
8877
+ decoder.free();
8878
+ throw error;
8879
+ }
8880
+ return decoder;
8881
+ }
8603
8882
  function requirePositiveInteger(value, path) {
8604
8883
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
8605
8884
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);