@blockcast/mmt-container 2.2.0-main.f480a7ede8b1 → 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 (54) hide show
  1. package/dist/browser-runtime.js +891 -141
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/delta-fallback-config.js.map +1 -1
  4. package/dist/fec-manager.d.ts +67 -11
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +283 -54
  7. package/dist/fec-manager.js.map +2 -2
  8. package/dist/fec-worker-setup.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +2 -2
  12. package/dist/iso-block-locator.d.ts +39 -13
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +80 -25
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/media-router.js.map +1 -1
  17. package/dist/mmtp-container-parser.d.ts +42 -8
  18. package/dist/mmtp-container-parser.d.ts.map +1 -1
  19. package/dist/mmtp-container-parser.js +121 -17
  20. package/dist/mmtp-container-parser.js.map +2 -2
  21. package/dist/mmtp-container-parser.no-modules.js +120 -18
  22. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  23. package/dist/mmtp-decode-pipeline.js +121 -17
  24. package/dist/mmtp-decode-pipeline.js.map +2 -2
  25. package/dist/mmtp-router.d.ts +28 -8
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +407 -52
  28. package/dist/mmtp-router.js.map +2 -2
  29. package/dist/multicast-manager.d.ts +13 -3
  30. package/dist/multicast-manager.d.ts.map +1 -1
  31. package/dist/multicast-manager.js +712 -115
  32. package/dist/multicast-manager.js.map +2 -2
  33. package/dist/multicast-transport.js +2 -2
  34. package/dist/multicast-transport.js.map +2 -2
  35. package/dist/no-modules.js +121 -17
  36. package/dist/no-modules.js.map +2 -2
  37. package/dist/repair-fetch.d.ts +36 -8
  38. package/dist/repair-fetch.d.ts.map +1 -1
  39. package/dist/repair-fetch.js +285 -26
  40. package/dist/repair-fetch.js.map +3 -3
  41. package/dist/track-session.d.ts.map +1 -1
  42. package/dist/track-session.js +1 -0
  43. package/dist/track-session.js.map +2 -2
  44. package/dist/wasm-mmtp-container-parser.js +121 -17
  45. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  46. package/package.json +5 -5
  47. package/src/fec-manager.ts +304 -28
  48. package/src/index.ts +2 -0
  49. package/src/iso-block-locator.ts +114 -31
  50. package/src/mmtp-container-parser.ts +210 -21
  51. package/src/mmtp-router.ts +488 -45
  52. package/src/multicast-manager.ts +44 -6
  53. package/src/repair-fetch.ts +363 -49
  54. package/src/track-session.ts +3 -0
@@ -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
  }
@@ -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";
@@ -12435,6 +12446,7 @@ var TrackSession = class {
12435
12446
  this.#state = "idle";
12436
12447
  this.#subscription = null;
12437
12448
  this.#disarmFec();
12449
+ this.#unsuppress();
12438
12450
  }
12439
12451
  // -----------------------------------------------------------------------
12440
12452
  // Internal
@@ -12928,6 +12940,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
12928
12940
  this.#heldEvicted = 0;
12929
12941
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
12930
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 = [];
12931
12952
  /**
12932
12953
  * Keep a gap that is missing a source as a pending hole rather than a span,
12933
12954
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -12951,6 +12972,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
12951
12972
  #newest;
12952
12973
  #newestSource;
12953
12974
  #holes;
12975
+ #expired;
12976
+ #newlyExpired;
12977
+ #closedBehind;
12954
12978
  static #checkedRetention(retentionBlocks) {
12955
12979
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
12956
12980
  throw new Error(
@@ -13020,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
13020
13044
  }
13021
13045
  /** The known block or span containing `ssId`, with its ESI. */
13022
13046
  locate(ssId) {
13023
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
13024
- const block = this.#blocks[i];
13025
- if (isoBlockContains(block.start, block.length, ssId)) {
13026
- 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
+ }
13027
13053
  }
13028
13054
  }
13029
13055
  return null;
13030
13056
  }
13031
- /** Number of pending holes. */
13057
+ /** Number of holes not closed yet, pending or past the horizon. */
13032
13058
  get holeCount() {
13033
- 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);
13034
13070
  }
13035
13071
  /** The stated block (never a span) ending exactly at `end`. */
13036
13072
  blockEndingAt(end) {
@@ -13086,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
13086
13122
  /**
13087
13123
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
13088
13124
  * block states its range and places every held source in it; later repair
13089
- * symbols of the same block change nothing. A range that contradicts a
13090
- * known block (a different length, or an overlap with another block or a
13091
- * 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.
13092
13130
  * A newly stated block retires behind its SS_Start (see the class note).
13093
13131
  * A block so far past `newest` that stating it would retire the position
13094
13132
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -13107,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
13107
13145
  const known = this.blockAt(start);
13108
13146
  if (known !== void 0) {
13109
13147
  if (known.span) {
13110
- 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}` };
13111
13158
  }
13112
13159
  if (known.length !== length) {
13113
13160
  return {
@@ -13175,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
13175
13222
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
13176
13223
  }
13177
13224
  /**
13178
- * Turn every pending hole inside `[start, start + length)` into a
13179
- * repair-less span, claiming its held sources: its range FETCH ended, so
13180
- * 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.
13181
13231
  */
13182
13232
  closeHolesWithin(start, length) {
13183
13233
  const closed = [];
13184
- for (const hole of [...this.#holes]) {
13185
- if (!isoBlockContains(start, length, hole.start)) continue;
13186
- this.#holes.splice(this.#holes.indexOf(hole), 1);
13187
- const spanBlock = { start: hole.start, length: hole.length, span: true };
13188
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13189
- 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
+ }
13190
13247
  }
13191
13248
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
13192
13249
  }
@@ -13200,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
13200
13257
  return void 0;
13201
13258
  }
13202
13259
  /**
13203
- * Retire every block and span whose last SS_ID, and every held source,
13204
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
13205
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
13206
- * 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.
13207
13265
  */
13208
13266
  retireBehind(position) {
13209
13267
  this.observe(position);
13210
13268
  const horizon = this.#retentionBlocks;
13211
13269
  const retired = [];
13270
+ this.#closedBehind.length = 0;
13212
13271
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
13213
13272
  const block = this.#blocks[i];
13214
13273
  const last = serial32Add(block.start, block.length - 1);
@@ -13220,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
13220
13279
  for (let i = this.#holes.length - 1; i >= 0; i--) {
13221
13280
  const hole = this.#holes[i];
13222
13281
  const last = serial32Add(hole.start, hole.length - 1);
13223
- 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);
13224
13286
  }
13225
13287
  for (const ssId of [...this.#held.keys()]) {
13226
- 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
+ }
13227
13291
  }
13228
13292
  return retired;
13229
13293
  }
@@ -13231,6 +13295,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
13231
13295
  reset() {
13232
13296
  this.#blocks.length = 0;
13233
13297
  this.#holes.length = 0;
13298
+ this.#expired.length = 0;
13299
+ this.#newlyExpired.length = 0;
13300
+ this.#closedBehind.length = 0;
13234
13301
  this.#held.clear();
13235
13302
  this.#coverageEnd = void 0;
13236
13303
  this.#newest = void 0;
@@ -13854,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13854
13921
  var WORKER_ERROR_KIND_CAP = 32;
13855
13922
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13856
13923
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13924
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13857
13925
  function isMulticastSymbolDelivery(value) {
13858
13926
  return value === "datagram" || value === "ordered-relay";
13859
13927
  }
@@ -13947,10 +14015,18 @@ var FECManager = class _FECManager {
13947
14015
  // 'pending' forever, which would park every later frame of its stream in
13948
14016
  // the in-order release gate. Each entry owns its handle; the callback
13949
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
+ */
13950
14022
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13951
14023
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13952
14024
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13953
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();
13954
14030
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13955
14031
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13956
14032
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -13959,6 +14035,9 @@ var FECManager = class _FECManager {
13959
14035
  // SSB_length source arrivals and recoverable from SSB_length received
13960
14036
  // symbols; waiting for K fails every one of them.
13961
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();
13962
14041
  this.#ssbLengthConflictCount = 0;
13963
14042
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13964
14043
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -14156,7 +14235,10 @@ var FECManager = class _FECManager {
14156
14235
  // arrives via onBlockRecovered callback.
14157
14236
  this.onBlockStarted = null;
14158
14237
  this.onBlockComplete = null;
14159
- /** 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
+ */
14160
14242
  this.onBlockFailed = null;
14161
14243
  /**
14162
14244
  * Fired once per stream when its Worker decoder stops being routable.
@@ -14198,6 +14280,8 @@ var FECManager = class _FECManager {
14198
14280
  * for it, which it must be.
14199
14281
  */
14200
14282
  this.#onUndelimitedLoss = null;
14283
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14284
+ this.#onIsoHoleExpired = null;
14201
14285
  /** A block every symbol of which vanished was created by SBN evidence. */
14202
14286
  this.onBlockVanished = null;
14203
14287
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -14240,7 +14324,9 @@ var FECManager = class _FECManager {
14240
14324
  #pendingWorkerCleanupBounds;
14241
14325
  #blockSourceCounts;
14242
14326
  #blockSourceEsis;
14327
+ #blockRebuiltEsis;
14243
14328
  #blockAuthoredLengths;
14329
+ #isoSpanKeys;
14244
14330
  #ssbLengthConflictCount;
14245
14331
  #blockRepairRsIds;
14246
14332
  #blockRsbLengths;
@@ -14307,6 +14393,7 @@ var FECManager = class _FECManager {
14307
14393
  #decoderUnavailableEvents;
14308
14394
  #repairSymbolSizeMismatchCount;
14309
14395
  #onUndelimitedLoss;
14396
+ #onIsoHoleExpired;
14310
14397
  #encodeTrackName(catalogTrackName) {
14311
14398
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14312
14399
  if (encoded === void 0) {
@@ -14778,7 +14865,10 @@ var FECManager = class _FECManager {
14778
14865
  timer.handle = null;
14779
14866
  this.#requestRepair(sbn, packetId, catalogTrackName);
14780
14867
  };
14781
- 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
+ );
14782
14872
  this.#repairDeadlines.set(key, timer);
14783
14873
  }
14784
14874
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14922,8 +15012,14 @@ var FECManager = class _FECManager {
14922
15012
  * Vanished blocks are not enumerated here: their starts are unknown until a
14923
15013
  * repair states them. The SS_IDs between two stated blocks become one
14924
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).
14925
15021
  */
14926
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15022
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14927
15023
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14928
15024
  const now = performance.now();
14929
15025
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14962,7 +15058,7 @@ var FECManager = class _FECManager {
14962
15058
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14963
15059
  };
14964
15060
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
14965
- 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))) {
14966
15062
  discontinuity();
14967
15063
  return;
14968
15064
  }
@@ -15224,17 +15320,21 @@ var FECManager = class _FECManager {
15224
15320
  this.#blockWaitDeadlinesMs.delete(key);
15225
15321
  this.#repairHoldUntilMs.delete(key);
15226
15322
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
15227
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15323
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
15228
15324
  };
15229
- 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
+ );
15230
15329
  this.#activeBlocks.set(key, timer);
15231
15330
  this.#indexBlockKey(key, sbn, packetId);
15232
15331
  }
15233
15332
  /**
15234
15333
  * Hand a block whose delivery is over to its decoder for finalization: the
15235
- * 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.
15236
15336
  */
15237
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15337
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
15238
15338
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
15239
15339
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
15240
15340
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -15250,7 +15350,7 @@ var FECManager = class _FECManager {
15250
15350
  if (workerClient) {
15251
15351
  this.#pendingWorkerCleanups.add(key);
15252
15352
  this.#pendingWorkerCleanupClients.set(key, workerClient);
15253
- this.#boundOwedCleanup(key, packetId, workerClient);
15353
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
15254
15354
  workerClient.cleanup(sbn);
15255
15355
  } else {
15256
15356
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15562,7 +15662,9 @@ var FECManager = class _FECManager {
15562
15662
  }
15563
15663
  resetMap(this.#blockSourceCounts);
15564
15664
  resetMap(this.#blockSourceEsis);
15665
+ resetMap(this.#blockRebuiltEsis);
15565
15666
  resetMap(this.#blockAuthoredLengths);
15667
+ resetSet(this.#isoSpanKeys);
15566
15668
  resetMap(this.#blockRepairRsIds);
15567
15669
  resetMap(this.#blockRsbLengths);
15568
15670
  resetMap(this.#heldLayerStates);
@@ -15686,7 +15788,9 @@ var FECManager = class _FECManager {
15686
15788
  dropMap(this.#pendingWorkerCleanupClients);
15687
15789
  dropMap(this.#blockSourceCounts);
15688
15790
  dropMap(this.#blockSourceEsis);
15791
+ dropMap(this.#blockRebuiltEsis);
15689
15792
  dropMap(this.#blockAuthoredLengths);
15793
+ dropSet(this.#isoSpanKeys);
15690
15794
  dropMap(this.#blockRepairRsIds);
15691
15795
  dropMap(this.#blockRsbLengths);
15692
15796
  dropMap(this.#blockPathCounts);
@@ -16029,8 +16133,9 @@ var FECManager = class _FECManager {
16029
16133
  return true;
16030
16134
  }
16031
16135
  /**
16032
- * Drop a block's repair hold: its FETCH ended without delivering. The block
16033
- * 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.
16034
16139
  */
16035
16140
  releaseRepairHold(sbn, packetId, catalogTrackName) {
16036
16141
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16168,6 +16273,7 @@ var FECManager = class _FECManager {
16168
16273
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16169
16274
  const evictedBefore = locator.heldEvicted;
16170
16275
  const held = locator.hold(ssId, source);
16276
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16171
16277
  if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16172
16278
  const evicted = locator.heldEvicted - evictedBefore;
16173
16279
  if (evicted > 0) {
@@ -16189,17 +16295,20 @@ var FECManager = class _FECManager {
16189
16295
  * contradicts a known block is refused (counted) and must be dropped.
16190
16296
  */
16191
16297
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
16192
- 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);
16193
16301
  if (placement.status !== "placed") {
16194
16302
  this.#isoRepairRefusedCount++;
16195
16303
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
16196
16304
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
16197
16305
  }
16306
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
16198
16307
  return placement;
16199
16308
  }
16309
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
16200
16310
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
16201
16311
  if (placement.hole !== void 0) {
16202
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
16203
16312
  const hole = placement.hole.block;
16204
16313
  const previous = locator.blockEndingAt(hole.start);
16205
16314
  const loss = {
@@ -16220,17 +16329,29 @@ var FECManager = class _FECManager {
16220
16329
  * `handler`, instead of a repair-less span; null restores spans. Set by the
16221
16330
  * range repair scheduler; applies to every stream from its next gap. The
16222
16331
  * handler and the deferral are one setting, so no hole is ever created with
16223
- * 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.
16224
16336
  */
16225
- setUndelimitedLossHandler(handler) {
16337
+ setUndelimitedLossHandler(handler, onExpired = null) {
16226
16338
  this.#onUndelimitedLoss = handler;
16339
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
16227
16340
  this.#deferIsoGaps = handler !== null;
16228
16341
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
16229
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
+ }
16230
16350
  /**
16231
- * The range repair for a hole ended: every hole still pending inside
16232
- * `[start, start + length)` becomes a repair-less span. Returns those spans
16233
- * 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).
16234
16355
  */
16235
16356
  closeIsoHoles(packetId, catalogTrackName, start, length) {
16236
16357
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -16249,7 +16370,10 @@ var FECManager = class _FECManager {
16249
16370
  */
16250
16371
  #openIsoSpan(span, packetId, catalogTrackName) {
16251
16372
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
16252
- 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
+ }
16253
16377
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
16254
16378
  }
16255
16379
  /**
@@ -16590,11 +16714,14 @@ var FECManager = class _FECManager {
16590
16714
  });
16591
16715
  client.onCleanupComplete((sbn) => {
16592
16716
  if (this.#releasedWorkerClients.has(client)) return;
16593
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16717
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16718
+ const cleanupKeys = owed.filter((key) => {
16594
16719
  const block = this.#parseBlockKey(key);
16595
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16720
+ return block.packetId === packetId && block.sbn === sbn;
16596
16721
  });
16597
- 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");
16598
16725
  });
16599
16726
  client.onError((msg, code) => {
16600
16727
  const requestedKind = code ?? msg;
@@ -16664,9 +16791,11 @@ var FECManager = class _FECManager {
16664
16791
  * fail deadline already passed, so failing it is the decision that deadline
16665
16792
  * made. A late ack then finds nothing owed and is a no-op.
16666
16793
  */
16667
- #boundOwedCleanup(key, packetId, client) {
16794
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16668
16795
  this.#clearOwedCleanupBound(key);
16669
16796
  const bound = {
16797
+ dueAtMs,
16798
+ askedAtMs: performance.now(),
16670
16799
  handle: setTimeout(() => {
16671
16800
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16672
16801
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -16764,7 +16893,15 @@ var FECManager = class _FECManager {
16764
16893
  this.#isoSourceOversizeCount++;
16765
16894
  return null;
16766
16895
  }
16767
- 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) {
16768
16905
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16769
16906
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16770
16907
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16793,6 +16930,74 @@ var FECManager = class _FECManager {
16793
16930
  }
16794
16931
  return recovered;
16795
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
+ }
16796
17001
  /**
16797
17002
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16798
17003
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -17035,7 +17240,10 @@ var FECManager = class _FECManager {
17035
17240
  const authoredLength = this.#blockAuthoredLengths.get(key);
17036
17241
  const rsbLength = this.#blockRsbLengths.get(key);
17037
17242
  if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17038
- if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) 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;
17039
17247
  const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17040
17248
  if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17041
17249
  return void 0;
@@ -17158,12 +17366,13 @@ var FECManager = class _FECManager {
17158
17366
  * the inactivity deadline on each symbol, records the symbol's in-band
17159
17367
  * classification evidence, and marks the block clean when all of its
17160
17368
  * authored source symbols arrived: the SSB_length a repair symbol declared,
17161
- * otherwise K.
17369
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17370
+ * placed only now (see #noteSbnSeen).
17162
17371
  *
17163
17372
  * Throws for a packetId that was never configured, so a classified block
17164
17373
  * never reads the manager-global K or timeout.
17165
17374
  */
17166
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17375
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
17167
17376
  const trackConfig = this.#trackConfigs.get(packetId);
17168
17377
  if (trackConfig === void 0) {
17169
17378
  throw new Error(
@@ -17179,7 +17388,8 @@ var FECManager = class _FECManager {
17179
17388
  catalogTrackName,
17180
17389
  "source",
17181
17390
  evidence.packetSequenceNumber,
17182
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17391
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17392
+ held
17183
17393
  );
17184
17394
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
17185
17395
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -17215,6 +17425,15 @@ var FECManager = class _FECManager {
17215
17425
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17216
17426
  return;
17217
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
+ }
17218
17437
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17219
17438
  }
17220
17439
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17223,7 +17442,9 @@ var FECManager = class _FECManager {
17223
17442
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17224
17443
  this.#blockSourceCounts.delete(key);
17225
17444
  this.#blockSourceEsis.delete(key);
17445
+ this.#blockRebuiltEsis.delete(key);
17226
17446
  this.#blockAuthoredLengths.delete(key);
17447
+ this.#isoSpanKeys.delete(key);
17227
17448
  this.#blockRepairRsIds.delete(key);
17228
17449
  this.#blockRsbLengths.delete(key);
17229
17450
  this.#markTerminal(key, sbn, packetId, true);
@@ -17254,6 +17475,19 @@ var FECManager = class _FECManager {
17254
17475
  #receiptNow() {
17255
17476
  return this.#receiptAtMs ?? performance.now();
17256
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
+ }
17257
17491
  /**
17258
17492
  * A held packet admitted late arms from its (earlier) receipt time; that
17259
17493
  * never pulls in a deadline later activity already armed. Outside a
@@ -17302,6 +17536,7 @@ var FECManager = class _FECManager {
17302
17536
  }
17303
17537
  #failBlock(sbn, packetId, catalogTrackName) {
17304
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;
17305
17540
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
17306
17541
  this.#clearRepairDeadline(key);
17307
17542
  this.#clearBlockTimer(key);
@@ -17311,6 +17546,7 @@ var FECManager = class _FECManager {
17311
17546
  }
17312
17547
  this.#blockSourceCounts.delete(key);
17313
17548
  this.#blockSourceEsis.delete(key);
17549
+ this.#blockRebuiltEsis.delete(key);
17314
17550
  this.#blockAuthoredLengths.delete(key);
17315
17551
  this.#blockRepairRsIds.delete(key);
17316
17552
  this.#blockRsbLengths.delete(key);
@@ -17320,7 +17556,8 @@ var FECManager = class _FECManager {
17320
17556
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
17321
17557
  this.#fecStats.blocksFailed++;
17322
17558
  this.#statsForPacket(packetId).blocksFailed++;
17323
- 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);
17324
17561
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
17325
17562
  }
17326
17563
  /**
@@ -17349,7 +17586,9 @@ var FECManager = class _FECManager {
17349
17586
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17350
17587
  this.#blockSourceCounts.delete(key);
17351
17588
  this.#blockSourceEsis.delete(key);
17589
+ this.#blockRebuiltEsis.delete(key);
17352
17590
  this.#blockAuthoredLengths.delete(key);
17591
+ this.#isoSpanKeys.delete(key);
17353
17592
  this.#blockRepairRsIds.delete(key);
17354
17593
  this.#blockRsbLengths.delete(key);
17355
17594
  this.#clearBlockTimer(key);
@@ -17440,7 +17679,9 @@ var FECManager = class _FECManager {
17440
17679
  this.#blockKeysBySbn.clear();
17441
17680
  this.#blockSourceCounts.clear();
17442
17681
  this.#blockSourceEsis.clear();
17682
+ this.#blockRebuiltEsis.clear();
17443
17683
  this.#blockAuthoredLengths.clear();
17684
+ this.#isoSpanKeys.clear();
17444
17685
  this.#blockRepairRsIds.clear();
17445
17686
  this.#blockRsbLengths.clear();
17446
17687
  this.#heldLayerStates.clear();
@@ -18342,6 +18583,9 @@ function authoredGap(before, after) {
18342
18583
  function ssIdAfter(a, b) {
18343
18584
  return (a - b | 0) > 0;
18344
18585
  }
18586
+ function holdsRebuilt(stream) {
18587
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18588
+ }
18345
18589
  var FecReleaseSequencer = class {
18346
18590
  #streams = /* @__PURE__ */ new Map();
18347
18591
  #gate;
@@ -18404,6 +18648,7 @@ var FecReleaseSequencer = class {
18404
18648
  stream.lastSbn = void 0;
18405
18649
  stream.discontinuity = false;
18406
18650
  stream.rapSsId = void 0;
18651
+ stream.sourced = false;
18407
18652
  }
18408
18653
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18409
18654
  this.#recordStat("droppedStale");
@@ -18430,6 +18675,7 @@ var FecReleaseSequencer = class {
18430
18675
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18431
18676
  else hi = mid;
18432
18677
  }
18678
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18433
18679
  stream.buf.splice(lo, 0, frame);
18434
18680
  this.#drainUnlessDeferred(stream);
18435
18681
  }
@@ -18465,6 +18711,41 @@ var FecReleaseSequencer = class {
18465
18711
  }
18466
18712
  return false;
18467
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
+ }
18468
18749
  /** Re-evaluate held gaps after FEC state changed. */
18469
18750
  poke(streamKey) {
18470
18751
  if (streamKey !== void 0) {
@@ -18556,16 +18837,32 @@ var FecReleaseSequencer = class {
18556
18837
  last: null,
18557
18838
  lastSbn: void 0,
18558
18839
  discontinuity: false,
18559
- rapSsId: void 0
18840
+ rapSsId: void 0,
18841
+ sourced: false,
18842
+ draining: 0
18560
18843
  };
18561
18844
  this.#streams.set(streamKey, stream);
18562
18845
  }
18563
18846
  return stream;
18564
18847
  }
18565
- #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) {
18566
18858
  while (stream.buf.length > 0 && stream.last !== null) {
18567
18859
  const head = stream.buf[0];
18568
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
+ }
18569
18866
  if (stream.discontinuity) {
18570
18867
  stream.discontinuity = false;
18571
18868
  stream.buf.shift();
@@ -18579,9 +18876,10 @@ var FecReleaseSequencer = class {
18579
18876
  }
18580
18877
  const headSbn = head.meta.fecSbn;
18581
18878
  const from = stream.lastSbn ?? headSbn;
18582
- const to = headSbn ?? stream.lastSbn;
18879
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18583
18880
  let lost = [];
18584
18881
  const unanchored = from === void 0 && to === void 0;
18882
+ let unresolved = false;
18585
18883
  if (from !== void 0 && to !== void 0) {
18586
18884
  const resolved = this.#gate.resolvedLoss(
18587
18885
  stream.packetId,
@@ -18590,7 +18888,8 @@ var FecReleaseSequencer = class {
18590
18888
  Math.max(from, to),
18591
18889
  from
18592
18890
  );
18593
- if (resolved === null) {
18891
+ unresolved = resolved === null;
18892
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18594
18893
  const heldMs = performance.now() - head.arrivedAtMs;
18595
18894
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18596
18895
  this.#recordStat("forcedWindowFlushes");
@@ -18598,22 +18897,56 @@ var FecReleaseSequencer = class {
18598
18897
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18599
18898
  continue;
18600
18899
  }
18601
- lost = resolved;
18900
+ lost = resolved ?? [];
18602
18901
  }
18603
18902
  stream.buf.shift();
18604
18903
  this.#release(
18605
18904
  stream,
18606
18905
  head,
18607
- 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
18608
18907
  );
18609
18908
  }
18610
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
+ }
18611
18939
  #release(stream, frame, decision) {
18940
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18941
+ if (stale) this.#recordStat("droppedStale");
18612
18942
  if (decision) frame.meta.gapBefore = decision;
18613
18943
  stream.last = frame;
18614
- 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;
18615
18948
  this.#gate.onReleased?.(frame.data, frame.meta);
18616
- this.#emit(frame.data, frame.meta);
18949
+ if (!stale) this.#emit(frame.data, frame.meta);
18617
18950
  }
18618
18951
  };
18619
18952
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18874,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18874
19207
  /**
18875
19208
  * Typed view of the ISO-BMFF timing-sample budget.
18876
19209
  *
18877
- * This is the acceptance signal for the budget ratchet: `pending` walking
18878
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18879
- * and it is observable here rather than only as media silently stopping.
18880
- * Per repo policy this is a producer-owned counter -- never infer budget
18881
- * 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.
18882
19216
  */
18883
19217
  get timingBudgetStats() {
18884
19218
  return {
@@ -18938,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18938
19272
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18939
19273
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18940
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
+ }
18941
19283
  /** Re-evaluate held gaps after FEC block state changed. */
18942
19284
  pokeFecRelease(packetId, catalogTrackName) {
18943
19285
  if (!this.#fecSequencer) return;
@@ -19191,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19191
19533
  if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19192
19534
  const pendingBefore = resolver2.pendingSampleCount;
19193
19535
  const evictedBefore = resolver2.evictedSampleCount;
19194
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19195
19537
  const admitted = resolver2.addMovieFragment(
19196
19538
  mpu.mpuSequenceNumber,
19197
19539
  data,
@@ -19264,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19264
19606
  info,
19265
19607
  resolver,
19266
19608
  options?.recovered === true,
19267
- options?.fecSbn
19609
+ options?.fecSbn,
19610
+ options?.fecSbnFirst
19268
19611
  );
19269
19612
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19270
19613
  }
@@ -19357,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19357
19700
  info,
19358
19701
  resolver,
19359
19702
  options?.recovered === true,
19360
- options?.fecSbn
19703
+ options?.fecSbn,
19704
+ options?.fecSbnFirst
19361
19705
  );
19362
19706
  if (outcome.record !== void 0) return outcome.record;
19363
19707
  return this.#drop(outcome.reason);
@@ -19377,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19377
19721
  * attribution exists to keep readable. Leaving the choice at the call site
19378
19722
  * is what keeps both true.
19379
19723
  */
19380
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19724
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19381
19725
  const timing = resolver.sampleTiming(
19382
19726
  combined.mpuSequenceNumber,
19383
19727
  combined.movieFragmentSequenceNumber,
@@ -19394,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19394
19738
  );
19395
19739
  if (!meta) return { reason: "combined-meta-build-failed" };
19396
19740
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19741
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19397
19742
  if (!resolver.consumeSample(
19398
19743
  combined.mpuSequenceNumber,
19399
19744
  combined.movieFragmentSequenceNumber,
@@ -22899,7 +23244,18 @@ function keyframeOverlaySsId(header) {
22899
23244
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22900
23245
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22901
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;
22902
23257
  var OVERLAY_OFFSET_MPUS = 16;
23258
+ var OVERLAY_REBUILT_LIMIT = 256;
22903
23259
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22904
23260
  function assertStreamIdentity(role, stream) {
22905
23261
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -23010,6 +23366,7 @@ var MmtpRouter = class {
23010
23366
  #provenance = /* @__PURE__ */ new Map();
23011
23367
  #onFlowProtected = null;
23012
23368
  #unadmittedListedDrops = 0;
23369
+ #admittedSourceDatagrams = 0;
23013
23370
  #unlistedRepairSetAside = 0;
23014
23371
  #assistBlocks = /* @__PURE__ */ new Map();
23015
23372
  #assistFailures = /* @__PURE__ */ new Map();
@@ -23066,6 +23423,26 @@ var MmtpRouter = class {
23066
23423
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
23067
23424
  */
23068
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();
23069
23446
  /**
23070
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23071
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -23135,7 +23512,9 @@ var MmtpRouter = class {
23135
23512
  recovered: 0,
23136
23513
  restarts: 0,
23137
23514
  unsequencedDropped: 0,
23138
- protectedDropped: 0
23515
+ protectedDropped: 0,
23516
+ baseFed: 0,
23517
+ baseUndetermined: 0
23139
23518
  };
23140
23519
  /** Callback fired when a delta-track FEC block completes or fails. */
23141
23520
  #onDeltaBlockCompleteCb = null;
@@ -23341,13 +23720,18 @@ var MmtpRouter = class {
23341
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23342
23721
  }
23343
23722
  };
23344
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23345
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23346
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23347
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23348
23727
  }
23349
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23350
- 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
+ }
23351
23735
  };
23352
23736
  }
23353
23737
  }
@@ -23392,8 +23776,9 @@ var MmtpRouter = class {
23392
23776
  }
23393
23777
  /**
23394
23778
  * FEC-recovered source symbols whose reconstructed header named a
23395
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23396
- * 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.
23397
23782
  */
23398
23783
  get restampedRecoveredSequenceCount() {
23399
23784
  return this.#restampedRecoveredSeqs;
@@ -23572,6 +23957,7 @@ var MmtpRouter = class {
23572
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23573
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23574
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23575
23961
  if (previous) {
23576
23962
  previous.flush();
23577
23963
  previous.dispose();
@@ -23722,7 +24108,8 @@ var MmtpRouter = class {
23722
24108
  fec,
23723
24109
  k,
23724
24110
  offsets: /* @__PURE__ */ new Map(),
23725
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23726
24113
  };
23727
24114
  this.#overlays.set(repairPacketId, inst);
23728
24115
  return () => {
@@ -23752,12 +24139,18 @@ var MmtpRouter = class {
23752
24139
  */
23753
24140
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23754
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
+ }
23755
24147
  }
23756
24148
  #resetOverlay(inst) {
23757
24149
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23758
24150
  inst.last = void 0;
23759
24151
  inst.offsets.clear();
23760
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23761
24154
  }
23762
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23763
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23808,18 +24201,33 @@ var MmtpRouter = class {
23808
24201
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23809
24202
  * every overlay installed for its source, at that SS_ID, with the bytes the
23810
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23811
- * included (7.1, 7.4.3).
23812
- */
23813
- #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) {
23814
24215
  if (this.#overlays.size === 0) return;
23815
24216
  const ssId = keyframeOverlaySsId(header);
23816
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23817
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23818
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23819
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23820
24224
  if (identity !== null) {
23821
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
23822
- 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
+ });
23823
24231
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23824
24232
  }
23825
24233
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23841,6 +24249,14 @@ var MmtpRouter = class {
23841
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23842
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23843
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
+ }
23844
24260
  }
23845
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23846
24262
  inst.moqBlocks.add(blockStart);
@@ -23874,11 +24290,16 @@ var MmtpRouter = class {
23874
24290
  }
23875
24291
  /**
23876
24292
  * Route an overlay-recovered block's packets as source media of the
23877
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23878
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23879
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23880
- * from its keyframe's own offset; without one, a stream the base never
23881
- * 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.
23882
24303
  */
23883
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
23884
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23898,22 +24319,139 @@ var MmtpRouter = class {
23898
24319
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23899
24320
  const restamped = this.#restampedStreams.has(streamKey);
23900
24321
  const restored = /* @__PURE__ */ new Set();
24322
+ const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
23901
24324
  const symbols = result.symbols.flatMap((sym) => {
23902
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23903
- if (offset !== void 0) {
23904
- 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;
23905
24329
  restored.add(packetSequenceNumber);
23906
- 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];
23907
24353
  }
23908
- if (!restamped) return [sym];
23909
24354
  this.#overlayStats.unsequencedDropped++;
23910
24355
  return [];
23911
24356
  });
23912
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
+ }
23913
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23914
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
23915
24373
  });
23916
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
+ }
23917
24455
  }
23918
24456
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23919
24457
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -24053,6 +24591,10 @@ var MmtpRouter = class {
24053
24591
  get unadmittedListedDropCount() {
24054
24592
  return this.#unadmittedListedDrops;
24055
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
+ }
24056
24598
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
24057
24599
  get unlistedRepairSetAsideCount() {
24058
24600
  return this.#unlistedRepairSetAside;
@@ -24539,6 +25081,8 @@ var MmtpRouter = class {
24539
25081
  )) return;
24540
25082
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24541
25083
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
25084
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
25085
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24542
25086
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24543
25087
  const recovered = fec.feedRepairSymbol(
24544
25088
  ownerPacketId,
@@ -24589,7 +25133,7 @@ var MmtpRouter = class {
24589
25133
  catalogTrackName
24590
25134
  )) continue;
24591
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24592
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24593
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24594
25138
  }
24595
25139
  if (payload.oversized) continue;
@@ -24676,6 +25220,8 @@ var MmtpRouter = class {
24676
25220
  this.#droppedPreSyncVideo.delete(streamKey);
24677
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24678
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25223
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24679
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24680
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24681
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24802,6 +25348,12 @@ var MmtpRouter = class {
24802
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24803
25349
  * own RAP cadence, so no timer is involved.
24804
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
+ *
24805
25357
  * The commit is one synchronous transition inside the frame path: the
24806
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
24807
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24852,6 +25404,9 @@ var MmtpRouter = class {
24852
25404
  droppedIncomingFrames: 0
24853
25405
  };
24854
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
24855
25410
  return {
24856
25411
  cancel: () => {
24857
25412
  if (this.#staged !== state) return false;
@@ -24934,7 +25489,17 @@ var MmtpRouter = class {
24934
25489
  */
24935
25490
  #routeParsedFrame(data, meta) {
24936
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
24937
- 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
+ }
24938
25503
  this.#deliverParsedFrame(data, meta);
24939
25504
  }
24940
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -25086,12 +25651,16 @@ var MmtpRouter = class {
25086
25651
  this.#parser.deferFecRelease(work);
25087
25652
  } finally {
25088
25653
  this.#fecPokeDepth--;
25089
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25090
- const pokes = [...this.#pendingFecPokes.values()];
25091
- this.#pendingFecPokes.clear();
25092
- for (const poke of pokes) {
25093
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25094
- }
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);
25095
25664
  }
25096
25665
  }
25097
25666
  }
@@ -25169,6 +25738,14 @@ var MmtpRouter = class {
25169
25738
  onRap: (packetId, catalogTrackName, ssId) => {
25170
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25171
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
+ },
25172
25749
  onReleased: (data, meta) => {
25173
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25174
25751
  const timing = meta.authoredTiming;
@@ -25310,6 +25887,7 @@ var MmtpRouter = class {
25310
25887
  const parsed = this.#parser.parsePacket(packet);
25311
25888
  if (!parsed) return;
25312
25889
  const info = parsed.info;
25890
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25313
25891
  if (source === "multicast" && !manifestAdmitted) {
25314
25892
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25315
25893
  }
@@ -25591,7 +26169,8 @@ var MmtpRouter = class {
25591
26169
  parsed.header,
25592
26170
  info.sequenceNumber,
25593
26171
  info.rapFlag,
25594
- source
26172
+ source,
26173
+ fecPayloadId?.ssId
25595
26174
  );
25596
26175
  }
25597
26176
  }
@@ -25617,7 +26196,8 @@ var MmtpRouter = class {
25617
26196
  source: parserOptions?.source,
25618
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25619
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25620
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25621
26201
  };
25622
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25623
26203
  else this.#parser.feed(packet, feedOptions);
@@ -25741,7 +26321,8 @@ var MmtpRouter = class {
25741
26321
  if (stream === void 0) {
25742
26322
  stream = {
25743
26323
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25744
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26324
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26325
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25745
26326
  };
25746
26327
  byName.set(catalogTrackName, stream);
25747
26328
  }
@@ -26019,6 +26600,13 @@ var MmtpRouter = class {
26019
26600
  * frames by epoch only if pre-protection input ever matters.
26020
26601
  */
26021
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
+ }
26022
26610
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
26023
26611
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
26024
26612
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -26075,7 +26663,12 @@ var MmtpRouter = class {
26075
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
26076
26664
  if (ownerPacketId == null) return;
26077
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
26078
- 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
+ };
26079
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
26080
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
26081
26674
  if (ssIdSeqOffset !== void 0) {
@@ -26083,7 +26676,10 @@ var MmtpRouter = class {
26083
26676
  ...result,
26084
26677
  symbols: result.symbols.map((sym) => {
26085
26678
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
26086
- if (wire === sym.packetSequenceNumber) return sym;
26679
+ if (wire === sym.packetSequenceNumber) {
26680
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26681
+ return sym;
26682
+ }
26087
26683
  this.#restampedRecoveredSeqs++;
26088
26684
  this.#restampedStreams.add(streamKey);
26089
26685
  return { ...sym, packetSequenceNumber: wire };
@@ -27868,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
27868
28464
  function isSymbolCounts(value) {
27869
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
27870
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
+ }
27871
28473
  function isNullableNonNegative(value) {
27872
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
27873
28475
  }
@@ -27887,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
27887
28489
  case "recovered":
27888
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"]);
27889
28491
  case "failed":
27890
- 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"]));
27891
28496
  case "repair-preempted":
27892
28497
  case "repair-deferred":
27893
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -28153,7 +28758,7 @@ var MulticastManager = class {
28153
28758
  onPublisherRestart: (packetId, catalogTrackName) => {
28154
28759
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
28155
28760
  },
28156
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28761
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
28157
28762
  const pathCount = this.#fec.blockPathCount(
28158
28763
  sbn,
28159
28764
  packetId,
@@ -28171,7 +28776,8 @@ var MulticastManager = class {
28171
28776
  symbols: {
28172
28777
  multicast: pathCount?.multicastSymbols ?? 0,
28173
28778
  moq: pathCount?.moqSymbols ?? 0
28174
- }
28779
+ },
28780
+ ...span === void 0 ? {} : { span }
28175
28781
  });
28176
28782
  },
28177
28783
  onFrameGap: (decision) => {
@@ -29551,10 +30157,12 @@ var MulticastManager = class {
29551
30157
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29552
30158
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29553
30159
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29554
- * 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.
29555
30163
  */
29556
- onUndelimitedLoss(cb) {
29557
- this.#fec.setUndelimitedLossHandler(cb);
30164
+ onUndelimitedLoss(cb, onExpired) {
30165
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29558
30166
  }
29559
30167
  /**
29560
30168
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -29790,7 +30398,8 @@ var MulticastManager = class {
29790
30398
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
29791
30399
  ...event,
29792
30400
  block: Object.freeze({ ...event.block }),
29793
- ..."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 }) } : {}
29794
30403
  }
29795
30404
  );
29796
30405
  if (this.#logRepairLifecycle) {
@@ -32462,6 +33071,7 @@ var RepairLayerSubscriptions = class {
32462
33071
 
32463
33072
  // src/repair-fetch.ts
32464
33073
  var MOQT_PRIORITY_MAX = 255;
33074
+ var WALK_LEAD_MS = 3;
32465
33075
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32466
33076
  function isMoqtPriority(value) {
32467
33077
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32512,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
32512
33122
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32513
33123
  };
32514
33124
  const clearEntry = (entry) => {
32515
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32516
- 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);
32517
33136
  };
32518
33137
  const releaseHold = (entry) => {
32519
33138
  if (entry.range) return;
@@ -32580,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
32580
33199
  const admit = (entry, now) => {
32581
33200
  const budgetMs = entry.deadlineAt - now;
32582
33201
  const rttMs = readRtt();
32583
- 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);
32584
33203
  if (reason === null) return rttMs;
32585
33204
  skip(entry, reason, rttMs, budgetMs);
32586
33205
  return null;
32587
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";
32588
33208
  const retire = (entry) => {
32589
33209
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32590
33210
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32591
33211
  };
32592
- 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) => {
32593
33220
  for (const entry of entries.values()) {
32594
- if (entry.priority < priority) return true;
33221
+ if (holdsBack(entry, priority, videoWalks)) return true;
32595
33222
  }
32596
33223
  return false;
32597
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
+ };
32598
33238
  const preemptBestEffort = () => {
32599
33239
  for (const entry of entries.values()) {
32600
33240
  if (entry.priority === 0 || !entry.running) continue;
@@ -32623,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
32623
33263
  }
32624
33264
  const rttMs = admit(entry, now);
32625
33265
  if (rttMs === null) return;
32626
- if (entry.priority === 0) preemptBestEffort();
33266
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33267
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32627
33268
  if (counted) entry.attempts++;
32628
33269
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32629
33270
  rttMs,
@@ -32645,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
32645
33286
  }
32646
33287
  void execute(entry, run);
32647
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
+ };
32648
33297
  const dispatch = () => {
32649
33298
  if (disposed) return;
32650
33299
  const now = performance.now();
@@ -32669,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
32669
33318
  for (const entry of queued) {
32670
33319
  if (entry.priority === 0) start(entry);
32671
33320
  }
33321
+ yieldVideoWalks(queued);
32672
33322
  if (outstandingBelow(1)) return;
32673
33323
  for (const entry of queued) {
32674
33324
  if (entry.priority === 1) start(entry);
@@ -32695,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
32695
33345
  }
32696
33346
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
32697
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
+ };
32698
33358
  const finish = (entry, run, fed, error) => {
32699
33359
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
32700
33360
  return;
@@ -32739,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
32739
33399
  signal: run.controller.signal,
32740
33400
  subscriberPriority: entry.subscriberPriority
32741
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
+ };
32742
33410
  try {
32743
- 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({
32744
33412
  ...common,
32745
- startGroup: entry.range.startGroup,
32746
- endGroup: entry.range.endGroup,
32747
- lostSsId: entry.range.lostSsId
32748
- }) : opts.transport({ ...common, sbn: entry.sbn });
33413
+ startGroup: range.groups.startGroup,
33414
+ endGroup: range.groups.endGroup,
33415
+ lostSsId: range.lostSsId
33416
+ });
32749
33417
  for await (const packet of packets) {
32750
33418
  if (run.stopped !== null || entry.running !== run) break;
32751
33419
  fed++;
32752
- if (entry.catalogTrackName === void 0) {
32753
- mc.processRepairFromMoQ(packet, entry.packetId);
32754
- } else {
32755
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
32756
- }
33420
+ if (range === void 0) feed(packet);
33421
+ else atReport(range, () => feed(packet));
32757
33422
  }
32758
33423
  finish(entry, run, fed, void 0);
32759
33424
  } catch (error) {
32760
33425
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
32761
33426
  }
32762
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
+ }
32763
33477
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
32764
33478
  for (const entry of [...entries.values()]) {
32765
33479
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -32955,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
32955
33669
  throw error;
32956
33670
  }
32957
33671
  };
32958
- 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) {
32959
33703
  mc.onUndelimitedLoss((loss) => {
32960
33704
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
32961
33705
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -32963,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
32963
33707
  const blockProfile = blockProfileOf(packetId);
32964
33708
  const target = opts.resolveTarget(packetId, catalogTrackName);
32965
33709
  if (!target) return close();
32966
- const priority = repairPriorityForClass("unknown");
32967
- const range = rangeFor(loss, target.repairTrack);
32968
- 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) {
32969
33715
  close();
32970
33716
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
32971
33717
  return;
@@ -32976,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
32976
33722
  packetId,
32977
33723
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
32978
33724
  };
32979
- const wire = wirePriority(identity, "unknown");
32980
- 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);
32981
33729
  if (deadline === null) {
32982
33730
  close();
32983
33731
  mc.noteRepairSkipped(
@@ -32989,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
32989
33737
  );
32990
33738
  return;
32991
33739
  }
33740
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
32992
33741
  const entry = {
32993
33742
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
32994
33743
  ...identity,
@@ -32996,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
32996
33745
  blockProfile,
32997
33746
  priority,
32998
33747
  subscriberPriority: wire.subscriberPriority,
32999
- deadlineAt: deadline.deadlineAt,
33000
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33748
+ deadlineAt,
33749
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
33001
33750
  anchorAt: deadline.anchorAt,
33002
33751
  delayMs: deadline.delayMs,
33003
33752
  bound: deadline.bound,
33004
33753
  order: enqueueOrder++,
33005
33754
  attempts: 0,
33006
33755
  running: null,
33007
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33756
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33757
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
33008
33758
  };
33009
33759
  const previous = entries.get(entry.key);
33010
33760
  if (previous) {
@@ -33014,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
33014
33764
  entries.set(entry.key, entry);
33015
33765
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
33016
33766
  dispatch();
33017
- });
33767
+ }, onHoleExpired);
33018
33768
  }
33019
33769
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
33020
33770
  queueMicrotask(() => {