@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 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 (73) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +934 -213
  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 +33 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +222 -6
  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 +3 -3
  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 +9 -0
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +17 -0
  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 +204 -26
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +206 -29
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +203 -26
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +27 -3
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +533 -75
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.js +757 -83
  48. package/dist/multicast-manager.js.map +2 -2
  49. package/dist/multicast-transport.js +28 -18
  50. package/dist/multicast-transport.js.map +2 -2
  51. package/dist/no-modules.js +203 -26
  52. package/dist/no-modules.js.map +2 -2
  53. package/dist/repair-layer-subscriptions.d.ts +6 -2
  54. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  55. package/dist/repair-layer-subscriptions.js +6 -3
  56. package/dist/repair-layer-subscriptions.js.map +2 -2
  57. package/dist/track-session.d.ts +1 -1
  58. package/dist/track-session.js.map +1 -1
  59. package/dist/wasm-mmtp-container-parser.js +203 -26
  60. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  61. package/package.json +8 -5
  62. package/schemas/catalog.schema.json +4 -0
  63. package/src/catalog-fec-params.ts +36 -1
  64. package/src/cmcd-producer.ts +189 -179
  65. package/src/fec-manager.ts +277 -6
  66. package/src/fec-worker-setup.ts +2 -2
  67. package/src/index.ts +7 -2
  68. package/src/iso-block-locator.ts +19 -0
  69. package/src/iso-bmff-timing.ts +40 -1
  70. package/src/mmtp-container-parser.ts +298 -29
  71. package/src/mmtp-router.ts +510 -37
  72. package/src/repair-layer-subscriptions.ts +13 -5
  73. package/src/track-session.ts +1 -1
@@ -7988,8 +7988,8 @@ var MulticastSubscriptionPool = class {
7988
7988
  record.claims--;
7989
7989
  return this.acquire(gateway, config, consumer);
7990
7990
  }
7991
- const token = Symbol(key);
7992
- entry.consumers.set(token, consumer);
7991
+ const token2 = Symbol(key);
7992
+ entry.consumers.set(token2, consumer);
7993
7993
  let released = false;
7994
7994
  return {
7995
7995
  key,
@@ -7997,7 +7997,7 @@ var MulticastSubscriptionPool = class {
7997
7997
  if (released)
7998
7998
  return;
7999
7999
  released = true;
8000
- entry.consumers.delete(token);
8000
+ entry.consumers.delete(token2);
8001
8001
  record.claims--;
8002
8002
  if (record.claims > 0)
8003
8003
  return;
@@ -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,14 +8746,15 @@ 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;
8753
+ messageListenerRegistered = false;
8752
8754
  pendingPackets = /* @__PURE__ */ new Map();
8753
8755
  constructor() {
8754
8756
  this.messageHandler = (event) => {
8755
8757
  const data = event.data;
8756
- console.log(`[AMT] SW message received:`, data.type);
8757
8758
  if (data.type === "UDP_PACKET") {
8758
8759
  this.handlePacket(data);
8759
8760
  } else if (data.type === "SOCKET_ERROR") {
@@ -8762,12 +8763,22 @@ var AMTGatewayManager = class {
8762
8763
  console.log(`[AMT SW Debug] ${data.message}`);
8763
8764
  }
8764
8765
  };
8765
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
8766
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
8767
- console.log("[AMT] Registered service worker message handler");
8768
- } else {
8769
- console.warn("[AMT] Service worker not available");
8766
+ }
8767
+ ensureMessageListener() {
8768
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8769
+ return;
8770
+ }
8771
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8772
+ this.messageListenerRegistered = true;
8773
+ console.log("[AMT] Registered service worker message handler");
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);
8770
8780
  }
8781
+ this.messageListenerRegistered = false;
8771
8782
  }
8772
8783
  /**
8773
8784
  * Subscribe via AMT tunnel
@@ -8777,6 +8788,7 @@ var AMTGatewayManager = class {
8777
8788
  if (config.protected) {
8778
8789
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8779
8790
  }
8791
+ this.ensureMessageListener();
8780
8792
  const keyStr = this.getKeyString(key);
8781
8793
  if (this.subscriptions.has(keyStr)) {
8782
8794
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8812,6 +8824,8 @@ var AMTGatewayManager = class {
8812
8824
  this.subscriptions.delete(keyStr);
8813
8825
  this.pendingPackets.delete(subscription.subscriptionId);
8814
8826
  }
8827
+ if (this.subscriptions.size === 0)
8828
+ this.removeMessageListener();
8815
8829
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8816
8830
  }
8817
8831
  }
@@ -8833,6 +8847,8 @@ var AMTGatewayManager = class {
8833
8847
  } finally {
8834
8848
  this.subscriptions.delete(handle);
8835
8849
  this.pendingPackets.delete(subscription.subscriptionId);
8850
+ if (this.subscriptions.size === 0)
8851
+ this.removeMessageListener();
8836
8852
  }
8837
8853
  }
8838
8854
  /**
@@ -8849,14 +8865,12 @@ var AMTGatewayManager = class {
8849
8865
  if (!subscriptionId)
8850
8866
  return;
8851
8867
  const subscription = this.subscriptions.get(subscriptionId);
8852
- if (!subscription) {
8853
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8868
+ if (!subscription)
8854
8869
  return;
8855
- }
8856
8870
  if (!subscription.handshakeComplete) {
8857
8871
  const queue = this.pendingPackets.get(subscriptionId);
8858
8872
  const packetData2 = data;
8859
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8860
8874
  queue.push({
8861
8875
  data: packetData2.data,
8862
8876
  remoteAddress: packetData2.remoteAddress,
@@ -8882,19 +8896,15 @@ var AMTGatewayManager = class {
8882
8896
  if (!subscriptionId)
8883
8897
  return;
8884
8898
  const subscription = this.subscriptions.get(subscriptionId);
8885
- if (!subscription) {
8886
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8899
+ if (!subscription)
8887
8900
  return;
8888
- }
8889
8901
  subscription.config.onError(new Error(error || "Unknown error"));
8890
8902
  }
8891
8903
  /**
8892
8904
  * Cleanup on destroy
8893
8905
  */
8894
8906
  destroy() {
8895
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8896
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8897
- }
8907
+ this.removeMessageListener();
8898
8908
  const handles = Array.from(this.subscriptions.keys());
8899
8909
  for (const handle of handles) {
8900
8910
  this.unsubscribe(handle).catch((err) => {
@@ -12939,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12939
12949
  #retentionBlocks;
12940
12950
  #heldEvicted;
12941
12951
  #newest;
12952
+ #newestSource;
12942
12953
  #holes;
12943
12954
  static #checkedRetention(retentionBlocks) {
12944
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12976,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12976
12987
  observe(position) {
12977
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12978
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
+ }
12979
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12980
13006
  get heldEvicted() {
12981
13007
  return this.#heldEvicted;
@@ -13208,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13208
13234
  this.#held.clear();
13209
13235
  this.#coverageEnd = void 0;
13210
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13211
13238
  }
13212
13239
  #claim(block) {
13213
13240
  const placed = [];
@@ -13827,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13827
13854
  var WORKER_ERROR_KIND_CAP = 32;
13828
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13829
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13830
13858
  function isMulticastSymbolDelivery(value) {
13831
13859
  return value === "datagram" || value === "ordered-relay";
13832
13860
  }
@@ -13924,6 +13952,10 @@ var FECManager = class _FECManager {
13924
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13925
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13926
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();
13927
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13928
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13929
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14052,6 +14084,10 @@ var FECManager = class _FECManager {
14052
14084
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14053
14085
  // One classifier per physical stream, built from that stream's geometry.
14054
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();
14055
14091
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14056
14092
  // rungs can share a packetId with a different framerate or GOP, so the
14057
14093
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14209,6 +14245,7 @@ var FECManager = class _FECManager {
14209
14245
  #pendingWorkerCleanupBounds;
14210
14246
  #blockSourceCounts;
14211
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14212
14249
  #blockAuthoredLengths;
14213
14250
  #ssbLengthConflictCount;
14214
14251
  #blockRepairRsIds;
@@ -14251,6 +14288,7 @@ var FECManager = class _FECManager {
14251
14288
  #orderedFrontiers;
14252
14289
  #orderedPendingBlocks;
14253
14290
  #classifiers;
14291
+ #noSpanRaps;
14254
14292
  #streamGeometries;
14255
14293
  #failedBlocks;
14256
14294
  #resolvedBlockClasses;
@@ -14912,6 +14950,7 @@ var FECManager = class _FECManager {
14912
14950
  };
14913
14951
  const discontinuity = () => {
14914
14952
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14953
+ this.#noSpanRaps.delete(streamKey);
14915
14954
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14916
14955
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14917
14956
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15324,6 +15363,9 @@ var FECManager = class _FECManager {
15324
15363
  }
15325
15364
  if (policyChanged) {
15326
15365
  this.#bumpEpoch(packetId);
15366
+ for (const key of [...this.#noSpanRaps.keys()]) {
15367
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15368
+ }
15327
15369
  for (const [key, classifier] of [...this.#classifiers]) {
15328
15370
  if (!this.#belongsToPacket(key, packetId)) continue;
15329
15371
  const stream = this.#parseStreamKey(key);
@@ -15526,6 +15568,7 @@ var FECManager = class _FECManager {
15526
15568
  }
15527
15569
  resetMap(this.#blockSourceCounts);
15528
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15529
15572
  resetMap(this.#blockAuthoredLengths);
15530
15573
  resetMap(this.#blockRepairRsIds);
15531
15574
  resetMap(this.#blockRsbLengths);
@@ -15559,6 +15602,7 @@ var FECManager = class _FECManager {
15559
15602
  resetMap(this.#isoLocators);
15560
15603
  resetMap(this.#sourceAnchors);
15561
15604
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15605
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15562
15606
  this.#bumpEpoch(packetId);
15563
15607
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15564
15608
  for (const key of keys.keys()) {
@@ -15649,6 +15693,7 @@ var FECManager = class _FECManager {
15649
15693
  dropMap(this.#pendingWorkerCleanupClients);
15650
15694
  dropMap(this.#blockSourceCounts);
15651
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15652
15697
  dropMap(this.#blockAuthoredLengths);
15653
15698
  dropMap(this.#blockRepairRsIds);
15654
15699
  dropMap(this.#blockRsbLengths);
@@ -15665,6 +15710,7 @@ var FECManager = class _FECManager {
15665
15710
  dropMap(this.#isoLocators);
15666
15711
  dropMap(this.#sourceAnchors);
15667
15712
  dropMap(this.#classifiers);
15713
+ dropMap(this.#noSpanRaps);
15668
15714
  dropMap(this.#streamGeometries);
15669
15715
  dropSet(this.#pendingWorkerCleanups);
15670
15716
  dropSet(this.#completedBlocks);
@@ -15810,6 +15856,9 @@ var FECManager = class _FECManager {
15810
15856
  this.#unindexBlockKey(key);
15811
15857
  }
15812
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15813
15862
  return true;
15814
15863
  }
15815
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16127,6 +16176,7 @@ var FECManager = class _FECManager {
16127
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16128
16177
  const evictedBefore = locator.heldEvicted;
16129
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16130
16180
  const evicted = locator.heldEvicted - evictedBefore;
16131
16181
  if (evicted > 0) {
16132
16182
  const before = this.#isoHeldEvictedCount;
@@ -16295,11 +16345,14 @@ var FECManager = class _FECManager {
16295
16345
  /**
16296
16346
  * Whether an MPU boundary still bounds this stream's RAPs (see
16297
16347
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16298
- * 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.
16299
16351
  */
16300
16352
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16301
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16302
- 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;
16303
16356
  }
16304
16357
  /** A block's class now, or frozen at its resolution. */
16305
16358
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16325,7 +16378,14 @@ var FECManager = class _FECManager {
16325
16378
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16326
16379
  */
16327
16380
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16328
- 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
+ }
16329
16389
  this.#applyClassifierChanges(
16330
16390
  packetId,
16331
16391
  catalogTrackName,
@@ -16712,7 +16772,15 @@ var FECManager = class _FECManager {
16712
16772
  this.#isoSourceOversizeCount++;
16713
16773
  return null;
16714
16774
  }
16715
- 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) {
16716
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16717
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16718
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16741,6 +16809,74 @@ var FECManager = class _FECManager {
16741
16809
  }
16742
16810
  return recovered;
16743
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
+ }
16744
16880
  /**
16745
16881
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16746
16882
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16943,8 +17079,75 @@ var FECManager = class _FECManager {
16943
17079
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16944
17080
  return null;
16945
17081
  }
17082
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16946
17083
  return recovered;
16947
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
+ }
16948
17151
  /**
16949
17152
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16950
17153
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17099,6 +17302,15 @@ var FECManager = class _FECManager {
17099
17302
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17100
17303
  return;
17101
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
+ }
17102
17314
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17103
17315
  }
17104
17316
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17107,6 +17319,7 @@ var FECManager = class _FECManager {
17107
17319
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17108
17320
  this.#blockSourceCounts.delete(key);
17109
17321
  this.#blockSourceEsis.delete(key);
17322
+ this.#blockRebuiltEsis.delete(key);
17110
17323
  this.#blockAuthoredLengths.delete(key);
17111
17324
  this.#blockRepairRsIds.delete(key);
17112
17325
  this.#blockRsbLengths.delete(key);
@@ -17173,7 +17386,16 @@ var FECManager = class _FECManager {
17173
17386
  this.#repairDeadlineMs(trackConfig)
17174
17387
  );
17175
17388
  }
17176
- 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
+ );
17177
17399
  }
17178
17400
  #failBlock(sbn, packetId, catalogTrackName) {
17179
17401
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17186,6 +17408,7 @@ var FECManager = class _FECManager {
17186
17408
  }
17187
17409
  this.#blockSourceCounts.delete(key);
17188
17410
  this.#blockSourceEsis.delete(key);
17411
+ this.#blockRebuiltEsis.delete(key);
17189
17412
  this.#blockAuthoredLengths.delete(key);
17190
17413
  this.#blockRepairRsIds.delete(key);
17191
17414
  this.#blockRsbLengths.delete(key);
@@ -17224,6 +17447,7 @@ var FECManager = class _FECManager {
17224
17447
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17225
17448
  this.#blockSourceCounts.delete(key);
17226
17449
  this.#blockSourceEsis.delete(key);
17450
+ this.#blockRebuiltEsis.delete(key);
17227
17451
  this.#blockAuthoredLengths.delete(key);
17228
17452
  this.#blockRepairRsIds.delete(key);
17229
17453
  this.#blockRsbLengths.delete(key);
@@ -17315,6 +17539,7 @@ var FECManager = class _FECManager {
17315
17539
  this.#blockKeysBySbn.clear();
17316
17540
  this.#blockSourceCounts.clear();
17317
17541
  this.#blockSourceEsis.clear();
17542
+ this.#blockRebuiltEsis.clear();
17318
17543
  this.#blockAuthoredLengths.clear();
17319
17544
  this.#blockRepairRsIds.clear();
17320
17545
  this.#blockRsbLengths.clear();
@@ -17340,6 +17565,7 @@ var FECManager = class _FECManager {
17340
17565
  this.#blockAnchorsMs.clear();
17341
17566
  this.#blockCatalogTrackNames.clear();
17342
17567
  this.#classifiers.clear();
17568
+ this.#noSpanRaps.clear();
17343
17569
  this.#streamGeometries.clear();
17344
17570
  this.#failedBlocks.clear();
17345
17571
  this.#resolvedBlockClasses.clear();
@@ -17870,9 +18096,21 @@ var IsoBmffTimingResolver = class {
17870
18096
  }
17871
18097
  #track = null;
17872
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();
17873
18107
  #pendingSamples = 0;
17874
18108
  #evictedSamples = 0;
17875
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
+ }
17876
18114
  get pendingSampleCount() {
17877
18115
  return this.#pendingSamples;
17878
18116
  }
@@ -17896,7 +18134,14 @@ var IsoBmffTimingResolver = class {
17896
18134
  this.#track = timing;
17897
18135
  return true;
17898
18136
  }
17899
- 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) {
17900
18145
  this.#lastMovieFragmentRejection = null;
17901
18146
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17902
18147
  this.#lastMovieFragmentRejection = "invalid";
@@ -17922,6 +18167,8 @@ var IsoBmffTimingResolver = class {
17922
18167
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17923
18168
  );
17924
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;
17925
18172
  let freed = 0;
17926
18173
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17927
18174
  freed += this.#evictOldestFragment();
@@ -17935,6 +18182,7 @@ var IsoBmffTimingResolver = class {
17935
18182
  return false;
17936
18183
  }
17937
18184
  this.#fragments.set(key, [...parsed.samples]);
18185
+ this.#firstSamples.set(key, parsed.samples[0]);
17938
18186
  this.#pendingSamples += parsed.samples.length;
17939
18187
  return true;
17940
18188
  }
@@ -17992,6 +18240,7 @@ var IsoBmffTimingResolver = class {
17992
18240
  let live = 0;
17993
18241
  for (const sample of samples) if (sample !== null) live++;
17994
18242
  this.#fragments.delete(oldest);
18243
+ this.#firstSamples.delete(oldest);
17995
18244
  this.#pendingSamples -= live;
17996
18245
  this.#evictedSamples += live;
17997
18246
  return live;
@@ -18007,12 +18256,23 @@ var IsoBmffTimingResolver = class {
18007
18256
  if (!samples || !samples[index]) return false;
18008
18257
  samples[index] = null;
18009
18258
  this.#pendingSamples--;
18010
- 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
+ }
18011
18269
  return true;
18012
18270
  }
18013
18271
  reset() {
18014
18272
  this.#track = null;
18015
18273
  this.#fragments.clear();
18274
+ this.#firstSamples.clear();
18275
+ this.#delivered.clear();
18016
18276
  this.#pendingSamples = 0;
18017
18277
  }
18018
18278
  };
@@ -18182,6 +18442,9 @@ function authoredGap(before, after) {
18182
18442
  function ssIdAfter(a, b) {
18183
18443
  return (a - b | 0) > 0;
18184
18444
  }
18445
+ function holdsRebuilt(stream) {
18446
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18447
+ }
18185
18448
  var FecReleaseSequencer = class {
18186
18449
  #streams = /* @__PURE__ */ new Map();
18187
18450
  #gate;
@@ -18244,6 +18507,7 @@ var FecReleaseSequencer = class {
18244
18507
  stream.lastSbn = void 0;
18245
18508
  stream.discontinuity = false;
18246
18509
  stream.rapSsId = void 0;
18510
+ stream.sourced = false;
18247
18511
  }
18248
18512
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18249
18513
  this.#recordStat("droppedStale");
@@ -18270,6 +18534,7 @@ var FecReleaseSequencer = class {
18270
18534
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18271
18535
  else hi = mid;
18272
18536
  }
18537
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18273
18538
  stream.buf.splice(lo, 0, frame);
18274
18539
  this.#drainUnlessDeferred(stream);
18275
18540
  }
@@ -18305,6 +18570,38 @@ var FecReleaseSequencer = class {
18305
18570
  }
18306
18571
  return false;
18307
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
+ }
18308
18605
  /** Re-evaluate held gaps after FEC state changed. */
18309
18606
  poke(streamKey) {
18310
18607
  if (streamKey !== void 0) {
@@ -18396,16 +18693,32 @@ var FecReleaseSequencer = class {
18396
18693
  last: null,
18397
18694
  lastSbn: void 0,
18398
18695
  discontinuity: false,
18399
- rapSsId: void 0
18696
+ rapSsId: void 0,
18697
+ sourced: false,
18698
+ draining: 0
18400
18699
  };
18401
18700
  this.#streams.set(streamKey, stream);
18402
18701
  }
18403
18702
  return stream;
18404
18703
  }
18405
- #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) {
18406
18714
  while (stream.buf.length > 0 && stream.last !== null) {
18407
18715
  const head = stream.buf[0];
18408
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
+ }
18409
18722
  if (stream.discontinuity) {
18410
18723
  stream.discontinuity = false;
18411
18724
  stream.buf.shift();
@@ -18419,9 +18732,10 @@ var FecReleaseSequencer = class {
18419
18732
  }
18420
18733
  const headSbn = head.meta.fecSbn;
18421
18734
  const from = stream.lastSbn ?? headSbn;
18422
- const to = headSbn ?? stream.lastSbn;
18735
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18423
18736
  let lost = [];
18424
18737
  const unanchored = from === void 0 && to === void 0;
18738
+ let unresolved = false;
18425
18739
  if (from !== void 0 && to !== void 0) {
18426
18740
  const resolved = this.#gate.resolvedLoss(
18427
18741
  stream.packetId,
@@ -18430,7 +18744,8 @@ var FecReleaseSequencer = class {
18430
18744
  Math.max(from, to),
18431
18745
  from
18432
18746
  );
18433
- if (resolved === null) {
18747
+ unresolved = resolved === null;
18748
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18434
18749
  const heldMs = performance.now() - head.arrivedAtMs;
18435
18750
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18436
18751
  this.#recordStat("forcedWindowFlushes");
@@ -18438,22 +18753,56 @@ var FecReleaseSequencer = class {
18438
18753
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18439
18754
  continue;
18440
18755
  }
18441
- lost = resolved;
18756
+ lost = resolved ?? [];
18442
18757
  }
18443
18758
  stream.buf.shift();
18444
18759
  this.#release(
18445
18760
  stream,
18446
18761
  head,
18447
- 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
18448
18763
  );
18449
18764
  }
18450
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
+ }
18451
18795
  #release(stream, frame, decision) {
18796
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18797
+ if (stale) this.#recordStat("droppedStale");
18452
18798
  if (decision) frame.meta.gapBefore = decision;
18453
18799
  stream.last = frame;
18454
- 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;
18455
18804
  this.#gate.onReleased?.(frame.data, frame.meta);
18456
- this.#emit(frame.data, frame.meta);
18805
+ if (!stale) this.#emit(frame.data, frame.meta);
18457
18806
  }
18458
18807
  };
18459
18808
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18494,6 +18843,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18494
18843
  this.#reclaimedTimingSamples = 0;
18495
18844
  this.#evictedTimingSamples = 0;
18496
18845
  this.#rejectedMovieFragments = 0;
18846
+ this.#recoveredHeadsAwaitingTiming = {
18847
+ held: 0,
18848
+ expired: 0,
18849
+ discarded: 0,
18850
+ superseded: 0
18851
+ };
18497
18852
  this.#sequencers = /* @__PURE__ */ new Map();
18498
18853
  this.#fecGate = null;
18499
18854
  this.#fecSequencer = null;
@@ -18602,6 +18957,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18602
18957
  #reclaimedTimingSamples;
18603
18958
  #evictedTimingSamples;
18604
18959
  #rejectedMovieFragments;
18960
+ #recoveredHeadsAwaitingTiming;
18605
18961
  #sequencers;
18606
18962
  #fecGate;
18607
18963
  #fecSequencer;
@@ -18707,11 +19063,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18707
19063
  /**
18708
19064
  * Typed view of the ISO-BMFF timing-sample budget.
18709
19065
  *
18710
- * This is the acceptance signal for the budget ratchet: `pending` walking
18711
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18712
- * and it is observable here rather than only as media silently stopping.
18713
- * Per repo policy this is a producer-owned counter -- never infer budget
18714
- * 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.
18715
19072
  */
18716
19073
  get timingBudgetStats() {
18717
19074
  return {
@@ -18722,6 +19079,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18722
19079
  rejectedMovieFragments: this.#rejectedMovieFragments
18723
19080
  };
18724
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
+ }
18725
19090
  get reorderStats() {
18726
19091
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18727
19092
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18763,6 +19128,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18763
19128
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18764
19129
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18765
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
+ }
18766
19139
  /** Re-evaluate held gaps after FEC block state changed. */
18767
19140
  pokeFecRelease(packetId, catalogTrackName) {
18768
19141
  if (!this.#fecSequencer) return;
@@ -18850,10 +19223,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18850
19223
  get reassemblerDataUnitsReturned() {
18851
19224
  return this.#reassemblerDataUnitsReturned;
18852
19225
  }
18853
- #countOutcome(token) {
18854
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18855
- this.#feedOutcomes.set(token, count);
18856
- this.#lastOutcome = token;
19226
+ #countOutcome(token2) {
19227
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19228
+ this.#feedOutcomes.set(token2, count);
19229
+ this.#lastOutcome = token2;
18857
19230
  this.#lastOutcomeCount = count;
18858
19231
  }
18859
19232
  #countDeferredOutcome(reason) {
@@ -18895,7 +19268,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18895
19268
  const now = Date.now();
18896
19269
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18897
19270
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18898
- awaitingTiming.shift();
19271
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18899
19272
  }
18900
19273
  }
18901
19274
  }
@@ -19013,11 +19386,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19013
19386
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19014
19387
  return this.#drop("fragment-unexpected-fragmentation");
19015
19388
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19016
- if (!resolver2) return this.#drop("fragment-no-resolver");
19389
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19017
19390
  const pendingBefore = resolver2.pendingSampleCount;
19018
19391
  const evictedBefore = resolver2.evictedSampleCount;
19019
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19020
- 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
+ );
19021
19399
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19022
19400
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19023
19401
  if (!admitted) {
@@ -19037,7 +19415,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19037
19415
  };
19038
19416
  }
19039
19417
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19040
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19418
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19041
19419
  if (!resolver) return this.#drop("mfu-no-resolver");
19042
19420
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19043
19421
  return this.#drop("mfu-empty-payload");
@@ -19058,6 +19436,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19058
19436
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19059
19437
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19060
19438
  sampleNumber: mfuDuHeader.sampleNumber,
19439
+ sequenceNumber: info.sequenceNumber,
19061
19440
  arrivedAt: Date.now()
19062
19441
  });
19063
19442
  return this.#drop("mfu-sample-timing-missing");
@@ -19083,7 +19462,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19083
19462
  info,
19084
19463
  resolver,
19085
19464
  options?.recovered === true,
19086
- options?.fecSbn
19465
+ options?.fecSbn,
19466
+ options?.fecSbnFirst
19087
19467
  );
19088
19468
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19089
19469
  }
@@ -19176,7 +19556,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19176
19556
  info,
19177
19557
  resolver,
19178
19558
  options?.recovered === true,
19179
- options?.fecSbn
19559
+ options?.fecSbn,
19560
+ options?.fecSbnFirst
19180
19561
  );
19181
19562
  if (outcome.record !== void 0) return outcome.record;
19182
19563
  return this.#drop(outcome.reason);
@@ -19196,7 +19577,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19196
19577
  * attribution exists to keep readable. Leaving the choice at the call site
19197
19578
  * is what keeps both true.
19198
19579
  */
19199
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19580
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19200
19581
  const timing = resolver.sampleTiming(
19201
19582
  combined.mpuSequenceNumber,
19202
19583
  combined.movieFragmentSequenceNumber,
@@ -19213,6 +19594,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19213
19594
  );
19214
19595
  if (!meta) return { reason: "combined-meta-build-failed" };
19215
19596
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19597
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19216
19598
  if (!resolver.consumeSample(
19217
19599
  combined.mpuSequenceNumber,
19218
19600
  combined.movieFragmentSequenceNumber,
@@ -19381,6 +19763,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19381
19763
  }
19382
19764
  this.#emitFrame(data, meta);
19383
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
+ }
19384
19770
  /**
19385
19771
  * Claim and return the authored timing for a recovered sample. Recovery does
19386
19772
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19442,6 +19828,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19442
19828
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19443
19829
  this.#sequencers.clear();
19444
19830
  this.#fecSequencer?.discard();
19831
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19832
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19833
+ }
19445
19834
  this.#timingStreams.clear();
19446
19835
  this.#pendingTimingSamples = 0;
19447
19836
  this.#reliableMoqStartupGate.clear();
@@ -19460,10 +19849,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19460
19849
  return resolver;
19461
19850
  }
19462
19851
  #awaitTiming(streamKey, head) {
19463
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19464
- 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;
19465
19860
  awaitingTiming.push(head);
19466
- 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]++;
19467
19869
  }
19468
19870
  /**
19469
19871
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19491,6 +19893,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19491
19893
  const state = this.#timingStreams.get(streamKey);
19492
19894
  if (!state) return;
19493
19895
  this.#timingStreams.delete(streamKey);
19896
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19494
19897
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19495
19898
  if (this.#pendingTimingSamples < 0) {
19496
19899
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22697,7 +23100,18 @@ function keyframeOverlaySsId(header) {
22697
23100
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22698
23101
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22699
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;
22700
23113
  var OVERLAY_OFFSET_MPUS = 16;
23114
+ var OVERLAY_REBUILT_LIMIT = 256;
22701
23115
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22702
23116
  function assertStreamIdentity(role, stream) {
22703
23117
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -22808,6 +23222,7 @@ var MmtpRouter = class {
22808
23222
  #provenance = /* @__PURE__ */ new Map();
22809
23223
  #onFlowProtected = null;
22810
23224
  #unadmittedListedDrops = 0;
23225
+ #admittedSourceDatagrams = 0;
22811
23226
  #unlistedRepairSetAside = 0;
22812
23227
  #assistBlocks = /* @__PURE__ */ new Map();
22813
23228
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22864,6 +23279,18 @@ var MmtpRouter = class {
22864
23279
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22865
23280
  */
22866
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();
22867
23294
  /**
22868
23295
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22869
23296
  * not parsed, typically because its overlap copy was parsed first
@@ -22925,7 +23352,9 @@ var MmtpRouter = class {
22925
23352
  /** Installed overlay instances, by overlay repair PID (opt-in only). */
22926
23353
  #overlays = /* @__PURE__ */ new Map();
22927
23354
  #overlayRecoveredObservers = /* @__PURE__ */ new Set();
22928
- /** 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. */
22929
23358
  #restampedStreams = /* @__PURE__ */ new Set();
22930
23359
  #overlayStats = {
22931
23360
  repairDropped: 0,
@@ -22933,7 +23362,9 @@ var MmtpRouter = class {
22933
23362
  recovered: 0,
22934
23363
  restarts: 0,
22935
23364
  unsequencedDropped: 0,
22936
- protectedDropped: 0
23365
+ protectedDropped: 0,
23366
+ baseFed: 0,
23367
+ baseUndetermined: 0
22937
23368
  };
22938
23369
  /** Callback fired when a delta-track FEC block completes or fails. */
22939
23370
  #onDeltaBlockCompleteCb = null;
@@ -23106,7 +23537,7 @@ var MmtpRouter = class {
23106
23537
  const cb = this.#getFrameCallback(packetId);
23107
23538
  if (isVideo && !assembler) {
23108
23539
  if (frameMeta.recovered) {
23109
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23540
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23110
23541
  } else {
23111
23542
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23112
23543
  }
@@ -23190,8 +23621,9 @@ var MmtpRouter = class {
23190
23621
  }
23191
23622
  /**
23192
23623
  * FEC-recovered source symbols whose reconstructed header named a
23193
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23194
- * 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.
23195
23627
  */
23196
23628
  get restampedRecoveredSequenceCount() {
23197
23629
  return this.#restampedRecoveredSeqs;
@@ -23370,6 +23802,7 @@ var MmtpRouter = class {
23370
23802
  enableMseAssembler(packetId, config, catalogTrackName) {
23371
23803
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23372
23804
  const previous = this.#mseAssemblers.get(assemblerKey);
23805
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23373
23806
  if (previous) {
23374
23807
  previous.flush();
23375
23808
  previous.dispose();
@@ -23496,6 +23929,10 @@ var MmtpRouter = class {
23496
23929
  get keyframeOverlayStats() {
23497
23930
  return { ...this.#overlayStats };
23498
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
+ }
23499
23936
  #isOverlayRepair(repairPacketId, repairTrackName) {
23500
23937
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23501
23938
  }
@@ -23516,7 +23953,8 @@ var MmtpRouter = class {
23516
23953
  fec,
23517
23954
  k,
23518
23955
  offsets: /* @__PURE__ */ new Map(),
23519
- moqBlocks: /* @__PURE__ */ new Set()
23956
+ moqBlocks: /* @__PURE__ */ new Set(),
23957
+ rebuilt: /* @__PURE__ */ new Map()
23520
23958
  };
23521
23959
  this.#overlays.set(repairPacketId, inst);
23522
23960
  return () => {
@@ -23546,12 +23984,18 @@ var MmtpRouter = class {
23546
23984
  */
23547
23985
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23548
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
+ }
23549
23992
  }
23550
23993
  #resetOverlay(inst) {
23551
23994
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23552
23995
  inst.last = void 0;
23553
23996
  inst.offsets.clear();
23554
23997
  inst.moqBlocks.clear();
23998
+ inst.rebuilt.clear();
23555
23999
  }
23556
24000
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23557
24001
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23602,18 +24046,32 @@ var MmtpRouter = class {
23602
24046
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23603
24047
  * every overlay installed for its source, at that SS_ID, with the bytes the
23604
24048
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23605
- * 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.
23606
24057
  */
23607
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24058
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23608
24059
  if (this.#overlays.size === 0) return;
23609
24060
  const ssId = keyframeOverlaySsId(header);
23610
24061
  if (ssId === void 0) return;
24062
+ let rebuiltPsn;
23611
24063
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23612
24064
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24065
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24066
+ inst.rebuilt.delete(ssId);
23613
24067
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23614
24068
  if (identity !== null) {
23615
24069
  inst.offsets.delete(identity.mpuSequenceNumber);
23616
- 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
+ });
23617
24075
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23618
24076
  }
23619
24077
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23635,6 +24093,15 @@ var MmtpRouter = class {
23635
24093
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23636
24094
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23637
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
+ }
23638
24105
  }
23639
24106
  #noteOverlayMoqBlock(inst, blockStart) {
23640
24107
  inst.moqBlocks.add(blockStart);
@@ -23668,11 +24135,16 @@ var MmtpRouter = class {
23668
24135
  }
23669
24136
  /**
23670
24137
  * Route an overlay-recovered block's packets as source media of the
23671
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23672
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23673
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23674
- * from its keyframe's own offset; without one, a stream the base never
23675
- * 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.
23676
24148
  */
23677
24149
  #routeOverlayRecovered(inst, blockStart, recovered) {
23678
24150
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23692,22 +24164,136 @@ var MmtpRouter = class {
23692
24164
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23693
24165
  const restamped = this.#restampedStreams.has(streamKey);
23694
24166
  const restored = /* @__PURE__ */ new Set();
24167
+ const baseSsIds = /* @__PURE__ */ new Map();
23695
24168
  const symbols = result.symbols.flatMap((sym) => {
23696
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23697
- if (offset !== void 0) {
23698
- 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;
23699
24173
  restored.add(packetSequenceNumber);
23700
- 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];
23701
24196
  }
23702
- if (!restamped) return [sym];
23703
24197
  this.#overlayStats.unsequencedDropped++;
23704
24198
  return [];
23705
24199
  });
23706
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
+ }
23707
24212
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23708
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24213
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24214
+ baseSsIds,
24215
+ firstSsIds
23709
24216
  });
23710
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
+ }
23711
24297
  }
23712
24298
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23713
24299
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23847,6 +24433,10 @@ var MmtpRouter = class {
23847
24433
  get unadmittedListedDropCount() {
23848
24434
  return this.#unadmittedListedDrops;
23849
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
+ }
23850
24440
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23851
24441
  get unlistedRepairSetAsideCount() {
23852
24442
  return this.#unlistedRepairSetAside;
@@ -24333,6 +24923,8 @@ var MmtpRouter = class {
24333
24923
  )) return;
24334
24924
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24335
24925
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24926
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24927
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24336
24928
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24337
24929
  const recovered = fec.feedRepairSymbol(
24338
24930
  ownerPacketId,
@@ -24470,6 +25062,7 @@ var MmtpRouter = class {
24470
25062
  this.#droppedPreSyncVideo.delete(streamKey);
24471
25063
  this.#latestVideoSeqByStream.delete(streamKey);
24472
25064
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25065
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24473
25066
  this.#multicastRapTwinByStream.delete(streamKey);
24474
25067
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24475
25068
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24596,6 +25189,12 @@ var MmtpRouter = class {
24596
25189
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24597
25190
  * own RAP cadence, so no timer is involved.
24598
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
+ *
24599
25198
  * The commit is one synchronous transition inside the frame path: the
24600
25199
  * outgoing stream's already-admitted frames are released (its queued FEC
24601
25200
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24646,6 +25245,9 @@ var MmtpRouter = class {
24646
25245
  droppedIncomingFrames: 0
24647
25246
  };
24648
25247
  this.#staged = state;
25248
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25249
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25250
+ }
24649
25251
  return {
24650
25252
  cancel: () => {
24651
25253
  if (this.#staged !== state) return false;
@@ -24728,7 +25330,17 @@ var MmtpRouter = class {
24728
25330
  */
24729
25331
  #routeParsedFrame(data, meta) {
24730
25332
  if (this.#classifyPacketId(meta.packetId) === null) return;
24731
- 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
+ }
24732
25344
  this.#deliverParsedFrame(data, meta);
24733
25345
  }
24734
25346
  #commitStreamSwitch(state, rap, reason) {
@@ -24880,12 +25492,16 @@ var MmtpRouter = class {
24880
25492
  this.#parser.deferFecRelease(work);
24881
25493
  } finally {
24882
25494
  this.#fecPokeDepth--;
24883
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
24884
- const pokes = [...this.#pendingFecPokes.values()];
24885
- this.#pendingFecPokes.clear();
24886
- for (const poke of pokes) {
24887
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24888
- }
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);
24889
25505
  }
24890
25506
  }
24891
25507
  }
@@ -24963,6 +25579,14 @@ var MmtpRouter = class {
24963
25579
  onRap: (packetId, catalogTrackName, ssId) => {
24964
25580
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
24965
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
+ },
24966
25590
  onReleased: (data, meta) => {
24967
25591
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
24968
25592
  const timing = meta.authoredTiming;
@@ -25104,6 +25728,7 @@ var MmtpRouter = class {
25104
25728
  const parsed = this.#parser.parsePacket(packet);
25105
25729
  if (!parsed) return;
25106
25730
  const info = parsed.info;
25731
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25107
25732
  if (source === "multicast" && !manifestAdmitted) {
25108
25733
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25109
25734
  }
@@ -25385,7 +26010,8 @@ var MmtpRouter = class {
25385
26010
  parsed.header,
25386
26011
  info.sequenceNumber,
25387
26012
  info.rapFlag,
25388
- source
26013
+ source,
26014
+ fecPayloadId?.ssId
25389
26015
  );
25390
26016
  }
25391
26017
  }
@@ -25411,7 +26037,8 @@ var MmtpRouter = class {
25411
26037
  source: parserOptions?.source,
25412
26038
  deliveryProvenance: parserOptions?.deliveryProvenance,
25413
26039
  catalogTrackName: parserOptions?.catalogTrackName,
25414
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26040
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26041
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25415
26042
  };
25416
26043
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25417
26044
  else this.#parser.feed(packet, feedOptions);
@@ -25535,7 +26162,8 @@ var MmtpRouter = class {
25535
26162
  if (stream === void 0) {
25536
26163
  stream = {
25537
26164
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25538
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26165
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26166
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25539
26167
  };
25540
26168
  byName.set(catalogTrackName, stream);
25541
26169
  }
@@ -25650,8 +26278,16 @@ var MmtpRouter = class {
25650
26278
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25651
26279
  this.#emitDirectVideo(packetId, data, meta, cb);
25652
26280
  }
25653
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26281
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25654
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
+ }
25655
26291
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25656
26292
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25657
26293
  this.#drainDirectRecoveredVideo(
@@ -25805,6 +26441,13 @@ var MmtpRouter = class {
25805
26441
  * frames by epoch only if pre-protection input ever matters.
25806
26442
  */
25807
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
+ }
25808
26451
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25809
26452
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25810
26453
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25861,7 +26504,12 @@ var MmtpRouter = class {
25861
26504
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25862
26505
  if (ownerPacketId == null) return;
25863
26506
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25864
- 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
+ };
25865
26513
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25866
26514
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25867
26515
  if (ssIdSeqOffset !== void 0) {
@@ -25869,7 +26517,10 @@ var MmtpRouter = class {
25869
26517
  ...result,
25870
26518
  symbols: result.symbols.map((sym) => {
25871
26519
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25872
- if (wire === sym.packetSequenceNumber) return sym;
26520
+ if (wire === sym.packetSequenceNumber) {
26521
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26522
+ return sym;
26523
+ }
25873
26524
  this.#restampedRecoveredSeqs++;
25874
26525
  this.#restampedStreams.add(streamKey);
25875
26526
  return { ...sym, packetSequenceNumber: wire };
@@ -25905,6 +26556,29 @@ var MmtpRouter = class {
25905
26556
  }
25906
26557
  );
25907
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)) };
25908
26582
  const gated = this.#isFecTrack(ownerPacketId);
25909
26583
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25910
26584
  if (assembler) {
@@ -29666,7 +30340,7 @@ async function setupFecWorkerForTrack(opts) {
29666
30340
  blockProfile,
29667
30341
  k: opts.fecK,
29668
30342
  // P as this receiver joined it, not the catalog total: the worker snapshot
29669
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
30343
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29670
30344
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29671
30345
  symbolSize: opts.fecSymbolSize,
29672
30346
  interleaveDepth: interleaveDepthBlocks,
@@ -30208,24 +30882,24 @@ var MediaRouter = class {
30208
30882
  if (!this.#multicastVideoKeyed) return;
30209
30883
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30210
30884
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30211
- const token = ++this.#sourceSwitchToken;
30885
+ const token2 = ++this.#sourceSwitchToken;
30212
30886
  const maybeReady = this.#onSourceSwitch?.("multicast");
30213
30887
  if (!maybeReady || typeof maybeReady.then !== "function") {
30214
- this.#commitMulticastActivation(token);
30888
+ this.#commitMulticastActivation(token2);
30215
30889
  return;
30216
30890
  }
30217
30891
  const ready = Promise.resolve(maybeReady);
30218
- this.#pendingMulticastToken = token;
30892
+ this.#pendingMulticastToken = token2;
30219
30893
  this.#sourceSwitchReady = ready;
30220
30894
  void ready.then(
30221
- () => this.#commitMulticastActivation(token),
30222
- (err) => this.#rejectMulticastActivation(token, err)
30895
+ () => this.#commitMulticastActivation(token2),
30896
+ (err) => this.#rejectMulticastActivation(token2, err)
30223
30897
  ).finally(() => {
30224
30898
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30225
30899
  });
30226
30900
  }
30227
- #commitMulticastActivation(token) {
30228
- if (token !== this.#sourceSwitchToken) return;
30901
+ #commitMulticastActivation(token2) {
30902
+ if (token2 !== this.#sourceSwitchToken) return;
30229
30903
  this.#active = "multicast";
30230
30904
  this.#suppressed = true;
30231
30905
  this.#pendingMulticastToken = null;
@@ -30236,9 +30910,9 @@ var MediaRouter = class {
30236
30910
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30237
30911
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30238
30912
  }
30239
- #rejectMulticastActivation(token, err) {
30240
- if (token !== this.#sourceSwitchToken) return;
30241
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30913
+ #rejectMulticastActivation(token2, err) {
30914
+ if (token2 !== this.#sourceSwitchToken) return;
30915
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30242
30916
  this.#pendingMulticastVideo = null;
30243
30917
  this.#pendingMulticastAudio = null;
30244
30918
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31207,63 +31881,65 @@ var FecStarvationDetector = class {
31207
31881
 
31208
31882
  // src/cmcd-producer.ts
31209
31883
  var PLAY_MODES = ["full", "base-only", "single-track"];
31210
- function sanitize(value) {
31211
- return value.replace(/[,\r\n]/g, "");
31212
- }
31213
- function buildFecParams(stats) {
31214
- const params = {};
31215
- const cfg = stats.config;
31216
- params["x-bc-fec-algo"] = cfg.algorithm;
31217
- params["x-bc-fec-k"] = cfg.k;
31218
- params["x-bc-fec-p"] = cfg.repairCount;
31219
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
31220
- return params;
31221
- }
31222
- function buildTransportParams(snap) {
31223
- if (!snap) return {};
31224
- const params = {};
31225
- params["x-bc-transport"] = snap.transport;
31226
- if (snap.multicastGroup != null) {
31227
- params["x-bc-multicast-group"] = snap.multicastGroup;
31228
- }
31229
- if (snap.multicastSource != null) {
31230
- params["x-bc-multicast-source"] = snap.multicastSource;
31231
- }
31232
- if (snap.amtRelay != null) {
31233
- params["x-bc-amt-relay"] = snap.amtRelay;
31234
- }
31235
- return params;
31236
- }
31237
- function formatQueryString(params) {
31238
- return Object.keys(params).sort().map((key) => {
31239
- const v = params[key];
31240
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
31241
- return `${key}=${v}`;
31884
+ var DEFAULT_INTERVAL_MS = 3e4;
31885
+ var CUSTOM_PREFIX = "com.blockcast-";
31886
+ var MAX_CUSTOM_VALUE = 64;
31887
+ var TRANSPORT_TOKEN = {
31888
+ native: "ssm",
31889
+ amt: "amt",
31890
+ moq: "moq"
31891
+ };
31892
+ function sfString(value) {
31893
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31894
+ }
31895
+ function encodeCmcdRecord(record) {
31896
+ return Object.keys(record).sort().map((key) => {
31897
+ const value = record[key];
31898
+ if (typeof value === "number") return `${key}=${value}`;
31899
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31900
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31901
+ return `${key}=${value.token}`;
31242
31902
  }).join(",");
31243
31903
  }
31904
+ var token = (value) => ({ token: value });
31905
+ function putCustom(record, name, value) {
31906
+ if (value === void 0) return;
31907
+ if (typeof value === "number") value = String(value);
31908
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31909
+ record[CUSTOM_PREFIX + name] = value;
31910
+ }
31244
31911
  var CmcdProducer = class {
31245
- #onCmcdParams;
31246
31912
  #onCmcdPost;
31913
+ #intervalMs;
31914
+ #sessionId;
31915
+ #now;
31916
+ #sn = 0;
31917
+ #lastHeartbeat = null;
31247
31918
  #transport;
31248
31919
  #lastStats;
31249
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31250
31920
  #playMode = null;
31251
31921
  #deltaFailRate = null;
31252
31922
  #switchMiscount = 0;
31253
31923
  #manifestStats = null;
31254
31924
  #fecFailClosed = null;
31925
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31926
+ #lifecycle = null;
31255
31927
  constructor(options) {
31256
- this.#onCmcdParams = options.onCmcdParams;
31257
31928
  this.#onCmcdPost = options.onCmcdPost;
31929
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31930
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31931
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31932
+ }
31933
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31934
+ this.#now = options.now ?? Date.now;
31258
31935
  }
31259
31936
  /** Update the stored transport state. Called by player wiring layer. */
31260
31937
  updateTransport(snapshot) {
31261
31938
  this.#transport = snapshot;
31262
31939
  }
31263
31940
  /**
31264
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
31265
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
31266
- * (CLAUDE.md: error on bad init).
31941
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31942
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31267
31943
  */
31268
31944
  updatePlayMode(mode) {
31269
31945
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31274,9 +31950,8 @@ var CmcdProducer = class {
31274
31950
  this.#playMode = mode;
31275
31951
  }
31276
31952
  /**
31277
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
31278
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
31279
- * Omitted on single-track streams.
31953
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31954
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31280
31955
  */
31281
31956
  updateDeltaFailRate(rate) {
31282
31957
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31286,112 +31961,139 @@ var CmcdProducer = class {
31286
31961
  }
31287
31962
  this.#deltaFailRate = rate;
31288
31963
  }
31289
- /**
31290
- * Bump the cumulative switching-set misalignment counter. Emitted as
31291
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31292
- */
31964
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31293
31965
  incrementSwitchMiscount() {
31294
31966
  this.#switchMiscount += 1;
31295
31967
  }
31296
31968
  /**
31297
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
31298
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
31299
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
31300
- * stream playing without FEC is told apart from one with no loss.
31969
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31970
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31971
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31972
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31973
+ * with no loss.
31301
31974
  */
31302
31975
  updateFecFailClosed(reason) {
31976
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31303
31977
  this.#fecFailClosed = reason;
31978
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31304
31979
  }
31305
31980
  updateManifestVerification(stats) {
31306
31981
  this.#manifestStats = stats;
31307
31982
  }
31308
31983
  /**
31309
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
31310
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
31311
- * include the drift key until the next emitSwitchDrift(). Mirrors the
31312
- * existing onTransition() pattern.
31984
+ * Count a video block's resolution from the repair lifecycle: recovered in
31985
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31986
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31987
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31313
31988
  */
31989
+ onRepairLifecycle(event) {
31990
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31991
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31992
+ if (event.via === "inband") this.#lifecycle.inband++;
31993
+ else this.#lifecycle.fetch++;
31994
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31995
+ this.#lifecycle.failedKeyframe++;
31996
+ }
31997
+ }
31998
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31314
31999
  emitSwitchDrift(driftUs) {
31315
32000
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31316
32001
  throw new Error(
31317
32002
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31318
32003
  );
31319
32004
  }
31320
- const params = {};
31321
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
31322
- Object.assign(params, buildTransportParams(this.#transport));
31323
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31324
- params["x-bc-switch-drift-us"] = driftUs;
31325
- this.#emit(params);
32005
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
32006
+ putCustom(record, "sdu", Math.round(driftUs));
32007
+ this.#post([record]);
31326
32008
  }
31327
- /**
31328
- * Process a FecTrackStats snapshot. Finds the video track, builds
31329
- * x-bc-* vendor keys, and fires both delivery callbacks.
31330
- */
32009
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31331
32010
  onSnapshot(data) {
31332
32011
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31333
32012
  if (!videoTrack) return;
31334
32013
  this.#lastStats = videoTrack;
31335
- const params = {
31336
- ...buildFecParams(videoTrack),
31337
- ...buildTransportParams(this.#transport)
31338
- };
31339
- if (this.#playMode !== null) {
31340
- params["x-bc-play-mode"] = this.#playMode;
31341
- }
31342
- if (this.#fecFailClosed) {
31343
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31344
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31345
- }
31346
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31347
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31348
- }
31349
- if (this.#switchMiscount > 0) {
31350
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31351
- }
31352
- if (this.#manifestStats) {
31353
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31354
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31355
- }
31356
- this.#emit(params);
32014
+ this.#heartbeat();
31357
32015
  }
31358
32016
  /**
31359
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31360
- * the caller because only the transport integration knows when state changed.
32017
+ * Take transport state for receivers that do not own FEC stats, and send the
32018
+ * heartbeat when due.
31361
32019
  */
31362
32020
  onTransportSnapshot(data) {
31363
32021
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31364
32022
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31365
32023
  }
31366
32024
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31367
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31368
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
32025
+ if (data.switchMiscount !== void 0) {
32026
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
32027
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
32028
+ }
32029
+ this.#switchMiscount = data.switchMiscount;
31369
32030
  }
31370
32031
  this.#transport = data.transport;
31371
- const params = buildTransportParams(data.transport);
31372
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31373
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31374
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31375
- this.#emit(params);
32032
+ this.#heartbeat();
31376
32033
  }
31377
- /**
31378
- * Signal a transport transition event. Fires an immediate out-of-band
31379
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31380
- * x-bc-ct (one-shot).
31381
- */
32034
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31382
32035
  onTransition(event) {
31383
- const params = {};
31384
- if (this.#lastStats) {
31385
- Object.assign(params, buildFecParams(this.#lastStats));
32036
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
32037
+ putCustom(record, "ct", token(event));
32038
+ this.#putTransport(record);
32039
+ this.#post([record]);
32040
+ }
32041
+ #base(event) {
32042
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
32043
+ }
32044
+ #heartbeat() {
32045
+ const now = this.#now();
32046
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
32047
+ this.#lastHeartbeat = now;
32048
+ this.#post([this.#base("t"), this.#counters()]);
32049
+ }
32050
+ #putTransport(record) {
32051
+ const snap = this.#transport;
32052
+ if (!snap) return;
32053
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
32054
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
32055
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
32056
+ }
32057
+ putCustom(record, "ar", snap.amtRelay);
32058
+ }
32059
+ /** The `e=ce` counter report. */
32060
+ #counters() {
32061
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
32062
+ const stats = this.#lastStats;
32063
+ if (stats) {
32064
+ putCustom(record, "fa", stats.config.algorithm);
32065
+ putCustom(record, "fk", stats.config.k);
32066
+ putCustom(record, "fp", stats.config.repairCount);
32067
+ putCustom(record, "pr", stats.sourceSymbols);
32068
+ putCustom(record, "pl", stats.sourcePacketsMissing);
32069
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
32070
+ putCustom(record, "fbf", stats.blocksFailed);
32071
+ }
32072
+ if (this.#lifecycle) {
32073
+ putCustom(record, "fbr", this.#lifecycle.inband);
32074
+ putCustom(record, "fbq", this.#lifecycle.fetch);
32075
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
32076
+ }
32077
+ this.#putTransport(record);
32078
+ if (this.#playMode !== null) {
32079
+ putCustom(record, "pm", token(this.#playMode));
32080
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
32081
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
32082
+ }
32083
+ }
32084
+ if (this.#fecFailClosed) {
32085
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
32086
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31386
32087
  }
31387
- Object.assign(params, buildTransportParams(this.#transport));
31388
- params["x-bc-ct"] = event;
31389
- this.#emit(params);
32088
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
32089
+ if (this.#manifestStats) {
32090
+ putCustom(record, "mv", this.#manifestStats.verified);
32091
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
32092
+ }
32093
+ return record;
31390
32094
  }
31391
- /** Fire both delivery callbacks with the given params. */
31392
- #emit(params) {
31393
- this.#onCmcdParams?.(params);
31394
- this.#onCmcdPost?.(formatQueryString(params));
32095
+ #post(records) {
32096
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31395
32097
  }
31396
32098
  };
31397
32099
 
@@ -31487,6 +32189,19 @@ function resolveRepairLayers(catalog, source) {
31487
32189
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31488
32190
  };
31489
32191
  }
32192
+ function valueAt(catalog, pointer) {
32193
+ let node = catalog;
32194
+ for (const segment of pointer.split("/").slice(1)) {
32195
+ if (typeof node !== "object" || node === null) return void 0;
32196
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
32197
+ }
32198
+ return node;
32199
+ }
32200
+ function catalogTrackDeliveryMode(catalog, source) {
32201
+ const binding = bindingOf(catalog, source);
32202
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
32203
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
32204
+ }
31490
32205
  function moqtPriority2(value) {
31491
32206
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31492
32207
  }
@@ -31530,6 +32245,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31530
32245
  repairTrackName: base.repairTrack,
31531
32246
  repairLayers: repair.baseLayers,
31532
32247
  repairOverlays: repair.overlays,
32248
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31533
32249
  ...mediaRate
31534
32250
  };
31535
32251
  }
@@ -32054,7 +32770,7 @@ var RepairLayerSubscriptions = class {
32054
32770
  * or interleaved sequences read as loss.
32055
32771
  */
32056
32772
  noteSourceSequence(seq) {
32057
- if (this.#disposed || this.#opts.delivery === "stream") return;
32773
+ if (this.#disposed || this.#delivery() === "stream") return;
32058
32774
  seq >>>= 0;
32059
32775
  if (this.#lastSeq === void 0) {
32060
32776
  this.#lastSeq = seq;
@@ -32087,12 +32803,12 @@ var RepairLayerSubscriptions = class {
32087
32803
  this.#expected = 0;
32088
32804
  this.#lost = 0;
32089
32805
  this.#wantFewer = 0;
32090
- this.#layerCount = Math.min(this.#layerCount, this.#layers().length);
32806
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32091
32807
  this.#reconcile();
32092
32808
  }
32093
32809
  snapshot() {
32094
32810
  return {
32095
- delivery: this.#opts.delivery,
32811
+ delivery: this.#delivery(),
32096
32812
  layerCount: this.#layerCount,
32097
32813
  subscribedTracks: [...this.#subs.keys()],
32098
32814
  repairPackets: this.#repairPackets,
@@ -32106,6 +32822,9 @@ var RepairLayerSubscriptions = class {
32106
32822
  this.#closeAll();
32107
32823
  this.#disposed = true;
32108
32824
  }
32825
+ #delivery() {
32826
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32827
+ }
32109
32828
  #layers() {
32110
32829
  return this.#source.repairLayers ?? [];
32111
32830
  }
@@ -32848,6 +33567,7 @@ export {
32848
33567
  catalogGroupDurationMs,
32849
33568
  catalogKeyframeCadenceMs,
32850
33569
  catalogRepairTarget,
33570
+ catalogTrackDeliveryMode,
32851
33571
  catalogTrackFecUnavailable,
32852
33572
  classifyH264Keyframe,
32853
33573
  classifyHevcKeyframe,
@@ -32868,6 +33588,7 @@ export {
32868
33588
  detectH264Keyframe,
32869
33589
  detectHevcKeyframe,
32870
33590
  driftCheck,
33591
+ encodeCmcdRecord,
32871
33592
  encodeDataSegment,
32872
33593
  encodeFecSourceInfo,
32873
33594
  encodeLocTimestamp,