@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 2.2.0-main.fb5c7dfbe2de

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 (82) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1336 -302
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +70 -12
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +403 -58
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +48 -13
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +97 -25
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +79 -10
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +207 -26
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +209 -29
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +206 -26
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +34 -8
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +555 -79
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.d.ts +13 -3
  48. package/dist/multicast-manager.d.ts.map +1 -1
  49. package/dist/multicast-manager.js +980 -146
  50. package/dist/multicast-manager.js.map +2 -2
  51. package/dist/multicast-transport.js +28 -18
  52. package/dist/multicast-transport.js.map +2 -2
  53. package/dist/no-modules.js +206 -26
  54. package/dist/no-modules.js.map +2 -2
  55. package/dist/repair-fetch.d.ts +36 -8
  56. package/dist/repair-fetch.d.ts.map +1 -1
  57. package/dist/repair-fetch.js +285 -26
  58. package/dist/repair-fetch.js.map +3 -3
  59. package/dist/repair-layer-subscriptions.d.ts +6 -2
  60. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  61. package/dist/repair-layer-subscriptions.js +6 -3
  62. package/dist/repair-layer-subscriptions.js.map +2 -2
  63. package/dist/track-session.d.ts +1 -1
  64. package/dist/track-session.d.ts.map +1 -1
  65. package/dist/track-session.js +1 -0
  66. package/dist/track-session.js.map +2 -2
  67. package/dist/wasm-mmtp-container-parser.js +206 -26
  68. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  69. package/package.json +8 -5
  70. package/src/catalog-fec-params.ts +36 -1
  71. package/src/cmcd-producer.ts +189 -179
  72. package/src/fec-manager.ts +433 -32
  73. package/src/fec-worker-setup.ts +2 -2
  74. package/src/index.ts +9 -2
  75. package/src/iso-block-locator.ts +133 -31
  76. package/src/iso-bmff-timing.ts +40 -1
  77. package/src/mmtp-container-parser.ts +306 -29
  78. package/src/mmtp-router.ts +548 -38
  79. package/src/multicast-manager.ts +44 -6
  80. package/src/repair-fetch.ts +363 -49
  81. package/src/repair-layer-subscriptions.ts +13 -5
  82. package/src/track-session.ts +4 -1
@@ -5568,9 +5568,21 @@ var IsoBmffTimingResolver = class {
5568
5568
  }
5569
5569
  #track = null;
5570
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();
5571
5579
  #pendingSamples = 0;
5572
5580
  #evictedSamples = 0;
5573
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
+ }
5574
5586
  get pendingSampleCount() {
5575
5587
  return this.#pendingSamples;
5576
5588
  }
@@ -5594,7 +5606,14 @@ var IsoBmffTimingResolver = class {
5594
5606
  this.#track = timing;
5595
5607
  return true;
5596
5608
  }
5597
- 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) {
5598
5617
  this.#lastMovieFragmentRejection = null;
5599
5618
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
5600
5619
  this.#lastMovieFragmentRejection = "invalid";
@@ -5620,6 +5639,8 @@ var IsoBmffTimingResolver = class {
5620
5639
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
5621
5640
  );
5622
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;
5623
5644
  let freed = 0;
5624
5645
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
5625
5646
  freed += this.#evictOldestFragment();
@@ -5633,6 +5654,7 @@ var IsoBmffTimingResolver = class {
5633
5654
  return false;
5634
5655
  }
5635
5656
  this.#fragments.set(key, [...parsed.samples]);
5657
+ this.#firstSamples.set(key, parsed.samples[0]);
5636
5658
  this.#pendingSamples += parsed.samples.length;
5637
5659
  return true;
5638
5660
  }
@@ -5690,6 +5712,7 @@ var IsoBmffTimingResolver = class {
5690
5712
  let live = 0;
5691
5713
  for (const sample of samples) if (sample !== null) live++;
5692
5714
  this.#fragments.delete(oldest);
5715
+ this.#firstSamples.delete(oldest);
5693
5716
  this.#pendingSamples -= live;
5694
5717
  this.#evictedSamples += live;
5695
5718
  return live;
@@ -5705,12 +5728,23 @@ var IsoBmffTimingResolver = class {
5705
5728
  if (!samples || !samples[index]) return false;
5706
5729
  samples[index] = null;
5707
5730
  this.#pendingSamples--;
5708
- 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
+ }
5709
5741
  return true;
5710
5742
  }
5711
5743
  reset() {
5712
5744
  this.#track = null;
5713
5745
  this.#fragments.clear();
5746
+ this.#firstSamples.clear();
5747
+ this.#delivered.clear();
5714
5748
  this.#pendingSamples = 0;
5715
5749
  }
5716
5750
  };
@@ -5880,6 +5914,9 @@ function authoredGap(before, after) {
5880
5914
  function ssIdAfter(a, b) {
5881
5915
  return (a - b | 0) > 0;
5882
5916
  }
5917
+ function holdsRebuilt(stream) {
5918
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
5919
+ }
5883
5920
  var FecReleaseSequencer = class {
5884
5921
  #streams = /* @__PURE__ */ new Map();
5885
5922
  #gate;
@@ -5942,6 +5979,7 @@ var FecReleaseSequencer = class {
5942
5979
  stream.lastSbn = void 0;
5943
5980
  stream.discontinuity = false;
5944
5981
  stream.rapSsId = void 0;
5982
+ stream.sourced = false;
5945
5983
  }
5946
5984
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
5947
5985
  this.#recordStat("droppedStale");
@@ -5968,6 +6006,7 @@ var FecReleaseSequencer = class {
5968
6006
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
5969
6007
  else hi = mid;
5970
6008
  }
6009
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
5971
6010
  stream.buf.splice(lo, 0, frame);
5972
6011
  this.#drainUnlessDeferred(stream);
5973
6012
  }
@@ -6003,6 +6042,41 @@ var FecReleaseSequencer = class {
6003
6042
  }
6004
6043
  return false;
6005
6044
  }
6045
+ /**
6046
+ * These streams' sink is being replaced by one that has seen no source
6047
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
6048
+ * release to the sink being replaced the frames the unanchored path had
6049
+ * already released to it (#cursorPassed): there they waited in its queue,
6050
+ * which its replacement flushes. A stream that holds none is not drained:
6051
+ * it holds nothing the anchor kept back, and a drain would still force out
6052
+ * a frame held past the hold limit, which before the anchor waited for the
6053
+ * next push. Then treat the streams as unsourced until a source frame is
6054
+ * released.
6055
+ */
6056
+ unsource(packetId, catalogTrackName) {
6057
+ for (const stream of this.#streams.values()) {
6058
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
6059
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
6060
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
6061
+ else this.#drain(stream, true);
6062
+ }
6063
+ stream.sourced = false;
6064
+ }
6065
+ }
6066
+ }
6067
+ /**
6068
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
6069
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
6070
+ * packetId.
6071
+ */
6072
+ holdsRebuilt(packetId, catalogTrackName) {
6073
+ for (const stream of this.#streams.values()) {
6074
+ if (stream.packetId !== packetId) continue;
6075
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
6076
+ if (holdsRebuilt(stream)) return true;
6077
+ }
6078
+ return false;
6079
+ }
6006
6080
  /** Re-evaluate held gaps after FEC state changed. */
6007
6081
  poke(streamKey) {
6008
6082
  if (streamKey !== void 0) {
@@ -6094,16 +6168,32 @@ var FecReleaseSequencer = class {
6094
6168
  last: null,
6095
6169
  lastSbn: void 0,
6096
6170
  discontinuity: false,
6097
- rapSsId: void 0
6171
+ rapSsId: void 0,
6172
+ sourced: false,
6173
+ draining: 0
6098
6174
  };
6099
6175
  this.#streams.set(streamKey, stream);
6100
6176
  }
6101
6177
  return stream;
6102
6178
  }
6103
- #drain(stream) {
6179
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
6180
+ #drain(stream, asUnanchored = false) {
6181
+ stream.draining++;
6182
+ try {
6183
+ this.#drainLoop(stream, asUnanchored);
6184
+ } finally {
6185
+ stream.draining--;
6186
+ }
6187
+ }
6188
+ #drainLoop(stream, asUnanchored) {
6104
6189
  while (stream.buf.length > 0 && stream.last !== null) {
6105
6190
  const head = stream.buf[0];
6106
6191
  const last = stream.last;
6192
+ if (compareFrameOrder(head, last) <= 0) {
6193
+ stream.buf.shift();
6194
+ this.#release(stream, head, void 0);
6195
+ continue;
6196
+ }
6107
6197
  if (stream.discontinuity) {
6108
6198
  stream.discontinuity = false;
6109
6199
  stream.buf.shift();
@@ -6117,9 +6207,10 @@ var FecReleaseSequencer = class {
6117
6207
  }
6118
6208
  const headSbn = head.meta.fecSbn;
6119
6209
  const from = stream.lastSbn ?? headSbn;
6120
- const to = headSbn ?? stream.lastSbn;
6210
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
6121
6211
  let lost = [];
6122
6212
  const unanchored = from === void 0 && to === void 0;
6213
+ let unresolved = false;
6123
6214
  if (from !== void 0 && to !== void 0) {
6124
6215
  const resolved = this.#gate.resolvedLoss(
6125
6216
  stream.packetId,
@@ -6128,7 +6219,8 @@ var FecReleaseSequencer = class {
6128
6219
  Math.max(from, to),
6129
6220
  from
6130
6221
  );
6131
- if (resolved === null) {
6222
+ unresolved = resolved === null;
6223
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
6132
6224
  const heldMs = performance.now() - head.arrivedAtMs;
6133
6225
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
6134
6226
  this.#recordStat("forcedWindowFlushes");
@@ -6136,22 +6228,56 @@ var FecReleaseSequencer = class {
6136
6228
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
6137
6229
  continue;
6138
6230
  }
6139
- lost = resolved;
6231
+ lost = resolved ?? [];
6140
6232
  }
6141
6233
  stream.buf.shift();
6142
6234
  this.#release(
6143
6235
  stream,
6144
6236
  head,
6145
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
6237
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
6146
6238
  );
6147
6239
  }
6148
6240
  }
6241
+ /**
6242
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
6243
+ * source cursor holds it unanchored, and neither does a later frame behind
6244
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
6245
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
6246
+ * like each frame that arrived in front of it since (stale there). The head
6247
+ * then goes, decided as it would be unanchored, once a frame the base path
6248
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
6249
+ * marked one: the base path has passed that one, so a frame still missing
6250
+ * before it can only come from a repair. It also goes while no source frame
6251
+ * has been released, when the direct path shows a recovered frame at once.
6252
+ * And it goes at once while a staged switch decides on when frames arrive
6253
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
6254
+ * sink being replaced: unanchored, it had already reached the sink.
6255
+ * The scan also writes: the `passed` marks it sets stay on the frames,
6256
+ * and push marks a frame it inserts in front of a marked one from them.
6257
+ */
6258
+ #cursorPassed(stream, asUnanchored) {
6259
+ let at = -1;
6260
+ for (let i = 0; i < stream.buf.length; i++) {
6261
+ const frame = stream.buf[i];
6262
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
6263
+ frame.passed = true;
6264
+ if (at < 0) at = i;
6265
+ } else if (frame.passed !== true) break;
6266
+ }
6267
+ if (at < 0) return false;
6268
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
6269
+ }
6149
6270
  #release(stream, frame, decision) {
6271
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
6272
+ if (stale) this.#recordStat("droppedStale");
6150
6273
  if (decision) frame.meta.gapBefore = decision;
6151
6274
  stream.last = frame;
6152
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
6275
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
6276
+ stream.lastSbn = frame.meta.fecSbn;
6277
+ }
6278
+ if (frame.meta.recovered !== true) stream.sourced = true;
6153
6279
  this.#gate.onReleased?.(frame.data, frame.meta);
6154
- this.#emit(frame.data, frame.meta);
6280
+ if (!stale) this.#emit(frame.data, frame.meta);
6155
6281
  }
6156
6282
  };
6157
6283
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -6192,6 +6318,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
6192
6318
  this.#reclaimedTimingSamples = 0;
6193
6319
  this.#evictedTimingSamples = 0;
6194
6320
  this.#rejectedMovieFragments = 0;
6321
+ this.#recoveredHeadsAwaitingTiming = {
6322
+ held: 0,
6323
+ expired: 0,
6324
+ discarded: 0,
6325
+ superseded: 0
6326
+ };
6195
6327
  this.#sequencers = /* @__PURE__ */ new Map();
6196
6328
  this.#fecGate = null;
6197
6329
  this.#fecSequencer = null;
@@ -6300,6 +6432,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6300
6432
  #reclaimedTimingSamples;
6301
6433
  #evictedTimingSamples;
6302
6434
  #rejectedMovieFragments;
6435
+ #recoveredHeadsAwaitingTiming;
6303
6436
  #sequencers;
6304
6437
  #fecGate;
6305
6438
  #fecSequencer;
@@ -6405,11 +6538,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
6405
6538
  /**
6406
6539
  * Typed view of the ISO-BMFF timing-sample budget.
6407
6540
  *
6408
- * This is the acceptance signal for the budget ratchet: `pending` walking
6409
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
6410
- * and it is observable here rather than only as media silently stopping.
6411
- * Per repo policy this is a producer-owned counter -- never infer budget
6412
- * state by parsing logs.
6541
+ * Each stream admits timing against its own window of `capacity` slots
6542
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
6543
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
6544
+ * timing a stream declared and never delivered shows up there. `pending`
6545
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
6546
+ * a producer-owned counter -- never infer budget state by parsing logs.
6413
6547
  */
6414
6548
  get timingBudgetStats() {
6415
6549
  return {
@@ -6420,6 +6554,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
6420
6554
  rejectedMovieFragments: this.#rejectedMovieFragments
6421
6555
  };
6422
6556
  }
6557
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
6558
+ get recoveredHeadsAwaitingTiming() {
6559
+ let pending = 0;
6560
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
6561
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
6562
+ }
6563
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
6564
+ }
6423
6565
  get reorderStats() {
6424
6566
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
6425
6567
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -6461,6 +6603,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
6461
6603
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
6462
6604
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
6463
6605
  }
6606
+ /** See FecReleaseSequencer.unsource. */
6607
+ unsourceFecRelease(packetId, catalogTrackName) {
6608
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
6609
+ }
6610
+ /** See FecReleaseSequencer.holdsRebuilt. */
6611
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
6612
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
6613
+ }
6464
6614
  /** Re-evaluate held gaps after FEC block state changed. */
6465
6615
  pokeFecRelease(packetId, catalogTrackName) {
6466
6616
  if (!this.#fecSequencer) return;
@@ -6593,7 +6743,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6593
6743
  const now = Date.now();
6594
6744
  for (const { awaitingTiming } of this.#timingStreams.values()) {
6595
6745
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
6596
- awaitingTiming.shift();
6746
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
6597
6747
  }
6598
6748
  }
6599
6749
  }
@@ -6711,11 +6861,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
6711
6861
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
6712
6862
  return this.#drop("fragment-unexpected-fragmentation");
6713
6863
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
6714
- if (!resolver2) return this.#drop("fragment-no-resolver");
6864
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
6715
6865
  const pendingBefore = resolver2.pendingSampleCount;
6716
6866
  const evictedBefore = resolver2.evictedSampleCount;
6717
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
6718
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
6867
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
6868
+ const admitted = resolver2.addMovieFragment(
6869
+ mpu.mpuSequenceNumber,
6870
+ data,
6871
+ remaining,
6872
+ options?.recovered === true
6873
+ );
6719
6874
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
6720
6875
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
6721
6876
  if (!admitted) {
@@ -6735,7 +6890,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6735
6890
  };
6736
6891
  }
6737
6892
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
6738
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
6893
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
6739
6894
  if (!resolver) return this.#drop("mfu-no-resolver");
6740
6895
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
6741
6896
  return this.#drop("mfu-empty-payload");
@@ -6756,6 +6911,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6756
6911
  mpuSequenceNumber: mpu.mpuSequenceNumber,
6757
6912
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
6758
6913
  sampleNumber: mfuDuHeader.sampleNumber,
6914
+ sequenceNumber: info.sequenceNumber,
6759
6915
  arrivedAt: Date.now()
6760
6916
  });
6761
6917
  return this.#drop("mfu-sample-timing-missing");
@@ -6781,7 +6937,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
6781
6937
  info,
6782
6938
  resolver,
6783
6939
  options?.recovered === true,
6784
- options?.fecSbn
6940
+ options?.fecSbn,
6941
+ options?.fecSbnFirst
6785
6942
  );
6786
6943
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
6787
6944
  }
@@ -6874,7 +7031,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
6874
7031
  info,
6875
7032
  resolver,
6876
7033
  options?.recovered === true,
6877
- options?.fecSbn
7034
+ options?.fecSbn,
7035
+ options?.fecSbnFirst
6878
7036
  );
6879
7037
  if (outcome.record !== void 0) return outcome.record;
6880
7038
  return this.#drop(outcome.reason);
@@ -6894,7 +7052,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6894
7052
  * attribution exists to keep readable. Leaving the choice at the call site
6895
7053
  * is what keeps both true.
6896
7054
  */
6897
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
7055
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
6898
7056
  const timing = resolver.sampleTiming(
6899
7057
  combined.mpuSequenceNumber,
6900
7058
  combined.movieFragmentSequenceNumber,
@@ -6911,6 +7069,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
6911
7069
  );
6912
7070
  if (!meta) return { reason: "combined-meta-build-failed" };
6913
7071
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
7072
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
6914
7073
  if (!resolver.consumeSample(
6915
7074
  combined.mpuSequenceNumber,
6916
7075
  combined.movieFragmentSequenceNumber,
@@ -7079,6 +7238,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
7079
7238
  }
7080
7239
  this.#emitFrame(data, meta);
7081
7240
  }
7241
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
7242
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
7243
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
7244
+ }
7082
7245
  /**
7083
7246
  * Claim and return the authored timing for a recovered sample. Recovery does
7084
7247
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -7140,6 +7303,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
7140
7303
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
7141
7304
  this.#sequencers.clear();
7142
7305
  this.#fecSequencer?.discard();
7306
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
7307
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7308
+ }
7143
7309
  this.#timingStreams.clear();
7144
7310
  this.#pendingTimingSamples = 0;
7145
7311
  this.#reliableMoqStartupGate.clear();
@@ -7158,10 +7324,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
7158
7324
  return resolver;
7159
7325
  }
7160
7326
  #awaitTiming(streamKey, head) {
7161
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
7162
- if (!awaitingTiming) return;
7327
+ const state = this.#timingStreams.get(streamKey);
7328
+ if (!state) return;
7329
+ if (!head.options?.recovered) {
7330
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
7331
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
7332
+ state.awaitingTiming = kept;
7333
+ }
7334
+ const awaitingTiming = state.awaitingTiming;
7163
7335
  awaitingTiming.push(head);
7164
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
7336
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
7337
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
7338
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
7339
+ }
7340
+ }
7341
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
7342
+ #leaveAwaitingTiming(head, outcome) {
7343
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
7165
7344
  }
7166
7345
  /**
7167
7346
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -7189,6 +7368,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
7189
7368
  const state = this.#timingStreams.get(streamKey);
7190
7369
  if (!state) return;
7191
7370
  this.#timingStreams.delete(streamKey);
7371
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
7192
7372
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
7193
7373
  if (this.#pendingTimingSamples < 0) {
7194
7374
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");