@blockcast/mmt-container 2.2.0-main.c3a95413582c → 2.2.0-main.d8ba524564ba

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 (46) hide show
  1. package/dist/browser-runtime.js +748 -79
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/delivery-catalog.js +17 -12
  4. package/dist/delivery-catalog.js.map +2 -2
  5. package/dist/fec-manager.d.ts +33 -1
  6. package/dist/fec-manager.d.ts.map +1 -1
  7. package/dist/fec-manager.js +222 -6
  8. package/dist/fec-manager.js.map +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +2 -2
  12. package/dist/iso-block-locator.d.ts +9 -0
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +17 -0
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/iso-bmff-timing.d.ts +10 -1
  17. package/dist/iso-bmff-timing.d.ts.map +1 -1
  18. package/dist/iso-bmff-timing.js +36 -2
  19. package/dist/iso-bmff-timing.js.map +2 -2
  20. package/dist/mmtp-container-parser.d.ts +79 -10
  21. package/dist/mmtp-container-parser.d.ts.map +1 -1
  22. package/dist/mmtp-container-parser.js +204 -26
  23. package/dist/mmtp-container-parser.js.map +2 -2
  24. package/dist/mmtp-container-parser.no-modules.js +206 -29
  25. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  26. package/dist/mmtp-decode-pipeline.js +203 -26
  27. package/dist/mmtp-decode-pipeline.js.map +2 -2
  28. package/dist/mmtp-router.d.ts +27 -3
  29. package/dist/mmtp-router.d.ts.map +1 -1
  30. package/dist/mmtp-router.js +524 -71
  31. package/dist/mmtp-router.js.map +2 -2
  32. package/dist/multicast-manager.js +748 -79
  33. package/dist/multicast-manager.js.map +2 -2
  34. package/dist/multicast-transport.js +19 -14
  35. package/dist/multicast-transport.js.map +2 -2
  36. package/dist/no-modules.js +203 -26
  37. package/dist/no-modules.js.map +2 -2
  38. package/dist/wasm-mmtp-container-parser.js +203 -26
  39. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  40. package/package.json +5 -5
  41. package/src/fec-manager.ts +277 -6
  42. package/src/index.ts +1 -0
  43. package/src/iso-block-locator.ts +19 -0
  44. package/src/iso-bmff-timing.ts +40 -1
  45. package/src/mmtp-container-parser.ts +298 -29
  46. package/src/mmtp-router.ts +510 -37
@@ -8625,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
8625
8625
  } catch (error) {
8626
8626
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8627
8627
  }
8628
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8628
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8629
8629
  await rx.close();
8630
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8630
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8631
8631
  }
8632
8632
  const events = new EventTarget();
8633
8633
  let state = "connected";
@@ -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;
@@ -8754,7 +8755,6 @@ var AMTGatewayManager = class {
8754
8755
  constructor() {
8755
8756
  this.messageHandler = (event) => {
8756
8757
  const data = event.data;
8757
- console.log(`[AMT] SW message received:`, data.type);
8758
8758
  if (data.type === "UDP_PACKET") {
8759
8759
  this.handlePacket(data);
8760
8760
  } else if (data.type === "SOCKET_ERROR") {
@@ -8772,6 +8772,14 @@ var AMTGatewayManager = class {
8772
8772
  this.messageListenerRegistered = true;
8773
8773
  console.log("[AMT] Registered service worker message handler");
8774
8774
  }
8775
+ removeMessageListener() {
8776
+ if (!this.messageListenerRegistered)
8777
+ return;
8778
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8779
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8780
+ }
8781
+ this.messageListenerRegistered = false;
8782
+ }
8775
8783
  /**
8776
8784
  * Subscribe via AMT tunnel
8777
8785
  */
@@ -8816,6 +8824,8 @@ var AMTGatewayManager = class {
8816
8824
  this.subscriptions.delete(keyStr);
8817
8825
  this.pendingPackets.delete(subscription.subscriptionId);
8818
8826
  }
8827
+ if (this.subscriptions.size === 0)
8828
+ this.removeMessageListener();
8819
8829
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8820
8830
  }
8821
8831
  }
@@ -8837,6 +8847,8 @@ var AMTGatewayManager = class {
8837
8847
  } finally {
8838
8848
  this.subscriptions.delete(handle);
8839
8849
  this.pendingPackets.delete(subscription.subscriptionId);
8850
+ if (this.subscriptions.size === 0)
8851
+ this.removeMessageListener();
8840
8852
  }
8841
8853
  }
8842
8854
  /**
@@ -8853,14 +8865,12 @@ var AMTGatewayManager = class {
8853
8865
  if (!subscriptionId)
8854
8866
  return;
8855
8867
  const subscription = this.subscriptions.get(subscriptionId);
8856
- if (!subscription) {
8857
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8868
+ if (!subscription)
8858
8869
  return;
8859
- }
8860
8870
  if (!subscription.handshakeComplete) {
8861
8871
  const queue = this.pendingPackets.get(subscriptionId);
8862
8872
  const packetData2 = data;
8863
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8864
8874
  queue.push({
8865
8875
  data: packetData2.data,
8866
8876
  remoteAddress: packetData2.remoteAddress,
@@ -8886,20 +8896,15 @@ var AMTGatewayManager = class {
8886
8896
  if (!subscriptionId)
8887
8897
  return;
8888
8898
  const subscription = this.subscriptions.get(subscriptionId);
8889
- if (!subscription) {
8890
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8899
+ if (!subscription)
8891
8900
  return;
8892
- }
8893
8901
  subscription.config.onError(new Error(error || "Unknown error"));
8894
8902
  }
8895
8903
  /**
8896
8904
  * Cleanup on destroy
8897
8905
  */
8898
8906
  destroy() {
8899
- if (this.messageListenerRegistered && this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8900
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8901
- this.messageListenerRegistered = false;
8902
- }
8907
+ this.removeMessageListener();
8903
8908
  const handles = Array.from(this.subscriptions.keys());
8904
8909
  for (const handle of handles) {
8905
8910
  this.unsubscribe(handle).catch((err) => {
@@ -12944,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12944
12949
  #retentionBlocks;
12945
12950
  #heldEvicted;
12946
12951
  #newest;
12952
+ #newestSource;
12947
12953
  #holes;
12948
12954
  static #checkedRetention(retentionBlocks) {
12949
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12981,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12981
12987
  observe(position) {
12982
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12983
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
+ }
12984
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12985
13006
  get heldEvicted() {
12986
13007
  return this.#heldEvicted;
@@ -13213,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13213
13234
  this.#held.clear();
13214
13235
  this.#coverageEnd = void 0;
13215
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13216
13238
  }
13217
13239
  #claim(block) {
13218
13240
  const placed = [];
@@ -13832,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13832
13854
  var WORKER_ERROR_KIND_CAP = 32;
13833
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13834
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13835
13858
  function isMulticastSymbolDelivery(value) {
13836
13859
  return value === "datagram" || value === "ordered-relay";
13837
13860
  }
@@ -13929,6 +13952,10 @@ var FECManager = class _FECManager {
13929
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13930
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13931
13954
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
13955
+ // Source ESIs per block that another FEC instance rebuilt and fed in
13956
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
13957
+ // not network arrivals and never make a block clean.
13958
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13932
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13933
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13934
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14057,6 +14084,10 @@ var FECManager = class _FECManager {
14057
14084
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14058
14085
  // One classifier per physical stream, built from that stream's geometry.
14059
14086
  this.#classifiers = /* @__PURE__ */ new Map();
14087
+ // A stream without catalog timing has no classifier, so the last RAP it
14088
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14089
+ // a second RAP is released inside one MPU (BLO-52085).
14090
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14060
14091
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14061
14092
  // rungs can share a packetId with a different framerate or GOP, so the
14062
14093
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14214,6 +14245,7 @@ var FECManager = class _FECManager {
14214
14245
  #pendingWorkerCleanupBounds;
14215
14246
  #blockSourceCounts;
14216
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14217
14249
  #blockAuthoredLengths;
14218
14250
  #ssbLengthConflictCount;
14219
14251
  #blockRepairRsIds;
@@ -14256,6 +14288,7 @@ var FECManager = class _FECManager {
14256
14288
  #orderedFrontiers;
14257
14289
  #orderedPendingBlocks;
14258
14290
  #classifiers;
14291
+ #noSpanRaps;
14259
14292
  #streamGeometries;
14260
14293
  #failedBlocks;
14261
14294
  #resolvedBlockClasses;
@@ -14917,6 +14950,7 @@ var FECManager = class _FECManager {
14917
14950
  };
14918
14951
  const discontinuity = () => {
14919
14952
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14953
+ this.#noSpanRaps.delete(streamKey);
14920
14954
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14921
14955
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14922
14956
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15329,6 +15363,9 @@ var FECManager = class _FECManager {
15329
15363
  }
15330
15364
  if (policyChanged) {
15331
15365
  this.#bumpEpoch(packetId);
15366
+ for (const key of [...this.#noSpanRaps.keys()]) {
15367
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15368
+ }
15332
15369
  for (const [key, classifier] of [...this.#classifiers]) {
15333
15370
  if (!this.#belongsToPacket(key, packetId)) continue;
15334
15371
  const stream = this.#parseStreamKey(key);
@@ -15531,6 +15568,7 @@ var FECManager = class _FECManager {
15531
15568
  }
15532
15569
  resetMap(this.#blockSourceCounts);
15533
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15534
15572
  resetMap(this.#blockAuthoredLengths);
15535
15573
  resetMap(this.#blockRepairRsIds);
15536
15574
  resetMap(this.#blockRsbLengths);
@@ -15564,6 +15602,7 @@ var FECManager = class _FECManager {
15564
15602
  resetMap(this.#isoLocators);
15565
15603
  resetMap(this.#sourceAnchors);
15566
15604
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15605
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15567
15606
  this.#bumpEpoch(packetId);
15568
15607
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15569
15608
  for (const key of keys.keys()) {
@@ -15654,6 +15693,7 @@ var FECManager = class _FECManager {
15654
15693
  dropMap(this.#pendingWorkerCleanupClients);
15655
15694
  dropMap(this.#blockSourceCounts);
15656
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15657
15697
  dropMap(this.#blockAuthoredLengths);
15658
15698
  dropMap(this.#blockRepairRsIds);
15659
15699
  dropMap(this.#blockRsbLengths);
@@ -15670,6 +15710,7 @@ var FECManager = class _FECManager {
15670
15710
  dropMap(this.#isoLocators);
15671
15711
  dropMap(this.#sourceAnchors);
15672
15712
  dropMap(this.#classifiers);
15713
+ dropMap(this.#noSpanRaps);
15673
15714
  dropMap(this.#streamGeometries);
15674
15715
  dropSet(this.#pendingWorkerCleanups);
15675
15716
  dropSet(this.#completedBlocks);
@@ -15815,6 +15856,9 @@ var FECManager = class _FECManager {
15815
15856
  this.#unindexBlockKey(key);
15816
15857
  }
15817
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15818
15862
  return true;
15819
15863
  }
15820
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16132,6 +16176,7 @@ var FECManager = class _FECManager {
16132
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16133
16177
  const evictedBefore = locator.heldEvicted;
16134
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16135
16180
  const evicted = locator.heldEvicted - evictedBefore;
16136
16181
  if (evicted > 0) {
16137
16182
  const before = this.#isoHeldEvictedCount;
@@ -16300,11 +16345,14 @@ var FECManager = class _FECManager {
16300
16345
  /**
16301
16346
  * Whether an MPU boundary still bounds this stream's RAPs (see
16302
16347
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16303
- * it does not. A stream with no classifier yet has shown nothing against it.
16348
+ * it does not. A stream with no classifier yet has shown nothing against it,
16349
+ * and neither has a stream without catalog timing until it releases two
16350
+ * RAPs inside one MPU.
16304
16351
  */
16305
16352
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16306
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16307
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16353
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16354
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16355
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16308
16356
  }
16309
16357
  /** A block's class now, or frozen at its resolution. */
16310
16358
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16330,7 +16378,14 @@ var FECManager = class _FECManager {
16330
16378
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16331
16379
  */
16332
16380
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16333
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16381
+ if (!this.#trackConfigs.has(packetId)) return;
16382
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16383
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16384
+ const last = this.#noSpanRaps.get(streamKey);
16385
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16386
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16387
+ return;
16388
+ }
16334
16389
  this.#applyClassifierChanges(
16335
16390
  packetId,
16336
16391
  catalogTrackName,
@@ -16717,7 +16772,15 @@ var FECManager = class _FECManager {
16717
16772
  this.#isoSourceOversizeCount++;
16718
16773
  return null;
16719
16774
  }
16720
- const sbn = located.block.start;
16775
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16776
+ }
16777
+ /**
16778
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16779
+ * answers through onFrame (null here), or its synchronous decoder, whose
16780
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16781
+ * stated for `ssId`.
16782
+ */
16783
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16721
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16722
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16723
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16746,6 +16809,74 @@ var FECManager = class _FECManager {
16746
16809
  }
16747
16810
  return recovered;
16748
16811
  }
16812
+ /**
16813
+ * Feed a source symbol of this stream that another FEC instance over the
16814
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16815
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16816
+ * BLO-52085). It joins its block's decoder as a received source would, so
16817
+ * it reduces the erasures the block still has to solve, but it is not a
16818
+ * network arrival: no transport accounting, no classification evidence,
16819
+ * no deadline refresh, and it never makes a block clean.
16820
+ *
16821
+ * Only a block a repair stated takes one (a repair-less span has no
16822
+ * decoder block). A block that is terminal, or already holds that source,
16823
+ * does not. The symbol is the exact protected packet, unpadded, as
16824
+ * feedSourceSymbol takes it.
16825
+ *
16826
+ * `recovered` is the block the synchronous decoder completed with it; the
16827
+ * caller routes it and completes the block, as for any recovery. When
16828
+ * every source of the block is then present, received or rebuilt, and no
16829
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16830
+ * block as clean on its own side), the block is completed here as
16831
+ * recovered, with nothing left to route.
16832
+ */
16833
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16834
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16835
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16836
+ if (located === null || located.block.span) return NOT_TAKEN;
16837
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16838
+ const sbn = located.block.start;
16839
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16840
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16841
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16842
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16843
+ if (rebuilt === void 0) {
16844
+ rebuilt = /* @__PURE__ */ new Set();
16845
+ this.#blockRebuiltEsis.set(key, rebuilt);
16846
+ }
16847
+ const config = this.#trackConfigs.get(packetId);
16848
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16849
+ rebuilt.add(located.esi);
16850
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16851
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16852
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16853
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16854
+ }
16855
+ }
16856
+ return { taken: true, recovered };
16857
+ }
16858
+ /**
16859
+ * A block that holds every one of its `length` sources, received or
16860
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16861
+ * every packet of it was delivered, by its own arrival or by the instance
16862
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16863
+ */
16864
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16865
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16866
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16867
+ let present = rebuilt.size;
16868
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16869
+ if (present < length) return false;
16870
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16871
+ this.countRecovery(packetId, sbn, catalogTrackName);
16872
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16873
+ if (catalogTrackName === void 0) {
16874
+ this.onBlockComplete?.(sbn, packetId);
16875
+ } else {
16876
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16877
+ }
16878
+ return true;
16879
+ }
16749
16880
  /**
16750
16881
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16751
16882
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16948,8 +17079,75 @@ var FECManager = class _FECManager {
16948
17079
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16949
17080
  return null;
16950
17081
  }
17082
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16951
17083
  return recovered;
16952
17084
  }
17085
+ /**
17086
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17087
+ * its sources in send order, so one that moves the stream's newest source
17088
+ * position past a block leaves the sources that block still misses
17089
+ * reordered behind it at best: it shortens the wait of every block it
17090
+ * passed that only a late source can complete. MoQ is left out: its streams
17091
+ * can overtake one another, so a MoQ source shows nothing about the sources
17092
+ * sent before it.
17093
+ */
17094
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17095
+ const previous = locator.newestSource;
17096
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17097
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17098
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17099
+ this.#shortenUnrecoverableWait(
17100
+ this.#blockKey(block.start, packetId, catalogTrackName),
17101
+ block.start,
17102
+ packetId,
17103
+ catalogTrackName
17104
+ );
17105
+ }
17106
+ }
17107
+ /**
17108
+ * The wait of a block that only a late source can complete (BLO-52085): a
17109
+ * repair stated it, its received sources plus every symbol its repair group
17110
+ * carries (RSB_length) are short of K' (the repair scheduler's
17111
+ * group-cannot-complete), and a later source of the stream has arrived, so
17112
+ * a source it misses is lost or reordered behind that one. The span, which
17113
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17114
+ * the late-symbol allowance alone, the multicast wait less the span (the
17115
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17116
+ * block.
17117
+ */
17118
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17119
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17120
+ const rsbLength = this.#blockRsbLengths.get(key);
17121
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17122
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17123
+ const received = this.#blockSourceEsis.get(key);
17124
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17125
+ if (present + rsbLength >= authoredLength) return void 0;
17126
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17127
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17128
+ return void 0;
17129
+ }
17130
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17131
+ }
17132
+ /**
17133
+ * Bring an open block's wait in to its late-source wait, when it has one.
17134
+ * Never later than it is armed: only a source can complete such a block,
17135
+ * so a repair of its group (paced across the span) must not keep it open.
17136
+ */
17137
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17138
+ if (!this.#activeBlocks.has(key)) return;
17139
+ const config = this.#trackConfigs.get(packetId);
17140
+ if (config === void 0) return;
17141
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17142
+ if (waitMs === void 0) return;
17143
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17144
+ this.#armBlockTimeout(
17145
+ sbn,
17146
+ packetId,
17147
+ catalogTrackName,
17148
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17149
+ );
17150
+ }
16953
17151
  /**
16954
17152
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16955
17153
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17104,6 +17302,15 @@ var FECManager = class _FECManager {
17104
17302
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17105
17303
  return;
17106
17304
  }
17305
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17306
+ key,
17307
+ sbn,
17308
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17309
+ packetId,
17310
+ catalogTrackName
17311
+ )) {
17312
+ return;
17313
+ }
17107
17314
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17108
17315
  }
17109
17316
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17112,6 +17319,7 @@ var FECManager = class _FECManager {
17112
17319
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17113
17320
  this.#blockSourceCounts.delete(key);
17114
17321
  this.#blockSourceEsis.delete(key);
17322
+ this.#blockRebuiltEsis.delete(key);
17115
17323
  this.#blockAuthoredLengths.delete(key);
17116
17324
  this.#blockRepairRsIds.delete(key);
17117
17325
  this.#blockRsbLengths.delete(key);
@@ -17178,7 +17386,16 @@ var FECManager = class _FECManager {
17178
17386
  this.#repairDeadlineMs(trackConfig)
17179
17387
  );
17180
17388
  }
17181
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17389
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17390
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17391
+ return;
17392
+ }
17393
+ this.#armBlockTimeout(
17394
+ sbn,
17395
+ packetId,
17396
+ catalogTrackName,
17397
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17398
+ );
17182
17399
  }
17183
17400
  #failBlock(sbn, packetId, catalogTrackName) {
17184
17401
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17191,6 +17408,7 @@ var FECManager = class _FECManager {
17191
17408
  }
17192
17409
  this.#blockSourceCounts.delete(key);
17193
17410
  this.#blockSourceEsis.delete(key);
17411
+ this.#blockRebuiltEsis.delete(key);
17194
17412
  this.#blockAuthoredLengths.delete(key);
17195
17413
  this.#blockRepairRsIds.delete(key);
17196
17414
  this.#blockRsbLengths.delete(key);
@@ -17229,6 +17447,7 @@ var FECManager = class _FECManager {
17229
17447
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17230
17448
  this.#blockSourceCounts.delete(key);
17231
17449
  this.#blockSourceEsis.delete(key);
17450
+ this.#blockRebuiltEsis.delete(key);
17232
17451
  this.#blockAuthoredLengths.delete(key);
17233
17452
  this.#blockRepairRsIds.delete(key);
17234
17453
  this.#blockRsbLengths.delete(key);
@@ -17320,6 +17539,7 @@ var FECManager = class _FECManager {
17320
17539
  this.#blockKeysBySbn.clear();
17321
17540
  this.#blockSourceCounts.clear();
17322
17541
  this.#blockSourceEsis.clear();
17542
+ this.#blockRebuiltEsis.clear();
17323
17543
  this.#blockAuthoredLengths.clear();
17324
17544
  this.#blockRepairRsIds.clear();
17325
17545
  this.#blockRsbLengths.clear();
@@ -17345,6 +17565,7 @@ var FECManager = class _FECManager {
17345
17565
  this.#blockAnchorsMs.clear();
17346
17566
  this.#blockCatalogTrackNames.clear();
17347
17567
  this.#classifiers.clear();
17568
+ this.#noSpanRaps.clear();
17348
17569
  this.#streamGeometries.clear();
17349
17570
  this.#failedBlocks.clear();
17350
17571
  this.#resolvedBlockClasses.clear();
@@ -17875,9 +18096,21 @@ var IsoBmffTimingResolver = class {
17875
18096
  }
17876
18097
  #track = null;
17877
18098
  #fragments = /* @__PURE__ */ new Map();
18099
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18100
+ #firstSamples = /* @__PURE__ */ new Map();
18101
+ /**
18102
+ * First sample and sample count of the newest fragments whose every slot was
18103
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18104
+ * matches the combiner, since it retires an abandoned sample as well.
18105
+ */
18106
+ #delivered = /* @__PURE__ */ new Map();
17878
18107
  #pendingSamples = 0;
17879
18108
  #evictedSamples = 0;
17880
18109
  #lastMovieFragmentRejection = null;
18110
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18111
+ get initialized() {
18112
+ return this.#track !== null;
18113
+ }
17881
18114
  get pendingSampleCount() {
17882
18115
  return this.#pendingSamples;
17883
18116
  }
@@ -17901,7 +18134,14 @@ var IsoBmffTimingResolver = class {
17901
18134
  this.#track = timing;
17902
18135
  return true;
17903
18136
  }
17904
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18137
+ /**
18138
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18139
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18140
+ * fragment whose samples are all done is a duplicate: admitting it again
18141
+ * would charge stale timing and could displace the live MPU's. A source
18142
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18143
+ */
18144
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17905
18145
  this.#lastMovieFragmentRejection = null;
17906
18146
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17907
18147
  this.#lastMovieFragmentRejection = "invalid";
@@ -17927,6 +18167,8 @@ var IsoBmffTimingResolver = class {
17927
18167
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17928
18168
  );
17929
18169
  }
18170
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18171
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17930
18172
  let freed = 0;
17931
18173
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17932
18174
  freed += this.#evictOldestFragment();
@@ -17940,6 +18182,7 @@ var IsoBmffTimingResolver = class {
17940
18182
  return false;
17941
18183
  }
17942
18184
  this.#fragments.set(key, [...parsed.samples]);
18185
+ this.#firstSamples.set(key, parsed.samples[0]);
17943
18186
  this.#pendingSamples += parsed.samples.length;
17944
18187
  return true;
17945
18188
  }
@@ -17997,6 +18240,7 @@ var IsoBmffTimingResolver = class {
17997
18240
  let live = 0;
17998
18241
  for (const sample of samples) if (sample !== null) live++;
17999
18242
  this.#fragments.delete(oldest);
18243
+ this.#firstSamples.delete(oldest);
18000
18244
  this.#pendingSamples -= live;
18001
18245
  this.#evictedSamples += live;
18002
18246
  return live;
@@ -18012,12 +18256,23 @@ var IsoBmffTimingResolver = class {
18012
18256
  if (!samples || !samples[index]) return false;
18013
18257
  samples[index] = null;
18014
18258
  this.#pendingSamples--;
18015
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18259
+ if (samples.every((value) => value === null)) {
18260
+ this.#fragments.delete(key);
18261
+ const first = this.#firstSamples.get(key);
18262
+ this.#firstSamples.delete(key);
18263
+ this.#delivered.delete(key);
18264
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18265
+ if (this.#delivered.size > this.maxPendingSamples) {
18266
+ this.#delivered.delete(this.#delivered.keys().next().value);
18267
+ }
18268
+ }
18016
18269
  return true;
18017
18270
  }
18018
18271
  reset() {
18019
18272
  this.#track = null;
18020
18273
  this.#fragments.clear();
18274
+ this.#firstSamples.clear();
18275
+ this.#delivered.clear();
18021
18276
  this.#pendingSamples = 0;
18022
18277
  }
18023
18278
  };
@@ -18187,6 +18442,9 @@ function authoredGap(before, after) {
18187
18442
  function ssIdAfter(a, b) {
18188
18443
  return (a - b | 0) > 0;
18189
18444
  }
18445
+ function holdsRebuilt(stream) {
18446
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18447
+ }
18190
18448
  var FecReleaseSequencer = class {
18191
18449
  #streams = /* @__PURE__ */ new Map();
18192
18450
  #gate;
@@ -18249,6 +18507,7 @@ var FecReleaseSequencer = class {
18249
18507
  stream.lastSbn = void 0;
18250
18508
  stream.discontinuity = false;
18251
18509
  stream.rapSsId = void 0;
18510
+ stream.sourced = false;
18252
18511
  }
18253
18512
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18254
18513
  this.#recordStat("droppedStale");
@@ -18275,6 +18534,7 @@ var FecReleaseSequencer = class {
18275
18534
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18276
18535
  else hi = mid;
18277
18536
  }
18537
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18278
18538
  stream.buf.splice(lo, 0, frame);
18279
18539
  this.#drainUnlessDeferred(stream);
18280
18540
  }
@@ -18310,6 +18570,38 @@ var FecReleaseSequencer = class {
18310
18570
  }
18311
18571
  return false;
18312
18572
  }
18573
+ /**
18574
+ * These streams' sink is being replaced by one that has seen no source
18575
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
18576
+ * release to the sink being replaced the frames the unanchored path had
18577
+ * already released to it (#cursorPassed): there they waited in its queue,
18578
+ * which its replacement flushes. A stream that holds none is not drained:
18579
+ * it holds nothing the anchor kept back, and a drain would still force out
18580
+ * a frame held past the hold limit, which before the anchor waited for the
18581
+ * next push. Then treat the streams as unsourced until a source frame is
18582
+ * released.
18583
+ */
18584
+ unsource(packetId, catalogTrackName) {
18585
+ for (const stream of this.#streams.values()) {
18586
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18587
+ if (stream.draining === 0 && holdsRebuilt(stream)) this.#drain(stream, true);
18588
+ stream.sourced = false;
18589
+ }
18590
+ }
18591
+ }
18592
+ /**
18593
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
18594
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
18595
+ * packetId.
18596
+ */
18597
+ holdsRebuilt(packetId, catalogTrackName) {
18598
+ for (const stream of this.#streams.values()) {
18599
+ if (stream.packetId !== packetId) continue;
18600
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
18601
+ if (holdsRebuilt(stream)) return true;
18602
+ }
18603
+ return false;
18604
+ }
18313
18605
  /** Re-evaluate held gaps after FEC state changed. */
18314
18606
  poke(streamKey) {
18315
18607
  if (streamKey !== void 0) {
@@ -18401,16 +18693,32 @@ var FecReleaseSequencer = class {
18401
18693
  last: null,
18402
18694
  lastSbn: void 0,
18403
18695
  discontinuity: false,
18404
- rapSsId: void 0
18696
+ rapSsId: void 0,
18697
+ sourced: false,
18698
+ draining: 0
18405
18699
  };
18406
18700
  this.#streams.set(streamKey, stream);
18407
18701
  }
18408
18702
  return stream;
18409
18703
  }
18410
- #drain(stream) {
18704
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18705
+ #drain(stream, asUnanchored = false) {
18706
+ stream.draining++;
18707
+ try {
18708
+ this.#drainLoop(stream, asUnanchored);
18709
+ } finally {
18710
+ stream.draining--;
18711
+ }
18712
+ }
18713
+ #drainLoop(stream, asUnanchored) {
18411
18714
  while (stream.buf.length > 0 && stream.last !== null) {
18412
18715
  const head = stream.buf[0];
18413
18716
  const last = stream.last;
18717
+ if (compareFrameOrder(head, last) <= 0) {
18718
+ stream.buf.shift();
18719
+ this.#release(stream, head, void 0);
18720
+ continue;
18721
+ }
18414
18722
  if (stream.discontinuity) {
18415
18723
  stream.discontinuity = false;
18416
18724
  stream.buf.shift();
@@ -18424,9 +18732,10 @@ var FecReleaseSequencer = class {
18424
18732
  }
18425
18733
  const headSbn = head.meta.fecSbn;
18426
18734
  const from = stream.lastSbn ?? headSbn;
18427
- const to = headSbn ?? stream.lastSbn;
18735
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18428
18736
  let lost = [];
18429
18737
  const unanchored = from === void 0 && to === void 0;
18738
+ let unresolved = false;
18430
18739
  if (from !== void 0 && to !== void 0) {
18431
18740
  const resolved = this.#gate.resolvedLoss(
18432
18741
  stream.packetId,
@@ -18435,7 +18744,8 @@ var FecReleaseSequencer = class {
18435
18744
  Math.max(from, to),
18436
18745
  from
18437
18746
  );
18438
- if (resolved === null) {
18747
+ unresolved = resolved === null;
18748
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18439
18749
  const heldMs = performance.now() - head.arrivedAtMs;
18440
18750
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18441
18751
  this.#recordStat("forcedWindowFlushes");
@@ -18443,22 +18753,56 @@ var FecReleaseSequencer = class {
18443
18753
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18444
18754
  continue;
18445
18755
  }
18446
- lost = resolved;
18756
+ lost = resolved ?? [];
18447
18757
  }
18448
18758
  stream.buf.shift();
18449
18759
  this.#release(
18450
18760
  stream,
18451
18761
  head,
18452
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18762
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18453
18763
  );
18454
18764
  }
18455
18765
  }
18766
+ /**
18767
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
18768
+ * source cursor holds it unanchored, and neither does a later frame behind
18769
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
18770
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
18771
+ * like each frame that arrived in front of it since (stale there). The head
18772
+ * then goes, decided as it would be unanchored, once a frame the base path
18773
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
18774
+ * marked one: the base path has passed that one, so a frame still missing
18775
+ * before it can only come from a repair. It also goes while no source frame
18776
+ * has been released, when the direct path shows a recovered frame at once.
18777
+ * And it goes at once while a staged switch decides on when frames arrive
18778
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18779
+ * sink being replaced: unanchored, it had already reached the sink.
18780
+ * The scan also writes: the `passed` marks it sets stay on the frames,
18781
+ * and push marks a frame it inserts in front of a marked one from them.
18782
+ */
18783
+ #cursorPassed(stream, asUnanchored) {
18784
+ let at2 = -1;
18785
+ for (let i = 0; i < stream.buf.length; i++) {
18786
+ const frame = stream.buf[i];
18787
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
18788
+ frame.passed = true;
18789
+ if (at2 < 0) at2 = i;
18790
+ } else if (frame.passed !== true) break;
18791
+ }
18792
+ if (at2 < 0) return false;
18793
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
18794
+ }
18456
18795
  #release(stream, frame, decision) {
18796
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18797
+ if (stale) this.#recordStat("droppedStale");
18457
18798
  if (decision) frame.meta.gapBefore = decision;
18458
18799
  stream.last = frame;
18459
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
18800
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
18801
+ stream.lastSbn = frame.meta.fecSbn;
18802
+ }
18803
+ if (frame.meta.recovered !== true) stream.sourced = true;
18460
18804
  this.#gate.onReleased?.(frame.data, frame.meta);
18461
- this.#emit(frame.data, frame.meta);
18805
+ if (!stale) this.#emit(frame.data, frame.meta);
18462
18806
  }
18463
18807
  };
18464
18808
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18499,6 +18843,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18499
18843
  this.#reclaimedTimingSamples = 0;
18500
18844
  this.#evictedTimingSamples = 0;
18501
18845
  this.#rejectedMovieFragments = 0;
18846
+ this.#recoveredHeadsAwaitingTiming = {
18847
+ held: 0,
18848
+ expired: 0,
18849
+ discarded: 0,
18850
+ superseded: 0
18851
+ };
18502
18852
  this.#sequencers = /* @__PURE__ */ new Map();
18503
18853
  this.#fecGate = null;
18504
18854
  this.#fecSequencer = null;
@@ -18607,6 +18957,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18607
18957
  #reclaimedTimingSamples;
18608
18958
  #evictedTimingSamples;
18609
18959
  #rejectedMovieFragments;
18960
+ #recoveredHeadsAwaitingTiming;
18610
18961
  #sequencers;
18611
18962
  #fecGate;
18612
18963
  #fecSequencer;
@@ -18712,11 +19063,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18712
19063
  /**
18713
19064
  * Typed view of the ISO-BMFF timing-sample budget.
18714
19065
  *
18715
- * This is the acceptance signal for the budget ratchet: `pending` walking
18716
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18717
- * and it is observable here rather than only as media silently stopping.
18718
- * Per repo policy this is a producer-owned counter -- never infer budget
18719
- * state by parsing logs.
19066
+ * Each stream admits timing against its own window of `capacity` slots
19067
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
19068
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
19069
+ * timing a stream declared and never delivered shows up there. `pending`
19070
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
19071
+ * a producer-owned counter -- never infer budget state by parsing logs.
18720
19072
  */
18721
19073
  get timingBudgetStats() {
18722
19074
  return {
@@ -18727,6 +19079,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18727
19079
  rejectedMovieFragments: this.#rejectedMovieFragments
18728
19080
  };
18729
19081
  }
19082
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
19083
+ get recoveredHeadsAwaitingTiming() {
19084
+ let pending = 0;
19085
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19086
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
19087
+ }
19088
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
19089
+ }
18730
19090
  get reorderStats() {
18731
19091
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18732
19092
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18768,6 +19128,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18768
19128
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18769
19129
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18770
19130
  }
19131
+ /** See FecReleaseSequencer.unsource. */
19132
+ unsourceFecRelease(packetId, catalogTrackName) {
19133
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
19134
+ }
19135
+ /** See FecReleaseSequencer.holdsRebuilt. */
19136
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
19137
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
19138
+ }
18771
19139
  /** Re-evaluate held gaps after FEC block state changed. */
18772
19140
  pokeFecRelease(packetId, catalogTrackName) {
18773
19141
  if (!this.#fecSequencer) return;
@@ -18900,7 +19268,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18900
19268
  const now = Date.now();
18901
19269
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18902
19270
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18903
- awaitingTiming.shift();
19271
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18904
19272
  }
18905
19273
  }
18906
19274
  }
@@ -19018,11 +19386,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19018
19386
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19019
19387
  return this.#drop("fragment-unexpected-fragmentation");
19020
19388
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19021
- if (!resolver2) return this.#drop("fragment-no-resolver");
19389
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19022
19390
  const pendingBefore = resolver2.pendingSampleCount;
19023
19391
  const evictedBefore = resolver2.evictedSampleCount;
19024
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19025
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19392
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19393
+ const admitted = resolver2.addMovieFragment(
19394
+ mpu.mpuSequenceNumber,
19395
+ data,
19396
+ remaining,
19397
+ options?.recovered === true
19398
+ );
19026
19399
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19027
19400
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19028
19401
  if (!admitted) {
@@ -19042,7 +19415,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19042
19415
  };
19043
19416
  }
19044
19417
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19045
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19418
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19046
19419
  if (!resolver) return this.#drop("mfu-no-resolver");
19047
19420
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19048
19421
  return this.#drop("mfu-empty-payload");
@@ -19063,6 +19436,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19063
19436
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19064
19437
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19065
19438
  sampleNumber: mfuDuHeader.sampleNumber,
19439
+ sequenceNumber: info.sequenceNumber,
19066
19440
  arrivedAt: Date.now()
19067
19441
  });
19068
19442
  return this.#drop("mfu-sample-timing-missing");
@@ -19088,7 +19462,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19088
19462
  info,
19089
19463
  resolver,
19090
19464
  options?.recovered === true,
19091
- options?.fecSbn
19465
+ options?.fecSbn,
19466
+ options?.fecSbnFirst
19092
19467
  );
19093
19468
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19094
19469
  }
@@ -19181,7 +19556,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19181
19556
  info,
19182
19557
  resolver,
19183
19558
  options?.recovered === true,
19184
- options?.fecSbn
19559
+ options?.fecSbn,
19560
+ options?.fecSbnFirst
19185
19561
  );
19186
19562
  if (outcome.record !== void 0) return outcome.record;
19187
19563
  return this.#drop(outcome.reason);
@@ -19201,7 +19577,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19201
19577
  * attribution exists to keep readable. Leaving the choice at the call site
19202
19578
  * is what keeps both true.
19203
19579
  */
19204
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19580
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19205
19581
  const timing = resolver.sampleTiming(
19206
19582
  combined.mpuSequenceNumber,
19207
19583
  combined.movieFragmentSequenceNumber,
@@ -19218,6 +19594,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19218
19594
  );
19219
19595
  if (!meta) return { reason: "combined-meta-build-failed" };
19220
19596
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19597
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19221
19598
  if (!resolver.consumeSample(
19222
19599
  combined.mpuSequenceNumber,
19223
19600
  combined.movieFragmentSequenceNumber,
@@ -19386,6 +19763,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19386
19763
  }
19387
19764
  this.#emitFrame(data, meta);
19388
19765
  }
19766
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19767
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19768
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19769
+ }
19389
19770
  /**
19390
19771
  * Claim and return the authored timing for a recovered sample. Recovery does
19391
19772
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19447,6 +19828,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19447
19828
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19448
19829
  this.#sequencers.clear();
19449
19830
  this.#fecSequencer?.discard();
19831
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19832
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19833
+ }
19450
19834
  this.#timingStreams.clear();
19451
19835
  this.#pendingTimingSamples = 0;
19452
19836
  this.#reliableMoqStartupGate.clear();
@@ -19465,10 +19849,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19465
19849
  return resolver;
19466
19850
  }
19467
19851
  #awaitTiming(streamKey, head) {
19468
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19469
- if (!awaitingTiming) return;
19852
+ const state = this.#timingStreams.get(streamKey);
19853
+ if (!state) return;
19854
+ if (!head.options?.recovered) {
19855
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19856
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19857
+ state.awaitingTiming = kept;
19858
+ }
19859
+ const awaitingTiming = state.awaitingTiming;
19470
19860
  awaitingTiming.push(head);
19471
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19861
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19862
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19863
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19864
+ }
19865
+ }
19866
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19867
+ #leaveAwaitingTiming(head, outcome) {
19868
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19472
19869
  }
19473
19870
  /**
19474
19871
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19496,6 +19893,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19496
19893
  const state = this.#timingStreams.get(streamKey);
19497
19894
  if (!state) return;
19498
19895
  this.#timingStreams.delete(streamKey);
19896
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19499
19897
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19500
19898
  if (this.#pendingTimingSamples < 0) {
19501
19899
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22702,7 +23100,18 @@ function keyframeOverlaySsId(header) {
22702
23100
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22703
23101
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22704
23102
  }
23103
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23104
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23105
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23106
+ }
23107
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23108
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23109
+ return headerPsn + offset >>> 0;
23110
+ }
23111
+ var OVERLAY_ROUTED_LIMIT = 1024;
23112
+ var OVERLAY_PENDING_LIMIT = 256;
22705
23113
  var OVERLAY_OFFSET_MPUS = 16;
23114
+ var OVERLAY_REBUILT_LIMIT = 256;
22706
23115
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22707
23116
  function assertStreamIdentity(role, stream) {
22708
23117
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -22813,6 +23222,7 @@ var MmtpRouter = class {
22813
23222
  #provenance = /* @__PURE__ */ new Map();
22814
23223
  #onFlowProtected = null;
22815
23224
  #unadmittedListedDrops = 0;
23225
+ #admittedSourceDatagrams = 0;
22816
23226
  #unlistedRepairSetAside = 0;
22817
23227
  #assistBlocks = /* @__PURE__ */ new Map();
22818
23228
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22869,6 +23279,18 @@ var MmtpRouter = class {
22869
23279
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22870
23280
  */
22871
23281
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23282
+ /**
23283
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23284
+ * sender's own packet_sequence_number: a recovered packet's header psn
23285
+ * equal to its SS_ID minus that offset; or a packet the overlay rebuilt
23286
+ * did, its base copy carrying its protected psn (#feedOverlaySource), or
23287
+ * its protected psn passing overlayBaseSsId's test at that offset
23288
+ * (#routeOverlayRecovered). While it equals the stream's current offset,
23289
+ * no hop re-stamps the stream, and a packet's protected psn names its
23290
+ * base SS_ID within unstampedBaseSsId's limits (joint decoding of an MPU
23291
+ * no member of which arrived).
23292
+ */
23293
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22872
23294
  /**
22873
23295
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22874
23296
  * not parsed, typically because its overlap copy was parsed first
@@ -22930,7 +23352,9 @@ var MmtpRouter = class {
22930
23352
  /** Installed overlay instances, by overlay repair PID (opt-in only). */
22931
23353
  #overlays = /* @__PURE__ */ new Map();
22932
23354
  #overlayRecoveredObservers = /* @__PURE__ */ new Set();
22933
- /** Streams whose recovered sequence the base path ever re-stamped. */
23355
+ /** Streams whose recovered sequence the base path ever re-stamped, or
23356
+ * where an overlay-rebuilt packet's base copy carried another psn than
23357
+ * the sender protected. */
22934
23358
  #restampedStreams = /* @__PURE__ */ new Set();
22935
23359
  #overlayStats = {
22936
23360
  repairDropped: 0,
@@ -22938,7 +23362,9 @@ var MmtpRouter = class {
22938
23362
  recovered: 0,
22939
23363
  restarts: 0,
22940
23364
  unsequencedDropped: 0,
22941
- protectedDropped: 0
23365
+ protectedDropped: 0,
23366
+ baseFed: 0,
23367
+ baseUndetermined: 0
22942
23368
  };
22943
23369
  /** Callback fired when a delta-track FEC block completes or fails. */
22944
23370
  #onDeltaBlockCompleteCb = null;
@@ -23111,7 +23537,7 @@ var MmtpRouter = class {
23111
23537
  const cb = this.#getFrameCallback(packetId);
23112
23538
  if (isVideo && !assembler) {
23113
23539
  if (frameMeta.recovered) {
23114
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23540
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23115
23541
  } else {
23116
23542
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23117
23543
  }
@@ -23195,8 +23621,9 @@ var MmtpRouter = class {
23195
23621
  }
23196
23622
  /**
23197
23623
  * FEC-recovered source symbols whose reconstructed header named a
23198
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23199
- * a hop re-stamped the sequence after the sender protected the packet.
23624
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
23625
+ * or, rebuilt by the overlay, than their base copy carries: a hop
23626
+ * re-stamped the sequence after the sender protected the packet.
23200
23627
  */
23201
23628
  get restampedRecoveredSequenceCount() {
23202
23629
  return this.#restampedRecoveredSeqs;
@@ -23375,6 +23802,7 @@ var MmtpRouter = class {
23375
23802
  enableMseAssembler(packetId, config, catalogTrackName) {
23376
23803
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23377
23804
  const previous = this.#mseAssemblers.get(assemblerKey);
23805
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23378
23806
  if (previous) {
23379
23807
  previous.flush();
23380
23808
  previous.dispose();
@@ -23501,6 +23929,10 @@ var MmtpRouter = class {
23501
23929
  get keyframeOverlayStats() {
23502
23930
  return { ...this.#overlayStats };
23503
23931
  }
23932
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23933
+ get recoveredHeadsAwaitingTiming() {
23934
+ return this.#parser.recoveredHeadsAwaitingTiming;
23935
+ }
23504
23936
  #isOverlayRepair(repairPacketId, repairTrackName) {
23505
23937
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23506
23938
  }
@@ -23521,7 +23953,8 @@ var MmtpRouter = class {
23521
23953
  fec,
23522
23954
  k,
23523
23955
  offsets: /* @__PURE__ */ new Map(),
23524
- moqBlocks: /* @__PURE__ */ new Set()
23956
+ moqBlocks: /* @__PURE__ */ new Set(),
23957
+ rebuilt: /* @__PURE__ */ new Map()
23525
23958
  };
23526
23959
  this.#overlays.set(repairPacketId, inst);
23527
23960
  return () => {
@@ -23551,12 +23984,18 @@ var MmtpRouter = class {
23551
23984
  */
23552
23985
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23553
23986
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23987
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23988
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23989
+ stream.overlayToBase.routed.clear();
23990
+ stream.overlayToBase.pending.clear();
23991
+ }
23554
23992
  }
23555
23993
  #resetOverlay(inst) {
23556
23994
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23557
23995
  inst.last = void 0;
23558
23996
  inst.offsets.clear();
23559
23997
  inst.moqBlocks.clear();
23998
+ inst.rebuilt.clear();
23560
23999
  }
23561
24000
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23562
24001
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23607,18 +24046,32 @@ var MmtpRouter = class {
23607
24046
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23608
24047
  * every overlay installed for its source, at that SS_ID, with the bytes the
23609
24048
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23610
- * included (7.1, 7.4.3).
24049
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
24050
+ * FEC reads it.
24051
+ *
24052
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24053
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24054
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24055
+ * the stream unstamped at the SS_ID offset it states (unstampedBaseSsId);
24056
+ * any other shows a hop that re-stamps it.
23611
24057
  */
23612
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24058
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23613
24059
  if (this.#overlays.size === 0) return;
23614
24060
  const ssId = keyframeOverlaySsId(header);
23615
24061
  if (ssId === void 0) return;
24062
+ let rebuiltPsn;
23616
24063
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23617
24064
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24065
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24066
+ inst.rebuilt.delete(ssId);
23618
24067
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23619
24068
  if (identity !== null) {
23620
24069
  inst.offsets.delete(identity.mpuSequenceNumber);
23621
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
24070
+ inst.offsets.set(identity.mpuSequenceNumber, {
24071
+ psnOffset: ssId - sequenceNumber >>> 0,
24072
+ overlaySsId: ssId,
24073
+ ...baseSsId === void 0 ? {} : { baseSsId }
24074
+ });
23622
24075
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23623
24076
  }
23624
24077
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23640,6 +24093,15 @@ var MmtpRouter = class {
23640
24093
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23641
24094
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23642
24095
  }
24096
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
24097
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
24098
+ if (rebuiltPsn !== sequenceNumber) {
24099
+ this.#restampedRecoveredSeqs++;
24100
+ this.#restampedStreams.add(streamKey);
24101
+ } else if (baseSsId !== void 0) {
24102
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24103
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#unstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24104
+ }
23643
24105
  }
23644
24106
  #noteOverlayMoqBlock(inst, blockStart) {
23645
24107
  inst.moqBlocks.add(blockStart);
@@ -23673,11 +24135,16 @@ var MmtpRouter = class {
23673
24135
  }
23674
24136
  /**
23675
24137
  * Route an overlay-recovered block's packets as source media of the
23676
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23677
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23678
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23679
- * from its keyframe's own offset; without one, a stream the base never
23680
- * re-stamped keeps the header's sequence and any other drops the packet.
24138
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
24139
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
24140
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
24141
+ * base FEC recovered does, so it waits behind base frames still in flight,
24142
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
24143
+ * longer than unanchored; one whose base SS_ID is undetermined has no
24144
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
24145
+ * advance. Each packet's wire sequence is restored from its keyframe's own
24146
+ * offset; without one, a stream the base never re-stamped keeps the
24147
+ * header's sequence and any other drops the packet.
23681
24148
  */
23682
24149
  #routeOverlayRecovered(inst, blockStart, recovered) {
23683
24150
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23697,22 +24164,136 @@ var MmtpRouter = class {
23697
24164
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23698
24165
  const restamped = this.#restampedStreams.has(streamKey);
23699
24166
  const restored = /* @__PURE__ */ new Set();
24167
+ const baseSsIds = /* @__PURE__ */ new Map();
23700
24168
  const symbols = result.symbols.flatMap((sym) => {
23701
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23702
- if (offset !== void 0) {
23703
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24169
+ const overlaySsId = serial32Add(blockStart, sym.index);
24170
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24171
+ if (anchor !== void 0) {
24172
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23704
24173
  restored.add(packetSequenceNumber);
23705
- return [{ ...sym, packetSequenceNumber }];
24174
+ const out = { ...sym, packetSequenceNumber };
24175
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24176
+ if (baseSsId !== void 0) {
24177
+ baseSsIds.set(sym.index, baseSsId);
24178
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24179
+ if (offset === baseSsId - packetSequenceNumber >>> 0) this.#unstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24180
+ }
24181
+ return [out];
24182
+ }
24183
+ if (!restamped) {
24184
+ const baseSsId = unstampedBaseSsId(
24185
+ this.#ssIdSeqOffsetByStream.get(streamKey),
24186
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24187
+ sym.packetSequenceNumber
24188
+ );
24189
+ if (baseSsId !== void 0) {
24190
+ baseSsIds.set(sym.index, baseSsId);
24191
+ } else {
24192
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24193
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24194
+ }
24195
+ return [sym];
23706
24196
  }
23707
- if (!restamped) return [sym];
23708
24197
  this.#overlayStats.unsequencedDropped++;
23709
24198
  return [];
23710
24199
  });
23711
24200
  this.#overlayStats.recovered++;
24201
+ const mpuFirst = /* @__PURE__ */ new Map();
24202
+ for (const sym of symbols) {
24203
+ const ssId = baseSsIds.get(sym.index);
24204
+ const mpu = sym.fragment.mpuSequenceNumber;
24205
+ const first = mpuFirst.get(mpu);
24206
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
24207
+ }
24208
+ const firstSsIds = /* @__PURE__ */ new Map();
24209
+ for (const sym of symbols) {
24210
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
24211
+ }
23712
24212
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23713
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24213
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24214
+ baseSsIds,
24215
+ firstSsIds
23714
24216
  });
23715
24217
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24218
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24219
+ }
24220
+ /**
24221
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24222
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24223
+ * from a received member of its MPU, else unstampedBaseSsId),
24224
+ * so it is fed to the base decoder as a rebuilt source
24225
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24226
+ * fewer to solve, and its repair can recover the block's other packets.
24227
+ * A base block no repair has stated yet takes it when one does
24228
+ * (#feedPendingRebuilt).
24229
+ *
24230
+ * The overlay route already handled each packet fed (delivered it, or
24231
+ * held an untimed head for its FT=1), so a base recovery routes none of
24232
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24233
+ * held head would wait in the parser for timing its first copy consumed.
24234
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24235
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24236
+ * that crossed no multicast socket, which the provenance counted from the
24237
+ * block's own arrivals cannot show.
24238
+ */
24239
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24240
+ const fec = this.#fecManager;
24241
+ const owner = inst.leg.sourcePacketId;
24242
+ const name = inst.leg.sourceCatalogTrackName;
24243
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24244
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24245
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24246
+ const feeds = [];
24247
+ for (const sym of symbols) {
24248
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24249
+ const ssId = baseSsIds.get(sym.index);
24250
+ if (ssId === void 0) {
24251
+ this.#overlayStats.baseUndetermined++;
24252
+ continue;
24253
+ }
24254
+ routed.delete(ssId);
24255
+ routed.add(ssId);
24256
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24257
+ feeds.push([ssId, sym.rawSymbol]);
24258
+ }
24259
+ for (const [ssId, symbolData] of feeds) {
24260
+ if (this.#classifyPacketId(owner) === null) return;
24261
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24262
+ }
24263
+ }
24264
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24265
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24266
+ const fec = this.#fecManager;
24267
+ if (!fec?.feedRebuiltSourceSymbol) return;
24268
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24269
+ if (coords == null) {
24270
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24271
+ pending.delete(ssId);
24272
+ pending.set(ssId, symbolData.slice());
24273
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24274
+ return;
24275
+ }
24276
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24277
+ if (!fed.taken) return;
24278
+ this.#overlayStats.baseFed++;
24279
+ if (fed.recovered === null) return;
24280
+ const sbn = coords.sbn;
24281
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24282
+ fec.cancelBlockTimeout(sbn, owner, name);
24283
+ fec.countRecovery(owner, sbn, name);
24284
+ fec.markBlockCompleted(sbn, owner, name);
24285
+ });
24286
+ }
24287
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24288
+ #feedPendingRebuilt(owner, name, block) {
24289
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24290
+ if (pending === void 0 || pending.size === 0) return;
24291
+ for (const [ssId, symbolData] of [...pending]) {
24292
+ if (this.#classifyPacketId(owner) === null) return;
24293
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24294
+ pending.delete(ssId);
24295
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24296
+ }
23716
24297
  }
23717
24298
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23718
24299
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23852,6 +24433,10 @@ var MmtpRouter = class {
23852
24433
  get unadmittedListedDropCount() {
23853
24434
  return this.#unadmittedListedDrops;
23854
24435
  }
24436
+ /** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
24437
+ get admittedSourceDatagramCount() {
24438
+ return this.#admittedSourceDatagrams;
24439
+ }
23855
24440
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23856
24441
  get unlistedRepairSetAsideCount() {
23857
24442
  return this.#unlistedRepairSetAside;
@@ -24338,6 +24923,8 @@ var MmtpRouter = class {
24338
24923
  )) return;
24339
24924
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24340
24925
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24926
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24927
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24341
24928
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24342
24929
  const recovered = fec.feedRepairSymbol(
24343
24930
  ownerPacketId,
@@ -24475,6 +25062,7 @@ var MmtpRouter = class {
24475
25062
  this.#droppedPreSyncVideo.delete(streamKey);
24476
25063
  this.#latestVideoSeqByStream.delete(streamKey);
24477
25064
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25065
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24478
25066
  this.#multicastRapTwinByStream.delete(streamKey);
24479
25067
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24480
25068
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24601,6 +25189,12 @@ var MmtpRouter = class {
24601
25189
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24602
25190
  * own RAP cadence, so no timer is involved.
24603
25191
  *
25192
+ * While the switch is staged, the FEC release gate lets a keyframe the
25193
+ * overlay rebuilt reach it, on either side, when the keyframe did before
25194
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
25195
+ * decides on what each side delivered, and the commit discards the gate's
25196
+ * frames of the outgoing stream.
25197
+ *
24604
25198
  * The commit is one synchronous transition inside the frame path: the
24605
25199
  * outgoing stream's already-admitted frames are released (its queued FEC
24606
25200
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24651,6 +25245,9 @@ var MmtpRouter = class {
24651
25245
  droppedIncomingFrames: 0
24652
25246
  };
24653
25247
  this.#staged = state;
25248
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25249
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25250
+ }
24654
25251
  return {
24655
25252
  cancel: () => {
24656
25253
  if (this.#staged !== state) return false;
@@ -24733,7 +25330,17 @@ var MmtpRouter = class {
24733
25330
  */
24734
25331
  #routeParsedFrame(data, meta) {
24735
25332
  if (this.#classifyPacketId(meta.packetId) === null) return;
24736
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
25333
+ if (this.#staged !== null) {
25334
+ let consumed = false;
25335
+ this.#fecPokeDepth++;
25336
+ try {
25337
+ consumed = this.#interceptSwitchFrame(data, meta);
25338
+ } finally {
25339
+ this.#fecPokeDepth--;
25340
+ this.#flushPendingFecPokes();
25341
+ }
25342
+ if (consumed) return;
25343
+ }
24737
25344
  this.#deliverParsedFrame(data, meta);
24738
25345
  }
24739
25346
  #commitStreamSwitch(state, rap, reason) {
@@ -24885,12 +25492,16 @@ var MmtpRouter = class {
24885
25492
  this.#parser.deferFecRelease(work);
24886
25493
  } finally {
24887
25494
  this.#fecPokeDepth--;
24888
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
24889
- const pokes = [...this.#pendingFecPokes.values()];
24890
- this.#pendingFecPokes.clear();
24891
- for (const poke of pokes) {
24892
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24893
- }
25495
+ this.#flushPendingFecPokes();
25496
+ }
25497
+ }
25498
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
25499
+ #flushPendingFecPokes() {
25500
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25501
+ const pokes = [...this.#pendingFecPokes.values()];
25502
+ this.#pendingFecPokes.clear();
25503
+ for (const poke of pokes) {
25504
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24894
25505
  }
24895
25506
  }
24896
25507
  }
@@ -24968,6 +25579,14 @@ var MmtpRouter = class {
24968
25579
  onRap: (packetId, catalogTrackName, ssId) => {
24969
25580
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
24970
25581
  },
25582
+ // A staged switch commits on when each side's frames reach it
25583
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
25584
+ // they did unanchored.
25585
+ releaseUnanchored: (packetId, catalogTrackName) => {
25586
+ const staged = this.#staged;
25587
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
25588
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
25589
+ },
24971
25590
  onReleased: (data, meta) => {
24972
25591
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
24973
25592
  const timing = meta.authoredTiming;
@@ -25109,6 +25728,7 @@ var MmtpRouter = class {
25109
25728
  const parsed = this.#parser.parsePacket(packet);
25110
25729
  if (!parsed) return;
25111
25730
  const info = parsed.info;
25731
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25112
25732
  if (source === "multicast" && !manifestAdmitted) {
25113
25733
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25114
25734
  }
@@ -25390,7 +26010,8 @@ var MmtpRouter = class {
25390
26010
  parsed.header,
25391
26011
  info.sequenceNumber,
25392
26012
  info.rapFlag,
25393
- source
26013
+ source,
26014
+ fecPayloadId?.ssId
25394
26015
  );
25395
26016
  }
25396
26017
  }
@@ -25416,7 +26037,8 @@ var MmtpRouter = class {
25416
26037
  source: parserOptions?.source,
25417
26038
  deliveryProvenance: parserOptions?.deliveryProvenance,
25418
26039
  catalogTrackName: parserOptions?.catalogTrackName,
25419
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26040
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26041
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25420
26042
  };
25421
26043
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25422
26044
  else this.#parser.feed(packet, feedOptions);
@@ -25540,7 +26162,8 @@ var MmtpRouter = class {
25540
26162
  if (stream === void 0) {
25541
26163
  stream = {
25542
26164
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25543
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26165
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26166
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25544
26167
  };
25545
26168
  byName.set(catalogTrackName, stream);
25546
26169
  }
@@ -25655,8 +26278,16 @@ var MmtpRouter = class {
25655
26278
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25656
26279
  this.#emitDirectVideo(packetId, data, meta, cb);
25657
26280
  }
25658
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26281
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25659
26282
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26283
+ if (this.#isFecTrack(packetId)) {
26284
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26285
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26286
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26287
+ this.#emitDirectVideo(packetId, data, meta, cb);
26288
+ return;
26289
+ }
26290
+ }
25660
26291
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25661
26292
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25662
26293
  this.#drainDirectRecoveredVideo(
@@ -25810,6 +26441,13 @@ var MmtpRouter = class {
25810
26441
  * frames by epoch only if pre-protection input ever matters.
25811
26442
  */
25812
26443
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26444
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26445
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26446
+ result = {
26447
+ ...result,
26448
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26449
+ };
26450
+ }
25813
26451
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25814
26452
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25815
26453
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25866,7 +26504,12 @@ var MmtpRouter = class {
25866
26504
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25867
26505
  if (ownerPacketId == null) return;
25868
26506
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25869
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26507
+ const fecSbnOf = (index) => {
26508
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26509
+ const fecSbn = overlay.baseSsIds.get(index);
26510
+ const fecSbnFirst = overlay.firstSsIds.get(index);
26511
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
26512
+ };
25870
26513
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25871
26514
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25872
26515
  if (ssIdSeqOffset !== void 0) {
@@ -25874,7 +26517,10 @@ var MmtpRouter = class {
25874
26517
  ...result,
25875
26518
  symbols: result.symbols.map((sym) => {
25876
26519
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25877
- if (wire === sym.packetSequenceNumber) return sym;
26520
+ if (wire === sym.packetSequenceNumber) {
26521
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26522
+ return sym;
26523
+ }
25878
26524
  this.#restampedRecoveredSeqs++;
25879
26525
  this.#restampedStreams.add(streamKey);
25880
26526
  return { ...sym, packetSequenceNumber: wire };
@@ -25910,6 +26556,29 @@ var MmtpRouter = class {
25910
26556
  }
25911
26557
  );
25912
26558
  }
26559
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26560
+ ownerPacketId,
26561
+ sym.fragment.mpuSequenceNumber,
26562
+ sym.fragment.movieFragmentSequenceNumber,
26563
+ sym.fragment.sampleNumber,
26564
+ catalogTrackName
26565
+ );
26566
+ for (const sym of result.symbols) {
26567
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26568
+ this.#feedViaParser(
26569
+ sym.rawSymbol,
26570
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26571
+ void 0,
26572
+ {
26573
+ skipFecPayloadId: true,
26574
+ recovered: true,
26575
+ deliveryProvenance,
26576
+ ...fecSbnOf(sym.index),
26577
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26578
+ }
26579
+ );
26580
+ }
26581
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25913
26582
  const gated = this.#isFecTrack(ownerPacketId);
25914
26583
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25915
26584
  if (assembler) {