@blockcast/mmt-container 2.2.0-main.da2e7cd1dd5d → 2.2.0-main.f480a7ede8b1

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 (45) hide show
  1. package/dist/browser-runtime.js +215 -23
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/delivery-catalog.js +2 -1
  4. package/dist/delivery-catalog.js.map +2 -2
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +95 -1
  7. package/dist/fec-manager.js.map +2 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +2 -2
  11. package/dist/iso-block-locator.d.ts +9 -0
  12. package/dist/iso-block-locator.d.ts.map +1 -1
  13. package/dist/iso-block-locator.js +17 -0
  14. package/dist/iso-block-locator.js.map +2 -2
  15. package/dist/iso-bmff-timing.d.ts +10 -1
  16. package/dist/iso-bmff-timing.d.ts.map +1 -1
  17. package/dist/iso-bmff-timing.js +36 -2
  18. package/dist/iso-bmff-timing.js.map +2 -2
  19. package/dist/mmtp-container-parser.d.ts +35 -2
  20. package/dist/mmtp-container-parser.d.ts.map +1 -1
  21. package/dist/mmtp-container-parser.js +82 -9
  22. package/dist/mmtp-container-parser.js.map +2 -2
  23. package/dist/mmtp-container-parser.no-modules.js +85 -11
  24. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  25. package/dist/mmtp-decode-pipeline.js +81 -9
  26. package/dist/mmtp-decode-pipeline.js.map +2 -2
  27. package/dist/mmtp-router.d.ts +6 -0
  28. package/dist/mmtp-router.d.ts.map +1 -1
  29. package/dist/mmtp-router.js +120 -22
  30. package/dist/mmtp-router.js.map +2 -2
  31. package/dist/multicast-manager.js +215 -23
  32. package/dist/multicast-manager.js.map +2 -2
  33. package/dist/multicast-transport.js +2 -1
  34. package/dist/multicast-transport.js.map +2 -2
  35. package/dist/no-modules.js +81 -9
  36. package/dist/no-modules.js.map +2 -2
  37. package/dist/wasm-mmtp-container-parser.js +81 -9
  38. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  39. package/package.json +5 -5
  40. package/src/fec-manager.ts +104 -1
  41. package/src/index.ts +1 -0
  42. package/src/iso-block-locator.ts +19 -0
  43. package/src/iso-bmff-timing.ts +40 -1
  44. package/src/mmtp-container-parser.ts +84 -8
  45. package/src/mmtp-router.ts +64 -18
@@ -1210,6 +1210,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1210
1210
  #retentionBlocks;
1211
1211
  #heldEvicted;
1212
1212
  #newest;
1213
+ #newestSource;
1213
1214
  #holes;
1214
1215
  static #checkedRetention(retentionBlocks) {
1215
1216
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -1247,6 +1248,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
1247
1248
  observe(position) {
1248
1249
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
1249
1250
  }
1251
+ /**
1252
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
1253
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
1254
+ * a source cannot run ahead of the sources sent before it except by
1255
+ * reordering.
1256
+ */
1257
+ get newestSource() {
1258
+ return this.#newestSource;
1259
+ }
1260
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
1261
+ noteSource(ssId) {
1262
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
1263
+ this.#newestSource = ssId;
1264
+ return true;
1265
+ }
1250
1266
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
1251
1267
  get heldEvicted() {
1252
1268
  return this.#heldEvicted;
@@ -1479,6 +1495,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1479
1495
  this.#held.clear();
1480
1496
  this.#coverageEnd = void 0;
1481
1497
  this.#newest = void 0;
1498
+ this.#newestSource = void 0;
1482
1499
  }
1483
1500
  #claim(block) {
1484
1501
  const placed = [];
@@ -4061,6 +4078,9 @@ var FECManager = class _FECManager {
4061
4078
  this.#unindexBlockKey(key);
4062
4079
  }
4063
4080
  }
4081
+ if (isSourceSymbol && source === "multicast") {
4082
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
4083
+ }
4064
4084
  return true;
4065
4085
  }
4066
4086
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -4378,6 +4398,7 @@ var FECManager = class _FECManager {
4378
4398
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4379
4399
  const evictedBefore = locator.heldEvicted;
4380
4400
  const held = locator.hold(ssId, source);
4401
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
4381
4402
  const evicted = locator.heldEvicted - evictedBefore;
4382
4403
  if (evicted > 0) {
4383
4404
  const before = this.#isoHeldEvictedCount;
@@ -5204,8 +5225,72 @@ var FECManager = class _FECManager {
5204
5225
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
5205
5226
  return null;
5206
5227
  }
5228
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
5207
5229
  return recovered;
5208
5230
  }
5231
+ /**
5232
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
5233
+ * its sources in send order, so one that moves the stream's newest source
5234
+ * position past a block leaves the sources that block still misses
5235
+ * reordered behind it at best: it shortens the wait of every block it
5236
+ * passed that only a late source can complete. MoQ is left out: its streams
5237
+ * can overtake one another, so a MoQ source shows nothing about the sources
5238
+ * sent before it.
5239
+ */
5240
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
5241
+ const previous = locator.newestSource;
5242
+ if (!locator.noteSource(ssId) || previous === void 0) return;
5243
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
5244
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
5245
+ this.#shortenUnrecoverableWait(
5246
+ this.#blockKey(block.start, packetId, catalogTrackName),
5247
+ block.start,
5248
+ packetId,
5249
+ catalogTrackName
5250
+ );
5251
+ }
5252
+ }
5253
+ /**
5254
+ * The wait of a block that only a late source can complete (BLO-52085): a
5255
+ * repair stated it, its received sources plus every symbol its repair group
5256
+ * carries (RSB_length) are short of K' (the repair scheduler's
5257
+ * group-cannot-complete), and a later source of the stream has arrived, so
5258
+ * a source it misses is lost or reordered behind that one. The span, which
5259
+ * covers repair symbols paced after its sources, buys it nothing: it waits
5260
+ * the late-symbol allowance alone, the multicast wait less the span (the
5261
+ * catalog tolerance or the measured delivery gap). Undefined for any other
5262
+ * block.
5263
+ */
5264
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
5265
+ const authoredLength = this.#blockAuthoredLengths.get(key);
5266
+ const rsbLength = this.#blockRsbLengths.get(key);
5267
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
5268
+ if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
5269
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
5270
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
5271
+ return void 0;
5272
+ }
5273
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
5274
+ }
5275
+ /**
5276
+ * Bring an open block's wait in to its late-source wait, when it has one.
5277
+ * Never later than it is armed: only a source can complete such a block,
5278
+ * so a repair of its group (paced across the span) must not keep it open.
5279
+ */
5280
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
5281
+ if (!this.#activeBlocks.has(key)) return;
5282
+ const config = this.#trackConfigs.get(packetId);
5283
+ if (config === void 0) return;
5284
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
5285
+ if (waitMs === void 0) return;
5286
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
5287
+ this.#armBlockTimeout(
5288
+ sbn,
5289
+ packetId,
5290
+ catalogTrackName,
5291
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
5292
+ );
5293
+ }
5209
5294
  /**
5210
5295
  * Record a block's authored SSB_length from a repair symbol. Returns false,
5211
5296
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -5434,7 +5519,16 @@ var FECManager = class _FECManager {
5434
5519
  this.#repairDeadlineMs(trackConfig)
5435
5520
  );
5436
5521
  }
5437
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
5522
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
5523
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
5524
+ return;
5525
+ }
5526
+ this.#armBlockTimeout(
5527
+ sbn,
5528
+ packetId,
5529
+ catalogTrackName,
5530
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
5531
+ );
5438
5532
  }
5439
5533
  #failBlock(sbn, packetId, catalogTrackName) {
5440
5534
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -7076,6 +7170,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7076
7170
  });
7077
7171
 
7078
7172
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7173
+ var MAX_PENDING_PACKETS = 256;
7079
7174
  var AMTGatewayManager = class {
7080
7175
  subscriptions = /* @__PURE__ */ new Map();
7081
7176
  messageHandler;
@@ -7199,7 +7294,7 @@ var AMTGatewayManager = class {
7199
7294
  if (!subscription.handshakeComplete) {
7200
7295
  const queue = this.pendingPackets.get(subscriptionId);
7201
7296
  const packetData2 = data;
7202
- if (queue) {
7297
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7203
7298
  queue.push({
7204
7299
  data: packetData2.data,
7205
7300
  remoteAddress: packetData2.remoteAddress,
@@ -13142,9 +13237,21 @@ var IsoBmffTimingResolver = class {
13142
13237
  }
13143
13238
  #track = null;
13144
13239
  #fragments = /* @__PURE__ */ new Map();
13240
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
13241
+ #firstSamples = /* @__PURE__ */ new Map();
13242
+ /**
13243
+ * First sample and sample count of the newest fragments whose every slot was
13244
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
13245
+ * matches the combiner, since it retires an abandoned sample as well.
13246
+ */
13247
+ #delivered = /* @__PURE__ */ new Map();
13145
13248
  #pendingSamples = 0;
13146
13249
  #evictedSamples = 0;
13147
13250
  #lastMovieFragmentRejection = null;
13251
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
13252
+ get initialized() {
13253
+ return this.#track !== null;
13254
+ }
13148
13255
  get pendingSampleCount() {
13149
13256
  return this.#pendingSamples;
13150
13257
  }
@@ -13168,7 +13275,14 @@ var IsoBmffTimingResolver = class {
13168
13275
  this.#track = timing;
13169
13276
  return true;
13170
13277
  }
13171
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
13278
+ /**
13279
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
13280
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
13281
+ * fragment whose samples are all done is a duplicate: admitting it again
13282
+ * would charge stale timing and could displace the live MPU's. A source
13283
+ * copy is still admitted again (a rendition switch re-arms timing that way).
13284
+ */
13285
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
13172
13286
  this.#lastMovieFragmentRejection = null;
13173
13287
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13174
13288
  this.#lastMovieFragmentRejection = "invalid";
@@ -13194,6 +13308,8 @@ var IsoBmffTimingResolver = class {
13194
13308
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13195
13309
  );
13196
13310
  }
13311
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
13312
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
13197
13313
  let freed = 0;
13198
13314
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13199
13315
  freed += this.#evictOldestFragment();
@@ -13207,6 +13323,7 @@ var IsoBmffTimingResolver = class {
13207
13323
  return false;
13208
13324
  }
13209
13325
  this.#fragments.set(key, [...parsed.samples]);
13326
+ this.#firstSamples.set(key, parsed.samples[0]);
13210
13327
  this.#pendingSamples += parsed.samples.length;
13211
13328
  return true;
13212
13329
  }
@@ -13264,6 +13381,7 @@ var IsoBmffTimingResolver = class {
13264
13381
  let live = 0;
13265
13382
  for (const sample of samples) if (sample !== null) live++;
13266
13383
  this.#fragments.delete(oldest);
13384
+ this.#firstSamples.delete(oldest);
13267
13385
  this.#pendingSamples -= live;
13268
13386
  this.#evictedSamples += live;
13269
13387
  return live;
@@ -13279,12 +13397,23 @@ var IsoBmffTimingResolver = class {
13279
13397
  if (!samples || !samples[index]) return false;
13280
13398
  samples[index] = null;
13281
13399
  this.#pendingSamples--;
13282
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
13400
+ if (samples.every((value) => value === null)) {
13401
+ this.#fragments.delete(key);
13402
+ const first = this.#firstSamples.get(key);
13403
+ this.#firstSamples.delete(key);
13404
+ this.#delivered.delete(key);
13405
+ this.#delivered.set(key, { first, sampleCount: samples.length });
13406
+ if (this.#delivered.size > this.maxPendingSamples) {
13407
+ this.#delivered.delete(this.#delivered.keys().next().value);
13408
+ }
13409
+ }
13283
13410
  return true;
13284
13411
  }
13285
13412
  reset() {
13286
13413
  this.#track = null;
13287
13414
  this.#fragments.clear();
13415
+ this.#firstSamples.clear();
13416
+ this.#delivered.clear();
13288
13417
  this.#pendingSamples = 0;
13289
13418
  }
13290
13419
  };
@@ -13766,6 +13895,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13766
13895
  this.#reclaimedTimingSamples = 0;
13767
13896
  this.#evictedTimingSamples = 0;
13768
13897
  this.#rejectedMovieFragments = 0;
13898
+ this.#recoveredHeadsAwaitingTiming = {
13899
+ held: 0,
13900
+ expired: 0,
13901
+ discarded: 0,
13902
+ superseded: 0
13903
+ };
13769
13904
  this.#sequencers = /* @__PURE__ */ new Map();
13770
13905
  this.#fecGate = null;
13771
13906
  this.#fecSequencer = null;
@@ -13874,6 +14009,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13874
14009
  #reclaimedTimingSamples;
13875
14010
  #evictedTimingSamples;
13876
14011
  #rejectedMovieFragments;
14012
+ #recoveredHeadsAwaitingTiming;
13877
14013
  #sequencers;
13878
14014
  #fecGate;
13879
14015
  #fecSequencer;
@@ -13994,6 +14130,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13994
14130
  rejectedMovieFragments: this.#rejectedMovieFragments
13995
14131
  };
13996
14132
  }
14133
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14134
+ get recoveredHeadsAwaitingTiming() {
14135
+ let pending = 0;
14136
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14137
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14138
+ }
14139
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14140
+ }
13997
14141
  get reorderStats() {
13998
14142
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13999
14143
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14167,7 +14311,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14167
14311
  const now = Date.now();
14168
14312
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14169
14313
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14170
- awaitingTiming.shift();
14314
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14171
14315
  }
14172
14316
  }
14173
14317
  }
@@ -14285,11 +14429,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14285
14429
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14286
14430
  return this.#drop("fragment-unexpected-fragmentation");
14287
14431
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14288
- if (!resolver2) return this.#drop("fragment-no-resolver");
14432
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14289
14433
  const pendingBefore = resolver2.pendingSampleCount;
14290
14434
  const evictedBefore = resolver2.evictedSampleCount;
14291
14435
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14292
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14436
+ const admitted = resolver2.addMovieFragment(
14437
+ mpu.mpuSequenceNumber,
14438
+ data,
14439
+ remaining,
14440
+ options?.recovered === true
14441
+ );
14293
14442
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14294
14443
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14295
14444
  if (!admitted) {
@@ -14309,7 +14458,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14309
14458
  };
14310
14459
  }
14311
14460
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14312
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14461
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14313
14462
  if (!resolver) return this.#drop("mfu-no-resolver");
14314
14463
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14315
14464
  return this.#drop("mfu-empty-payload");
@@ -14330,6 +14479,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14330
14479
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14331
14480
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14332
14481
  sampleNumber: mfuDuHeader.sampleNumber,
14482
+ sequenceNumber: info.sequenceNumber,
14333
14483
  arrivedAt: Date.now()
14334
14484
  });
14335
14485
  return this.#drop("mfu-sample-timing-missing");
@@ -14718,6 +14868,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14718
14868
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14719
14869
  this.#sequencers.clear();
14720
14870
  this.#fecSequencer?.discard();
14871
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14872
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14873
+ }
14721
14874
  this.#timingStreams.clear();
14722
14875
  this.#pendingTimingSamples = 0;
14723
14876
  this.#reliableMoqStartupGate.clear();
@@ -14736,10 +14889,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14736
14889
  return resolver;
14737
14890
  }
14738
14891
  #awaitTiming(streamKey, head) {
14739
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14740
- if (!awaitingTiming) return;
14892
+ const state = this.#timingStreams.get(streamKey);
14893
+ if (!state) return;
14894
+ if (!head.options?.recovered) {
14895
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
14896
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
14897
+ state.awaitingTiming = kept;
14898
+ }
14899
+ const awaitingTiming = state.awaitingTiming;
14741
14900
  awaitingTiming.push(head);
14742
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
14901
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
14902
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
14903
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14904
+ }
14905
+ }
14906
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
14907
+ #leaveAwaitingTiming(head, outcome) {
14908
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14743
14909
  }
14744
14910
  /**
14745
14911
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14767,6 +14933,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14767
14933
  const state = this.#timingStreams.get(streamKey);
14768
14934
  if (!state) return;
14769
14935
  this.#timingStreams.delete(streamKey);
14936
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14770
14937
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14771
14938
  if (this.#pendingTimingSamples < 0) {
14772
14939
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -18381,7 +18548,7 @@ var MmtpRouter = class {
18381
18548
  const cb = this.#getFrameCallback(packetId);
18382
18549
  if (isVideo && !assembler) {
18383
18550
  if (frameMeta.recovered) {
18384
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
18551
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
18385
18552
  } else {
18386
18553
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
18387
18554
  }
@@ -18771,6 +18938,10 @@ var MmtpRouter = class {
18771
18938
  get keyframeOverlayStats() {
18772
18939
  return { ...this.#overlayStats };
18773
18940
  }
18941
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
18942
+ get recoveredHeadsAwaitingTiming() {
18943
+ return this.#parser.recoveredHeadsAwaitingTiming;
18944
+ }
18774
18945
  #isOverlayRepair(repairPacketId, repairTrackName) {
18775
18946
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18776
18947
  }
@@ -20925,8 +21096,16 @@ var MmtpRouter = class {
20925
21096
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
20926
21097
  this.#emitDirectVideo(packetId, data, meta, cb);
20927
21098
  }
20928
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
21099
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
20929
21100
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
21101
+ if (this.#isFecTrack(packetId)) {
21102
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
21103
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
21104
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
21105
+ this.#emitDirectVideo(packetId, data, meta, cb);
21106
+ return;
21107
+ }
21108
+ }
20930
21109
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
20931
21110
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
20932
21111
  this.#drainDirectRecoveredVideo(
@@ -21180,16 +21359,29 @@ var MmtpRouter = class {
21180
21359
  }
21181
21360
  );
21182
21361
  }
21183
- result = {
21184
- ...result,
21185
- symbols: result.symbols.filter((sym) => sym.fragmentType !== 2 || sym.fragment.fragmentationIndicator !== 1 || sym.fragment.movieFragmentSequenceNumber === void 0 || sym.fragment.sampleNumber === void 0 || this.#parser.hasAuthoredTimingFor(
21186
- ownerPacketId,
21187
- sym.fragment.mpuSequenceNumber,
21188
- sym.fragment.movieFragmentSequenceNumber,
21189
- sym.fragment.sampleNumber,
21190
- catalogTrackName
21191
- ))
21192
- };
21362
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21363
+ ownerPacketId,
21364
+ sym.fragment.mpuSequenceNumber,
21365
+ sym.fragment.movieFragmentSequenceNumber,
21366
+ sym.fragment.sampleNumber,
21367
+ catalogTrackName
21368
+ );
21369
+ for (const sym of result.symbols) {
21370
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21371
+ this.#feedViaParser(
21372
+ sym.rawSymbol,
21373
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21374
+ void 0,
21375
+ {
21376
+ skipFecPayloadId: true,
21377
+ recovered: true,
21378
+ deliveryProvenance,
21379
+ ...fecSbnOf(sym.index),
21380
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21381
+ }
21382
+ );
21383
+ }
21384
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21193
21385
  const gated = this.#isFecTrack(ownerPacketId);
21194
21386
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21195
21387
  if (assembler) {