@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.75b39a2cdddf

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 (93) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1001 -215
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +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 +222 -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 +58 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +105 -34
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +229 -51
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +104 -34
  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 +18 -1
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +323 -64
  56. package/dist/mmtp-router.js.map +2 -2
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.js +547 -72
  60. package/dist/multicast-manager.js.map +2 -2
  61. package/dist/multicast-transport.js +28 -18
  62. package/dist/multicast-transport.js.map +2 -2
  63. package/dist/multicast-unwrap.js +12 -19
  64. package/dist/multicast-unwrap.js.map +2 -2
  65. package/dist/no-modules.js +222 -48
  66. package/dist/no-modules.js.map +3 -3
  67. package/dist/repair-fetch.js.map +1 -1
  68. package/dist/repair-layer-subscriptions.d.ts +82 -0
  69. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  70. package/dist/repair-layer-subscriptions.js +192 -0
  71. package/dist/repair-layer-subscriptions.js.map +7 -0
  72. package/dist/signaling.js.map +1 -1
  73. package/dist/timestamp-codec.js +12 -19
  74. package/dist/timestamp-codec.js.map +2 -2
  75. package/dist/track-session.d.ts +1 -1
  76. package/dist/track-session.js.map +1 -1
  77. package/dist/wasm-mfu-reassembler.js.map +1 -1
  78. package/dist/wasm-mmtp-container-parser.js +104 -34
  79. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  80. package/package.json +8 -5
  81. package/schemas/catalog.schema.json +4 -0
  82. package/src/catalog-fec-params.ts +36 -1
  83. package/src/cmcd-producer.ts +189 -179
  84. package/src/fec-manager.ts +277 -6
  85. package/src/fec-worker-setup.ts +2 -2
  86. package/src/index.ts +18 -2
  87. package/src/iso-block-locator.ts +19 -0
  88. package/src/iso-bmff-timing.ts +40 -1
  89. package/src/mmtp-container-parser.ts +125 -19
  90. package/src/mmtp-fec-session.ts +115 -15
  91. package/src/mmtp-router.ts +350 -13
  92. package/src/repair-layer-subscriptions.ts +261 -0
  93. package/src/track-session.ts +1 -1
@@ -148,30 +148,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
148
148
  const authLength = view2.getUint16(2, false);
149
149
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
150
150
  }
151
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
151
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
152
152
  if (declaredEnd > payload.length)
153
153
  return null;
154
154
  if (options?.skipFecId) {
155
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
155
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
156
156
  }
157
- const overlay = options?.overlayDeclared === true && rapFlag;
158
157
  if (fecType === 1) {
159
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
158
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
160
159
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
161
160
  return null;
162
161
  }
163
- return { hasFecId: true, hasOverlayFecId: overlay };
162
+ return { hasFecId: true };
164
163
  }
165
- if (fecType !== 0 || overlay)
164
+ if (fecType !== 0)
166
165
  return null;
167
166
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
168
- return { hasFecId: false, hasOverlayFecId: false };
167
+ return { hasFecId: false };
169
168
  }
170
169
  return null;
171
170
  }
172
- function sourceFecTrailerSize(overlay) {
173
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
174
- }
175
171
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
176
172
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
177
173
  return {
@@ -224,11 +220,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
224
220
  if ("rejection" in layoutResult)
225
221
  return layoutResult;
226
222
  const { layout } = layoutResult;
227
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
223
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
228
224
  if (!trailerLayout) {
229
225
  if (!options?.skipFecId && header.fecType === 1) {
230
226
  const observed = payload.length - layout.dataEnd;
231
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
227
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
232
228
  if (observed !== expected) {
233
229
  return {
234
230
  rejection: {
@@ -260,13 +256,10 @@ function extractPlannedMfu(mmtpPacket, result) {
260
256
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
261
257
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
262
258
  const data = payload.subarray(mfuOffset, dataEnd);
263
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
264
- const ssIdAt = (offset) => ({
265
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
266
- });
267
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
268
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
269
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
259
+ const fecPayloadId = trailerLayout.hasFecId ? {
260
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
261
+ } : null;
262
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
270
263
  }
271
264
  function sourceFecPayloadIdOf(mmtpPacket, header) {
272
265
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -4858,9 +4851,21 @@ var IsoBmffTimingResolver = class {
4858
4851
  }
4859
4852
  #track = null;
4860
4853
  #fragments = /* @__PURE__ */ new Map();
4854
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
4855
+ #firstSamples = /* @__PURE__ */ new Map();
4856
+ /**
4857
+ * First sample and sample count of the newest fragments whose every slot was
4858
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
4859
+ * matches the combiner, since it retires an abandoned sample as well.
4860
+ */
4861
+ #delivered = /* @__PURE__ */ new Map();
4861
4862
  #pendingSamples = 0;
4862
4863
  #evictedSamples = 0;
4863
4864
  #lastMovieFragmentRejection = null;
4865
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
4866
+ get initialized() {
4867
+ return this.#track !== null;
4868
+ }
4864
4869
  get pendingSampleCount() {
4865
4870
  return this.#pendingSamples;
4866
4871
  }
@@ -4884,7 +4889,14 @@ var IsoBmffTimingResolver = class {
4884
4889
  this.#track = timing;
4885
4890
  return true;
4886
4891
  }
4887
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
4892
+ /**
4893
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
4894
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
4895
+ * fragment whose samples are all done is a duplicate: admitting it again
4896
+ * would charge stale timing and could displace the live MPU's. A source
4897
+ * copy is still admitted again (a rendition switch re-arms timing that way).
4898
+ */
4899
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
4888
4900
  this.#lastMovieFragmentRejection = null;
4889
4901
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
4890
4902
  this.#lastMovieFragmentRejection = "invalid";
@@ -4910,6 +4922,8 @@ var IsoBmffTimingResolver = class {
4910
4922
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
4911
4923
  );
4912
4924
  }
4925
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
4926
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
4913
4927
  let freed = 0;
4914
4928
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
4915
4929
  freed += this.#evictOldestFragment();
@@ -4923,6 +4937,7 @@ var IsoBmffTimingResolver = class {
4923
4937
  return false;
4924
4938
  }
4925
4939
  this.#fragments.set(key, [...parsed.samples]);
4940
+ this.#firstSamples.set(key, parsed.samples[0]);
4926
4941
  this.#pendingSamples += parsed.samples.length;
4927
4942
  return true;
4928
4943
  }
@@ -4980,6 +4995,7 @@ var IsoBmffTimingResolver = class {
4980
4995
  let live = 0;
4981
4996
  for (const sample of samples) if (sample !== null) live++;
4982
4997
  this.#fragments.delete(oldest);
4998
+ this.#firstSamples.delete(oldest);
4983
4999
  this.#pendingSamples -= live;
4984
5000
  this.#evictedSamples += live;
4985
5001
  return live;
@@ -4995,12 +5011,23 @@ var IsoBmffTimingResolver = class {
4995
5011
  if (!samples || !samples[index]) return false;
4996
5012
  samples[index] = null;
4997
5013
  this.#pendingSamples--;
4998
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
5014
+ if (samples.every((value) => value === null)) {
5015
+ this.#fragments.delete(key);
5016
+ const first = this.#firstSamples.get(key);
5017
+ this.#firstSamples.delete(key);
5018
+ this.#delivered.delete(key);
5019
+ this.#delivered.set(key, { first, sampleCount: samples.length });
5020
+ if (this.#delivered.size > this.maxPendingSamples) {
5021
+ this.#delivered.delete(this.#delivered.keys().next().value);
5022
+ }
5023
+ }
4999
5024
  return true;
5000
5025
  }
5001
5026
  reset() {
5002
5027
  this.#track = null;
5003
5028
  this.#fragments.clear();
5029
+ this.#firstSamples.clear();
5030
+ this.#delivered.clear();
5004
5031
  this.#pendingSamples = 0;
5005
5032
  }
5006
5033
  };
@@ -5482,6 +5509,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
5482
5509
  this.#reclaimedTimingSamples = 0;
5483
5510
  this.#evictedTimingSamples = 0;
5484
5511
  this.#rejectedMovieFragments = 0;
5512
+ this.#recoveredHeadsAwaitingTiming = {
5513
+ held: 0,
5514
+ expired: 0,
5515
+ discarded: 0,
5516
+ superseded: 0
5517
+ };
5485
5518
  this.#sequencers = /* @__PURE__ */ new Map();
5486
5519
  this.#fecGate = null;
5487
5520
  this.#fecSequencer = null;
@@ -5590,6 +5623,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
5590
5623
  #reclaimedTimingSamples;
5591
5624
  #evictedTimingSamples;
5592
5625
  #rejectedMovieFragments;
5626
+ #recoveredHeadsAwaitingTiming;
5593
5627
  #sequencers;
5594
5628
  #fecGate;
5595
5629
  #fecSequencer;
@@ -5695,11 +5729,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
5695
5729
  /**
5696
5730
  * Typed view of the ISO-BMFF timing-sample budget.
5697
5731
  *
5698
- * This is the acceptance signal for the budget ratchet: `pending` walking
5699
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
5700
- * and it is observable here rather than only as media silently stopping.
5701
- * Per repo policy this is a producer-owned counter -- never infer budget
5702
- * state by parsing logs.
5732
+ * Each stream admits timing against its own window of `capacity` slots
5733
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
5734
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
5735
+ * timing a stream declared and never delivered shows up there. `pending`
5736
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
5737
+ * a producer-owned counter -- never infer budget state by parsing logs.
5703
5738
  */
5704
5739
  get timingBudgetStats() {
5705
5740
  return {
@@ -5710,6 +5745,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
5710
5745
  rejectedMovieFragments: this.#rejectedMovieFragments
5711
5746
  };
5712
5747
  }
5748
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
5749
+ get recoveredHeadsAwaitingTiming() {
5750
+ let pending = 0;
5751
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
5752
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
5753
+ }
5754
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
5755
+ }
5713
5756
  get reorderStats() {
5714
5757
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
5715
5758
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -5883,7 +5926,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
5883
5926
  const now = Date.now();
5884
5927
  for (const { awaitingTiming } of this.#timingStreams.values()) {
5885
5928
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
5886
- awaitingTiming.shift();
5929
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
5887
5930
  }
5888
5931
  }
5889
5932
  }
@@ -6001,11 +6044,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
6001
6044
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
6002
6045
  return this.#drop("fragment-unexpected-fragmentation");
6003
6046
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
6004
- if (!resolver2) return this.#drop("fragment-no-resolver");
6047
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
6005
6048
  const pendingBefore = resolver2.pendingSampleCount;
6006
6049
  const evictedBefore = resolver2.evictedSampleCount;
6007
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
6008
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
6050
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
6051
+ const admitted = resolver2.addMovieFragment(
6052
+ mpu.mpuSequenceNumber,
6053
+ data,
6054
+ remaining,
6055
+ options?.recovered === true
6056
+ );
6009
6057
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
6010
6058
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
6011
6059
  if (!admitted) {
@@ -6025,7 +6073,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6025
6073
  };
6026
6074
  }
6027
6075
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
6028
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
6076
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
6029
6077
  if (!resolver) return this.#drop("mfu-no-resolver");
6030
6078
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
6031
6079
  return this.#drop("mfu-empty-payload");
@@ -6046,6 +6094,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6046
6094
  mpuSequenceNumber: mpu.mpuSequenceNumber,
6047
6095
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
6048
6096
  sampleNumber: mfuDuHeader.sampleNumber,
6097
+ sequenceNumber: info.sequenceNumber,
6049
6098
  arrivedAt: Date.now()
6050
6099
  });
6051
6100
  return this.#drop("mfu-sample-timing-missing");
@@ -6369,6 +6418,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
6369
6418
  }
6370
6419
  this.#emitFrame(data, meta);
6371
6420
  }
6421
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
6422
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
6423
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
6424
+ }
6372
6425
  /**
6373
6426
  * Claim and return the authored timing for a recovered sample. Recovery does
6374
6427
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -6430,6 +6483,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
6430
6483
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
6431
6484
  this.#sequencers.clear();
6432
6485
  this.#fecSequencer?.discard();
6486
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
6487
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
6488
+ }
6433
6489
  this.#timingStreams.clear();
6434
6490
  this.#pendingTimingSamples = 0;
6435
6491
  this.#reliableMoqStartupGate.clear();
@@ -6448,10 +6504,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
6448
6504
  return resolver;
6449
6505
  }
6450
6506
  #awaitTiming(streamKey, head) {
6451
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
6452
- if (!awaitingTiming) return;
6507
+ const state = this.#timingStreams.get(streamKey);
6508
+ if (!state) return;
6509
+ if (!head.options?.recovered) {
6510
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
6511
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
6512
+ state.awaitingTiming = kept;
6513
+ }
6514
+ const awaitingTiming = state.awaitingTiming;
6453
6515
  awaitingTiming.push(head);
6454
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
6516
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
6517
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
6518
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
6519
+ }
6520
+ }
6521
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
6522
+ #leaveAwaitingTiming(head, outcome) {
6523
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
6455
6524
  }
6456
6525
  /**
6457
6526
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -6479,6 +6548,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6479
6548
  const state = this.#timingStreams.get(streamKey);
6480
6549
  if (!state) return;
6481
6550
  this.#timingStreams.delete(streamKey);
6551
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
6482
6552
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
6483
6553
  if (this.#pendingTimingSamples < 0) {
6484
6554
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");