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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1336 -302
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +70 -12
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +403 -58
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +48 -13
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +97 -25
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +79 -10
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +207 -26
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +209 -29
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +206 -26
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +34 -8
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +555 -79
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.d.ts +13 -3
  48. package/dist/multicast-manager.d.ts.map +1 -1
  49. package/dist/multicast-manager.js +980 -146
  50. package/dist/multicast-manager.js.map +2 -2
  51. package/dist/multicast-transport.js +28 -18
  52. package/dist/multicast-transport.js.map +2 -2
  53. package/dist/no-modules.js +206 -26
  54. package/dist/no-modules.js.map +2 -2
  55. package/dist/repair-fetch.d.ts +36 -8
  56. package/dist/repair-fetch.d.ts.map +1 -1
  57. package/dist/repair-fetch.js +285 -26
  58. package/dist/repair-fetch.js.map +3 -3
  59. package/dist/repair-layer-subscriptions.d.ts +6 -2
  60. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  61. package/dist/repair-layer-subscriptions.js +6 -3
  62. package/dist/repair-layer-subscriptions.js.map +2 -2
  63. package/dist/track-session.d.ts +1 -1
  64. package/dist/track-session.d.ts.map +1 -1
  65. package/dist/track-session.js +1 -0
  66. package/dist/track-session.js.map +2 -2
  67. package/dist/wasm-mmtp-container-parser.js +206 -26
  68. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  69. package/package.json +8 -5
  70. package/src/catalog-fec-params.ts +36 -1
  71. package/src/cmcd-producer.ts +189 -179
  72. package/src/fec-manager.ts +433 -32
  73. package/src/fec-worker-setup.ts +2 -2
  74. package/src/index.ts +9 -2
  75. package/src/iso-block-locator.ts +133 -31
  76. package/src/iso-bmff-timing.ts +40 -1
  77. package/src/mmtp-container-parser.ts +306 -29
  78. package/src/mmtp-router.ts +548 -38
  79. package/src/multicast-manager.ts +44 -6
  80. package/src/repair-fetch.ts +363 -49
  81. package/src/repair-layer-subscriptions.ts +13 -5
  82. package/src/track-session.ts +4 -1
@@ -851,6 +851,17 @@ function readRepairFecPayloadIdFields(mmtpPacket, parsedHeader) {
851
851
  dataOffset: off + REPAIR_FEC_PID_SIZE
852
852
  };
853
853
  }
854
+ function extractRepairFecPayloadId(mmtpPacket) {
855
+ const fields = readRepairFecPayloadIdFields(mmtpPacket);
856
+ if (!fields)
857
+ return null;
858
+ return {
859
+ ssStart: fields.ssStart,
860
+ rsbLength: fields.rsbLength,
861
+ rsId: fields.rsId,
862
+ ssbLength: fields.ssbLength
863
+ };
864
+ }
854
865
  function parseMoqRepairFrame(frame) {
855
866
  return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame));
856
867
  }
@@ -7988,8 +7999,8 @@ var MulticastSubscriptionPool = class {
7988
7999
  record.claims--;
7989
8000
  return this.acquire(gateway, config, consumer);
7990
8001
  }
7991
- const token = Symbol(key);
7992
- entry.consumers.set(token, consumer);
8002
+ const token2 = Symbol(key);
8003
+ entry.consumers.set(token2, consumer);
7993
8004
  let released = false;
7994
8005
  return {
7995
8006
  key,
@@ -7997,7 +8008,7 @@ var MulticastSubscriptionPool = class {
7997
8008
  if (released)
7998
8009
  return;
7999
8010
  released = true;
8000
- entry.consumers.delete(token);
8011
+ entry.consumers.delete(token2);
8001
8012
  record.claims--;
8002
8013
  if (record.claims > 0)
8003
8014
  return;
@@ -8625,9 +8636,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
8625
8636
  } catch (error) {
8626
8637
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8627
8638
  }
8628
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8639
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8629
8640
  await rx.close();
8630
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8641
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8631
8642
  }
8632
8643
  const events = new EventTarget();
8633
8644
  let state = "connected";
@@ -8746,14 +8757,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8746
8757
  });
8747
8758
 
8748
8759
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8760
+ var MAX_PENDING_PACKETS = 256;
8749
8761
  var AMTGatewayManager = class {
8750
8762
  subscriptions = /* @__PURE__ */ new Map();
8751
8763
  messageHandler;
8764
+ messageListenerRegistered = false;
8752
8765
  pendingPackets = /* @__PURE__ */ new Map();
8753
8766
  constructor() {
8754
8767
  this.messageHandler = (event) => {
8755
8768
  const data = event.data;
8756
- console.log(`[AMT] SW message received:`, data.type);
8757
8769
  if (data.type === "UDP_PACKET") {
8758
8770
  this.handlePacket(data);
8759
8771
  } else if (data.type === "SOCKET_ERROR") {
@@ -8762,12 +8774,22 @@ var AMTGatewayManager = class {
8762
8774
  console.log(`[AMT SW Debug] ${data.message}`);
8763
8775
  }
8764
8776
  };
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");
8777
+ }
8778
+ ensureMessageListener() {
8779
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8780
+ return;
8770
8781
  }
8782
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8783
+ this.messageListenerRegistered = true;
8784
+ console.log("[AMT] Registered service worker message handler");
8785
+ }
8786
+ removeMessageListener() {
8787
+ if (!this.messageListenerRegistered)
8788
+ return;
8789
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8790
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8791
+ }
8792
+ this.messageListenerRegistered = false;
8771
8793
  }
8772
8794
  /**
8773
8795
  * Subscribe via AMT tunnel
@@ -8777,6 +8799,7 @@ var AMTGatewayManager = class {
8777
8799
  if (config.protected) {
8778
8800
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8779
8801
  }
8802
+ this.ensureMessageListener();
8780
8803
  const keyStr = this.getKeyString(key);
8781
8804
  if (this.subscriptions.has(keyStr)) {
8782
8805
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8812,6 +8835,8 @@ var AMTGatewayManager = class {
8812
8835
  this.subscriptions.delete(keyStr);
8813
8836
  this.pendingPackets.delete(subscription.subscriptionId);
8814
8837
  }
8838
+ if (this.subscriptions.size === 0)
8839
+ this.removeMessageListener();
8815
8840
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8816
8841
  }
8817
8842
  }
@@ -8833,6 +8858,8 @@ var AMTGatewayManager = class {
8833
8858
  } finally {
8834
8859
  this.subscriptions.delete(handle);
8835
8860
  this.pendingPackets.delete(subscription.subscriptionId);
8861
+ if (this.subscriptions.size === 0)
8862
+ this.removeMessageListener();
8836
8863
  }
8837
8864
  }
8838
8865
  /**
@@ -8849,14 +8876,12 @@ var AMTGatewayManager = class {
8849
8876
  if (!subscriptionId)
8850
8877
  return;
8851
8878
  const subscription = this.subscriptions.get(subscriptionId);
8852
- if (!subscription) {
8853
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8879
+ if (!subscription)
8854
8880
  return;
8855
- }
8856
8881
  if (!subscription.handshakeComplete) {
8857
8882
  const queue = this.pendingPackets.get(subscriptionId);
8858
8883
  const packetData2 = data;
8859
- if (queue) {
8884
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8860
8885
  queue.push({
8861
8886
  data: packetData2.data,
8862
8887
  remoteAddress: packetData2.remoteAddress,
@@ -8882,19 +8907,15 @@ var AMTGatewayManager = class {
8882
8907
  if (!subscriptionId)
8883
8908
  return;
8884
8909
  const subscription = this.subscriptions.get(subscriptionId);
8885
- if (!subscription) {
8886
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8910
+ if (!subscription)
8887
8911
  return;
8888
- }
8889
8912
  subscription.config.onError(new Error(error || "Unknown error"));
8890
8913
  }
8891
8914
  /**
8892
8915
  * Cleanup on destroy
8893
8916
  */
8894
8917
  destroy() {
8895
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8896
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8897
- }
8918
+ this.removeMessageListener();
8898
8919
  const handles = Array.from(this.subscriptions.keys());
8899
8920
  for (const handle of handles) {
8900
8921
  this.unsubscribe(handle).catch((err) => {
@@ -12425,6 +12446,7 @@ var TrackSession = class {
12425
12446
  this.#state = "idle";
12426
12447
  this.#subscription = null;
12427
12448
  this.#disarmFec();
12449
+ this.#unsuppress();
12428
12450
  }
12429
12451
  // -----------------------------------------------------------------------
12430
12452
  // Internal
@@ -12918,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
12918
12940
  this.#heldEvicted = 0;
12919
12941
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
12920
12942
  this.#holes = [];
12943
+ /** Holes the horizon passed while pending: sources held until closed. */
12944
+ this.#expired = [];
12945
+ /** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
12946
+ this.#newlyExpired = [];
12947
+ /**
12948
+ * Spans closed from expired holes: behind the horizon, so kept only for
12949
+ * `locate` while their sources are fed, until the next retirement.
12950
+ */
12951
+ this.#closedBehind = [];
12921
12952
  /**
12922
12953
  * Keep a gap that is missing a source as a pending hole rather than a span,
12923
12954
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -12939,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
12939
12970
  #retentionBlocks;
12940
12971
  #heldEvicted;
12941
12972
  #newest;
12973
+ #newestSource;
12942
12974
  #holes;
12975
+ #expired;
12976
+ #newlyExpired;
12977
+ #closedBehind;
12943
12978
  static #checkedRetention(retentionBlocks) {
12944
12979
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
12945
12980
  throw new Error(
@@ -12976,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12976
13011
  observe(position) {
12977
13012
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12978
13013
  }
13014
+ /**
13015
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
13016
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
13017
+ * a source cannot run ahead of the sources sent before it except by
13018
+ * reordering.
13019
+ */
13020
+ get newestSource() {
13021
+ return this.#newestSource;
13022
+ }
13023
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13024
+ noteSource(ssId) {
13025
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13026
+ this.#newestSource = ssId;
13027
+ return true;
13028
+ }
12979
13029
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12980
13030
  get heldEvicted() {
12981
13031
  return this.#heldEvicted;
@@ -12994,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
12994
13044
  }
12995
13045
  /** The known block or span containing `ssId`, with its ESI. */
12996
13046
  locate(ssId) {
12997
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
12998
- const block = this.#blocks[i];
12999
- if (isoBlockContains(block.start, block.length, ssId)) {
13000
- return { block, esi: serial32Distance(block.start, ssId) };
13047
+ for (const blocks of [this.#blocks, this.#closedBehind]) {
13048
+ for (let i = blocks.length - 1; i >= 0; i--) {
13049
+ const block = blocks[i];
13050
+ if (isoBlockContains(block.start, block.length, ssId)) {
13051
+ return { block, esi: serial32Distance(block.start, ssId) };
13052
+ }
13001
13053
  }
13002
13054
  }
13003
13055
  return null;
13004
13056
  }
13005
- /** Number of pending holes. */
13057
+ /** Number of holes not closed yet, pending or past the horizon. */
13006
13058
  get holeCount() {
13007
- return this.#holes.length;
13059
+ return this.#holes.length + this.#expired.length;
13060
+ }
13061
+ /**
13062
+ * The pending holes the horizon has passed since the last call, in the
13063
+ * order it passed them. Each is no longer pending: no repair states a block
13064
+ * in it (that repair is `stale`, as for a span the horizon retired) and the
13065
+ * release gate does not wait for it. Its owner must close it
13066
+ * (`closeHolesWithin`), which turns it into a span that is not kept here.
13067
+ */
13068
+ takeExpiredHoles() {
13069
+ return this.#newlyExpired.splice(0);
13008
13070
  }
13009
13071
  /** The stated block (never a span) ending exactly at `end`. */
13010
13072
  blockEndingAt(end) {
@@ -13060,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
13060
13122
  /**
13061
13123
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
13062
13124
  * block states its range and places every held source in it; later repair
13063
- * symbols of the same block change nothing. A range that contradicts a
13064
- * known block (a different length, or an overlap with another block or a
13065
- * span) fails closed: the repair symbol is dropped and no block changes.
13125
+ * symbols of the same block change nothing. A repair whose range is exactly
13126
+ * a repair-less span states that span's one block. A range that contradicts
13127
+ * a known block (a different length, or an overlap with another block or
13128
+ * with part of a span) fails closed: the repair symbol is dropped and no
13129
+ * block changes.
13066
13130
  * A newly stated block retires behind its SS_Start (see the class note).
13067
13131
  * A block so far past `newest` that stating it would retire the position
13068
13132
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -13081,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
13081
13145
  const known = this.blockAt(start);
13082
13146
  if (known !== void 0) {
13083
13147
  if (known.span) {
13084
- return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
13148
+ if (known.length === length) {
13149
+ const block2 = { start, length, span: false };
13150
+ this.#blocks[this.#blocks.indexOf(known)] = block2;
13151
+ return { status: "placed", block: block2, placed: [] };
13152
+ }
13153
+ return length < known.length ? {
13154
+ status: "overlap",
13155
+ reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
13156
+ ofSpan: known
13157
+ } : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
13085
13158
  }
13086
13159
  if (known.length !== length) {
13087
13160
  return {
@@ -13149,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
13149
13222
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
13150
13223
  }
13151
13224
  /**
13152
- * Turn every pending hole inside `[start, start + length)` into a
13153
- * repair-less span, claiming its held sources: its range FETCH ended, so
13154
- * nothing more will state its blocks. Returns the spans, in SS_ID order.
13225
+ * Turn every hole inside `[start, start + length)` into a repair-less span,
13226
+ * claiming its held sources: its range FETCH ended, so nothing more will
13227
+ * state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
13228
+ * span that is not kept among the blocks, as the horizon would retire it at
13229
+ * once; only `locate` finds it, until the next retirement, so its sources
13230
+ * can be fed. Returns the spans, in SS_ID order.
13155
13231
  */
13156
13232
  closeHolesWithin(start, length) {
13157
13233
  const closed = [];
13158
- for (const hole of [...this.#holes]) {
13159
- if (!isoBlockContains(start, length, hole.start)) continue;
13160
- this.#holes.splice(this.#holes.indexOf(hole), 1);
13161
- const spanBlock = { start: hole.start, length: hole.length, span: true };
13162
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13163
- this.#blocks.push(spanBlock);
13234
+ for (const holes of [this.#holes, this.#expired]) {
13235
+ for (const hole of [...holes]) {
13236
+ if (!isoBlockContains(start, length, hole.start)) continue;
13237
+ holes.splice(holes.indexOf(hole), 1);
13238
+ const spanBlock = { start: hole.start, length: hole.length, span: true };
13239
+ closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13240
+ if (holes === this.#holes) {
13241
+ this.#blocks.push(spanBlock);
13242
+ } else {
13243
+ this.#closedBehind.push(spanBlock);
13244
+ if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
13245
+ }
13246
+ }
13164
13247
  }
13165
13248
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
13166
13249
  }
@@ -13174,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
13174
13257
  return void 0;
13175
13258
  }
13176
13259
  /**
13177
- * Retire every block and span whose last SS_ID, and every held source,
13178
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
13179
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
13180
- * horizon uses. Returns the retired block starts.
13260
+ * Retire every block and span whose last SS_ID, and every held source
13261
+ * outside a hole, lies more than `retentionBlocks` blocks behind
13262
+ * `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
13263
+ * every ISO interleave horizon uses; a pending hole so far behind expires
13264
+ * (`takeExpiredHoles`). Returns the retired block starts.
13181
13265
  */
13182
13266
  retireBehind(position) {
13183
13267
  this.observe(position);
13184
13268
  const horizon = this.#retentionBlocks;
13185
13269
  const retired = [];
13270
+ this.#closedBehind.length = 0;
13186
13271
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
13187
13272
  const block = this.#blocks[i];
13188
13273
  const last = serial32Add(block.start, block.length - 1);
@@ -13194,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
13194
13279
  for (let i = this.#holes.length - 1; i >= 0; i--) {
13195
13280
  const hole = this.#holes[i];
13196
13281
  const last = serial32Add(hole.start, hole.length - 1);
13197
- if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) this.#holes.splice(i, 1);
13282
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
13283
+ this.#holes.splice(i, 1);
13284
+ this.#expired.push(hole);
13285
+ this.#newlyExpired.push(hole);
13198
13286
  }
13199
13287
  for (const ssId of [...this.#held.keys()]) {
13200
- if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
13288
+ if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon && !this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) && !this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))) {
13289
+ this.#held.delete(ssId);
13290
+ }
13201
13291
  }
13202
13292
  return retired;
13203
13293
  }
@@ -13205,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
13205
13295
  reset() {
13206
13296
  this.#blocks.length = 0;
13207
13297
  this.#holes.length = 0;
13298
+ this.#expired.length = 0;
13299
+ this.#newlyExpired.length = 0;
13300
+ this.#closedBehind.length = 0;
13208
13301
  this.#held.clear();
13209
13302
  this.#coverageEnd = void 0;
13210
13303
  this.#newest = void 0;
13304
+ this.#newestSource = void 0;
13211
13305
  }
13212
13306
  #claim(block) {
13213
13307
  const placed = [];
@@ -13827,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13827
13921
  var WORKER_ERROR_KIND_CAP = 32;
13828
13922
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13829
13923
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13924
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13830
13925
  function isMulticastSymbolDelivery(value) {
13831
13926
  return value === "datagram" || value === "ordered-relay";
13832
13927
  }
@@ -13920,10 +14015,18 @@ var FECManager = class _FECManager {
13920
14015
  // 'pending' forever, which would park every later frame of its stream in
13921
14016
  // the in-order release gate. Each entry owns its handle; the callback
13922
14017
  // checks it still owns the key, so a settled or reset key is never touched.
14018
+ /**
14019
+ * Per owed Worker cleanup: the bound on its acknowledgement, when its block
14020
+ * was due, and when its cleanup was asked for (see `onCleanupComplete`).
14021
+ */
13923
14022
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13924
14023
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13925
14024
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13926
14025
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
14026
+ // Source ESIs per block that another FEC instance rebuilt and fed in
14027
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
14028
+ // not network arrivals and never make a block clean.
14029
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13927
14030
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13928
14031
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13929
14032
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -13932,6 +14035,9 @@ var FECManager = class _FECManager {
13932
14035
  // SSB_length source arrivals and recoverable from SSB_length received
13933
14036
  // symbols; waiting for K fails every one of them.
13934
14037
  this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
14038
+ // Keys whose authored length is a repair-less span's (#openIsoSpan), until
14039
+ // a repair states the block there or the span resolves.
14040
+ this.#isoSpanKeys = /* @__PURE__ */ new Set();
13935
14041
  this.#ssbLengthConflictCount = 0;
13936
14042
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13937
14043
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -14052,6 +14158,10 @@ var FECManager = class _FECManager {
14052
14158
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14053
14159
  // One classifier per physical stream, built from that stream's geometry.
14054
14160
  this.#classifiers = /* @__PURE__ */ new Map();
14161
+ // A stream without catalog timing has no classifier, so the last RAP it
14162
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14163
+ // a second RAP is released inside one MPU (BLO-52085).
14164
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14055
14165
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14056
14166
  // rungs can share a packetId with a different framerate or GOP, so the
14057
14167
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14125,7 +14235,10 @@ var FECManager = class _FECManager {
14125
14235
  // arrives via onBlockRecovered callback.
14126
14236
  this.onBlockStarted = null;
14127
14237
  this.onBlockComplete = null;
14128
- /** A block reached its fail bound. `blockClass` is its class at failure. */
14238
+ /**
14239
+ * A block reached its fail bound. `blockClass` is its class at failure.
14240
+ * `span` is set when what failed is a repair-less span starting at `sbn`.
14241
+ */
14129
14242
  this.onBlockFailed = null;
14130
14243
  /**
14131
14244
  * Fired once per stream when its Worker decoder stops being routable.
@@ -14167,6 +14280,8 @@ var FECManager = class _FECManager {
14167
14280
  * for it, which it must be.
14168
14281
  */
14169
14282
  this.#onUndelimitedLoss = null;
14283
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14284
+ this.#onIsoHoleExpired = null;
14170
14285
  /** A block every symbol of which vanished was created by SBN evidence. */
14171
14286
  this.onBlockVanished = null;
14172
14287
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -14209,7 +14324,9 @@ var FECManager = class _FECManager {
14209
14324
  #pendingWorkerCleanupBounds;
14210
14325
  #blockSourceCounts;
14211
14326
  #blockSourceEsis;
14327
+ #blockRebuiltEsis;
14212
14328
  #blockAuthoredLengths;
14329
+ #isoSpanKeys;
14213
14330
  #ssbLengthConflictCount;
14214
14331
  #blockRepairRsIds;
14215
14332
  #blockRsbLengths;
@@ -14251,6 +14368,7 @@ var FECManager = class _FECManager {
14251
14368
  #orderedFrontiers;
14252
14369
  #orderedPendingBlocks;
14253
14370
  #classifiers;
14371
+ #noSpanRaps;
14254
14372
  #streamGeometries;
14255
14373
  #failedBlocks;
14256
14374
  #resolvedBlockClasses;
@@ -14275,6 +14393,7 @@ var FECManager = class _FECManager {
14275
14393
  #decoderUnavailableEvents;
14276
14394
  #repairSymbolSizeMismatchCount;
14277
14395
  #onUndelimitedLoss;
14396
+ #onIsoHoleExpired;
14278
14397
  #encodeTrackName(catalogTrackName) {
14279
14398
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14280
14399
  if (encoded === void 0) {
@@ -14746,7 +14865,10 @@ var FECManager = class _FECManager {
14746
14865
  timer.handle = null;
14747
14866
  this.#requestRepair(sbn, packetId, catalogTrackName);
14748
14867
  };
14749
- timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()));
14868
+ timer.handle = setTimeout(
14869
+ onFire,
14870
+ this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
14871
+ );
14750
14872
  this.#repairDeadlines.set(key, timer);
14751
14873
  }
14752
14874
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14890,8 +15012,14 @@ var FECManager = class _FECManager {
14890
15012
  * Vanished blocks are not enumerated here: their starts are unknown until a
14891
15013
  * repair states them. The SS_IDs between two stated blocks become one
14892
15014
  * repair-less span when the later block's repair arrives (placeIsoRepair).
15015
+ *
15016
+ * A `held` source arrived before its block was known and is fed only now
15017
+ * that a repair, a span or a closed hole placed it: how far its block start
15018
+ * then lies behind the frontier measures how long the receiver held it, not
15019
+ * where the stream was, so only its SS_ID and packet_sequence_number judge
15020
+ * it (BLO-52085).
14893
15021
  */
14894
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15022
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14895
15023
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14896
15024
  const now = performance.now();
14897
15025
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14912,6 +15040,7 @@ var FECManager = class _FECManager {
14912
15040
  };
14913
15041
  const discontinuity = () => {
14914
15042
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
15043
+ this.#noSpanRaps.delete(streamKey);
14915
15044
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14916
15045
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14917
15046
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -14929,7 +15058,7 @@ var FECManager = class _FECManager {
14929
15058
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14930
15059
  };
14931
15060
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
14932
- if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
15061
+ if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
14933
15062
  discontinuity();
14934
15063
  return;
14935
15064
  }
@@ -15191,17 +15320,21 @@ var FECManager = class _FECManager {
15191
15320
  this.#blockWaitDeadlinesMs.delete(key);
15192
15321
  this.#repairHoldUntilMs.delete(key);
15193
15322
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
15194
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15323
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
15195
15324
  };
15196
- timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
15325
+ timer.handle = setTimeout(
15326
+ onFire,
15327
+ this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
15328
+ );
15197
15329
  this.#activeBlocks.set(key, timer);
15198
15330
  this.#indexBlockKey(key, sbn, packetId);
15199
15331
  }
15200
15332
  /**
15201
15333
  * Hand a block whose delivery is over to its decoder for finalization: the
15202
- * datagram deadline fired, or the ordered relay moved past it.
15334
+ * datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
15335
+ * is when it was due: its deadline, or now.
15203
15336
  */
15204
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15337
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
15205
15338
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
15206
15339
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
15207
15340
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -15217,7 +15350,7 @@ var FECManager = class _FECManager {
15217
15350
  if (workerClient) {
15218
15351
  this.#pendingWorkerCleanups.add(key);
15219
15352
  this.#pendingWorkerCleanupClients.set(key, workerClient);
15220
- this.#boundOwedCleanup(key, packetId, workerClient);
15353
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
15221
15354
  workerClient.cleanup(sbn);
15222
15355
  } else {
15223
15356
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15324,6 +15457,9 @@ var FECManager = class _FECManager {
15324
15457
  }
15325
15458
  if (policyChanged) {
15326
15459
  this.#bumpEpoch(packetId);
15460
+ for (const key of [...this.#noSpanRaps.keys()]) {
15461
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15462
+ }
15327
15463
  for (const [key, classifier] of [...this.#classifiers]) {
15328
15464
  if (!this.#belongsToPacket(key, packetId)) continue;
15329
15465
  const stream = this.#parseStreamKey(key);
@@ -15526,7 +15662,9 @@ var FECManager = class _FECManager {
15526
15662
  }
15527
15663
  resetMap(this.#blockSourceCounts);
15528
15664
  resetMap(this.#blockSourceEsis);
15665
+ resetMap(this.#blockRebuiltEsis);
15529
15666
  resetMap(this.#blockAuthoredLengths);
15667
+ resetSet(this.#isoSpanKeys);
15530
15668
  resetMap(this.#blockRepairRsIds);
15531
15669
  resetMap(this.#blockRsbLengths);
15532
15670
  resetMap(this.#heldLayerStates);
@@ -15559,6 +15697,7 @@ var FECManager = class _FECManager {
15559
15697
  resetMap(this.#isoLocators);
15560
15698
  resetMap(this.#sourceAnchors);
15561
15699
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15700
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15562
15701
  this.#bumpEpoch(packetId);
15563
15702
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15564
15703
  for (const key of keys.keys()) {
@@ -15649,7 +15788,9 @@ var FECManager = class _FECManager {
15649
15788
  dropMap(this.#pendingWorkerCleanupClients);
15650
15789
  dropMap(this.#blockSourceCounts);
15651
15790
  dropMap(this.#blockSourceEsis);
15791
+ dropMap(this.#blockRebuiltEsis);
15652
15792
  dropMap(this.#blockAuthoredLengths);
15793
+ dropSet(this.#isoSpanKeys);
15653
15794
  dropMap(this.#blockRepairRsIds);
15654
15795
  dropMap(this.#blockRsbLengths);
15655
15796
  dropMap(this.#blockPathCounts);
@@ -15665,6 +15806,7 @@ var FECManager = class _FECManager {
15665
15806
  dropMap(this.#isoLocators);
15666
15807
  dropMap(this.#sourceAnchors);
15667
15808
  dropMap(this.#classifiers);
15809
+ dropMap(this.#noSpanRaps);
15668
15810
  dropMap(this.#streamGeometries);
15669
15811
  dropSet(this.#pendingWorkerCleanups);
15670
15812
  dropSet(this.#completedBlocks);
@@ -15810,6 +15952,9 @@ var FECManager = class _FECManager {
15810
15952
  this.#unindexBlockKey(key);
15811
15953
  }
15812
15954
  }
15955
+ if (isSourceSymbol && source === "multicast") {
15956
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15957
+ }
15813
15958
  return true;
15814
15959
  }
15815
15960
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -15988,8 +16133,9 @@ var FECManager = class _FECManager {
15988
16133
  return true;
15989
16134
  }
15990
16135
  /**
15991
- * Drop a block's repair hold: its FETCH ended without delivering. The block
15992
- * falls back to its multicast wait, which may already have passed.
16136
+ * Drop a block's repair hold: its FETCH ended without delivering, or brought
16137
+ * back only a repair that cannot complete it. The block falls back to its
16138
+ * multicast wait, which may already have passed.
15993
16139
  */
15994
16140
  releaseRepairHold(sbn, packetId, catalogTrackName) {
15995
16141
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16127,6 +16273,8 @@ var FECManager = class _FECManager {
16127
16273
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16128
16274
  const evictedBefore = locator.heldEvicted;
16129
16275
  const held = locator.hold(ssId, source);
16276
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16277
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16130
16278
  const evicted = locator.heldEvicted - evictedBefore;
16131
16279
  if (evicted > 0) {
16132
16280
  const before = this.#isoHeldEvictedCount;
@@ -16147,17 +16295,20 @@ var FECManager = class _FECManager {
16147
16295
  * contradicts a known block is refused (counted) and must be dropped.
16148
16296
  */
16149
16297
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
16150
- const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
16298
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16299
+ const placement = locator.placeRepair(ssStart, ssbLength);
16300
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16151
16301
  if (placement.status !== "placed") {
16152
16302
  this.#isoRepairRefusedCount++;
16153
16303
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
16154
16304
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
16155
16305
  }
16306
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
16156
16307
  return placement;
16157
16308
  }
16309
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
16158
16310
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
16159
16311
  if (placement.hole !== void 0) {
16160
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
16161
16312
  const hole = placement.hole.block;
16162
16313
  const previous = locator.blockEndingAt(hole.start);
16163
16314
  const loss = {
@@ -16178,17 +16329,29 @@ var FECManager = class _FECManager {
16178
16329
  * `handler`, instead of a repair-less span; null restores spans. Set by the
16179
16330
  * range repair scheduler; applies to every stream from its next gap. The
16180
16331
  * handler and the deferral are one setting, so no hole is ever created with
16181
- * nobody to close it.
16332
+ * nobody to close it. `onExpired` is told of each pending hole the stream
16333
+ * runs past the locator's horizon (BLO-52085): returning true, it closes
16334
+ * the hole (`closeIsoHoles`), so the span it leaves still fails and is
16335
+ * reported; otherwise the hole is discarded with its sources.
16182
16336
  */
16183
- setUndelimitedLossHandler(handler) {
16337
+ setUndelimitedLossHandler(handler, onExpired = null) {
16184
16338
  this.#onUndelimitedLoss = handler;
16339
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
16185
16340
  this.#deferIsoGaps = handler !== null;
16186
16341
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
16187
16342
  }
16343
+ /** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
16344
+ #settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
16345
+ for (const hole of locator.takeExpiredHoles()) {
16346
+ const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
16347
+ if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
16348
+ }
16349
+ }
16188
16350
  /**
16189
- * The range repair for a hole ended: every hole still pending inside
16190
- * `[start, start + length)` becomes a repair-less span. Returns those spans
16191
- * with the held sources they placed, for the caller to feed (MmtpRouter).
16351
+ * The range repair for a hole ended: every hole inside `[start, start +
16352
+ * length)`, pending or expired, becomes a repair-less span. Returns those
16353
+ * spans with the held sources they placed, for the caller to feed
16354
+ * (MmtpRouter).
16192
16355
  */
16193
16356
  closeIsoHoles(packetId, catalogTrackName, start, length) {
16194
16357
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -16207,7 +16370,10 @@ var FECManager = class _FECManager {
16207
16370
  */
16208
16371
  #openIsoSpan(span, packetId, catalogTrackName) {
16209
16372
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
16210
- if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, span.block.length);
16373
+ if (!this.#blockAuthoredLengths.has(key)) {
16374
+ this.#blockAuthoredLengths.set(key, span.block.length);
16375
+ this.#isoSpanKeys.add(key);
16376
+ }
16211
16377
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
16212
16378
  }
16213
16379
  /**
@@ -16295,11 +16461,14 @@ var FECManager = class _FECManager {
16295
16461
  /**
16296
16462
  * Whether an MPU boundary still bounds this stream's RAPs (see
16297
16463
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16298
- * it does not. A stream with no classifier yet has shown nothing against it.
16464
+ * it does not. A stream with no classifier yet has shown nothing against it,
16465
+ * and neither has a stream without catalog timing until it releases two
16466
+ * RAPs inside one MPU.
16299
16467
  */
16300
16468
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16301
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16302
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16469
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16470
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16471
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16303
16472
  }
16304
16473
  /** A block's class now, or frozen at its resolution. */
16305
16474
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16325,7 +16494,14 @@ var FECManager = class _FECManager {
16325
16494
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16326
16495
  */
16327
16496
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16328
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16497
+ if (!this.#trackConfigs.has(packetId)) return;
16498
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16499
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16500
+ const last = this.#noSpanRaps.get(streamKey);
16501
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16502
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16503
+ return;
16504
+ }
16329
16505
  this.#applyClassifierChanges(
16330
16506
  packetId,
16331
16507
  catalogTrackName,
@@ -16538,11 +16714,14 @@ var FECManager = class _FECManager {
16538
16714
  });
16539
16715
  client.onCleanupComplete((sbn) => {
16540
16716
  if (this.#releasedWorkerClients.has(client)) return;
16541
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16717
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16718
+ const cleanupKeys = owed.filter((key) => {
16542
16719
  const block = this.#parseBlockKey(key);
16543
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16720
+ return block.packetId === packetId && block.sbn === sbn;
16544
16721
  });
16545
- for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
16722
+ const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
16723
+ const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
16724
+ for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
16546
16725
  });
16547
16726
  client.onError((msg, code) => {
16548
16727
  const requestedKind = code ?? msg;
@@ -16612,9 +16791,11 @@ var FECManager = class _FECManager {
16612
16791
  * fail deadline already passed, so failing it is the decision that deadline
16613
16792
  * made. A late ack then finds nothing owed and is a no-op.
16614
16793
  */
16615
- #boundOwedCleanup(key, packetId, client) {
16794
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16616
16795
  this.#clearOwedCleanupBound(key);
16617
16796
  const bound = {
16797
+ dueAtMs,
16798
+ askedAtMs: performance.now(),
16618
16799
  handle: setTimeout(() => {
16619
16800
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16620
16801
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -16712,7 +16893,15 @@ var FECManager = class _FECManager {
16712
16893
  this.#isoSourceOversizeCount++;
16713
16894
  return null;
16714
16895
  }
16715
- const sbn = located.block.start;
16896
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16897
+ }
16898
+ /**
16899
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16900
+ * answers through onFrame (null here), or its synchronous decoder, whose
16901
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16902
+ * stated for `ssId`.
16903
+ */
16904
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16716
16905
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16717
16906
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16718
16907
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16741,6 +16930,74 @@ var FECManager = class _FECManager {
16741
16930
  }
16742
16931
  return recovered;
16743
16932
  }
16933
+ /**
16934
+ * Feed a source symbol of this stream that another FEC instance over the
16935
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16936
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16937
+ * BLO-52085). It joins its block's decoder as a received source would, so
16938
+ * it reduces the erasures the block still has to solve, but it is not a
16939
+ * network arrival: no transport accounting, no classification evidence,
16940
+ * no deadline refresh, and it never makes a block clean.
16941
+ *
16942
+ * Only a block a repair stated takes one (a repair-less span has no
16943
+ * decoder block). A block that is terminal, or already holds that source,
16944
+ * does not. The symbol is the exact protected packet, unpadded, as
16945
+ * feedSourceSymbol takes it.
16946
+ *
16947
+ * `recovered` is the block the synchronous decoder completed with it; the
16948
+ * caller routes it and completes the block, as for any recovery. When
16949
+ * every source of the block is then present, received or rebuilt, and no
16950
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16951
+ * block as clean on its own side), the block is completed here as
16952
+ * recovered, with nothing left to route.
16953
+ */
16954
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16955
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16956
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16957
+ if (located === null || located.block.span) return NOT_TAKEN;
16958
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16959
+ const sbn = located.block.start;
16960
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16961
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16962
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16963
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16964
+ if (rebuilt === void 0) {
16965
+ rebuilt = /* @__PURE__ */ new Set();
16966
+ this.#blockRebuiltEsis.set(key, rebuilt);
16967
+ }
16968
+ const config = this.#trackConfigs.get(packetId);
16969
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16970
+ rebuilt.add(located.esi);
16971
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16972
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16973
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16974
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16975
+ }
16976
+ }
16977
+ return { taken: true, recovered };
16978
+ }
16979
+ /**
16980
+ * A block that holds every one of its `length` sources, received or
16981
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16982
+ * every packet of it was delivered, by its own arrival or by the instance
16983
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16984
+ */
16985
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16986
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16987
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16988
+ let present = rebuilt.size;
16989
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16990
+ if (present < length) return false;
16991
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16992
+ this.countRecovery(packetId, sbn, catalogTrackName);
16993
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16994
+ if (catalogTrackName === void 0) {
16995
+ this.onBlockComplete?.(sbn, packetId);
16996
+ } else {
16997
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16998
+ }
16999
+ return true;
17000
+ }
16744
17001
  /**
16745
17002
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16746
17003
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16943,8 +17200,75 @@ var FECManager = class _FECManager {
16943
17200
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16944
17201
  return null;
16945
17202
  }
17203
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16946
17204
  return recovered;
16947
17205
  }
17206
+ /**
17207
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17208
+ * its sources in send order, so one that moves the stream's newest source
17209
+ * position past a block leaves the sources that block still misses
17210
+ * reordered behind it at best: it shortens the wait of every block it
17211
+ * passed that only a late source can complete. MoQ is left out: its streams
17212
+ * can overtake one another, so a MoQ source shows nothing about the sources
17213
+ * sent before it.
17214
+ */
17215
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17216
+ const previous = locator.newestSource;
17217
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17218
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17219
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17220
+ this.#shortenUnrecoverableWait(
17221
+ this.#blockKey(block.start, packetId, catalogTrackName),
17222
+ block.start,
17223
+ packetId,
17224
+ catalogTrackName
17225
+ );
17226
+ }
17227
+ }
17228
+ /**
17229
+ * The wait of a block that only a late source can complete (BLO-52085): a
17230
+ * repair stated it, its received sources plus every symbol its repair group
17231
+ * carries (RSB_length) are short of K' (the repair scheduler's
17232
+ * group-cannot-complete), and a later source of the stream has arrived, so
17233
+ * a source it misses is lost or reordered behind that one. The span, which
17234
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17235
+ * the late-symbol allowance alone, the multicast wait less the span (the
17236
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17237
+ * block.
17238
+ */
17239
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17240
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17241
+ const rsbLength = this.#blockRsbLengths.get(key);
17242
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17243
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17244
+ const received = this.#blockSourceEsis.get(key);
17245
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17246
+ if (present + rsbLength >= authoredLength) return void 0;
17247
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17248
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17249
+ return void 0;
17250
+ }
17251
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17252
+ }
17253
+ /**
17254
+ * Bring an open block's wait in to its late-source wait, when it has one.
17255
+ * Never later than it is armed: only a source can complete such a block,
17256
+ * so a repair of its group (paced across the span) must not keep it open.
17257
+ */
17258
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17259
+ if (!this.#activeBlocks.has(key)) return;
17260
+ const config = this.#trackConfigs.get(packetId);
17261
+ if (config === void 0) return;
17262
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17263
+ if (waitMs === void 0) return;
17264
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17265
+ this.#armBlockTimeout(
17266
+ sbn,
17267
+ packetId,
17268
+ catalogTrackName,
17269
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17270
+ );
17271
+ }
16948
17272
  /**
16949
17273
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16950
17274
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17042,12 +17366,13 @@ var FECManager = class _FECManager {
17042
17366
  * the inactivity deadline on each symbol, records the symbol's in-band
17043
17367
  * classification evidence, and marks the block clean when all of its
17044
17368
  * authored source symbols arrived: the SSB_length a repair symbol declared,
17045
- * otherwise K.
17369
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17370
+ * placed only now (see #noteSbnSeen).
17046
17371
  *
17047
17372
  * Throws for a packetId that was never configured, so a classified block
17048
17373
  * never reads the manager-global K or timeout.
17049
17374
  */
17050
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17375
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
17051
17376
  const trackConfig = this.#trackConfigs.get(packetId);
17052
17377
  if (trackConfig === void 0) {
17053
17378
  throw new Error(
@@ -17063,7 +17388,8 @@ var FECManager = class _FECManager {
17063
17388
  catalogTrackName,
17064
17389
  "source",
17065
17390
  evidence.packetSequenceNumber,
17066
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17391
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17392
+ held
17067
17393
  );
17068
17394
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
17069
17395
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -17099,6 +17425,15 @@ var FECManager = class _FECManager {
17099
17425
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17100
17426
  return;
17101
17427
  }
17428
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17429
+ key,
17430
+ sbn,
17431
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17432
+ packetId,
17433
+ catalogTrackName
17434
+ )) {
17435
+ return;
17436
+ }
17102
17437
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17103
17438
  }
17104
17439
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17107,7 +17442,9 @@ var FECManager = class _FECManager {
17107
17442
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17108
17443
  this.#blockSourceCounts.delete(key);
17109
17444
  this.#blockSourceEsis.delete(key);
17445
+ this.#blockRebuiltEsis.delete(key);
17110
17446
  this.#blockAuthoredLengths.delete(key);
17447
+ this.#isoSpanKeys.delete(key);
17111
17448
  this.#blockRepairRsIds.delete(key);
17112
17449
  this.#blockRsbLengths.delete(key);
17113
17450
  this.#markTerminal(key, sbn, packetId, true);
@@ -17138,6 +17475,19 @@ var FECManager = class _FECManager {
17138
17475
  #receiptNow() {
17139
17476
  return this.#receiptAtMs ?? performance.now();
17140
17477
  }
17478
+ /**
17479
+ * The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
17480
+ * scope: counted from the receipt time, as the timer of a symbol fed when
17481
+ * it was received was, less the whole ms since. So it fires on the tick
17482
+ * that timer would have, never later than a delay counted from now, and
17483
+ * its handler still re-arms a timer that fires before its deadline.
17484
+ * Counted from now, a deadline set from the receipt time is rounded up
17485
+ * from another fraction of a ms (BLO-52085).
17486
+ */
17487
+ #receiptTimerDelayMs(deadlineMs, minMs) {
17488
+ const receivedAtMs = this.#receiptAtMs;
17489
+ return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
17490
+ }
17141
17491
  /**
17142
17492
  * A held packet admitted late arms from its (earlier) receipt time; that
17143
17493
  * never pulls in a deadline later activity already armed. Outside a
@@ -17173,10 +17523,20 @@ var FECManager = class _FECManager {
17173
17523
  this.#repairDeadlineMs(trackConfig)
17174
17524
  );
17175
17525
  }
17176
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17526
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17527
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17528
+ return;
17529
+ }
17530
+ this.#armBlockTimeout(
17531
+ sbn,
17532
+ packetId,
17533
+ catalogTrackName,
17534
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17535
+ );
17177
17536
  }
17178
17537
  #failBlock(sbn, packetId, catalogTrackName) {
17179
17538
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
17539
+ const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
17180
17540
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
17181
17541
  this.#clearRepairDeadline(key);
17182
17542
  this.#clearBlockTimer(key);
@@ -17186,6 +17546,7 @@ var FECManager = class _FECManager {
17186
17546
  }
17187
17547
  this.#blockSourceCounts.delete(key);
17188
17548
  this.#blockSourceEsis.delete(key);
17549
+ this.#blockRebuiltEsis.delete(key);
17189
17550
  this.#blockAuthoredLengths.delete(key);
17190
17551
  this.#blockRepairRsIds.delete(key);
17191
17552
  this.#blockRsbLengths.delete(key);
@@ -17195,7 +17556,8 @@ var FECManager = class _FECManager {
17195
17556
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
17196
17557
  this.#fecStats.blocksFailed++;
17197
17558
  this.#statsForPacket(packetId).blocksFailed++;
17198
- this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17559
+ if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17560
+ else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
17199
17561
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
17200
17562
  }
17201
17563
  /**
@@ -17224,7 +17586,9 @@ var FECManager = class _FECManager {
17224
17586
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17225
17587
  this.#blockSourceCounts.delete(key);
17226
17588
  this.#blockSourceEsis.delete(key);
17589
+ this.#blockRebuiltEsis.delete(key);
17227
17590
  this.#blockAuthoredLengths.delete(key);
17591
+ this.#isoSpanKeys.delete(key);
17228
17592
  this.#blockRepairRsIds.delete(key);
17229
17593
  this.#blockRsbLengths.delete(key);
17230
17594
  this.#clearBlockTimer(key);
@@ -17315,7 +17679,9 @@ var FECManager = class _FECManager {
17315
17679
  this.#blockKeysBySbn.clear();
17316
17680
  this.#blockSourceCounts.clear();
17317
17681
  this.#blockSourceEsis.clear();
17682
+ this.#blockRebuiltEsis.clear();
17318
17683
  this.#blockAuthoredLengths.clear();
17684
+ this.#isoSpanKeys.clear();
17319
17685
  this.#blockRepairRsIds.clear();
17320
17686
  this.#blockRsbLengths.clear();
17321
17687
  this.#heldLayerStates.clear();
@@ -17340,6 +17706,7 @@ var FECManager = class _FECManager {
17340
17706
  this.#blockAnchorsMs.clear();
17341
17707
  this.#blockCatalogTrackNames.clear();
17342
17708
  this.#classifiers.clear();
17709
+ this.#noSpanRaps.clear();
17343
17710
  this.#streamGeometries.clear();
17344
17711
  this.#failedBlocks.clear();
17345
17712
  this.#resolvedBlockClasses.clear();
@@ -17870,9 +18237,21 @@ var IsoBmffTimingResolver = class {
17870
18237
  }
17871
18238
  #track = null;
17872
18239
  #fragments = /* @__PURE__ */ new Map();
18240
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18241
+ #firstSamples = /* @__PURE__ */ new Map();
18242
+ /**
18243
+ * First sample and sample count of the newest fragments whose every slot was
18244
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18245
+ * matches the combiner, since it retires an abandoned sample as well.
18246
+ */
18247
+ #delivered = /* @__PURE__ */ new Map();
17873
18248
  #pendingSamples = 0;
17874
18249
  #evictedSamples = 0;
17875
18250
  #lastMovieFragmentRejection = null;
18251
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18252
+ get initialized() {
18253
+ return this.#track !== null;
18254
+ }
17876
18255
  get pendingSampleCount() {
17877
18256
  return this.#pendingSamples;
17878
18257
  }
@@ -17896,7 +18275,14 @@ var IsoBmffTimingResolver = class {
17896
18275
  this.#track = timing;
17897
18276
  return true;
17898
18277
  }
17899
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18278
+ /**
18279
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18280
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18281
+ * fragment whose samples are all done is a duplicate: admitting it again
18282
+ * would charge stale timing and could displace the live MPU's. A source
18283
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18284
+ */
18285
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17900
18286
  this.#lastMovieFragmentRejection = null;
17901
18287
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17902
18288
  this.#lastMovieFragmentRejection = "invalid";
@@ -17922,6 +18308,8 @@ var IsoBmffTimingResolver = class {
17922
18308
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17923
18309
  );
17924
18310
  }
18311
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18312
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17925
18313
  let freed = 0;
17926
18314
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17927
18315
  freed += this.#evictOldestFragment();
@@ -17935,6 +18323,7 @@ var IsoBmffTimingResolver = class {
17935
18323
  return false;
17936
18324
  }
17937
18325
  this.#fragments.set(key, [...parsed.samples]);
18326
+ this.#firstSamples.set(key, parsed.samples[0]);
17938
18327
  this.#pendingSamples += parsed.samples.length;
17939
18328
  return true;
17940
18329
  }
@@ -17992,6 +18381,7 @@ var IsoBmffTimingResolver = class {
17992
18381
  let live = 0;
17993
18382
  for (const sample of samples) if (sample !== null) live++;
17994
18383
  this.#fragments.delete(oldest);
18384
+ this.#firstSamples.delete(oldest);
17995
18385
  this.#pendingSamples -= live;
17996
18386
  this.#evictedSamples += live;
17997
18387
  return live;
@@ -18007,12 +18397,23 @@ var IsoBmffTimingResolver = class {
18007
18397
  if (!samples || !samples[index]) return false;
18008
18398
  samples[index] = null;
18009
18399
  this.#pendingSamples--;
18010
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18400
+ if (samples.every((value) => value === null)) {
18401
+ this.#fragments.delete(key);
18402
+ const first = this.#firstSamples.get(key);
18403
+ this.#firstSamples.delete(key);
18404
+ this.#delivered.delete(key);
18405
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18406
+ if (this.#delivered.size > this.maxPendingSamples) {
18407
+ this.#delivered.delete(this.#delivered.keys().next().value);
18408
+ }
18409
+ }
18011
18410
  return true;
18012
18411
  }
18013
18412
  reset() {
18014
18413
  this.#track = null;
18015
18414
  this.#fragments.clear();
18415
+ this.#firstSamples.clear();
18416
+ this.#delivered.clear();
18016
18417
  this.#pendingSamples = 0;
18017
18418
  }
18018
18419
  };
@@ -18182,6 +18583,9 @@ function authoredGap(before, after) {
18182
18583
  function ssIdAfter(a, b) {
18183
18584
  return (a - b | 0) > 0;
18184
18585
  }
18586
+ function holdsRebuilt(stream) {
18587
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18588
+ }
18185
18589
  var FecReleaseSequencer = class {
18186
18590
  #streams = /* @__PURE__ */ new Map();
18187
18591
  #gate;
@@ -18244,6 +18648,7 @@ var FecReleaseSequencer = class {
18244
18648
  stream.lastSbn = void 0;
18245
18649
  stream.discontinuity = false;
18246
18650
  stream.rapSsId = void 0;
18651
+ stream.sourced = false;
18247
18652
  }
18248
18653
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18249
18654
  this.#recordStat("droppedStale");
@@ -18270,6 +18675,7 @@ var FecReleaseSequencer = class {
18270
18675
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18271
18676
  else hi = mid;
18272
18677
  }
18678
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18273
18679
  stream.buf.splice(lo, 0, frame);
18274
18680
  this.#drainUnlessDeferred(stream);
18275
18681
  }
@@ -18305,6 +18711,41 @@ var FecReleaseSequencer = class {
18305
18711
  }
18306
18712
  return false;
18307
18713
  }
18714
+ /**
18715
+ * These streams' sink is being replaced by one that has seen no source
18716
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
18717
+ * release to the sink being replaced the frames the unanchored path had
18718
+ * already released to it (#cursorPassed): there they waited in its queue,
18719
+ * which its replacement flushes. A stream that holds none is not drained:
18720
+ * it holds nothing the anchor kept back, and a drain would still force out
18721
+ * a frame held past the hold limit, which before the anchor waited for the
18722
+ * next push. Then treat the streams as unsourced until a source frame is
18723
+ * released.
18724
+ */
18725
+ unsource(packetId, catalogTrackName) {
18726
+ for (const stream of this.#streams.values()) {
18727
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18728
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
18729
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
18730
+ else this.#drain(stream, true);
18731
+ }
18732
+ stream.sourced = false;
18733
+ }
18734
+ }
18735
+ }
18736
+ /**
18737
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
18738
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
18739
+ * packetId.
18740
+ */
18741
+ holdsRebuilt(packetId, catalogTrackName) {
18742
+ for (const stream of this.#streams.values()) {
18743
+ if (stream.packetId !== packetId) continue;
18744
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
18745
+ if (holdsRebuilt(stream)) return true;
18746
+ }
18747
+ return false;
18748
+ }
18308
18749
  /** Re-evaluate held gaps after FEC state changed. */
18309
18750
  poke(streamKey) {
18310
18751
  if (streamKey !== void 0) {
@@ -18396,16 +18837,32 @@ var FecReleaseSequencer = class {
18396
18837
  last: null,
18397
18838
  lastSbn: void 0,
18398
18839
  discontinuity: false,
18399
- rapSsId: void 0
18840
+ rapSsId: void 0,
18841
+ sourced: false,
18842
+ draining: 0
18400
18843
  };
18401
18844
  this.#streams.set(streamKey, stream);
18402
18845
  }
18403
18846
  return stream;
18404
18847
  }
18405
- #drain(stream) {
18848
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18849
+ #drain(stream, asUnanchored = false) {
18850
+ stream.draining++;
18851
+ try {
18852
+ this.#drainLoop(stream, asUnanchored);
18853
+ } finally {
18854
+ stream.draining--;
18855
+ }
18856
+ }
18857
+ #drainLoop(stream, asUnanchored) {
18406
18858
  while (stream.buf.length > 0 && stream.last !== null) {
18407
18859
  const head = stream.buf[0];
18408
18860
  const last = stream.last;
18861
+ if (compareFrameOrder(head, last) <= 0) {
18862
+ stream.buf.shift();
18863
+ this.#release(stream, head, void 0);
18864
+ continue;
18865
+ }
18409
18866
  if (stream.discontinuity) {
18410
18867
  stream.discontinuity = false;
18411
18868
  stream.buf.shift();
@@ -18419,9 +18876,10 @@ var FecReleaseSequencer = class {
18419
18876
  }
18420
18877
  const headSbn = head.meta.fecSbn;
18421
18878
  const from = stream.lastSbn ?? headSbn;
18422
- const to = headSbn ?? stream.lastSbn;
18879
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18423
18880
  let lost = [];
18424
18881
  const unanchored = from === void 0 && to === void 0;
18882
+ let unresolved = false;
18425
18883
  if (from !== void 0 && to !== void 0) {
18426
18884
  const resolved = this.#gate.resolvedLoss(
18427
18885
  stream.packetId,
@@ -18430,7 +18888,8 @@ var FecReleaseSequencer = class {
18430
18888
  Math.max(from, to),
18431
18889
  from
18432
18890
  );
18433
- if (resolved === null) {
18891
+ unresolved = resolved === null;
18892
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18434
18893
  const heldMs = performance.now() - head.arrivedAtMs;
18435
18894
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18436
18895
  this.#recordStat("forcedWindowFlushes");
@@ -18438,22 +18897,56 @@ var FecReleaseSequencer = class {
18438
18897
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18439
18898
  continue;
18440
18899
  }
18441
- lost = resolved;
18900
+ lost = resolved ?? [];
18442
18901
  }
18443
18902
  stream.buf.shift();
18444
18903
  this.#release(
18445
18904
  stream,
18446
18905
  head,
18447
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18906
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18448
18907
  );
18449
18908
  }
18450
18909
  }
18910
+ /**
18911
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
18912
+ * source cursor holds it unanchored, and neither does a later frame behind
18913
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
18914
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
18915
+ * like each frame that arrived in front of it since (stale there). The head
18916
+ * then goes, decided as it would be unanchored, once a frame the base path
18917
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
18918
+ * marked one: the base path has passed that one, so a frame still missing
18919
+ * before it can only come from a repair. It also goes while no source frame
18920
+ * has been released, when the direct path shows a recovered frame at once.
18921
+ * And it goes at once while a staged switch decides on when frames arrive
18922
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18923
+ * sink being replaced: unanchored, it had already reached the sink.
18924
+ * The scan also writes: the `passed` marks it sets stay on the frames,
18925
+ * and push marks a frame it inserts in front of a marked one from them.
18926
+ */
18927
+ #cursorPassed(stream, asUnanchored) {
18928
+ let at2 = -1;
18929
+ for (let i = 0; i < stream.buf.length; i++) {
18930
+ const frame = stream.buf[i];
18931
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
18932
+ frame.passed = true;
18933
+ if (at2 < 0) at2 = i;
18934
+ } else if (frame.passed !== true) break;
18935
+ }
18936
+ if (at2 < 0) return false;
18937
+ 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));
18938
+ }
18451
18939
  #release(stream, frame, decision) {
18940
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18941
+ if (stale) this.#recordStat("droppedStale");
18452
18942
  if (decision) frame.meta.gapBefore = decision;
18453
18943
  stream.last = frame;
18454
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
18944
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
18945
+ stream.lastSbn = frame.meta.fecSbn;
18946
+ }
18947
+ if (frame.meta.recovered !== true) stream.sourced = true;
18455
18948
  this.#gate.onReleased?.(frame.data, frame.meta);
18456
- this.#emit(frame.data, frame.meta);
18949
+ if (!stale) this.#emit(frame.data, frame.meta);
18457
18950
  }
18458
18951
  };
18459
18952
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18494,6 +18987,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18494
18987
  this.#reclaimedTimingSamples = 0;
18495
18988
  this.#evictedTimingSamples = 0;
18496
18989
  this.#rejectedMovieFragments = 0;
18990
+ this.#recoveredHeadsAwaitingTiming = {
18991
+ held: 0,
18992
+ expired: 0,
18993
+ discarded: 0,
18994
+ superseded: 0
18995
+ };
18497
18996
  this.#sequencers = /* @__PURE__ */ new Map();
18498
18997
  this.#fecGate = null;
18499
18998
  this.#fecSequencer = null;
@@ -18602,6 +19101,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18602
19101
  #reclaimedTimingSamples;
18603
19102
  #evictedTimingSamples;
18604
19103
  #rejectedMovieFragments;
19104
+ #recoveredHeadsAwaitingTiming;
18605
19105
  #sequencers;
18606
19106
  #fecGate;
18607
19107
  #fecSequencer;
@@ -18707,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18707
19207
  /**
18708
19208
  * Typed view of the ISO-BMFF timing-sample budget.
18709
19209
  *
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.
19210
+ * Each stream admits timing against its own window of `capacity` slots
19211
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
19212
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
19213
+ * timing a stream declared and never delivered shows up there. `pending`
19214
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
19215
+ * a producer-owned counter -- never infer budget state by parsing logs.
18715
19216
  */
18716
19217
  get timingBudgetStats() {
18717
19218
  return {
@@ -18722,6 +19223,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18722
19223
  rejectedMovieFragments: this.#rejectedMovieFragments
18723
19224
  };
18724
19225
  }
19226
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
19227
+ get recoveredHeadsAwaitingTiming() {
19228
+ let pending = 0;
19229
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19230
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
19231
+ }
19232
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
19233
+ }
18725
19234
  get reorderStats() {
18726
19235
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18727
19236
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18763,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18763
19272
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18764
19273
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18765
19274
  }
19275
+ /** See FecReleaseSequencer.unsource. */
19276
+ unsourceFecRelease(packetId, catalogTrackName) {
19277
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
19278
+ }
19279
+ /** See FecReleaseSequencer.holdsRebuilt. */
19280
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
19281
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
19282
+ }
18766
19283
  /** Re-evaluate held gaps after FEC block state changed. */
18767
19284
  pokeFecRelease(packetId, catalogTrackName) {
18768
19285
  if (!this.#fecSequencer) return;
@@ -18850,10 +19367,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18850
19367
  get reassemblerDataUnitsReturned() {
18851
19368
  return this.#reassemblerDataUnitsReturned;
18852
19369
  }
18853
- #countOutcome(token) {
18854
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18855
- this.#feedOutcomes.set(token, count);
18856
- this.#lastOutcome = token;
19370
+ #countOutcome(token2) {
19371
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19372
+ this.#feedOutcomes.set(token2, count);
19373
+ this.#lastOutcome = token2;
18857
19374
  this.#lastOutcomeCount = count;
18858
19375
  }
18859
19376
  #countDeferredOutcome(reason) {
@@ -18895,7 +19412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18895
19412
  const now = Date.now();
18896
19413
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18897
19414
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18898
- awaitingTiming.shift();
19415
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18899
19416
  }
18900
19417
  }
18901
19418
  }
@@ -19013,11 +19530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19013
19530
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19014
19531
  return this.#drop("fragment-unexpected-fragmentation");
19015
19532
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19016
- if (!resolver2) return this.#drop("fragment-no-resolver");
19533
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19017
19534
  const pendingBefore = resolver2.pendingSampleCount;
19018
19535
  const evictedBefore = resolver2.evictedSampleCount;
19019
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19020
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19537
+ const admitted = resolver2.addMovieFragment(
19538
+ mpu.mpuSequenceNumber,
19539
+ data,
19540
+ remaining,
19541
+ options?.recovered === true
19542
+ );
19021
19543
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19022
19544
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19023
19545
  if (!admitted) {
@@ -19037,7 +19559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19037
19559
  };
19038
19560
  }
19039
19561
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19040
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19562
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19041
19563
  if (!resolver) return this.#drop("mfu-no-resolver");
19042
19564
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19043
19565
  return this.#drop("mfu-empty-payload");
@@ -19058,6 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19058
19580
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19059
19581
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19060
19582
  sampleNumber: mfuDuHeader.sampleNumber,
19583
+ sequenceNumber: info.sequenceNumber,
19061
19584
  arrivedAt: Date.now()
19062
19585
  });
19063
19586
  return this.#drop("mfu-sample-timing-missing");
@@ -19083,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19083
19606
  info,
19084
19607
  resolver,
19085
19608
  options?.recovered === true,
19086
- options?.fecSbn
19609
+ options?.fecSbn,
19610
+ options?.fecSbnFirst
19087
19611
  );
19088
19612
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19089
19613
  }
@@ -19176,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19176
19700
  info,
19177
19701
  resolver,
19178
19702
  options?.recovered === true,
19179
- options?.fecSbn
19703
+ options?.fecSbn,
19704
+ options?.fecSbnFirst
19180
19705
  );
19181
19706
  if (outcome.record !== void 0) return outcome.record;
19182
19707
  return this.#drop(outcome.reason);
@@ -19196,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19196
19721
  * attribution exists to keep readable. Leaving the choice at the call site
19197
19722
  * is what keeps both true.
19198
19723
  */
19199
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19724
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19200
19725
  const timing = resolver.sampleTiming(
19201
19726
  combined.mpuSequenceNumber,
19202
19727
  combined.movieFragmentSequenceNumber,
@@ -19213,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19213
19738
  );
19214
19739
  if (!meta) return { reason: "combined-meta-build-failed" };
19215
19740
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19741
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19216
19742
  if (!resolver.consumeSample(
19217
19743
  combined.mpuSequenceNumber,
19218
19744
  combined.movieFragmentSequenceNumber,
@@ -19381,6 +19907,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19381
19907
  }
19382
19908
  this.#emitFrame(data, meta);
19383
19909
  }
19910
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19911
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19912
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19913
+ }
19384
19914
  /**
19385
19915
  * Claim and return the authored timing for a recovered sample. Recovery does
19386
19916
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19442,6 +19972,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19442
19972
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19443
19973
  this.#sequencers.clear();
19444
19974
  this.#fecSequencer?.discard();
19975
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19976
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19977
+ }
19445
19978
  this.#timingStreams.clear();
19446
19979
  this.#pendingTimingSamples = 0;
19447
19980
  this.#reliableMoqStartupGate.clear();
@@ -19460,10 +19993,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19460
19993
  return resolver;
19461
19994
  }
19462
19995
  #awaitTiming(streamKey, head) {
19463
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19464
- if (!awaitingTiming) return;
19996
+ const state = this.#timingStreams.get(streamKey);
19997
+ if (!state) return;
19998
+ if (!head.options?.recovered) {
19999
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
20000
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
20001
+ state.awaitingTiming = kept;
20002
+ }
20003
+ const awaitingTiming = state.awaitingTiming;
19465
20004
  awaitingTiming.push(head);
19466
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
20005
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
20006
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
20007
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
20008
+ }
20009
+ }
20010
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
20011
+ #leaveAwaitingTiming(head, outcome) {
20012
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19467
20013
  }
19468
20014
  /**
19469
20015
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19491,6 +20037,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19491
20037
  const state = this.#timingStreams.get(streamKey);
19492
20038
  if (!state) return;
19493
20039
  this.#timingStreams.delete(streamKey);
20040
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19494
20041
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19495
20042
  if (this.#pendingTimingSamples < 0) {
19496
20043
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22697,7 +23244,18 @@ function keyframeOverlaySsId(header) {
22697
23244
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22698
23245
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22699
23246
  }
23247
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23248
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23249
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23250
+ }
23251
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23252
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23253
+ return headerPsn + offset >>> 0;
23254
+ }
23255
+ var OVERLAY_ROUTED_LIMIT = 1024;
23256
+ var OVERLAY_PENDING_LIMIT = 256;
22700
23257
  var OVERLAY_OFFSET_MPUS = 16;
23258
+ var OVERLAY_REBUILT_LIMIT = 256;
22701
23259
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22702
23260
  function assertStreamIdentity(role, stream) {
22703
23261
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -22808,6 +23366,7 @@ var MmtpRouter = class {
22808
23366
  #provenance = /* @__PURE__ */ new Map();
22809
23367
  #onFlowProtected = null;
22810
23368
  #unadmittedListedDrops = 0;
23369
+ #admittedSourceDatagrams = 0;
22811
23370
  #unlistedRepairSetAside = 0;
22812
23371
  #assistBlocks = /* @__PURE__ */ new Map();
22813
23372
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22864,6 +23423,26 @@ var MmtpRouter = class {
22864
23423
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22865
23424
  */
22866
23425
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23426
+ /**
23427
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23428
+ * sender's own packet_sequence_number: a recovered packet's header psn
23429
+ * equal to its SS_ID minus that offset. While it equals the stream's
23430
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23431
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23432
+ * an MPU no member of which arrived).
23433
+ */
23434
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23435
+ /**
23436
+ * The same, shown by the overlay alone, before any base recovery did: a
23437
+ * packet it rebuilt whose base copy carries its protected psn
23438
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
23439
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
23440
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
23441
+ * make the base drop a late base copy of a keyframe the overlay never
23442
+ * finished, and lift a block's late-source cut, which nothing before
23443
+ * this offset did.
23444
+ */
23445
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22867
23446
  /**
22868
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22869
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -22933,7 +23512,9 @@ var MmtpRouter = class {
22933
23512
  recovered: 0,
22934
23513
  restarts: 0,
22935
23514
  unsequencedDropped: 0,
22936
- protectedDropped: 0
23515
+ protectedDropped: 0,
23516
+ baseFed: 0,
23517
+ baseUndetermined: 0
22937
23518
  };
22938
23519
  /** Callback fired when a delta-track FEC block completes or fails. */
22939
23520
  #onDeltaBlockCompleteCb = null;
@@ -23106,7 +23687,7 @@ var MmtpRouter = class {
23106
23687
  const cb = this.#getFrameCallback(packetId);
23107
23688
  if (isVideo && !assembler) {
23108
23689
  if (frameMeta.recovered) {
23109
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23690
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23110
23691
  } else {
23111
23692
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23112
23693
  }
@@ -23139,13 +23720,18 @@ var MmtpRouter = class {
23139
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23140
23721
  }
23141
23722
  };
23142
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23143
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23144
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23145
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23146
23727
  }
23147
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23148
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23728
+ if (span === void 0) {
23729
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23730
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23731
+ } else {
23732
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23733
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23734
+ }
23149
23735
  };
23150
23736
  }
23151
23737
  }
@@ -23190,8 +23776,9 @@ var MmtpRouter = class {
23190
23776
  }
23191
23777
  /**
23192
23778
  * 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.
23779
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
23780
+ * or, rebuilt by the overlay, than their base copy carries: a hop
23781
+ * re-stamped the sequence after the sender protected the packet.
23195
23782
  */
23196
23783
  get restampedRecoveredSequenceCount() {
23197
23784
  return this.#restampedRecoveredSeqs;
@@ -23370,6 +23957,7 @@ var MmtpRouter = class {
23370
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23371
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23372
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23373
23961
  if (previous) {
23374
23962
  previous.flush();
23375
23963
  previous.dispose();
@@ -23496,6 +24084,10 @@ var MmtpRouter = class {
23496
24084
  get keyframeOverlayStats() {
23497
24085
  return { ...this.#overlayStats };
23498
24086
  }
24087
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
24088
+ get recoveredHeadsAwaitingTiming() {
24089
+ return this.#parser.recoveredHeadsAwaitingTiming;
24090
+ }
23499
24091
  #isOverlayRepair(repairPacketId, repairTrackName) {
23500
24092
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23501
24093
  }
@@ -23516,7 +24108,8 @@ var MmtpRouter = class {
23516
24108
  fec,
23517
24109
  k,
23518
24110
  offsets: /* @__PURE__ */ new Map(),
23519
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23520
24113
  };
23521
24114
  this.#overlays.set(repairPacketId, inst);
23522
24115
  return () => {
@@ -23546,12 +24139,18 @@ var MmtpRouter = class {
23546
24139
  */
23547
24140
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23548
24141
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
24142
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
24143
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
24144
+ stream.overlayToBase.routed.clear();
24145
+ stream.overlayToBase.pending.clear();
24146
+ }
23549
24147
  }
23550
24148
  #resetOverlay(inst) {
23551
24149
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23552
24150
  inst.last = void 0;
23553
24151
  inst.offsets.clear();
23554
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23555
24154
  }
23556
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23557
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23602,18 +24201,33 @@ var MmtpRouter = class {
23602
24201
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23603
24202
  * every overlay installed for its source, at that SS_ID, with the bytes the
23604
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23605
- * included (7.1, 7.4.3).
23606
- */
23607
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24204
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
24205
+ * FEC reads it.
24206
+ *
24207
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24208
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24209
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24210
+ * the stream unstamped at the SS_ID offset it states, for the release gate
24211
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
24212
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
24213
+ */
24214
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23608
24215
  if (this.#overlays.size === 0) return;
23609
24216
  const ssId = keyframeOverlaySsId(header);
23610
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23611
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23612
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23613
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23614
24224
  if (identity !== null) {
23615
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
23616
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
24226
+ inst.offsets.set(identity.mpuSequenceNumber, {
24227
+ psnOffset: ssId - sequenceNumber >>> 0,
24228
+ overlaySsId: ssId,
24229
+ ...baseSsId === void 0 ? {} : { baseSsId }
24230
+ });
23617
24231
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23618
24232
  }
23619
24233
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23635,6 +24249,14 @@ var MmtpRouter = class {
23635
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23636
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23637
24251
  }
24252
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
24253
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
24254
+ if (rebuiltPsn !== sequenceNumber) {
24255
+ this.#restampedRecoveredSeqs++;
24256
+ } else if (baseSsId !== void 0) {
24257
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24258
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24259
+ }
23638
24260
  }
23639
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23640
24262
  inst.moqBlocks.add(blockStart);
@@ -23668,11 +24290,16 @@ var MmtpRouter = class {
23668
24290
  }
23669
24291
  /**
23670
24292
  * 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.
24293
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
24294
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
24295
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
24296
+ * base FEC recovered does, so it waits behind base frames still in flight,
24297
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
24298
+ * longer than unanchored; one whose base SS_ID is undetermined has no
24299
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
24300
+ * advance. Each packet's wire sequence is restored from its keyframe's own
24301
+ * offset; without one, a stream the base never re-stamped keeps the
24302
+ * header's sequence and any other drops the packet.
23676
24303
  */
23677
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
23678
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23692,22 +24319,139 @@ var MmtpRouter = class {
23692
24319
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23693
24320
  const restamped = this.#restampedStreams.has(streamKey);
23694
24321
  const restored = /* @__PURE__ */ new Set();
24322
+ const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
23695
24324
  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;
24325
+ const overlaySsId = serial32Add(blockStart, sym.index);
24326
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24327
+ if (anchor !== void 0) {
24328
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23699
24329
  restored.add(packetSequenceNumber);
23700
- return [{ ...sym, packetSequenceNumber }];
24330
+ const out = { ...sym, packetSequenceNumber };
24331
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24332
+ if (baseSsId !== void 0) {
24333
+ baseSsIds.set(sym.index, baseSsId);
24334
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24335
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
24336
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24337
+ }
24338
+ }
24339
+ return [out];
24340
+ }
24341
+ if (!restamped) {
24342
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24343
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24344
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24345
+ if (baseSsId !== void 0) {
24346
+ baseSsIds.set(sym.index, baseSsId);
24347
+ if (fedSsId === void 0) gateOnly.add(sym.index);
24348
+ } else {
24349
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24350
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24351
+ }
24352
+ return [sym];
23701
24353
  }
23702
- if (!restamped) return [sym];
23703
24354
  this.#overlayStats.unsequencedDropped++;
23704
24355
  return [];
23705
24356
  });
23706
24357
  this.#overlayStats.recovered++;
24358
+ const mpuFirst = /* @__PURE__ */ new Map();
24359
+ for (const sym of symbols) {
24360
+ const ssId = baseSsIds.get(sym.index);
24361
+ const mpu = sym.fragment.mpuSequenceNumber;
24362
+ const first = mpuFirst.get(mpu);
24363
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
24364
+ }
24365
+ const firstSsIds = /* @__PURE__ */ new Map();
24366
+ for (const sym of symbols) {
24367
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
24368
+ }
23707
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23708
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
23709
24373
  });
23710
24374
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24375
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24376
+ }
24377
+ /**
24378
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24379
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24380
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24381
+ * base recovery showed; `baseSsIds` holds no other),
24382
+ * so it is fed to the base decoder as a rebuilt source
24383
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24384
+ * fewer to solve, and its repair can recover the block's other packets.
24385
+ * A base block no repair has stated yet takes it when one does
24386
+ * (#feedPendingRebuilt).
24387
+ *
24388
+ * The overlay route already handled each packet fed (delivered it, or
24389
+ * held an untimed head for its FT=1), so a base recovery routes none of
24390
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24391
+ * held head would wait in the parser for timing its first copy consumed.
24392
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24393
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24394
+ * that crossed no multicast socket, which the provenance counted from the
24395
+ * block's own arrivals cannot show.
24396
+ */
24397
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24398
+ const fec = this.#fecManager;
24399
+ const owner = inst.leg.sourcePacketId;
24400
+ const name = inst.leg.sourceCatalogTrackName;
24401
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24402
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24403
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24404
+ const feeds = [];
24405
+ for (const sym of symbols) {
24406
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24407
+ const ssId = baseSsIds.get(sym.index);
24408
+ if (ssId === void 0) {
24409
+ this.#overlayStats.baseUndetermined++;
24410
+ continue;
24411
+ }
24412
+ routed.delete(ssId);
24413
+ routed.add(ssId);
24414
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24415
+ feeds.push([ssId, sym.rawSymbol]);
24416
+ }
24417
+ for (const [ssId, symbolData] of feeds) {
24418
+ if (this.#classifyPacketId(owner) === null) return;
24419
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24420
+ }
24421
+ }
24422
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24423
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24424
+ const fec = this.#fecManager;
24425
+ if (!fec?.feedRebuiltSourceSymbol) return;
24426
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24427
+ if (coords == null) {
24428
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24429
+ pending.delete(ssId);
24430
+ pending.set(ssId, symbolData.slice());
24431
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24432
+ return;
24433
+ }
24434
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24435
+ if (!fed.taken) return;
24436
+ this.#overlayStats.baseFed++;
24437
+ if (fed.recovered === null) return;
24438
+ const sbn = coords.sbn;
24439
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24440
+ fec.cancelBlockTimeout(sbn, owner, name);
24441
+ fec.countRecovery(owner, sbn, name);
24442
+ fec.markBlockCompleted(sbn, owner, name);
24443
+ });
24444
+ }
24445
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24446
+ #feedPendingRebuilt(owner, name, block) {
24447
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24448
+ if (pending === void 0 || pending.size === 0) return;
24449
+ for (const [ssId, symbolData] of [...pending]) {
24450
+ if (this.#classifyPacketId(owner) === null) return;
24451
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24452
+ pending.delete(ssId);
24453
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24454
+ }
23711
24455
  }
23712
24456
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23713
24457
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23847,6 +24591,10 @@ var MmtpRouter = class {
23847
24591
  get unadmittedListedDropCount() {
23848
24592
  return this.#unadmittedListedDrops;
23849
24593
  }
24594
+ /** 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. */
24595
+ get admittedSourceDatagramCount() {
24596
+ return this.#admittedSourceDatagrams;
24597
+ }
23850
24598
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23851
24599
  get unlistedRepairSetAsideCount() {
23852
24600
  return this.#unlistedRepairSetAside;
@@ -24333,6 +25081,8 @@ var MmtpRouter = class {
24333
25081
  )) return;
24334
25082
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24335
25083
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
25084
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
25085
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24336
25086
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24337
25087
  const recovered = fec.feedRepairSymbol(
24338
25088
  ownerPacketId,
@@ -24383,7 +25133,7 @@ var MmtpRouter = class {
24383
25133
  catalogTrackName
24384
25134
  )) continue;
24385
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24386
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24387
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24388
25138
  }
24389
25139
  if (payload.oversized) continue;
@@ -24470,6 +25220,8 @@ var MmtpRouter = class {
24470
25220
  this.#droppedPreSyncVideo.delete(streamKey);
24471
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24472
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25223
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24473
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24474
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24475
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24596,6 +25348,12 @@ var MmtpRouter = class {
24596
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24597
25349
  * own RAP cadence, so no timer is involved.
24598
25350
  *
25351
+ * While the switch is staged, the FEC release gate lets a keyframe the
25352
+ * overlay rebuilt reach it, on either side, when the keyframe did before
25353
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
25354
+ * decides on what each side delivered, and the commit discards the gate's
25355
+ * frames of the outgoing stream.
25356
+ *
24599
25357
  * The commit is one synchronous transition inside the frame path: the
24600
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
24601
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24646,6 +25404,9 @@ var MmtpRouter = class {
24646
25404
  droppedIncomingFrames: 0
24647
25405
  };
24648
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
24649
25410
  return {
24650
25411
  cancel: () => {
24651
25412
  if (this.#staged !== state) return false;
@@ -24728,7 +25489,17 @@ var MmtpRouter = class {
24728
25489
  */
24729
25490
  #routeParsedFrame(data, meta) {
24730
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
24731
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
25492
+ if (this.#staged !== null) {
25493
+ let consumed = false;
25494
+ this.#fecPokeDepth++;
25495
+ try {
25496
+ consumed = this.#interceptSwitchFrame(data, meta);
25497
+ } finally {
25498
+ this.#fecPokeDepth--;
25499
+ this.#flushPendingFecPokes();
25500
+ }
25501
+ if (consumed) return;
25502
+ }
24732
25503
  this.#deliverParsedFrame(data, meta);
24733
25504
  }
24734
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -24880,12 +25651,16 @@ var MmtpRouter = class {
24880
25651
  this.#parser.deferFecRelease(work);
24881
25652
  } finally {
24882
25653
  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
- }
25654
+ this.#flushPendingFecPokes();
25655
+ }
25656
+ }
25657
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
25658
+ #flushPendingFecPokes() {
25659
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25660
+ const pokes = [...this.#pendingFecPokes.values()];
25661
+ this.#pendingFecPokes.clear();
25662
+ for (const poke of pokes) {
25663
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
24889
25664
  }
24890
25665
  }
24891
25666
  }
@@ -24963,6 +25738,14 @@ var MmtpRouter = class {
24963
25738
  onRap: (packetId, catalogTrackName, ssId) => {
24964
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
24965
25740
  },
25741
+ // A staged switch commits on when each side's frames reach it
25742
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
25743
+ // they did unanchored.
25744
+ releaseUnanchored: (packetId, catalogTrackName) => {
25745
+ const staged = this.#staged;
25746
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
25747
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
25748
+ },
24966
25749
  onReleased: (data, meta) => {
24967
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
24968
25751
  const timing = meta.authoredTiming;
@@ -25104,6 +25887,7 @@ var MmtpRouter = class {
25104
25887
  const parsed = this.#parser.parsePacket(packet);
25105
25888
  if (!parsed) return;
25106
25889
  const info = parsed.info;
25890
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25107
25891
  if (source === "multicast" && !manifestAdmitted) {
25108
25892
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25109
25893
  }
@@ -25385,7 +26169,8 @@ var MmtpRouter = class {
25385
26169
  parsed.header,
25386
26170
  info.sequenceNumber,
25387
26171
  info.rapFlag,
25388
- source
26172
+ source,
26173
+ fecPayloadId?.ssId
25389
26174
  );
25390
26175
  }
25391
26176
  }
@@ -25411,7 +26196,8 @@ var MmtpRouter = class {
25411
26196
  source: parserOptions?.source,
25412
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25413
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25414
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25415
26201
  };
25416
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25417
26203
  else this.#parser.feed(packet, feedOptions);
@@ -25535,7 +26321,8 @@ var MmtpRouter = class {
25535
26321
  if (stream === void 0) {
25536
26322
  stream = {
25537
26323
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25538
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26324
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26325
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25539
26326
  };
25540
26327
  byName.set(catalogTrackName, stream);
25541
26328
  }
@@ -25650,8 +26437,16 @@ var MmtpRouter = class {
25650
26437
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25651
26438
  this.#emitDirectVideo(packetId, data, meta, cb);
25652
26439
  }
25653
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26440
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25654
26441
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26442
+ if (this.#isFecTrack(packetId)) {
26443
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26444
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26445
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26446
+ this.#emitDirectVideo(packetId, data, meta, cb);
26447
+ return;
26448
+ }
26449
+ }
25655
26450
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25656
26451
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25657
26452
  this.#drainDirectRecoveredVideo(
@@ -25805,6 +26600,13 @@ var MmtpRouter = class {
25805
26600
  * frames by epoch only if pre-protection input ever matters.
25806
26601
  */
25807
26602
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26603
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26604
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26605
+ result = {
26606
+ ...result,
26607
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26608
+ };
26609
+ }
25808
26610
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25809
26611
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25810
26612
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25861,7 +26663,12 @@ var MmtpRouter = class {
25861
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25862
26664
  if (ownerPacketId == null) return;
25863
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25864
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26666
+ const fecSbnOf = (index) => {
26667
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26668
+ const fecSbn = overlay.baseSsIds.get(index);
26669
+ const fecSbnFirst = overlay.firstSsIds.get(index);
26670
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
26671
+ };
25865
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25866
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25867
26674
  if (ssIdSeqOffset !== void 0) {
@@ -25869,7 +26676,10 @@ var MmtpRouter = class {
25869
26676
  ...result,
25870
26677
  symbols: result.symbols.map((sym) => {
25871
26678
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25872
- if (wire === sym.packetSequenceNumber) return sym;
26679
+ if (wire === sym.packetSequenceNumber) {
26680
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26681
+ return sym;
26682
+ }
25873
26683
  this.#restampedRecoveredSeqs++;
25874
26684
  this.#restampedStreams.add(streamKey);
25875
26685
  return { ...sym, packetSequenceNumber: wire };
@@ -25905,6 +26715,29 @@ var MmtpRouter = class {
25905
26715
  }
25906
26716
  );
25907
26717
  }
26718
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26719
+ ownerPacketId,
26720
+ sym.fragment.mpuSequenceNumber,
26721
+ sym.fragment.movieFragmentSequenceNumber,
26722
+ sym.fragment.sampleNumber,
26723
+ catalogTrackName
26724
+ );
26725
+ for (const sym of result.symbols) {
26726
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26727
+ this.#feedViaParser(
26728
+ sym.rawSymbol,
26729
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26730
+ void 0,
26731
+ {
26732
+ skipFecPayloadId: true,
26733
+ recovered: true,
26734
+ deliveryProvenance,
26735
+ ...fecSbnOf(sym.index),
26736
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26737
+ }
26738
+ );
26739
+ }
26740
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25908
26741
  const gated = this.#isFecTrack(ownerPacketId);
25909
26742
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25910
26743
  if (assembler) {
@@ -27631,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
27631
28464
  function isSymbolCounts(value) {
27632
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
27633
28466
  }
28467
+ function isSpanFailure(value) {
28468
+ if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
28469
+ const length = value["length"];
28470
+ const sources = value["sources"];
28471
+ return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
28472
+ }
27634
28473
  function isNullableNonNegative(value) {
27635
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
27636
28475
  }
@@ -27650,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
27650
28489
  case "recovered":
27651
28490
  return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
27652
28491
  case "failed":
27653
- return hasExactKeys(value, ["version", "type", "block", "blockClass", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]);
28492
+ return hasExactKeys(
28493
+ value,
28494
+ "span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
28495
+ ) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
27654
28496
  case "repair-preempted":
27655
28497
  case "repair-deferred":
27656
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -27916,7 +28758,7 @@ var MulticastManager = class {
27916
28758
  onPublisherRestart: (packetId, catalogTrackName) => {
27917
28759
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
27918
28760
  },
27919
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28761
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
27920
28762
  const pathCount = this.#fec.blockPathCount(
27921
28763
  sbn,
27922
28764
  packetId,
@@ -27934,7 +28776,8 @@ var MulticastManager = class {
27934
28776
  symbols: {
27935
28777
  multicast: pathCount?.multicastSymbols ?? 0,
27936
28778
  moq: pathCount?.moqSymbols ?? 0
27937
- }
28779
+ },
28780
+ ...span === void 0 ? {} : { span }
27938
28781
  });
27939
28782
  },
27940
28783
  onFrameGap: (decision) => {
@@ -29314,10 +30157,12 @@ var MulticastManager = class {
29314
30157
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29315
30158
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29316
30159
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29317
- * once its range FETCH ends (or none is sent).
30160
+ * once its range FETCH ends (or none is sent). `onExpired` is told of a
30161
+ * pending hole the stream runs past the locator's horizon: returning true,
30162
+ * the caller closes it (`closeIsoHoles`); otherwise it is discarded.
29318
30163
  */
29319
- onUndelimitedLoss(cb) {
29320
- this.#fec.setUndelimitedLossHandler(cb);
30164
+ onUndelimitedLoss(cb, onExpired) {
30165
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29321
30166
  }
29322
30167
  /**
29323
30168
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -29553,7 +30398,8 @@ var MulticastManager = class {
29553
30398
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
29554
30399
  ...event,
29555
30400
  block: Object.freeze({ ...event.block }),
29556
- ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
30401
+ ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
30402
+ ..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
29557
30403
  }
29558
30404
  );
29559
30405
  if (this.#logRepairLifecycle) {
@@ -29666,7 +30512,7 @@ async function setupFecWorkerForTrack(opts) {
29666
30512
  blockProfile,
29667
30513
  k: opts.fecK,
29668
30514
  // 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.
30515
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29670
30516
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29671
30517
  symbolSize: opts.fecSymbolSize,
29672
30518
  interleaveDepth: interleaveDepthBlocks,
@@ -30208,24 +31054,24 @@ var MediaRouter = class {
30208
31054
  if (!this.#multicastVideoKeyed) return;
30209
31055
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30210
31056
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30211
- const token = ++this.#sourceSwitchToken;
31057
+ const token2 = ++this.#sourceSwitchToken;
30212
31058
  const maybeReady = this.#onSourceSwitch?.("multicast");
30213
31059
  if (!maybeReady || typeof maybeReady.then !== "function") {
30214
- this.#commitMulticastActivation(token);
31060
+ this.#commitMulticastActivation(token2);
30215
31061
  return;
30216
31062
  }
30217
31063
  const ready = Promise.resolve(maybeReady);
30218
- this.#pendingMulticastToken = token;
31064
+ this.#pendingMulticastToken = token2;
30219
31065
  this.#sourceSwitchReady = ready;
30220
31066
  void ready.then(
30221
- () => this.#commitMulticastActivation(token),
30222
- (err) => this.#rejectMulticastActivation(token, err)
31067
+ () => this.#commitMulticastActivation(token2),
31068
+ (err) => this.#rejectMulticastActivation(token2, err)
30223
31069
  ).finally(() => {
30224
31070
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30225
31071
  });
30226
31072
  }
30227
- #commitMulticastActivation(token) {
30228
- if (token !== this.#sourceSwitchToken) return;
31073
+ #commitMulticastActivation(token2) {
31074
+ if (token2 !== this.#sourceSwitchToken) return;
30229
31075
  this.#active = "multicast";
30230
31076
  this.#suppressed = true;
30231
31077
  this.#pendingMulticastToken = null;
@@ -30236,9 +31082,9 @@ var MediaRouter = class {
30236
31082
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30237
31083
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30238
31084
  }
30239
- #rejectMulticastActivation(token, err) {
30240
- if (token !== this.#sourceSwitchToken) return;
30241
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
31085
+ #rejectMulticastActivation(token2, err) {
31086
+ if (token2 !== this.#sourceSwitchToken) return;
31087
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30242
31088
  this.#pendingMulticastVideo = null;
30243
31089
  this.#pendingMulticastAudio = null;
30244
31090
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31207,63 +32053,65 @@ var FecStarvationDetector = class {
31207
32053
 
31208
32054
  // src/cmcd-producer.ts
31209
32055
  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}`;
32056
+ var DEFAULT_INTERVAL_MS = 3e4;
32057
+ var CUSTOM_PREFIX = "com.blockcast-";
32058
+ var MAX_CUSTOM_VALUE = 64;
32059
+ var TRANSPORT_TOKEN = {
32060
+ native: "ssm",
32061
+ amt: "amt",
32062
+ moq: "moq"
32063
+ };
32064
+ function sfString(value) {
32065
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
32066
+ }
32067
+ function encodeCmcdRecord(record) {
32068
+ return Object.keys(record).sort().map((key) => {
32069
+ const value = record[key];
32070
+ if (typeof value === "number") return `${key}=${value}`;
32071
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
32072
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
32073
+ return `${key}=${value.token}`;
31242
32074
  }).join(",");
31243
32075
  }
32076
+ var token = (value) => ({ token: value });
32077
+ function putCustom(record, name, value) {
32078
+ if (value === void 0) return;
32079
+ if (typeof value === "number") value = String(value);
32080
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
32081
+ record[CUSTOM_PREFIX + name] = value;
32082
+ }
31244
32083
  var CmcdProducer = class {
31245
- #onCmcdParams;
31246
32084
  #onCmcdPost;
32085
+ #intervalMs;
32086
+ #sessionId;
32087
+ #now;
32088
+ #sn = 0;
32089
+ #lastHeartbeat = null;
31247
32090
  #transport;
31248
32091
  #lastStats;
31249
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31250
32092
  #playMode = null;
31251
32093
  #deltaFailRate = null;
31252
32094
  #switchMiscount = 0;
31253
32095
  #manifestStats = null;
31254
32096
  #fecFailClosed = null;
32097
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
32098
+ #lifecycle = null;
31255
32099
  constructor(options) {
31256
- this.#onCmcdParams = options.onCmcdParams;
31257
32100
  this.#onCmcdPost = options.onCmcdPost;
32101
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
32102
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
32103
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
32104
+ }
32105
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
32106
+ this.#now = options.now ?? Date.now;
31258
32107
  }
31259
32108
  /** Update the stored transport state. Called by player wiring layer. */
31260
32109
  updateTransport(snapshot) {
31261
32110
  this.#transport = snapshot;
31262
32111
  }
31263
32112
  /**
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).
32113
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
32114
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31267
32115
  */
31268
32116
  updatePlayMode(mode) {
31269
32117
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31274,9 +32122,8 @@ var CmcdProducer = class {
31274
32122
  this.#playMode = mode;
31275
32123
  }
31276
32124
  /**
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.
32125
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
32126
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31280
32127
  */
31281
32128
  updateDeltaFailRate(rate) {
31282
32129
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31286,112 +32133,139 @@ var CmcdProducer = class {
31286
32133
  }
31287
32134
  this.#deltaFailRate = rate;
31288
32135
  }
31289
- /**
31290
- * Bump the cumulative switching-set misalignment counter. Emitted as
31291
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31292
- */
32136
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31293
32137
  incrementSwitchMiscount() {
31294
32138
  this.#switchMiscount += 1;
31295
32139
  }
31296
32140
  /**
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.
32141
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
32142
+ * as `e=e` with the reason code in `ec`, and on every later counter report
32143
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
32144
+ * B_i was undercut), so a stream playing without FEC is told apart from one
32145
+ * with no loss.
31301
32146
  */
31302
32147
  updateFecFailClosed(reason) {
32148
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31303
32149
  this.#fecFailClosed = reason;
32150
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31304
32151
  }
31305
32152
  updateManifestVerification(stats) {
31306
32153
  this.#manifestStats = stats;
31307
32154
  }
31308
32155
  /**
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.
32156
+ * Count a video block's resolution from the repair lifecycle: recovered in
32157
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
32158
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
32159
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31313
32160
  */
32161
+ onRepairLifecycle(event) {
32162
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
32163
+ if (event.type === "recovered" && event.blockClass !== "audio") {
32164
+ if (event.via === "inband") this.#lifecycle.inband++;
32165
+ else this.#lifecycle.fetch++;
32166
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
32167
+ this.#lifecycle.failedKeyframe++;
32168
+ }
32169
+ }
32170
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31314
32171
  emitSwitchDrift(driftUs) {
31315
32172
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31316
32173
  throw new Error(
31317
32174
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31318
32175
  );
31319
32176
  }
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);
32177
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
32178
+ putCustom(record, "sdu", Math.round(driftUs));
32179
+ this.#post([record]);
31326
32180
  }
31327
- /**
31328
- * Process a FecTrackStats snapshot. Finds the video track, builds
31329
- * x-bc-* vendor keys, and fires both delivery callbacks.
31330
- */
32181
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31331
32182
  onSnapshot(data) {
31332
32183
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31333
32184
  if (!videoTrack) return;
31334
32185
  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);
32186
+ this.#heartbeat();
31357
32187
  }
31358
32188
  /**
31359
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31360
- * the caller because only the transport integration knows when state changed.
32189
+ * Take transport state for receivers that do not own FEC stats, and send the
32190
+ * heartbeat when due.
31361
32191
  */
31362
32192
  onTransportSnapshot(data) {
31363
32193
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31364
32194
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31365
32195
  }
31366
32196
  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");
32197
+ if (data.switchMiscount !== void 0) {
32198
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
32199
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
32200
+ }
32201
+ this.#switchMiscount = data.switchMiscount;
31369
32202
  }
31370
32203
  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);
32204
+ this.#heartbeat();
31376
32205
  }
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
- */
32206
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31382
32207
  onTransition(event) {
31383
- const params = {};
31384
- if (this.#lastStats) {
31385
- Object.assign(params, buildFecParams(this.#lastStats));
32208
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
32209
+ putCustom(record, "ct", token(event));
32210
+ this.#putTransport(record);
32211
+ this.#post([record]);
32212
+ }
32213
+ #base(event) {
32214
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
32215
+ }
32216
+ #heartbeat() {
32217
+ const now = this.#now();
32218
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
32219
+ this.#lastHeartbeat = now;
32220
+ this.#post([this.#base("t"), this.#counters()]);
32221
+ }
32222
+ #putTransport(record) {
32223
+ const snap = this.#transport;
32224
+ if (!snap) return;
32225
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
32226
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
32227
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
32228
+ }
32229
+ putCustom(record, "ar", snap.amtRelay);
32230
+ }
32231
+ /** The `e=ce` counter report. */
32232
+ #counters() {
32233
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
32234
+ const stats = this.#lastStats;
32235
+ if (stats) {
32236
+ putCustom(record, "fa", stats.config.algorithm);
32237
+ putCustom(record, "fk", stats.config.k);
32238
+ putCustom(record, "fp", stats.config.repairCount);
32239
+ putCustom(record, "pr", stats.sourceSymbols);
32240
+ putCustom(record, "pl", stats.sourcePacketsMissing);
32241
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
32242
+ putCustom(record, "fbf", stats.blocksFailed);
32243
+ }
32244
+ if (this.#lifecycle) {
32245
+ putCustom(record, "fbr", this.#lifecycle.inband);
32246
+ putCustom(record, "fbq", this.#lifecycle.fetch);
32247
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
32248
+ }
32249
+ this.#putTransport(record);
32250
+ if (this.#playMode !== null) {
32251
+ putCustom(record, "pm", token(this.#playMode));
32252
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
32253
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
32254
+ }
31386
32255
  }
31387
- Object.assign(params, buildTransportParams(this.#transport));
31388
- params["x-bc-ct"] = event;
31389
- this.#emit(params);
32256
+ if (this.#fecFailClosed) {
32257
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
32258
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
32259
+ }
32260
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
32261
+ if (this.#manifestStats) {
32262
+ putCustom(record, "mv", this.#manifestStats.verified);
32263
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
32264
+ }
32265
+ return record;
31390
32266
  }
31391
- /** Fire both delivery callbacks with the given params. */
31392
- #emit(params) {
31393
- this.#onCmcdParams?.(params);
31394
- this.#onCmcdPost?.(formatQueryString(params));
32267
+ #post(records) {
32268
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31395
32269
  }
31396
32270
  };
31397
32271
 
@@ -31487,6 +32361,19 @@ function resolveRepairLayers(catalog, source) {
31487
32361
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31488
32362
  };
31489
32363
  }
32364
+ function valueAt(catalog, pointer) {
32365
+ let node = catalog;
32366
+ for (const segment of pointer.split("/").slice(1)) {
32367
+ if (typeof node !== "object" || node === null) return void 0;
32368
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
32369
+ }
32370
+ return node;
32371
+ }
32372
+ function catalogTrackDeliveryMode(catalog, source) {
32373
+ const binding = bindingOf(catalog, source);
32374
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
32375
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
32376
+ }
31490
32377
  function moqtPriority2(value) {
31491
32378
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31492
32379
  }
@@ -31530,6 +32417,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31530
32417
  repairTrackName: base.repairTrack,
31531
32418
  repairLayers: repair.baseLayers,
31532
32419
  repairOverlays: repair.overlays,
32420
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31533
32421
  ...mediaRate
31534
32422
  };
31535
32423
  }
@@ -32054,7 +32942,7 @@ var RepairLayerSubscriptions = class {
32054
32942
  * or interleaved sequences read as loss.
32055
32943
  */
32056
32944
  noteSourceSequence(seq) {
32057
- if (this.#disposed || this.#opts.delivery === "stream") return;
32945
+ if (this.#disposed || this.#delivery() === "stream") return;
32058
32946
  seq >>>= 0;
32059
32947
  if (this.#lastSeq === void 0) {
32060
32948
  this.#lastSeq = seq;
@@ -32087,12 +32975,12 @@ var RepairLayerSubscriptions = class {
32087
32975
  this.#expected = 0;
32088
32976
  this.#lost = 0;
32089
32977
  this.#wantFewer = 0;
32090
- this.#layerCount = Math.min(this.#layerCount, this.#layers().length);
32978
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32091
32979
  this.#reconcile();
32092
32980
  }
32093
32981
  snapshot() {
32094
32982
  return {
32095
- delivery: this.#opts.delivery,
32983
+ delivery: this.#delivery(),
32096
32984
  layerCount: this.#layerCount,
32097
32985
  subscribedTracks: [...this.#subs.keys()],
32098
32986
  repairPackets: this.#repairPackets,
@@ -32106,6 +32994,9 @@ var RepairLayerSubscriptions = class {
32106
32994
  this.#closeAll();
32107
32995
  this.#disposed = true;
32108
32996
  }
32997
+ #delivery() {
32998
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32999
+ }
32109
33000
  #layers() {
32110
33001
  return this.#source.repairLayers ?? [];
32111
33002
  }
@@ -32180,6 +33071,7 @@ var RepairLayerSubscriptions = class {
32180
33071
 
32181
33072
  // src/repair-fetch.ts
32182
33073
  var MOQT_PRIORITY_MAX = 255;
33074
+ var WALK_LEAD_MS = 3;
32183
33075
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32184
33076
  function isMoqtPriority(value) {
32185
33077
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32230,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
32230
33122
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32231
33123
  };
32232
33124
  const clearEntry = (entry) => {
32233
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32234
- else clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33125
+ const { range } = entry;
33126
+ if (range) {
33127
+ atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
33128
+ } else {
33129
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33130
+ }
33131
+ };
33132
+ const atReport = (range, work) => {
33133
+ const fec = mc.fecManager;
33134
+ if (range.groups !== null || fec.withReceiptTime === void 0) work();
33135
+ else fec.withReceiptTime(range.reportedAt, work);
32235
33136
  };
32236
33137
  const releaseHold = (entry) => {
32237
33138
  if (entry.range) return;
@@ -32298,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
32298
33199
  const admit = (entry, now) => {
32299
33200
  const budgetMs = entry.deadlineAt - now;
32300
33201
  const rttMs = readRtt();
32301
- const reason = !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < budgetMs ? null : "rtt-exceeds-budget";
33202
+ const reason = refusal(entry, now, rttMs);
32302
33203
  if (reason === null) return rttMs;
32303
33204
  skip(entry, reason, rttMs, budgetMs);
32304
33205
  return null;
32305
33206
  };
33207
+ const refusal = (entry, now, rttMs) => !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < entry.deadlineAt - now ? null : "rtt-exceeds-budget";
32306
33208
  const retire = (entry) => {
32307
33209
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32308
33210
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32309
33211
  };
32310
- const outstandingBelow = (priority) => {
33212
+ const holdsBack = (entry, priority, videoWalks = true) => {
33213
+ if (entry.priority >= priority) return false;
33214
+ const { walk } = entry;
33215
+ if (walk === void 0) return true;
33216
+ if (entry.priority !== 0 || !videoWalks) return false;
33217
+ return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
33218
+ };
33219
+ const outstandingBelow = (priority, videoWalks = true) => {
32311
33220
  for (const entry of entries.values()) {
32312
- if (entry.priority < priority) return true;
33221
+ if (holdsBack(entry, priority, videoWalks)) return true;
32313
33222
  }
32314
33223
  return false;
32315
33224
  };
33225
+ const yieldVideoWalks = (queued) => {
33226
+ const now = performance.now();
33227
+ const rttMs = readRtt();
33228
+ const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
33229
+ if (!due) return;
33230
+ for (const entry of entries.values()) {
33231
+ const walk = entry.walk;
33232
+ if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
33233
+ if (walk.group.controller.signal.aborted) continue;
33234
+ walk.yielded = true;
33235
+ walk.group.controller.abort();
33236
+ }
33237
+ };
32316
33238
  const preemptBestEffort = () => {
32317
33239
  for (const entry of entries.values()) {
32318
33240
  if (entry.priority === 0 || !entry.running) continue;
@@ -32341,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
32341
33263
  }
32342
33264
  const rttMs = admit(entry, now);
32343
33265
  if (rttMs === null) return;
32344
- if (entry.priority === 0) preemptBestEffort();
33266
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33267
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32345
33268
  if (counted) entry.attempts++;
32346
33269
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32347
33270
  rttMs,
@@ -32363,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
32363
33286
  }
32364
33287
  void execute(entry, run);
32365
33288
  };
33289
+ const leaveWalkedGroup = (block) => {
33290
+ for (const entry of entries.values()) {
33291
+ const group = entry.walk?.group;
33292
+ if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
33293
+ group.controller.abort();
33294
+ }
33295
+ }
33296
+ };
32366
33297
  const dispatch = () => {
32367
33298
  if (disposed) return;
32368
33299
  const now = performance.now();
@@ -32387,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
32387
33318
  for (const entry of queued) {
32388
33319
  if (entry.priority === 0) start(entry);
32389
33320
  }
33321
+ yieldVideoWalks(queued);
32390
33322
  if (outstandingBelow(1)) return;
32391
33323
  for (const entry of queued) {
32392
33324
  if (entry.priority === 1) start(entry);
@@ -32413,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
32413
33345
  }
32414
33346
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
32415
33347
  };
33348
+ const groupServed = (sbn, packetId, catalogTrackName) => {
33349
+ if (mc.fecManager.blockSymbolCounts === void 0) return false;
33350
+ const counts = symbolCounts(sbn, packetId, catalogTrackName);
33351
+ if (counts === void 0 || counts.authoredLength === void 0) return true;
33352
+ return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
33353
+ };
33354
+ const statedEnd = (sbn, packetId, catalogTrackName) => {
33355
+ const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
33356
+ return length === void 0 ? void 0 : serial32Add(sbn, length);
33357
+ };
32416
33358
  const finish = (entry, run, fed, error) => {
32417
33359
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
32418
33360
  return;
@@ -32457,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
32457
33399
  signal: run.controller.signal,
32458
33400
  subscriberPriority: entry.subscriberPriority
32459
33401
  };
33402
+ const { range } = entry;
33403
+ const feed = (packet) => {
33404
+ if (entry.catalogTrackName === void 0) {
33405
+ mc.processRepairFromMoQ(packet, entry.packetId);
33406
+ } else {
33407
+ mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
33408
+ }
33409
+ };
32460
33410
  try {
32461
- const packets = entry.range ? opts.rangeTransport({
33411
+ const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
32462
33412
  ...common,
32463
- startGroup: entry.range.startGroup,
32464
- endGroup: entry.range.endGroup,
32465
- lostSsId: entry.range.lostSsId
32466
- }) : opts.transport({ ...common, sbn: entry.sbn });
33413
+ startGroup: range.groups.startGroup,
33414
+ endGroup: range.groups.endGroup,
33415
+ lostSsId: range.lostSsId
33416
+ });
32467
33417
  for await (const packet of packets) {
32468
33418
  if (run.stopped !== null || entry.running !== run) break;
32469
33419
  fed++;
32470
- if (entry.catalogTrackName === void 0) {
32471
- mc.processRepairFromMoQ(packet, entry.packetId);
32472
- } else {
32473
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
32474
- }
33420
+ if (range === void 0) feed(packet);
33421
+ else atReport(range, () => feed(packet));
32475
33422
  }
32476
33423
  finish(entry, run, fed, void 0);
32477
33424
  } catch (error) {
32478
33425
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
32479
33426
  }
32480
33427
  };
33428
+ async function* walkHole(entry, walk, hole, common) {
33429
+ const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
33430
+ for (let first = true; !common.signal.aborted; first = false) {
33431
+ if (!first && entry.priority !== 0 && outstandingBelow(1)) {
33432
+ stop(entry, "preempted");
33433
+ note("repair-preempted", entry);
33434
+ return;
33435
+ }
33436
+ if (!first && entry.priority === 0) {
33437
+ dispatch();
33438
+ if (common.signal.aborted) return;
33439
+ }
33440
+ const sbn = walk.next;
33441
+ let next;
33442
+ if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
33443
+ const rsIds = /* @__PURE__ */ new Set();
33444
+ const group = new AbortController();
33445
+ const cancel = () => group.abort();
33446
+ common.signal.addEventListener("abort", cancel, { once: true });
33447
+ walk.group = { sbn, controller: group };
33448
+ try {
33449
+ for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
33450
+ yield packet;
33451
+ const id = extractRepairFecPayloadId(packet);
33452
+ if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
33453
+ next ??= serial32Add(sbn, id.ssbLength);
33454
+ rsIds.add(id.rsId);
33455
+ if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
33456
+ group.abort();
33457
+ break;
33458
+ }
33459
+ }
33460
+ } catch (error) {
33461
+ if (!group.signal.aborted || common.signal.aborted) throw error;
33462
+ } finally {
33463
+ common.signal.removeEventListener("abort", cancel);
33464
+ walk.group = null;
33465
+ }
33466
+ if (walk.yielded) {
33467
+ walk.yielded = false;
33468
+ continue;
33469
+ }
33470
+ }
33471
+ next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
33472
+ if (next === void 0) return;
33473
+ if (serial32Distance(entry.sbn, next) > lostAt) return;
33474
+ walk.next = next;
33475
+ }
33476
+ }
32481
33477
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
32482
33478
  for (const entry of [...entries.values()]) {
32483
33479
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -32673,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
32673
33669
  throw error;
32674
33670
  }
32675
33671
  };
32676
- if (opts.rangeTransport !== void 0) {
33672
+ const trackKind = (packetId) => {
33673
+ try {
33674
+ return mc.fecManager.trackKindFor?.(packetId);
33675
+ } catch {
33676
+ return void 0;
33677
+ }
33678
+ };
33679
+ const walkDeadlineAt = (packetId, now, deadlineAt) => {
33680
+ let repairDeadlineMs;
33681
+ try {
33682
+ repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
33683
+ } catch {
33684
+ repairDeadlineMs = void 0;
33685
+ }
33686
+ if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
33687
+ return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
33688
+ };
33689
+ const onHoleExpired = (hole) => {
33690
+ if (disposed) return false;
33691
+ const entry = [...entries.values()].find((candidate) => candidate.walk !== void 0 && candidate.packetId === hole.packetId && candidate.catalogTrackName === hole.catalogTrackName && serial32Distance(candidate.sbn, hole.holeStart) < candidate.range.holeLength);
33692
+ if (entry === void 0) return false;
33693
+ queueMicrotask(() => {
33694
+ if (entries.get(entry.key) !== entry) return;
33695
+ retire(entry);
33696
+ stop(entry, "expired");
33697
+ clearEntry(entry);
33698
+ dispatch();
33699
+ });
33700
+ return true;
33701
+ };
33702
+ if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
32677
33703
  mc.onUndelimitedLoss((loss) => {
32678
33704
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
32679
33705
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -32681,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
32681
33707
  const blockProfile = blockProfileOf(packetId);
32682
33708
  const target = opts.resolveTarget(packetId, catalogTrackName);
32683
33709
  if (!target) return close();
32684
- const priority = repairPriorityForClass("unknown");
32685
- const range = rangeFor(loss, target.repairTrack);
32686
- if (range === null) {
33710
+ const walked = opts.rangeTransport === void 0;
33711
+ const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
33712
+ const priority = repairPriorityForClass(holeClass);
33713
+ const groups = walked ? null : rangeFor(loss, target.repairTrack);
33714
+ if (!walked && groups === null) {
32687
33715
  close();
32688
33716
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
32689
33717
  return;
@@ -32694,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
32694
33722
  packetId,
32695
33723
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
32696
33724
  };
32697
- const wire = wirePriority(identity, "unknown");
32698
- const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, performance.now(), "unknown");
33725
+ const wire = wirePriority(identity, holeClass);
33726
+ if (wire === null) return close();
33727
+ const now = performance.now();
33728
+ const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
32699
33729
  if (deadline === null) {
32700
33730
  close();
32701
33731
  mc.noteRepairSkipped(
@@ -32707,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
32707
33737
  );
32708
33738
  return;
32709
33739
  }
33740
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
32710
33741
  const entry = {
32711
33742
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
32712
33743
  ...identity,
@@ -32714,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
32714
33745
  blockProfile,
32715
33746
  priority,
32716
33747
  subscriberPriority: wire.subscriberPriority,
32717
- deadlineAt: deadline.deadlineAt,
32718
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33748
+ deadlineAt,
33749
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
32719
33750
  anchorAt: deadline.anchorAt,
32720
33751
  delayMs: deadline.delayMs,
32721
33752
  bound: deadline.bound,
32722
33753
  order: enqueueOrder++,
32723
33754
  attempts: 0,
32724
33755
  running: null,
32725
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33756
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33757
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
32726
33758
  };
32727
33759
  const previous = entries.get(entry.key);
32728
33760
  if (previous) {
@@ -32732,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
32732
33764
  entries.set(entry.key, entry);
32733
33765
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
32734
33766
  dispatch();
32735
- });
33767
+ }, onHoleExpired);
32736
33768
  }
32737
33769
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
32738
33770
  queueMicrotask(() => {
@@ -32848,6 +33880,7 @@ export {
32848
33880
  catalogGroupDurationMs,
32849
33881
  catalogKeyframeCadenceMs,
32850
33882
  catalogRepairTarget,
33883
+ catalogTrackDeliveryMode,
32851
33884
  catalogTrackFecUnavailable,
32852
33885
  classifyH264Keyframe,
32853
33886
  classifyHevcKeyframe,
@@ -32868,6 +33901,7 @@ export {
32868
33901
  detectH264Keyframe,
32869
33902
  detectHevcKeyframe,
32870
33903
  driftCheck,
33904
+ encodeCmcdRecord,
32871
33905
  encodeDataSegment,
32872
33906
  encodeFecSourceInfo,
32873
33907
  encodeLocTimestamp,