@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
@@ -8746,6 +8746,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8746
8746
  });
8747
8747
 
8748
8748
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8749
+ var MAX_PENDING_PACKETS = 256;
8749
8750
  var AMTGatewayManager = class {
8750
8751
  subscriptions = /* @__PURE__ */ new Map();
8751
8752
  messageHandler;
@@ -8869,7 +8870,7 @@ var AMTGatewayManager = class {
8869
8870
  if (!subscription.handshakeComplete) {
8870
8871
  const queue = this.pendingPackets.get(subscriptionId);
8871
8872
  const packetData2 = data;
8872
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8873
8874
  queue.push({
8874
8875
  data: packetData2.data,
8875
8876
  remoteAddress: packetData2.remoteAddress,
@@ -12948,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12948
12949
  #retentionBlocks;
12949
12950
  #heldEvicted;
12950
12951
  #newest;
12952
+ #newestSource;
12951
12953
  #holes;
12952
12954
  static #checkedRetention(retentionBlocks) {
12953
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12985,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12985
12987
  observe(position) {
12986
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12987
12989
  }
12990
+ /**
12991
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
12992
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
12993
+ * a source cannot run ahead of the sources sent before it except by
12994
+ * reordering.
12995
+ */
12996
+ get newestSource() {
12997
+ return this.#newestSource;
12998
+ }
12999
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13000
+ noteSource(ssId) {
13001
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13002
+ this.#newestSource = ssId;
13003
+ return true;
13004
+ }
12988
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12989
13006
  get heldEvicted() {
12990
13007
  return this.#heldEvicted;
@@ -13217,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13217
13234
  this.#held.clear();
13218
13235
  this.#coverageEnd = void 0;
13219
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13220
13238
  }
13221
13239
  #claim(block) {
13222
13240
  const placed = [];
@@ -15830,6 +15848,9 @@ var FECManager = class _FECManager {
15830
15848
  this.#unindexBlockKey(key);
15831
15849
  }
15832
15850
  }
15851
+ if (isSourceSymbol && source === "multicast") {
15852
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15853
+ }
15833
15854
  return true;
15834
15855
  }
15835
15856
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16147,6 +16168,7 @@ var FECManager = class _FECManager {
16147
16168
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16148
16169
  const evictedBefore = locator.heldEvicted;
16149
16170
  const held = locator.hold(ssId, source);
16171
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16150
16172
  const evicted = locator.heldEvicted - evictedBefore;
16151
16173
  if (evicted > 0) {
16152
16174
  const before = this.#isoHeldEvictedCount;
@@ -16973,8 +16995,72 @@ var FECManager = class _FECManager {
16973
16995
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16974
16996
  return null;
16975
16997
  }
16998
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16976
16999
  return recovered;
16977
17000
  }
17001
+ /**
17002
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17003
+ * its sources in send order, so one that moves the stream's newest source
17004
+ * position past a block leaves the sources that block still misses
17005
+ * reordered behind it at best: it shortens the wait of every block it
17006
+ * passed that only a late source can complete. MoQ is left out: its streams
17007
+ * can overtake one another, so a MoQ source shows nothing about the sources
17008
+ * sent before it.
17009
+ */
17010
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17011
+ const previous = locator.newestSource;
17012
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17013
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17014
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17015
+ this.#shortenUnrecoverableWait(
17016
+ this.#blockKey(block.start, packetId, catalogTrackName),
17017
+ block.start,
17018
+ packetId,
17019
+ catalogTrackName
17020
+ );
17021
+ }
17022
+ }
17023
+ /**
17024
+ * The wait of a block that only a late source can complete (BLO-52085): a
17025
+ * repair stated it, its received sources plus every symbol its repair group
17026
+ * carries (RSB_length) are short of K' (the repair scheduler's
17027
+ * group-cannot-complete), and a later source of the stream has arrived, so
17028
+ * a source it misses is lost or reordered behind that one. The span, which
17029
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17030
+ * the late-symbol allowance alone, the multicast wait less the span (the
17031
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17032
+ * block.
17033
+ */
17034
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17035
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17036
+ const rsbLength = this.#blockRsbLengths.get(key);
17037
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17038
+ if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
17039
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17040
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17041
+ return void 0;
17042
+ }
17043
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17044
+ }
17045
+ /**
17046
+ * Bring an open block's wait in to its late-source wait, when it has one.
17047
+ * Never later than it is armed: only a source can complete such a block,
17048
+ * so a repair of its group (paced across the span) must not keep it open.
17049
+ */
17050
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17051
+ if (!this.#activeBlocks.has(key)) return;
17052
+ const config = this.#trackConfigs.get(packetId);
17053
+ if (config === void 0) return;
17054
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17055
+ if (waitMs === void 0) return;
17056
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17057
+ this.#armBlockTimeout(
17058
+ sbn,
17059
+ packetId,
17060
+ catalogTrackName,
17061
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17062
+ );
17063
+ }
16978
17064
  /**
16979
17065
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16980
17066
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17203,7 +17289,16 @@ var FECManager = class _FECManager {
17203
17289
  this.#repairDeadlineMs(trackConfig)
17204
17290
  );
17205
17291
  }
17206
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17292
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17293
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17294
+ return;
17295
+ }
17296
+ this.#armBlockTimeout(
17297
+ sbn,
17298
+ packetId,
17299
+ catalogTrackName,
17300
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17301
+ );
17207
17302
  }
17208
17303
  #failBlock(sbn, packetId, catalogTrackName) {
17209
17304
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17901,9 +17996,21 @@ var IsoBmffTimingResolver = class {
17901
17996
  }
17902
17997
  #track = null;
17903
17998
  #fragments = /* @__PURE__ */ new Map();
17999
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18000
+ #firstSamples = /* @__PURE__ */ new Map();
18001
+ /**
18002
+ * First sample and sample count of the newest fragments whose every slot was
18003
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18004
+ * matches the combiner, since it retires an abandoned sample as well.
18005
+ */
18006
+ #delivered = /* @__PURE__ */ new Map();
17904
18007
  #pendingSamples = 0;
17905
18008
  #evictedSamples = 0;
17906
18009
  #lastMovieFragmentRejection = null;
18010
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18011
+ get initialized() {
18012
+ return this.#track !== null;
18013
+ }
17907
18014
  get pendingSampleCount() {
17908
18015
  return this.#pendingSamples;
17909
18016
  }
@@ -17927,7 +18034,14 @@ var IsoBmffTimingResolver = class {
17927
18034
  this.#track = timing;
17928
18035
  return true;
17929
18036
  }
17930
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18037
+ /**
18038
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18039
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18040
+ * fragment whose samples are all done is a duplicate: admitting it again
18041
+ * would charge stale timing and could displace the live MPU's. A source
18042
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18043
+ */
18044
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17931
18045
  this.#lastMovieFragmentRejection = null;
17932
18046
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17933
18047
  this.#lastMovieFragmentRejection = "invalid";
@@ -17953,6 +18067,8 @@ var IsoBmffTimingResolver = class {
17953
18067
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17954
18068
  );
17955
18069
  }
18070
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18071
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17956
18072
  let freed = 0;
17957
18073
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17958
18074
  freed += this.#evictOldestFragment();
@@ -17966,6 +18082,7 @@ var IsoBmffTimingResolver = class {
17966
18082
  return false;
17967
18083
  }
17968
18084
  this.#fragments.set(key, [...parsed.samples]);
18085
+ this.#firstSamples.set(key, parsed.samples[0]);
17969
18086
  this.#pendingSamples += parsed.samples.length;
17970
18087
  return true;
17971
18088
  }
@@ -18023,6 +18140,7 @@ var IsoBmffTimingResolver = class {
18023
18140
  let live = 0;
18024
18141
  for (const sample of samples) if (sample !== null) live++;
18025
18142
  this.#fragments.delete(oldest);
18143
+ this.#firstSamples.delete(oldest);
18026
18144
  this.#pendingSamples -= live;
18027
18145
  this.#evictedSamples += live;
18028
18146
  return live;
@@ -18038,12 +18156,23 @@ var IsoBmffTimingResolver = class {
18038
18156
  if (!samples || !samples[index]) return false;
18039
18157
  samples[index] = null;
18040
18158
  this.#pendingSamples--;
18041
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18159
+ if (samples.every((value) => value === null)) {
18160
+ this.#fragments.delete(key);
18161
+ const first = this.#firstSamples.get(key);
18162
+ this.#firstSamples.delete(key);
18163
+ this.#delivered.delete(key);
18164
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18165
+ if (this.#delivered.size > this.maxPendingSamples) {
18166
+ this.#delivered.delete(this.#delivered.keys().next().value);
18167
+ }
18168
+ }
18042
18169
  return true;
18043
18170
  }
18044
18171
  reset() {
18045
18172
  this.#track = null;
18046
18173
  this.#fragments.clear();
18174
+ this.#firstSamples.clear();
18175
+ this.#delivered.clear();
18047
18176
  this.#pendingSamples = 0;
18048
18177
  }
18049
18178
  };
@@ -18525,6 +18654,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18525
18654
  this.#reclaimedTimingSamples = 0;
18526
18655
  this.#evictedTimingSamples = 0;
18527
18656
  this.#rejectedMovieFragments = 0;
18657
+ this.#recoveredHeadsAwaitingTiming = {
18658
+ held: 0,
18659
+ expired: 0,
18660
+ discarded: 0,
18661
+ superseded: 0
18662
+ };
18528
18663
  this.#sequencers = /* @__PURE__ */ new Map();
18529
18664
  this.#fecGate = null;
18530
18665
  this.#fecSequencer = null;
@@ -18633,6 +18768,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18633
18768
  #reclaimedTimingSamples;
18634
18769
  #evictedTimingSamples;
18635
18770
  #rejectedMovieFragments;
18771
+ #recoveredHeadsAwaitingTiming;
18636
18772
  #sequencers;
18637
18773
  #fecGate;
18638
18774
  #fecSequencer;
@@ -18753,6 +18889,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18753
18889
  rejectedMovieFragments: this.#rejectedMovieFragments
18754
18890
  };
18755
18891
  }
18892
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18893
+ get recoveredHeadsAwaitingTiming() {
18894
+ let pending = 0;
18895
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18896
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18897
+ }
18898
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
18899
+ }
18756
18900
  get reorderStats() {
18757
18901
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18758
18902
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18926,7 +19070,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18926
19070
  const now = Date.now();
18927
19071
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18928
19072
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18929
- awaitingTiming.shift();
19073
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18930
19074
  }
18931
19075
  }
18932
19076
  }
@@ -19044,11 +19188,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19044
19188
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19045
19189
  return this.#drop("fragment-unexpected-fragmentation");
19046
19190
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19047
- if (!resolver2) return this.#drop("fragment-no-resolver");
19191
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19048
19192
  const pendingBefore = resolver2.pendingSampleCount;
19049
19193
  const evictedBefore = resolver2.evictedSampleCount;
19050
19194
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19051
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19195
+ const admitted = resolver2.addMovieFragment(
19196
+ mpu.mpuSequenceNumber,
19197
+ data,
19198
+ remaining,
19199
+ options?.recovered === true
19200
+ );
19052
19201
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19053
19202
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19054
19203
  if (!admitted) {
@@ -19068,7 +19217,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19068
19217
  };
19069
19218
  }
19070
19219
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19071
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19220
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19072
19221
  if (!resolver) return this.#drop("mfu-no-resolver");
19073
19222
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19074
19223
  return this.#drop("mfu-empty-payload");
@@ -19089,6 +19238,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19089
19238
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19090
19239
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19091
19240
  sampleNumber: mfuDuHeader.sampleNumber,
19241
+ sequenceNumber: info.sequenceNumber,
19092
19242
  arrivedAt: Date.now()
19093
19243
  });
19094
19244
  return this.#drop("mfu-sample-timing-missing");
@@ -19477,6 +19627,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19477
19627
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19478
19628
  this.#sequencers.clear();
19479
19629
  this.#fecSequencer?.discard();
19630
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19631
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19632
+ }
19480
19633
  this.#timingStreams.clear();
19481
19634
  this.#pendingTimingSamples = 0;
19482
19635
  this.#reliableMoqStartupGate.clear();
@@ -19495,10 +19648,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19495
19648
  return resolver;
19496
19649
  }
19497
19650
  #awaitTiming(streamKey, head) {
19498
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19499
- if (!awaitingTiming) return;
19651
+ const state = this.#timingStreams.get(streamKey);
19652
+ if (!state) return;
19653
+ if (!head.options?.recovered) {
19654
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19655
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19656
+ state.awaitingTiming = kept;
19657
+ }
19658
+ const awaitingTiming = state.awaitingTiming;
19500
19659
  awaitingTiming.push(head);
19501
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19660
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19661
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19662
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19663
+ }
19664
+ }
19665
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19666
+ #leaveAwaitingTiming(head, outcome) {
19667
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19502
19668
  }
19503
19669
  /**
19504
19670
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19526,6 +19692,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19526
19692
  const state = this.#timingStreams.get(streamKey);
19527
19693
  if (!state) return;
19528
19694
  this.#timingStreams.delete(streamKey);
19695
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19529
19696
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19530
19697
  if (this.#pendingTimingSamples < 0) {
19531
19698
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -23141,7 +23308,7 @@ var MmtpRouter = class {
23141
23308
  const cb = this.#getFrameCallback(packetId);
23142
23309
  if (isVideo && !assembler) {
23143
23310
  if (frameMeta.recovered) {
23144
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23311
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23145
23312
  } else {
23146
23313
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23147
23314
  }
@@ -23531,6 +23698,10 @@ var MmtpRouter = class {
23531
23698
  get keyframeOverlayStats() {
23532
23699
  return { ...this.#overlayStats };
23533
23700
  }
23701
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23702
+ get recoveredHeadsAwaitingTiming() {
23703
+ return this.#parser.recoveredHeadsAwaitingTiming;
23704
+ }
23534
23705
  #isOverlayRepair(repairPacketId, repairTrackName) {
23535
23706
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23536
23707
  }
@@ -25685,8 +25856,16 @@ var MmtpRouter = class {
25685
25856
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25686
25857
  this.#emitDirectVideo(packetId, data, meta, cb);
25687
25858
  }
25688
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
25859
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25689
25860
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
25861
+ if (this.#isFecTrack(packetId)) {
25862
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
25863
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
25864
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25865
+ this.#emitDirectVideo(packetId, data, meta, cb);
25866
+ return;
25867
+ }
25868
+ }
25690
25869
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25691
25870
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25692
25871
  this.#drainDirectRecoveredVideo(
@@ -25940,16 +26119,29 @@ var MmtpRouter = class {
25940
26119
  }
25941
26120
  );
25942
26121
  }
25943
- result = {
25944
- ...result,
25945
- 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(
25946
- ownerPacketId,
25947
- sym.fragment.mpuSequenceNumber,
25948
- sym.fragment.movieFragmentSequenceNumber,
25949
- sym.fragment.sampleNumber,
25950
- catalogTrackName
25951
- ))
25952
- };
26122
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26123
+ ownerPacketId,
26124
+ sym.fragment.mpuSequenceNumber,
26125
+ sym.fragment.movieFragmentSequenceNumber,
26126
+ sym.fragment.sampleNumber,
26127
+ catalogTrackName
26128
+ );
26129
+ for (const sym of result.symbols) {
26130
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26131
+ this.#feedViaParser(
26132
+ sym.rawSymbol,
26133
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26134
+ void 0,
26135
+ {
26136
+ skipFecPayloadId: true,
26137
+ recovered: true,
26138
+ deliveryProvenance,
26139
+ ...fecSbnOf(sym.index),
26140
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26141
+ }
26142
+ );
26143
+ }
26144
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25953
26145
  const gated = this.#isFecTrack(ownerPacketId);
25954
26146
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25955
26147
  if (assembler) {