@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca

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 (99) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1302 -249
  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 +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +213 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  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 +37 -2
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +98 -28
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +222 -45
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +97 -28
  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 +25 -2
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +1749 -56
  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 +15 -2
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +15683 -14941
  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 +215 -42
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.js.map +1 -1
  72. package/dist/repair-layer-subscriptions.d.ts +82 -0
  73. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  74. package/dist/repair-layer-subscriptions.js +192 -0
  75. package/dist/repair-layer-subscriptions.js.map +7 -0
  76. package/dist/signaling.js.map +2 -2
  77. package/dist/timestamp-codec.js +12 -19
  78. package/dist/timestamp-codec.js.map +2 -2
  79. package/dist/track-session.d.ts +1 -1
  80. package/dist/track-session.js.map +1 -1
  81. package/dist/wasm-mfu-reassembler.js.map +1 -1
  82. package/dist/wasm-mmtp-container-parser.js +97 -28
  83. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  84. package/package.json +8 -5
  85. package/schemas/catalog.schema.json +4 -0
  86. package/src/catalog-fec-params.ts +36 -1
  87. package/src/cmcd-producer.ts +194 -179
  88. package/src/fec-manager.ts +256 -6
  89. package/src/fec-worker-setup.ts +2 -2
  90. package/src/index.ts +18 -2
  91. package/src/iso-block-locator.ts +19 -0
  92. package/src/iso-bmff-timing.ts +40 -1
  93. package/src/mmtp-container-parser.ts +96 -8
  94. package/src/mmtp-fec-session.ts +115 -15
  95. package/src/mmtp-router.ts +414 -29
  96. package/src/multicast-manager.ts +26 -5
  97. package/src/multicast-transport.ts +44 -20
  98. package/src/repair-layer-subscriptions.ts +261 -0
  99. package/src/track-session.ts +1 -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
  };
@@ -6153,6 +6226,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
6153
6226
  this.#reclaimedTimingSamples = 0;
6154
6227
  this.#evictedTimingSamples = 0;
6155
6228
  this.#rejectedMovieFragments = 0;
6229
+ this.#recoveredHeadsAwaitingTiming = {
6230
+ held: 0,
6231
+ expired: 0,
6232
+ discarded: 0,
6233
+ superseded: 0
6234
+ };
6156
6235
  this.#sequencers = /* @__PURE__ */ new Map();
6157
6236
  this.#fecGate = null;
6158
6237
  this.#fecSequencer = null;
@@ -6261,6 +6340,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6261
6340
  #reclaimedTimingSamples;
6262
6341
  #evictedTimingSamples;
6263
6342
  #rejectedMovieFragments;
6343
+ #recoveredHeadsAwaitingTiming;
6264
6344
  #sequencers;
6265
6345
  #fecGate;
6266
6346
  #fecSequencer;
@@ -6381,6 +6461,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
6381
6461
  rejectedMovieFragments: this.#rejectedMovieFragments
6382
6462
  };
6383
6463
  }
6464
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
6465
+ get recoveredHeadsAwaitingTiming() {
6466
+ let pending = 0;
6467
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
6468
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
6469
+ }
6470
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
6471
+ }
6384
6472
  get reorderStats() {
6385
6473
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
6386
6474
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -6554,7 +6642,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6554
6642
  const now = Date.now();
6555
6643
  for (const { awaitingTiming } of this.#timingStreams.values()) {
6556
6644
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
6557
- awaitingTiming.shift();
6645
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
6558
6646
  }
6559
6647
  }
6560
6648
  }
@@ -6672,11 +6760,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
6672
6760
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
6673
6761
  return this.#drop("fragment-unexpected-fragmentation");
6674
6762
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
6675
- if (!resolver2) return this.#drop("fragment-no-resolver");
6763
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
6676
6764
  const pendingBefore = resolver2.pendingSampleCount;
6677
6765
  const evictedBefore = resolver2.evictedSampleCount;
6678
6766
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
6679
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
6767
+ const admitted = resolver2.addMovieFragment(
6768
+ mpu.mpuSequenceNumber,
6769
+ data,
6770
+ remaining,
6771
+ options?.recovered === true
6772
+ );
6680
6773
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
6681
6774
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
6682
6775
  if (!admitted) {
@@ -6696,7 +6789,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6696
6789
  };
6697
6790
  }
6698
6791
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
6699
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
6792
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
6700
6793
  if (!resolver) return this.#drop("mfu-no-resolver");
6701
6794
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
6702
6795
  return this.#drop("mfu-empty-payload");
@@ -6717,6 +6810,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6717
6810
  mpuSequenceNumber: mpu.mpuSequenceNumber,
6718
6811
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
6719
6812
  sampleNumber: mfuDuHeader.sampleNumber,
6813
+ sequenceNumber: info.sequenceNumber,
6720
6814
  arrivedAt: Date.now()
6721
6815
  });
6722
6816
  return this.#drop("mfu-sample-timing-missing");
@@ -7040,6 +7134,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
7040
7134
  }
7041
7135
  this.#emitFrame(data, meta);
7042
7136
  }
7137
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
7138
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
7139
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
7140
+ }
7043
7141
  /**
7044
7142
  * Claim and return the authored timing for a recovered sample. Recovery does
7045
7143
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -7101,6 +7199,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
7101
7199
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
7102
7200
  this.#sequencers.clear();
7103
7201
  this.#fecSequencer?.discard();
7202
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
7203
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7204
+ }
7104
7205
  this.#timingStreams.clear();
7105
7206
  this.#pendingTimingSamples = 0;
7106
7207
  this.#reliableMoqStartupGate.clear();
@@ -7119,10 +7220,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
7119
7220
  return resolver;
7120
7221
  }
7121
7222
  #awaitTiming(streamKey, head) {
7122
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
7123
- if (!awaitingTiming) return;
7223
+ const state = this.#timingStreams.get(streamKey);
7224
+ if (!state) return;
7225
+ if (!head.options?.recovered) {
7226
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
7227
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
7228
+ state.awaitingTiming = kept;
7229
+ }
7230
+ const awaitingTiming = state.awaitingTiming;
7124
7231
  awaitingTiming.push(head);
7125
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
7232
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
7233
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
7234
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
7235
+ }
7236
+ }
7237
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
7238
+ #leaveAwaitingTiming(head, outcome) {
7239
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
7126
7240
  }
7127
7241
  /**
7128
7242
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -7150,6 +7264,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
7150
7264
  const state = this.#timingStreams.get(streamKey);
7151
7265
  if (!state) return;
7152
7266
  this.#timingStreams.delete(streamKey);
7267
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7153
7268
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
7154
7269
  if (this.#pendingTimingSamples < 0) {
7155
7270
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -8071,7 +8186,14 @@ var HeldLayerAdmission = class {
8071
8186
  var PATH_BITS = { moq: 1, multicast: 2 };
8072
8187
  var MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast;
8073
8188
  var MmtpFecSession = class {
8189
+ /** P_0 = B_0, the held total every repair packet is checked against. */
8190
+ get repairSymbols() {
8191
+ return this.#repairSymbols;
8192
+ }
8074
8193
  #decoder;
8194
+ #repairSymbols;
8195
+ /** The message entry a catalog-less session was built from; undefined under a catalog. */
8196
+ #message;
8075
8197
  /** Blocks a repair has defined and that are not settled yet, by SS_Start. */
8076
8198
  #blocks = /* @__PURE__ */ new Map();
8077
8199
  /** SS_Starts of settled blocks, so a later repair for one is refused. */
@@ -8109,6 +8231,24 @@ var MmtpFecSession = class {
8109
8231
  * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
8110
8232
  */
8111
8233
  constructor(config) {
8234
+ if (config && "alFecFlow" in config) {
8235
+ const message = alFecRaptorQFlowConfig(config.alFecFlow);
8236
+ if (!message) {
8237
+ throw new Error("MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)");
8238
+ }
8239
+ this.#message = message;
8240
+ this.blockProfile = parseFecBlockProfile("iso-ssbg1-v1", "alFecFlow");
8241
+ this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? "raptorq" : "reed-solomon";
8242
+ this.sourceSymbols = message.maximumK;
8243
+ this.#repairSymbols = message.maximumP;
8244
+ this.symbolSize = message.repairSymbolSize;
8245
+ this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, "retentionBlocks");
8246
+ this.#repairPacketId = message.repairFlowId;
8247
+ this.#heldLayers = layerZero(message.maximumP);
8248
+ this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols);
8249
+ this.#decoder = buildDecoder(config.Decoder, this);
8250
+ return;
8251
+ }
8112
8252
  const fec = config?.fec;
8113
8253
  if (!fec || typeof fec !== "object") {
8114
8254
  throw new Error("MmtpFecSession: the catalog fec instance is required");
@@ -8119,7 +8259,7 @@ var MmtpFecSession = class {
8119
8259
  }
8120
8260
  this.algorithm = fec.scheme;
8121
8261
  this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, "fec.sourceSymbols");
8122
- this.repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
8262
+ this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, "fec.repairSymbols");
8123
8263
  this.symbolSize = requirePositiveInteger(fec.symbolSize, "fec.symbolSize");
8124
8264
  const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, "fec.interleaveDepthMs");
8125
8265
  const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, "fec.reorderToleranceMs");
@@ -8139,17 +8279,30 @@ var MmtpFecSession = class {
8139
8279
  config.heldLayers
8140
8280
  );
8141
8281
  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;
8282
+ this.#decoder = buildDecoder(config.Decoder, this);
8283
+ }
8284
+ /**
8285
+ * A later version of the AL-FEC message, catalog-less sessions only. A
8286
+ * maximum_p_for_repair_flow above the held total raises it (`raised`): the
8287
+ * anchored check restarts at the new total, so a block generated under the
8288
+ * previous one stays valid before the first block carrying the new total
8289
+ * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
8290
+ * (`unchanged`): no publisher lowers it, and the check catches a wire that
8291
+ * does. An entry with another asset, repair flow, code point, K or T, or
8292
+ * one outside 8.3, is `refused`: that is a new source encoding, which needs
8293
+ * a new session. A latched fail-closed stays latched either way.
8294
+ */
8295
+ updateAlFecFlow(flow) {
8296
+ const held = this.#message;
8297
+ if (!held) throw new Error("MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry");
8298
+ const next = alFecRaptorQFlowConfig(flow);
8299
+ if (!next || next.sourcePacketIds[0] !== held.sourcePacketIds[0] || next.repairFlowId !== held.repairFlowId || next.fecCodeId !== held.fecCodeId || next.maximumK !== held.maximumK || next.repairSymbolSize !== held.repairSymbolSize) {
8300
+ return "refused";
8301
+ }
8302
+ if (next.maximumP <= this.#repairSymbols) return "unchanged";
8303
+ this.#repairSymbols = next.maximumP;
8304
+ this.#heldLayers = layerZero(next.maximumP);
8305
+ return "raised";
8153
8306
  }
8154
8307
  /** True once release() ran. */
8155
8308
  get released() {
@@ -8244,7 +8397,7 @@ var MmtpFecSession = class {
8244
8397
  }
8245
8398
  if (admission.reject) {
8246
8399
  const verdict2 = this.#heldLayers.failsClosed(
8247
- repairTrackName,
8400
+ this.#trackFor(repairTrackName),
8248
8401
  frame.ssStart,
8249
8402
  frame.rsbLength,
8250
8403
  this.#anchorHorizon,
@@ -8259,7 +8412,7 @@ var MmtpFecSession = class {
8259
8412
  this.#repairPacketId ??= frame.packetId;
8260
8413
  if (frame.ssbLength > this.sourceSymbols) return this.#drop("repair:ssb-length");
8261
8414
  const verdict = this.#heldLayers.admit(
8262
- repairTrackName,
8415
+ this.#trackFor(repairTrackName),
8263
8416
  frame.ssStart,
8264
8417
  frame.rsbLength,
8265
8418
  this.#anchorHorizon,
@@ -8318,6 +8471,10 @@ var MmtpFecSession = class {
8318
8471
  decoder.free();
8319
8472
  return true;
8320
8473
  }
8474
+ /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
8475
+ #trackFor(repairTrackName) {
8476
+ return this.#message ? void 0 : repairTrackName;
8477
+ }
8321
8478
  #drop(reason) {
8322
8479
  this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1;
8323
8480
  return [];
@@ -8439,6 +8596,22 @@ var MmtpFecSession = class {
8439
8596
  }
8440
8597
  }
8441
8598
  };
8599
+ function layerZero(total) {
8600
+ return new HeldLayerAdmission([{ layer: 0, repairTrackName: "", rsIdOffset: 0, repairSymbols: total }]);
8601
+ }
8602
+ function buildDecoder(Decoder, session) {
8603
+ if (typeof Decoder !== "function") {
8604
+ throw new Error("MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required");
8605
+ }
8606
+ const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm);
8607
+ try {
8608
+ decoder.configure_params(session.sourceSymbols, session.symbolSize);
8609
+ } catch (error) {
8610
+ decoder.free();
8611
+ throw error;
8612
+ }
8613
+ return decoder;
8614
+ }
8442
8615
  function requirePositiveInteger(value, path) {
8443
8616
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
8444
8617
  throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`);