@blockcast/mmt-container 2.2.0-main.66f097ed5157 → 2.2.0-main.6a18a111b136

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 (45) hide show
  1. package/dist/browser-runtime.js +523 -117
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/fec-manager.d.ts +37 -11
  4. package/dist/fec-manager.d.ts.map +1 -1
  5. package/dist/fec-manager.js +181 -52
  6. package/dist/fec-manager.js.map +2 -2
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js.map +2 -2
  10. package/dist/iso-block-locator.d.ts +39 -13
  11. package/dist/iso-block-locator.d.ts.map +1 -1
  12. package/dist/iso-block-locator.js +80 -25
  13. package/dist/iso-block-locator.js.map +2 -2
  14. package/dist/mmtp-container-parser.d.ts +2 -0
  15. package/dist/mmtp-container-parser.d.ts.map +1 -1
  16. package/dist/mmtp-container-parser.js +52 -7
  17. package/dist/mmtp-container-parser.js.map +2 -2
  18. package/dist/mmtp-container-parser.no-modules.js +53 -8
  19. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  20. package/dist/mmtp-decode-pipeline.js +52 -7
  21. package/dist/mmtp-decode-pipeline.js.map +2 -2
  22. package/dist/mmtp-router.d.ts +12 -9
  23. package/dist/mmtp-router.d.ts.map +1 -1
  24. package/dist/mmtp-router.js +144 -32
  25. package/dist/mmtp-router.js.map +2 -2
  26. package/dist/multicast-manager.d.ts +13 -3
  27. package/dist/multicast-manager.d.ts.map +1 -1
  28. package/dist/multicast-manager.js +345 -91
  29. package/dist/multicast-manager.js.map +2 -2
  30. package/dist/no-modules.js +52 -7
  31. package/dist/no-modules.js.map +2 -2
  32. package/dist/repair-fetch.d.ts +36 -8
  33. package/dist/repair-fetch.d.ts.map +1 -1
  34. package/dist/repair-fetch.js +285 -26
  35. package/dist/repair-fetch.js.map +3 -3
  36. package/dist/wasm-mmtp-container-parser.js +52 -7
  37. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  38. package/package.json +5 -5
  39. package/src/fec-manager.ts +156 -26
  40. package/src/index.ts +2 -0
  41. package/src/iso-block-locator.ts +114 -31
  42. package/src/mmtp-container-parser.ts +77 -6
  43. package/src/mmtp-router.ts +157 -42
  44. package/src/multicast-manager.ts +44 -6
  45. package/src/repair-fetch.ts +363 -49
@@ -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
  }
@@ -12928,6 +12939,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
12928
12939
  this.#heldEvicted = 0;
12929
12940
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
12930
12941
  this.#holes = [];
12942
+ /** Holes the horizon passed while pending: sources held until closed. */
12943
+ this.#expired = [];
12944
+ /** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
12945
+ this.#newlyExpired = [];
12946
+ /**
12947
+ * Spans closed from expired holes: behind the horizon, so kept only for
12948
+ * `locate` while their sources are fed, until the next retirement.
12949
+ */
12950
+ this.#closedBehind = [];
12931
12951
  /**
12932
12952
  * Keep a gap that is missing a source as a pending hole rather than a span,
12933
12953
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -12951,6 +12971,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
12951
12971
  #newest;
12952
12972
  #newestSource;
12953
12973
  #holes;
12974
+ #expired;
12975
+ #newlyExpired;
12976
+ #closedBehind;
12954
12977
  static #checkedRetention(retentionBlocks) {
12955
12978
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
12956
12979
  throw new Error(
@@ -13020,17 +13043,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
13020
13043
  }
13021
13044
  /** The known block or span containing `ssId`, with its ESI. */
13022
13045
  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) };
13046
+ for (const blocks of [this.#blocks, this.#closedBehind]) {
13047
+ for (let i = blocks.length - 1; i >= 0; i--) {
13048
+ const block = blocks[i];
13049
+ if (isoBlockContains(block.start, block.length, ssId)) {
13050
+ return { block, esi: serial32Distance(block.start, ssId) };
13051
+ }
13027
13052
  }
13028
13053
  }
13029
13054
  return null;
13030
13055
  }
13031
- /** Number of pending holes. */
13056
+ /** Number of holes not closed yet, pending or past the horizon. */
13032
13057
  get holeCount() {
13033
- return this.#holes.length;
13058
+ return this.#holes.length + this.#expired.length;
13059
+ }
13060
+ /**
13061
+ * The pending holes the horizon has passed since the last call, in the
13062
+ * order it passed them. Each is no longer pending: no repair states a block
13063
+ * in it (that repair is `stale`, as for a span the horizon retired) and the
13064
+ * release gate does not wait for it. Its owner must close it
13065
+ * (`closeHolesWithin`), which turns it into a span that is not kept here.
13066
+ */
13067
+ takeExpiredHoles() {
13068
+ return this.#newlyExpired.splice(0);
13034
13069
  }
13035
13070
  /** The stated block (never a span) ending exactly at `end`. */
13036
13071
  blockEndingAt(end) {
@@ -13086,9 +13121,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
13086
13121
  /**
13087
13122
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
13088
13123
  * 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.
13124
+ * symbols of the same block change nothing. A repair whose range is exactly
13125
+ * a repair-less span states that span's one block. A range that contradicts
13126
+ * a known block (a different length, or an overlap with another block or
13127
+ * with part of a span) fails closed: the repair symbol is dropped and no
13128
+ * block changes.
13092
13129
  * A newly stated block retires behind its SS_Start (see the class note).
13093
13130
  * A block so far past `newest` that stating it would retire the position
13094
13131
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -13107,7 +13144,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
13107
13144
  const known = this.blockAt(start);
13108
13145
  if (known !== void 0) {
13109
13146
  if (known.span) {
13110
- return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
13147
+ if (known.length === length) {
13148
+ const block2 = { start, length, span: false };
13149
+ this.#blocks[this.#blocks.indexOf(known)] = block2;
13150
+ return { status: "placed", block: block2, placed: [] };
13151
+ }
13152
+ return length < known.length ? {
13153
+ status: "overlap",
13154
+ reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
13155
+ ofSpan: known
13156
+ } : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
13111
13157
  }
13112
13158
  if (known.length !== length) {
13113
13159
  return {
@@ -13175,18 +13221,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
13175
13221
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
13176
13222
  }
13177
13223
  /**
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.
13224
+ * Turn every hole inside `[start, start + length)` into a repair-less span,
13225
+ * claiming its held sources: its range FETCH ended, so nothing more will
13226
+ * state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
13227
+ * span that is not kept among the blocks, as the horizon would retire it at
13228
+ * once; only `locate` finds it, until the next retirement, so its sources
13229
+ * can be fed. Returns the spans, in SS_ID order.
13181
13230
  */
13182
13231
  closeHolesWithin(start, length) {
13183
13232
  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);
13233
+ for (const holes of [this.#holes, this.#expired]) {
13234
+ for (const hole of [...holes]) {
13235
+ if (!isoBlockContains(start, length, hole.start)) continue;
13236
+ holes.splice(holes.indexOf(hole), 1);
13237
+ const spanBlock = { start: hole.start, length: hole.length, span: true };
13238
+ closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13239
+ if (holes === this.#holes) {
13240
+ this.#blocks.push(spanBlock);
13241
+ } else {
13242
+ this.#closedBehind.push(spanBlock);
13243
+ if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
13244
+ }
13245
+ }
13190
13246
  }
13191
13247
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
13192
13248
  }
@@ -13200,15 +13256,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
13200
13256
  return void 0;
13201
13257
  }
13202
13258
  /**
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.
13259
+ * Retire every block and span whose last SS_ID, and every held source
13260
+ * outside a hole, lies more than `retentionBlocks` blocks behind
13261
+ * `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
13262
+ * every ISO interleave horizon uses; a pending hole so far behind expires
13263
+ * (`takeExpiredHoles`). Returns the retired block starts.
13207
13264
  */
13208
13265
  retireBehind(position) {
13209
13266
  this.observe(position);
13210
13267
  const horizon = this.#retentionBlocks;
13211
13268
  const retired = [];
13269
+ this.#closedBehind.length = 0;
13212
13270
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
13213
13271
  const block = this.#blocks[i];
13214
13272
  const last = serial32Add(block.start, block.length - 1);
@@ -13220,10 +13278,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
13220
13278
  for (let i = this.#holes.length - 1; i >= 0; i--) {
13221
13279
  const hole = this.#holes[i];
13222
13280
  const last = serial32Add(hole.start, hole.length - 1);
13223
- if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) this.#holes.splice(i, 1);
13281
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
13282
+ this.#holes.splice(i, 1);
13283
+ this.#expired.push(hole);
13284
+ this.#newlyExpired.push(hole);
13224
13285
  }
13225
13286
  for (const ssId of [...this.#held.keys()]) {
13226
- if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
13287
+ 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))) {
13288
+ this.#held.delete(ssId);
13289
+ }
13227
13290
  }
13228
13291
  return retired;
13229
13292
  }
@@ -13231,6 +13294,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
13231
13294
  reset() {
13232
13295
  this.#blocks.length = 0;
13233
13296
  this.#holes.length = 0;
13297
+ this.#expired.length = 0;
13298
+ this.#newlyExpired.length = 0;
13299
+ this.#closedBehind.length = 0;
13234
13300
  this.#held.clear();
13235
13301
  this.#coverageEnd = void 0;
13236
13302
  this.#newest = void 0;
@@ -13948,6 +14014,10 @@ var FECManager = class _FECManager {
13948
14014
  // 'pending' forever, which would park every later frame of its stream in
13949
14015
  // the in-order release gate. Each entry owns its handle; the callback
13950
14016
  // checks it still owns the key, so a settled or reset key is never touched.
14017
+ /**
14018
+ * Per owed Worker cleanup: the bound on its acknowledgement, when its block
14019
+ * was due, and when its cleanup was asked for (see `onCleanupComplete`).
14020
+ */
13951
14021
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13952
14022
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13953
14023
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
@@ -13964,6 +14034,9 @@ var FECManager = class _FECManager {
13964
14034
  // SSB_length source arrivals and recoverable from SSB_length received
13965
14035
  // symbols; waiting for K fails every one of them.
13966
14036
  this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
14037
+ // Keys whose authored length is a repair-less span's (#openIsoSpan), until
14038
+ // a repair states the block there or the span resolves.
14039
+ this.#isoSpanKeys = /* @__PURE__ */ new Set();
13967
14040
  this.#ssbLengthConflictCount = 0;
13968
14041
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13969
14042
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -14161,7 +14234,10 @@ var FECManager = class _FECManager {
14161
14234
  // arrives via onBlockRecovered callback.
14162
14235
  this.onBlockStarted = null;
14163
14236
  this.onBlockComplete = null;
14164
- /** A block reached its fail bound. `blockClass` is its class at failure. */
14237
+ /**
14238
+ * A block reached its fail bound. `blockClass` is its class at failure.
14239
+ * `span` is set when what failed is a repair-less span starting at `sbn`.
14240
+ */
14165
14241
  this.onBlockFailed = null;
14166
14242
  /**
14167
14243
  * Fired once per stream when its Worker decoder stops being routable.
@@ -14203,6 +14279,8 @@ var FECManager = class _FECManager {
14203
14279
  * for it, which it must be.
14204
14280
  */
14205
14281
  this.#onUndelimitedLoss = null;
14282
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14283
+ this.#onIsoHoleExpired = null;
14206
14284
  /** A block every symbol of which vanished was created by SBN evidence. */
14207
14285
  this.onBlockVanished = null;
14208
14286
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -14247,6 +14325,7 @@ var FECManager = class _FECManager {
14247
14325
  #blockSourceEsis;
14248
14326
  #blockRebuiltEsis;
14249
14327
  #blockAuthoredLengths;
14328
+ #isoSpanKeys;
14250
14329
  #ssbLengthConflictCount;
14251
14330
  #blockRepairRsIds;
14252
14331
  #blockRsbLengths;
@@ -14313,6 +14392,7 @@ var FECManager = class _FECManager {
14313
14392
  #decoderUnavailableEvents;
14314
14393
  #repairSymbolSizeMismatchCount;
14315
14394
  #onUndelimitedLoss;
14395
+ #onIsoHoleExpired;
14316
14396
  #encodeTrackName(catalogTrackName) {
14317
14397
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14318
14398
  if (encoded === void 0) {
@@ -14784,7 +14864,10 @@ var FECManager = class _FECManager {
14784
14864
  timer.handle = null;
14785
14865
  this.#requestRepair(sbn, packetId, catalogTrackName);
14786
14866
  };
14787
- timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()));
14867
+ timer.handle = setTimeout(
14868
+ onFire,
14869
+ this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
14870
+ );
14788
14871
  this.#repairDeadlines.set(key, timer);
14789
14872
  }
14790
14873
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14928,8 +15011,14 @@ var FECManager = class _FECManager {
14928
15011
  * Vanished blocks are not enumerated here: their starts are unknown until a
14929
15012
  * repair states them. The SS_IDs between two stated blocks become one
14930
15013
  * repair-less span when the later block's repair arrives (placeIsoRepair).
15014
+ *
15015
+ * A `held` source arrived before its block was known and is fed only now
15016
+ * that a repair, a span or a closed hole placed it: how far its block start
15017
+ * then lies behind the frontier measures how long the receiver held it, not
15018
+ * where the stream was, so only its SS_ID and packet_sequence_number judge
15019
+ * it (BLO-52085).
14931
15020
  */
14932
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15021
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14933
15022
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14934
15023
  const now = performance.now();
14935
15024
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14968,7 +15057,7 @@ var FECManager = class _FECManager {
14968
15057
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14969
15058
  };
14970
15059
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
14971
- 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))) {
15060
+ 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))) {
14972
15061
  discontinuity();
14973
15062
  return;
14974
15063
  }
@@ -15230,17 +15319,21 @@ var FECManager = class _FECManager {
15230
15319
  this.#blockWaitDeadlinesMs.delete(key);
15231
15320
  this.#repairHoldUntilMs.delete(key);
15232
15321
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
15233
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15322
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
15234
15323
  };
15235
- timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
15324
+ timer.handle = setTimeout(
15325
+ onFire,
15326
+ this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
15327
+ );
15236
15328
  this.#activeBlocks.set(key, timer);
15237
15329
  this.#indexBlockKey(key, sbn, packetId);
15238
15330
  }
15239
15331
  /**
15240
15332
  * Hand a block whose delivery is over to its decoder for finalization: the
15241
- * datagram deadline fired, or the ordered relay moved past it.
15333
+ * datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
15334
+ * is when it was due: its deadline, or now.
15242
15335
  */
15243
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15336
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
15244
15337
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
15245
15338
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
15246
15339
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -15256,7 +15349,7 @@ var FECManager = class _FECManager {
15256
15349
  if (workerClient) {
15257
15350
  this.#pendingWorkerCleanups.add(key);
15258
15351
  this.#pendingWorkerCleanupClients.set(key, workerClient);
15259
- this.#boundOwedCleanup(key, packetId, workerClient);
15352
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
15260
15353
  workerClient.cleanup(sbn);
15261
15354
  } else {
15262
15355
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15570,6 +15663,7 @@ var FECManager = class _FECManager {
15570
15663
  resetMap(this.#blockSourceEsis);
15571
15664
  resetMap(this.#blockRebuiltEsis);
15572
15665
  resetMap(this.#blockAuthoredLengths);
15666
+ resetSet(this.#isoSpanKeys);
15573
15667
  resetMap(this.#blockRepairRsIds);
15574
15668
  resetMap(this.#blockRsbLengths);
15575
15669
  resetMap(this.#heldLayerStates);
@@ -15695,6 +15789,7 @@ var FECManager = class _FECManager {
15695
15789
  dropMap(this.#blockSourceEsis);
15696
15790
  dropMap(this.#blockRebuiltEsis);
15697
15791
  dropMap(this.#blockAuthoredLengths);
15792
+ dropSet(this.#isoSpanKeys);
15698
15793
  dropMap(this.#blockRepairRsIds);
15699
15794
  dropMap(this.#blockRsbLengths);
15700
15795
  dropMap(this.#blockPathCounts);
@@ -16037,8 +16132,9 @@ var FECManager = class _FECManager {
16037
16132
  return true;
16038
16133
  }
16039
16134
  /**
16040
- * Drop a block's repair hold: its FETCH ended without delivering. The block
16041
- * falls back to its multicast wait, which may already have passed.
16135
+ * Drop a block's repair hold: its FETCH ended without delivering, or brought
16136
+ * back only a repair that cannot complete it. The block falls back to its
16137
+ * multicast wait, which may already have passed.
16042
16138
  */
16043
16139
  releaseRepairHold(sbn, packetId, catalogTrackName) {
16044
16140
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16176,6 +16272,7 @@ var FECManager = class _FECManager {
16176
16272
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16177
16273
  const evictedBefore = locator.heldEvicted;
16178
16274
  const held = locator.hold(ssId, source);
16275
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16179
16276
  if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16180
16277
  const evicted = locator.heldEvicted - evictedBefore;
16181
16278
  if (evicted > 0) {
@@ -16197,17 +16294,20 @@ var FECManager = class _FECManager {
16197
16294
  * contradicts a known block is refused (counted) and must be dropped.
16198
16295
  */
16199
16296
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
16200
- const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
16297
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16298
+ const placement = locator.placeRepair(ssStart, ssbLength);
16299
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16201
16300
  if (placement.status !== "placed") {
16202
16301
  this.#isoRepairRefusedCount++;
16203
16302
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
16204
16303
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
16205
16304
  }
16305
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
16206
16306
  return placement;
16207
16307
  }
16308
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
16208
16309
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
16209
16310
  if (placement.hole !== void 0) {
16210
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
16211
16311
  const hole = placement.hole.block;
16212
16312
  const previous = locator.blockEndingAt(hole.start);
16213
16313
  const loss = {
@@ -16228,17 +16328,29 @@ var FECManager = class _FECManager {
16228
16328
  * `handler`, instead of a repair-less span; null restores spans. Set by the
16229
16329
  * range repair scheduler; applies to every stream from its next gap. The
16230
16330
  * handler and the deferral are one setting, so no hole is ever created with
16231
- * nobody to close it.
16331
+ * nobody to close it. `onExpired` is told of each pending hole the stream
16332
+ * runs past the locator's horizon (BLO-52085): returning true, it closes
16333
+ * the hole (`closeIsoHoles`), so the span it leaves still fails and is
16334
+ * reported; otherwise the hole is discarded with its sources.
16232
16335
  */
16233
- setUndelimitedLossHandler(handler) {
16336
+ setUndelimitedLossHandler(handler, onExpired = null) {
16234
16337
  this.#onUndelimitedLoss = handler;
16338
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
16235
16339
  this.#deferIsoGaps = handler !== null;
16236
16340
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
16237
16341
  }
16342
+ /** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
16343
+ #settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
16344
+ for (const hole of locator.takeExpiredHoles()) {
16345
+ const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
16346
+ if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
16347
+ }
16348
+ }
16238
16349
  /**
16239
- * The range repair for a hole ended: every hole still pending inside
16240
- * `[start, start + length)` becomes a repair-less span. Returns those spans
16241
- * with the held sources they placed, for the caller to feed (MmtpRouter).
16350
+ * The range repair for a hole ended: every hole inside `[start, start +
16351
+ * length)`, pending or expired, becomes a repair-less span. Returns those
16352
+ * spans with the held sources they placed, for the caller to feed
16353
+ * (MmtpRouter).
16242
16354
  */
16243
16355
  closeIsoHoles(packetId, catalogTrackName, start, length) {
16244
16356
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -16257,7 +16369,10 @@ var FECManager = class _FECManager {
16257
16369
  */
16258
16370
  #openIsoSpan(span, packetId, catalogTrackName) {
16259
16371
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
16260
- if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, span.block.length);
16372
+ if (!this.#blockAuthoredLengths.has(key)) {
16373
+ this.#blockAuthoredLengths.set(key, span.block.length);
16374
+ this.#isoSpanKeys.add(key);
16375
+ }
16261
16376
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
16262
16377
  }
16263
16378
  /**
@@ -16598,11 +16713,14 @@ var FECManager = class _FECManager {
16598
16713
  });
16599
16714
  client.onCleanupComplete((sbn) => {
16600
16715
  if (this.#releasedWorkerClients.has(client)) return;
16601
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16716
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16717
+ const cleanupKeys = owed.filter((key) => {
16602
16718
  const block = this.#parseBlockKey(key);
16603
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16719
+ return block.packetId === packetId && block.sbn === sbn;
16604
16720
  });
16605
- for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
16721
+ const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
16722
+ const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
16723
+ for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
16606
16724
  });
16607
16725
  client.onError((msg, code) => {
16608
16726
  const requestedKind = code ?? msg;
@@ -16672,9 +16790,11 @@ var FECManager = class _FECManager {
16672
16790
  * fail deadline already passed, so failing it is the decision that deadline
16673
16791
  * made. A late ack then finds nothing owed and is a no-op.
16674
16792
  */
16675
- #boundOwedCleanup(key, packetId, client) {
16793
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16676
16794
  this.#clearOwedCleanupBound(key);
16677
16795
  const bound = {
16796
+ dueAtMs,
16797
+ askedAtMs: performance.now(),
16678
16798
  handle: setTimeout(() => {
16679
16799
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16680
16800
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -17245,12 +17365,13 @@ var FECManager = class _FECManager {
17245
17365
  * the inactivity deadline on each symbol, records the symbol's in-band
17246
17366
  * classification evidence, and marks the block clean when all of its
17247
17367
  * authored source symbols arrived: the SSB_length a repair symbol declared,
17248
- * otherwise K.
17368
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17369
+ * placed only now (see #noteSbnSeen).
17249
17370
  *
17250
17371
  * Throws for a packetId that was never configured, so a classified block
17251
17372
  * never reads the manager-global K or timeout.
17252
17373
  */
17253
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17374
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
17254
17375
  const trackConfig = this.#trackConfigs.get(packetId);
17255
17376
  if (trackConfig === void 0) {
17256
17377
  throw new Error(
@@ -17266,7 +17387,8 @@ var FECManager = class _FECManager {
17266
17387
  catalogTrackName,
17267
17388
  "source",
17268
17389
  evidence.packetSequenceNumber,
17269
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17390
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17391
+ held
17270
17392
  );
17271
17393
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
17272
17394
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -17321,6 +17443,7 @@ var FECManager = class _FECManager {
17321
17443
  this.#blockSourceEsis.delete(key);
17322
17444
  this.#blockRebuiltEsis.delete(key);
17323
17445
  this.#blockAuthoredLengths.delete(key);
17446
+ this.#isoSpanKeys.delete(key);
17324
17447
  this.#blockRepairRsIds.delete(key);
17325
17448
  this.#blockRsbLengths.delete(key);
17326
17449
  this.#markTerminal(key, sbn, packetId, true);
@@ -17351,6 +17474,19 @@ var FECManager = class _FECManager {
17351
17474
  #receiptNow() {
17352
17475
  return this.#receiptAtMs ?? performance.now();
17353
17476
  }
17477
+ /**
17478
+ * The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
17479
+ * scope: counted from the receipt time, as the timer of a symbol fed when
17480
+ * it was received was, less the whole ms since. So it fires on the tick
17481
+ * that timer would have, never later than a delay counted from now, and
17482
+ * its handler still re-arms a timer that fires before its deadline.
17483
+ * Counted from now, a deadline set from the receipt time is rounded up
17484
+ * from another fraction of a ms (BLO-52085).
17485
+ */
17486
+ #receiptTimerDelayMs(deadlineMs, minMs) {
17487
+ const receivedAtMs = this.#receiptAtMs;
17488
+ return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
17489
+ }
17354
17490
  /**
17355
17491
  * A held packet admitted late arms from its (earlier) receipt time; that
17356
17492
  * never pulls in a deadline later activity already armed. Outside a
@@ -17399,6 +17535,7 @@ var FECManager = class _FECManager {
17399
17535
  }
17400
17536
  #failBlock(sbn, packetId, catalogTrackName) {
17401
17537
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
17538
+ const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
17402
17539
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
17403
17540
  this.#clearRepairDeadline(key);
17404
17541
  this.#clearBlockTimer(key);
@@ -17418,7 +17555,8 @@ var FECManager = class _FECManager {
17418
17555
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
17419
17556
  this.#fecStats.blocksFailed++;
17420
17557
  this.#statsForPacket(packetId).blocksFailed++;
17421
- this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17558
+ if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
17559
+ else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
17422
17560
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
17423
17561
  }
17424
17562
  /**
@@ -17449,6 +17587,7 @@ var FECManager = class _FECManager {
17449
17587
  this.#blockSourceEsis.delete(key);
17450
17588
  this.#blockRebuiltEsis.delete(key);
17451
17589
  this.#blockAuthoredLengths.delete(key);
17590
+ this.#isoSpanKeys.delete(key);
17452
17591
  this.#blockRepairRsIds.delete(key);
17453
17592
  this.#blockRsbLengths.delete(key);
17454
17593
  this.#clearBlockTimer(key);
@@ -17541,6 +17680,7 @@ var FECManager = class _FECManager {
17541
17680
  this.#blockSourceEsis.clear();
17542
17681
  this.#blockRebuiltEsis.clear();
17543
17682
  this.#blockAuthoredLengths.clear();
17683
+ this.#isoSpanKeys.clear();
17544
17684
  this.#blockRepairRsIds.clear();
17545
17685
  this.#blockRsbLengths.clear();
17546
17686
  this.#heldLayerStates.clear();
@@ -18442,6 +18582,9 @@ function authoredGap(before, after) {
18442
18582
  function ssIdAfter(a, b) {
18443
18583
  return (a - b | 0) > 0;
18444
18584
  }
18585
+ function holdsRebuilt(stream) {
18586
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18587
+ }
18445
18588
  var FecReleaseSequencer = class {
18446
18589
  #streams = /* @__PURE__ */ new Map();
18447
18590
  #gate;
@@ -18569,19 +18712,39 @@ var FecReleaseSequencer = class {
18569
18712
  }
18570
18713
  /**
18571
18714
  * These streams' sink is being replaced by one that has seen no source
18572
- * frame yet. First release, to the sink being replaced, the frames the
18573
- * unanchored path had already released to it (#cursorPassed): there they
18574
- * waited in its queue, which its replacement flushes. Then treat the
18575
- * streams as unsourced until a source frame is released.
18715
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
18716
+ * release to the sink being replaced the frames the unanchored path had
18717
+ * already released to it (#cursorPassed): there they waited in its queue,
18718
+ * which its replacement flushes. A stream that holds none is not drained:
18719
+ * it holds nothing the anchor kept back, and a drain would still force out
18720
+ * a frame held past the hold limit, which before the anchor waited for the
18721
+ * next push. Then treat the streams as unsourced until a source frame is
18722
+ * released.
18576
18723
  */
18577
18724
  unsource(packetId, catalogTrackName) {
18578
18725
  for (const stream of this.#streams.values()) {
18579
18726
  if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18580
- this.#drain(stream, true);
18727
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
18728
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
18729
+ else this.#drain(stream, true);
18730
+ }
18581
18731
  stream.sourced = false;
18582
18732
  }
18583
18733
  }
18584
18734
  }
18735
+ /**
18736
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
18737
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
18738
+ * packetId.
18739
+ */
18740
+ holdsRebuilt(packetId, catalogTrackName) {
18741
+ for (const stream of this.#streams.values()) {
18742
+ if (stream.packetId !== packetId) continue;
18743
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
18744
+ if (holdsRebuilt(stream)) return true;
18745
+ }
18746
+ return false;
18747
+ }
18585
18748
  /** Re-evaluate held gaps after FEC state changed. */
18586
18749
  poke(streamKey) {
18587
18750
  if (streamKey !== void 0) {
@@ -18674,7 +18837,8 @@ var FecReleaseSequencer = class {
18674
18837
  lastSbn: void 0,
18675
18838
  discontinuity: false,
18676
18839
  rapSsId: void 0,
18677
- sourced: false
18840
+ sourced: false,
18841
+ draining: 0
18678
18842
  };
18679
18843
  this.#streams.set(streamKey, stream);
18680
18844
  }
@@ -18682,9 +18846,22 @@ var FecReleaseSequencer = class {
18682
18846
  }
18683
18847
  /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18684
18848
  #drain(stream, asUnanchored = false) {
18849
+ stream.draining++;
18850
+ try {
18851
+ this.#drainLoop(stream, asUnanchored);
18852
+ } finally {
18853
+ stream.draining--;
18854
+ }
18855
+ }
18856
+ #drainLoop(stream, asUnanchored) {
18685
18857
  while (stream.buf.length > 0 && stream.last !== null) {
18686
18858
  const head = stream.buf[0];
18687
18859
  const last = stream.last;
18860
+ if (compareFrameOrder(head, last) <= 0) {
18861
+ stream.buf.shift();
18862
+ this.#release(stream, head, void 0);
18863
+ continue;
18864
+ }
18688
18865
  if (stream.discontinuity) {
18689
18866
  stream.discontinuity = false;
18690
18867
  stream.buf.shift();
@@ -18743,6 +18920,8 @@ var FecReleaseSequencer = class {
18743
18920
  * And it goes at once while a staged switch decides on when frames arrive
18744
18921
  * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18745
18922
  * sink being replaced: unanchored, it had already reached the sink.
18923
+ * The scan also writes: the `passed` marks it sets stay on the frames,
18924
+ * and push marks a frame it inserts in front of a marked one from them.
18746
18925
  */
18747
18926
  #cursorPassed(stream, asUnanchored) {
18748
18927
  let at2 = -1;
@@ -18757,6 +18936,8 @@ var FecReleaseSequencer = class {
18757
18936
  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));
18758
18937
  }
18759
18938
  #release(stream, frame, decision) {
18939
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18940
+ if (stale) this.#recordStat("droppedStale");
18760
18941
  if (decision) frame.meta.gapBefore = decision;
18761
18942
  stream.last = frame;
18762
18943
  if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
@@ -18764,7 +18945,7 @@ var FecReleaseSequencer = class {
18764
18945
  }
18765
18946
  if (frame.meta.recovered !== true) stream.sourced = true;
18766
18947
  this.#gate.onReleased?.(frame.data, frame.meta);
18767
- this.#emit(frame.data, frame.meta);
18948
+ if (!stale) this.#emit(frame.data, frame.meta);
18768
18949
  }
18769
18950
  };
18770
18951
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -19094,6 +19275,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19094
19275
  unsourceFecRelease(packetId, catalogTrackName) {
19095
19276
  this.#fecSequencer?.unsource(packetId, catalogTrackName);
19096
19277
  }
19278
+ /** See FecReleaseSequencer.holdsRebuilt. */
19279
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
19280
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
19281
+ }
19097
19282
  /** Re-evaluate held gaps after FEC block state changed. */
19098
19283
  pokeFecRelease(packetId, catalogTrackName) {
19099
19284
  if (!this.#fecSequencer) return;
@@ -23069,6 +23254,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23069
23254
  var OVERLAY_ROUTED_LIMIT = 1024;
23070
23255
  var OVERLAY_PENDING_LIMIT = 256;
23071
23256
  var OVERLAY_OFFSET_MPUS = 16;
23257
+ var OVERLAY_REBUILT_LIMIT = 256;
23072
23258
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
23073
23259
  function assertStreamIdentity(role, stream) {
23074
23260
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -23245,6 +23431,17 @@ var MmtpRouter = class {
23245
23431
  * an MPU no member of which arrived).
23246
23432
  */
23247
23433
  #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23434
+ /**
23435
+ * The same, shown by the overlay alone, before any base recovery did: a
23436
+ * packet it rebuilt whose base copy carries its protected psn
23437
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
23438
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
23439
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
23440
+ * make the base drop a late base copy of a keyframe the overlay never
23441
+ * finished, and lift a block's late-source cut, which nothing before
23442
+ * this offset did.
23443
+ */
23444
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23248
23445
  /**
23249
23446
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23250
23447
  * not parsed, typically because its overlap copy was parsed first
@@ -23522,13 +23719,18 @@ var MmtpRouter = class {
23522
23719
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23523
23720
  }
23524
23721
  };
23525
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23722
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23526
23723
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23527
23724
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23528
23725
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23529
23726
  }
23530
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23531
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23727
+ if (span === void 0) {
23728
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23729
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23730
+ } else {
23731
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23732
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
23733
+ }
23532
23734
  };
23533
23735
  }
23534
23736
  }
@@ -23573,8 +23775,9 @@ var MmtpRouter = class {
23573
23775
  }
23574
23776
  /**
23575
23777
  * FEC-recovered source symbols whose reconstructed header named a
23576
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23577
- * a hop re-stamped the sequence after the sender protected the packet.
23778
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
23779
+ * or, rebuilt by the overlay, than their base copy carries: a hop
23780
+ * re-stamped the sequence after the sender protected the packet.
23578
23781
  */
23579
23782
  get restampedRecoveredSequenceCount() {
23580
23783
  return this.#restampedRecoveredSeqs;
@@ -23904,7 +24107,8 @@ var MmtpRouter = class {
23904
24107
  fec,
23905
24108
  k,
23906
24109
  offsets: /* @__PURE__ */ new Map(),
23907
- moqBlocks: /* @__PURE__ */ new Set()
24110
+ moqBlocks: /* @__PURE__ */ new Set(),
24111
+ rebuilt: /* @__PURE__ */ new Map()
23908
24112
  };
23909
24113
  this.#overlays.set(repairPacketId, inst);
23910
24114
  return () => {
@@ -23945,6 +24149,7 @@ var MmtpRouter = class {
23945
24149
  inst.last = void 0;
23946
24150
  inst.offsets.clear();
23947
24151
  inst.moqBlocks.clear();
24152
+ inst.rebuilt.clear();
23948
24153
  }
23949
24154
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23950
24155
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23997,13 +24202,23 @@ var MmtpRouter = class {
23997
24202
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23998
24203
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23999
24204
  * FEC reads it.
24205
+ *
24206
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24207
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24208
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24209
+ * the stream unstamped at the SS_ID offset it states, for the release gate
24210
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
24211
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
24000
24212
  */
24001
24213
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
24002
24214
  if (this.#overlays.size === 0) return;
24003
24215
  const ssId = keyframeOverlaySsId(header);
24004
24216
  if (ssId === void 0) return;
24217
+ let rebuiltPsn;
24005
24218
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
24006
24219
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24220
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24221
+ inst.rebuilt.delete(ssId);
24007
24222
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
24008
24223
  if (identity !== null) {
24009
24224
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -24033,6 +24248,14 @@ var MmtpRouter = class {
24033
24248
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
24034
24249
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
24035
24250
  }
24251
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
24252
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
24253
+ if (rebuiltPsn !== sequenceNumber) {
24254
+ this.#restampedRecoveredSeqs++;
24255
+ } else if (baseSsId !== void 0) {
24256
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24257
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24258
+ }
24036
24259
  }
24037
24260
  #noteOverlayMoqBlock(inst, blockStart) {
24038
24261
  inst.moqBlocks.add(blockStart);
@@ -24096,24 +24319,35 @@ var MmtpRouter = class {
24096
24319
  const restamped = this.#restampedStreams.has(streamKey);
24097
24320
  const restored = /* @__PURE__ */ new Set();
24098
24321
  const baseSsIds = /* @__PURE__ */ new Map();
24322
+ const gateOnly = /* @__PURE__ */ new Set();
24099
24323
  const symbols = result.symbols.flatMap((sym) => {
24324
+ const overlaySsId = serial32Add(blockStart, sym.index);
24100
24325
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24101
24326
  if (anchor !== void 0) {
24102
- const overlaySsId = serial32Add(blockStart, sym.index);
24103
24327
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
24104
24328
  restored.add(packetSequenceNumber);
24105
24329
  const out = { ...sym, packetSequenceNumber };
24106
24330
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24107
- if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
24331
+ if (baseSsId !== void 0) {
24332
+ baseSsIds.set(sym.index, baseSsId);
24333
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24334
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
24335
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24336
+ }
24337
+ }
24108
24338
  return [out];
24109
24339
  }
24110
24340
  if (!restamped) {
24111
- const baseSsId = unstampedBaseSsId(
24112
- this.#ssIdSeqOffsetByStream.get(streamKey),
24113
- this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24114
- sym.packetSequenceNumber
24115
- );
24116
- if (baseSsId !== void 0) baseSsIds.set(sym.index, baseSsId);
24341
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24342
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24343
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24344
+ if (baseSsId !== void 0) {
24345
+ baseSsIds.set(sym.index, baseSsId);
24346
+ if (fedSsId === void 0) gateOnly.add(sym.index);
24347
+ } else {
24348
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24349
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24350
+ }
24117
24351
  return [sym];
24118
24352
  }
24119
24353
  this.#overlayStats.unsequencedDropped++;
@@ -24137,12 +24371,13 @@ var MmtpRouter = class {
24137
24371
  firstSsIds
24138
24372
  });
24139
24373
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24140
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24374
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24141
24375
  }
24142
24376
  /**
24143
24377
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24144
24378
  * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24145
- * from a received member of its MPU, else unstampedBaseSsId),
24379
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24380
+ * base recovery showed; `baseSsIds` holds no other),
24146
24381
  * so it is fed to the base decoder as a rebuilt source
24147
24382
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24148
24383
  * fewer to solve, and its repair can recover the block's other packets.
@@ -24897,7 +25132,7 @@ var MmtpRouter = class {
24897
25132
  catalogTrackName
24898
25133
  )) continue;
24899
25134
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24900
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25135
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24901
25136
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24902
25137
  }
24903
25138
  if (payload.oversized) continue;
@@ -24985,6 +25220,7 @@ var MmtpRouter = class {
24985
25220
  this.#latestVideoSeqByStream.delete(streamKey);
24986
25221
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24987
25222
  this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25223
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24988
25224
  this.#multicastRapTwinByStream.delete(streamKey);
24989
25225
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24990
25226
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25167,7 +25403,9 @@ var MmtpRouter = class {
25167
25403
  droppedIncomingFrames: 0
25168
25404
  };
25169
25405
  this.#staged = state;
25170
- this.#requestFecPoke(from.packetId, from.catalogTrackName);
25406
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25407
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25408
+ }
25171
25409
  return {
25172
25410
  cancel: () => {
25173
25411
  if (this.#staged !== state) return false;
@@ -25250,7 +25488,17 @@ var MmtpRouter = class {
25250
25488
  */
25251
25489
  #routeParsedFrame(data, meta) {
25252
25490
  if (this.#classifyPacketId(meta.packetId) === null) return;
25253
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
25491
+ if (this.#staged !== null) {
25492
+ let consumed = false;
25493
+ this.#fecPokeDepth++;
25494
+ try {
25495
+ consumed = this.#interceptSwitchFrame(data, meta);
25496
+ } finally {
25497
+ this.#fecPokeDepth--;
25498
+ this.#flushPendingFecPokes();
25499
+ }
25500
+ if (consumed) return;
25501
+ }
25254
25502
  this.#deliverParsedFrame(data, meta);
25255
25503
  }
25256
25504
  #commitStreamSwitch(state, rap, reason) {
@@ -25402,12 +25650,16 @@ var MmtpRouter = class {
25402
25650
  this.#parser.deferFecRelease(work);
25403
25651
  } finally {
25404
25652
  this.#fecPokeDepth--;
25405
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25406
- const pokes = [...this.#pendingFecPokes.values()];
25407
- this.#pendingFecPokes.clear();
25408
- for (const poke of pokes) {
25409
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25410
- }
25653
+ this.#flushPendingFecPokes();
25654
+ }
25655
+ }
25656
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
25657
+ #flushPendingFecPokes() {
25658
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25659
+ const pokes = [...this.#pendingFecPokes.values()];
25660
+ this.#pendingFecPokes.clear();
25661
+ for (const poke of pokes) {
25662
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25411
25663
  }
25412
25664
  }
25413
25665
  }
@@ -28211,6 +28463,12 @@ function isNonNegativeSafeInteger(value) {
28211
28463
  function isSymbolCounts(value) {
28212
28464
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
28213
28465
  }
28466
+ function isSpanFailure(value) {
28467
+ if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
28468
+ const length = value["length"];
28469
+ const sources = value["sources"];
28470
+ return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
28471
+ }
28214
28472
  function isNullableNonNegative(value) {
28215
28473
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
28216
28474
  }
@@ -28230,7 +28488,10 @@ function isRepairLifecycleEvent(value) {
28230
28488
  case "recovered":
28231
28489
  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"]);
28232
28490
  case "failed":
28233
- return hasExactKeys(value, ["version", "type", "block", "blockClass", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]);
28491
+ return hasExactKeys(
28492
+ value,
28493
+ "span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
28494
+ ) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
28234
28495
  case "repair-preempted":
28235
28496
  case "repair-deferred":
28236
28497
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -28496,7 +28757,7 @@ var MulticastManager = class {
28496
28757
  onPublisherRestart: (packetId, catalogTrackName) => {
28497
28758
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
28498
28759
  },
28499
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28760
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
28500
28761
  const pathCount = this.#fec.blockPathCount(
28501
28762
  sbn,
28502
28763
  packetId,
@@ -28514,7 +28775,8 @@ var MulticastManager = class {
28514
28775
  symbols: {
28515
28776
  multicast: pathCount?.multicastSymbols ?? 0,
28516
28777
  moq: pathCount?.moqSymbols ?? 0
28517
- }
28778
+ },
28779
+ ...span === void 0 ? {} : { span }
28518
28780
  });
28519
28781
  },
28520
28782
  onFrameGap: (decision) => {
@@ -29894,10 +30156,12 @@ var MulticastManager = class {
29894
30156
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29895
30157
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29896
30158
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29897
- * once its range FETCH ends (or none is sent).
30159
+ * once its range FETCH ends (or none is sent). `onExpired` is told of a
30160
+ * pending hole the stream runs past the locator's horizon: returning true,
30161
+ * the caller closes it (`closeIsoHoles`); otherwise it is discarded.
29898
30162
  */
29899
- onUndelimitedLoss(cb) {
29900
- this.#fec.setUndelimitedLossHandler(cb);
30163
+ onUndelimitedLoss(cb, onExpired) {
30164
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29901
30165
  }
29902
30166
  /**
29903
30167
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -30133,7 +30397,8 @@ var MulticastManager = class {
30133
30397
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
30134
30398
  ...event,
30135
30399
  block: Object.freeze({ ...event.block }),
30136
- ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
30400
+ ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
30401
+ ..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
30137
30402
  }
30138
30403
  );
30139
30404
  if (this.#logRepairLifecycle) {
@@ -32805,6 +33070,7 @@ var RepairLayerSubscriptions = class {
32805
33070
 
32806
33071
  // src/repair-fetch.ts
32807
33072
  var MOQT_PRIORITY_MAX = 255;
33073
+ var WALK_LEAD_MS = 3;
32808
33074
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32809
33075
  function isMoqtPriority(value) {
32810
33076
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32855,8 +33121,17 @@ function wireRepairFetch(mc, opts) {
32855
33121
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32856
33122
  };
32857
33123
  const clearEntry = (entry) => {
32858
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32859
- else clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33124
+ const { range } = entry;
33125
+ if (range) {
33126
+ atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
33127
+ } else {
33128
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33129
+ }
33130
+ };
33131
+ const atReport = (range, work) => {
33132
+ const fec = mc.fecManager;
33133
+ if (range.groups !== null || fec.withReceiptTime === void 0) work();
33134
+ else fec.withReceiptTime(range.reportedAt, work);
32860
33135
  };
32861
33136
  const releaseHold = (entry) => {
32862
33137
  if (entry.range) return;
@@ -32923,21 +33198,42 @@ function wireRepairFetch(mc, opts) {
32923
33198
  const admit = (entry, now) => {
32924
33199
  const budgetMs = entry.deadlineAt - now;
32925
33200
  const rttMs = readRtt();
32926
- const reason = !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < budgetMs ? null : "rtt-exceeds-budget";
33201
+ const reason = refusal(entry, now, rttMs);
32927
33202
  if (reason === null) return rttMs;
32928
33203
  skip(entry, reason, rttMs, budgetMs);
32929
33204
  return null;
32930
33205
  };
33206
+ 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";
32931
33207
  const retire = (entry) => {
32932
33208
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32933
33209
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32934
33210
  };
32935
- const outstandingBelow = (priority) => {
33211
+ const holdsBack = (entry, priority, videoWalks = true) => {
33212
+ if (entry.priority >= priority) return false;
33213
+ const { walk } = entry;
33214
+ if (walk === void 0) return true;
33215
+ if (entry.priority !== 0 || !videoWalks) return false;
33216
+ return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
33217
+ };
33218
+ const outstandingBelow = (priority, videoWalks = true) => {
32936
33219
  for (const entry of entries.values()) {
32937
- if (entry.priority < priority) return true;
33220
+ if (holdsBack(entry, priority, videoWalks)) return true;
32938
33221
  }
32939
33222
  return false;
32940
33223
  };
33224
+ const yieldVideoWalks = (queued) => {
33225
+ const now = performance.now();
33226
+ const rttMs = readRtt();
33227
+ const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
33228
+ if (!due) return;
33229
+ for (const entry of entries.values()) {
33230
+ const walk = entry.walk;
33231
+ if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
33232
+ if (walk.group.controller.signal.aborted) continue;
33233
+ walk.yielded = true;
33234
+ walk.group.controller.abort();
33235
+ }
33236
+ };
32941
33237
  const preemptBestEffort = () => {
32942
33238
  for (const entry of entries.values()) {
32943
33239
  if (entry.priority === 0 || !entry.running) continue;
@@ -32966,7 +33262,8 @@ function wireRepairFetch(mc, opts) {
32966
33262
  }
32967
33263
  const rttMs = admit(entry, now);
32968
33264
  if (rttMs === null) return;
32969
- if (entry.priority === 0) preemptBestEffort();
33265
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33266
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32970
33267
  if (counted) entry.attempts++;
32971
33268
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32972
33269
  rttMs,
@@ -32988,6 +33285,14 @@ function wireRepairFetch(mc, opts) {
32988
33285
  }
32989
33286
  void execute(entry, run);
32990
33287
  };
33288
+ const leaveWalkedGroup = (block) => {
33289
+ for (const entry of entries.values()) {
33290
+ const group = entry.walk?.group;
33291
+ if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
33292
+ group.controller.abort();
33293
+ }
33294
+ }
33295
+ };
32991
33296
  const dispatch = () => {
32992
33297
  if (disposed) return;
32993
33298
  const now = performance.now();
@@ -33012,6 +33317,7 @@ function wireRepairFetch(mc, opts) {
33012
33317
  for (const entry of queued) {
33013
33318
  if (entry.priority === 0) start(entry);
33014
33319
  }
33320
+ yieldVideoWalks(queued);
33015
33321
  if (outstandingBelow(1)) return;
33016
33322
  for (const entry of queued) {
33017
33323
  if (entry.priority === 1) start(entry);
@@ -33038,6 +33344,16 @@ function wireRepairFetch(mc, opts) {
33038
33344
  }
33039
33345
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
33040
33346
  };
33347
+ const groupServed = (sbn, packetId, catalogTrackName) => {
33348
+ if (mc.fecManager.blockSymbolCounts === void 0) return false;
33349
+ const counts = symbolCounts(sbn, packetId, catalogTrackName);
33350
+ if (counts === void 0 || counts.authoredLength === void 0) return true;
33351
+ return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
33352
+ };
33353
+ const statedEnd = (sbn, packetId, catalogTrackName) => {
33354
+ const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
33355
+ return length === void 0 ? void 0 : serial32Add(sbn, length);
33356
+ };
33041
33357
  const finish = (entry, run, fed, error) => {
33042
33358
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
33043
33359
  return;
@@ -33082,27 +33398,81 @@ function wireRepairFetch(mc, opts) {
33082
33398
  signal: run.controller.signal,
33083
33399
  subscriberPriority: entry.subscriberPriority
33084
33400
  };
33401
+ const { range } = entry;
33402
+ const feed = (packet) => {
33403
+ if (entry.catalogTrackName === void 0) {
33404
+ mc.processRepairFromMoQ(packet, entry.packetId);
33405
+ } else {
33406
+ mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
33407
+ }
33408
+ };
33085
33409
  try {
33086
- const packets = entry.range ? opts.rangeTransport({
33410
+ const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
33087
33411
  ...common,
33088
- startGroup: entry.range.startGroup,
33089
- endGroup: entry.range.endGroup,
33090
- lostSsId: entry.range.lostSsId
33091
- }) : opts.transport({ ...common, sbn: entry.sbn });
33412
+ startGroup: range.groups.startGroup,
33413
+ endGroup: range.groups.endGroup,
33414
+ lostSsId: range.lostSsId
33415
+ });
33092
33416
  for await (const packet of packets) {
33093
33417
  if (run.stopped !== null || entry.running !== run) break;
33094
33418
  fed++;
33095
- if (entry.catalogTrackName === void 0) {
33096
- mc.processRepairFromMoQ(packet, entry.packetId);
33097
- } else {
33098
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
33099
- }
33419
+ if (range === void 0) feed(packet);
33420
+ else atReport(range, () => feed(packet));
33100
33421
  }
33101
33422
  finish(entry, run, fed, void 0);
33102
33423
  } catch (error) {
33103
33424
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
33104
33425
  }
33105
33426
  };
33427
+ async function* walkHole(entry, walk, hole, common) {
33428
+ const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
33429
+ for (let first = true; !common.signal.aborted; first = false) {
33430
+ if (!first && entry.priority !== 0 && outstandingBelow(1)) {
33431
+ stop(entry, "preempted");
33432
+ note("repair-preempted", entry);
33433
+ return;
33434
+ }
33435
+ if (!first && entry.priority === 0) {
33436
+ dispatch();
33437
+ if (common.signal.aborted) return;
33438
+ }
33439
+ const sbn = walk.next;
33440
+ let next;
33441
+ if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
33442
+ const rsIds = /* @__PURE__ */ new Set();
33443
+ const group = new AbortController();
33444
+ const cancel = () => group.abort();
33445
+ common.signal.addEventListener("abort", cancel, { once: true });
33446
+ walk.group = { sbn, controller: group };
33447
+ try {
33448
+ for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
33449
+ yield packet;
33450
+ const id = extractRepairFecPayloadId(packet);
33451
+ if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
33452
+ next ??= serial32Add(sbn, id.ssbLength);
33453
+ rsIds.add(id.rsId);
33454
+ if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
33455
+ group.abort();
33456
+ break;
33457
+ }
33458
+ }
33459
+ } catch (error) {
33460
+ if (!group.signal.aborted || common.signal.aborted) throw error;
33461
+ } finally {
33462
+ common.signal.removeEventListener("abort", cancel);
33463
+ walk.group = null;
33464
+ }
33465
+ if (walk.yielded) {
33466
+ walk.yielded = false;
33467
+ continue;
33468
+ }
33469
+ }
33470
+ next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
33471
+ if (next === void 0) return;
33472
+ if (serial32Distance(entry.sbn, next) > lostAt) return;
33473
+ walk.next = next;
33474
+ }
33475
+ }
33106
33476
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
33107
33477
  for (const entry of [...entries.values()]) {
33108
33478
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -33298,7 +33668,37 @@ function wireRepairFetch(mc, opts) {
33298
33668
  throw error;
33299
33669
  }
33300
33670
  };
33301
- if (opts.rangeTransport !== void 0) {
33671
+ const trackKind = (packetId) => {
33672
+ try {
33673
+ return mc.fecManager.trackKindFor?.(packetId);
33674
+ } catch {
33675
+ return void 0;
33676
+ }
33677
+ };
33678
+ const walkDeadlineAt = (packetId, now, deadlineAt) => {
33679
+ let repairDeadlineMs;
33680
+ try {
33681
+ repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
33682
+ } catch {
33683
+ repairDeadlineMs = void 0;
33684
+ }
33685
+ if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
33686
+ return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
33687
+ };
33688
+ const onHoleExpired = (hole) => {
33689
+ if (disposed) return false;
33690
+ 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);
33691
+ if (entry === void 0) return false;
33692
+ queueMicrotask(() => {
33693
+ if (entries.get(entry.key) !== entry) return;
33694
+ retire(entry);
33695
+ stop(entry, "expired");
33696
+ clearEntry(entry);
33697
+ dispatch();
33698
+ });
33699
+ return true;
33700
+ };
33701
+ if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
33302
33702
  mc.onUndelimitedLoss((loss) => {
33303
33703
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
33304
33704
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -33306,9 +33706,11 @@ function wireRepairFetch(mc, opts) {
33306
33706
  const blockProfile = blockProfileOf(packetId);
33307
33707
  const target = opts.resolveTarget(packetId, catalogTrackName);
33308
33708
  if (!target) return close();
33309
- const priority = repairPriorityForClass("unknown");
33310
- const range = rangeFor(loss, target.repairTrack);
33311
- if (range === null) {
33709
+ const walked = opts.rangeTransport === void 0;
33710
+ const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
33711
+ const priority = repairPriorityForClass(holeClass);
33712
+ const groups = walked ? null : rangeFor(loss, target.repairTrack);
33713
+ if (!walked && groups === null) {
33312
33714
  close();
33313
33715
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
33314
33716
  return;
@@ -33319,8 +33721,10 @@ function wireRepairFetch(mc, opts) {
33319
33721
  packetId,
33320
33722
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
33321
33723
  };
33322
- const wire = wirePriority(identity, "unknown");
33323
- const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, performance.now(), "unknown");
33724
+ const wire = wirePriority(identity, holeClass);
33725
+ if (wire === null) return close();
33726
+ const now = performance.now();
33727
+ const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
33324
33728
  if (deadline === null) {
33325
33729
  close();
33326
33730
  mc.noteRepairSkipped(
@@ -33332,6 +33736,7 @@ function wireRepairFetch(mc, opts) {
33332
33736
  );
33333
33737
  return;
33334
33738
  }
33739
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
33335
33740
  const entry = {
33336
33741
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
33337
33742
  ...identity,
@@ -33339,15 +33744,16 @@ function wireRepairFetch(mc, opts) {
33339
33744
  blockProfile,
33340
33745
  priority,
33341
33746
  subscriberPriority: wire.subscriberPriority,
33342
- deadlineAt: deadline.deadlineAt,
33343
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33747
+ deadlineAt,
33748
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
33344
33749
  anchorAt: deadline.anchorAt,
33345
33750
  delayMs: deadline.delayMs,
33346
33751
  bound: deadline.bound,
33347
33752
  order: enqueueOrder++,
33348
33753
  attempts: 0,
33349
33754
  running: null,
33350
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33755
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33756
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
33351
33757
  };
33352
33758
  const previous = entries.get(entry.key);
33353
33759
  if (previous) {
@@ -33357,7 +33763,7 @@ function wireRepairFetch(mc, opts) {
33357
33763
  entries.set(entry.key, entry);
33358
33764
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
33359
33765
  dispatch();
33360
- });
33766
+ }, onHoleExpired);
33361
33767
  }
33362
33768
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
33363
33769
  queueMicrotask(() => {