@blockcast/mmt-container 2.2.0-main.02321582f1ca → 2.2.0-main.050b52fb9b00

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 +663 -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 +37 -11
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +192 -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 +20 -9
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +270 -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 +484 -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 +179 -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 +249 -59
  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;
@@ -13948,6 +14015,10 @@ var FECManager = class _FECManager {
13948
14015
  // 'pending' forever, which would park every later frame of its stream in
13949
14016
  // the in-order release gate. Each entry owns its handle; the callback
13950
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
+ */
13951
14022
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13952
14023
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13953
14024
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
@@ -13964,6 +14035,9 @@ var FECManager = class _FECManager {
13964
14035
  // SSB_length source arrivals and recoverable from SSB_length received
13965
14036
  // symbols; waiting for K fails every one of them.
13966
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();
13967
14041
  this.#ssbLengthConflictCount = 0;
13968
14042
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13969
14043
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -14161,7 +14235,10 @@ var FECManager = class _FECManager {
14161
14235
  // arrives via onBlockRecovered callback.
14162
14236
  this.onBlockStarted = null;
14163
14237
  this.onBlockComplete = null;
14164
- /** 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
+ */
14165
14242
  this.onBlockFailed = null;
14166
14243
  /**
14167
14244
  * Fired once per stream when its Worker decoder stops being routable.
@@ -14203,6 +14280,8 @@ var FECManager = class _FECManager {
14203
14280
  * for it, which it must be.
14204
14281
  */
14205
14282
  this.#onUndelimitedLoss = null;
14283
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14284
+ this.#onIsoHoleExpired = null;
14206
14285
  /** A block every symbol of which vanished was created by SBN evidence. */
14207
14286
  this.onBlockVanished = null;
14208
14287
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -14247,6 +14326,7 @@ var FECManager = class _FECManager {
14247
14326
  #blockSourceEsis;
14248
14327
  #blockRebuiltEsis;
14249
14328
  #blockAuthoredLengths;
14329
+ #isoSpanKeys;
14250
14330
  #ssbLengthConflictCount;
14251
14331
  #blockRepairRsIds;
14252
14332
  #blockRsbLengths;
@@ -14313,6 +14393,7 @@ var FECManager = class _FECManager {
14313
14393
  #decoderUnavailableEvents;
14314
14394
  #repairSymbolSizeMismatchCount;
14315
14395
  #onUndelimitedLoss;
14396
+ #onIsoHoleExpired;
14316
14397
  #encodeTrackName(catalogTrackName) {
14317
14398
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14318
14399
  if (encoded === void 0) {
@@ -14784,7 +14865,10 @@ var FECManager = class _FECManager {
14784
14865
  timer.handle = null;
14785
14866
  this.#requestRepair(sbn, packetId, catalogTrackName);
14786
14867
  };
14787
- 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
+ );
14788
14872
  this.#repairDeadlines.set(key, timer);
14789
14873
  }
14790
14874
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14928,8 +15012,14 @@ var FECManager = class _FECManager {
14928
15012
  * Vanished blocks are not enumerated here: their starts are unknown until a
14929
15013
  * repair states them. The SS_IDs between two stated blocks become one
14930
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).
14931
15021
  */
14932
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15022
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14933
15023
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14934
15024
  const now = performance.now();
14935
15025
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14968,7 +15058,7 @@ var FECManager = class _FECManager {
14968
15058
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14969
15059
  };
14970
15060
  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))) {
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))) {
14972
15062
  discontinuity();
14973
15063
  return;
14974
15064
  }
@@ -15230,17 +15320,21 @@ var FECManager = class _FECManager {
15230
15320
  this.#blockWaitDeadlinesMs.delete(key);
15231
15321
  this.#repairHoldUntilMs.delete(key);
15232
15322
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
15233
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15323
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
15234
15324
  };
15235
- 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
+ );
15236
15329
  this.#activeBlocks.set(key, timer);
15237
15330
  this.#indexBlockKey(key, sbn, packetId);
15238
15331
  }
15239
15332
  /**
15240
15333
  * Hand a block whose delivery is over to its decoder for finalization: the
15241
- * 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.
15242
15336
  */
15243
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15337
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
15244
15338
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
15245
15339
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
15246
15340
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -15256,7 +15350,7 @@ var FECManager = class _FECManager {
15256
15350
  if (workerClient) {
15257
15351
  this.#pendingWorkerCleanups.add(key);
15258
15352
  this.#pendingWorkerCleanupClients.set(key, workerClient);
15259
- this.#boundOwedCleanup(key, packetId, workerClient);
15353
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
15260
15354
  workerClient.cleanup(sbn);
15261
15355
  } else {
15262
15356
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15570,6 +15664,7 @@ var FECManager = class _FECManager {
15570
15664
  resetMap(this.#blockSourceEsis);
15571
15665
  resetMap(this.#blockRebuiltEsis);
15572
15666
  resetMap(this.#blockAuthoredLengths);
15667
+ resetSet(this.#isoSpanKeys);
15573
15668
  resetMap(this.#blockRepairRsIds);
15574
15669
  resetMap(this.#blockRsbLengths);
15575
15670
  resetMap(this.#heldLayerStates);
@@ -15695,6 +15790,7 @@ var FECManager = class _FECManager {
15695
15790
  dropMap(this.#blockSourceEsis);
15696
15791
  dropMap(this.#blockRebuiltEsis);
15697
15792
  dropMap(this.#blockAuthoredLengths);
15793
+ dropSet(this.#isoSpanKeys);
15698
15794
  dropMap(this.#blockRepairRsIds);
15699
15795
  dropMap(this.#blockRsbLengths);
15700
15796
  dropMap(this.#blockPathCounts);
@@ -16037,8 +16133,9 @@ var FECManager = class _FECManager {
16037
16133
  return true;
16038
16134
  }
16039
16135
  /**
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.
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.
16042
16139
  */
16043
16140
  releaseRepairHold(sbn, packetId, catalogTrackName) {
16044
16141
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16176,6 +16273,7 @@ var FECManager = class _FECManager {
16176
16273
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16177
16274
  const evictedBefore = locator.heldEvicted;
16178
16275
  const held = locator.hold(ssId, source);
16276
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16179
16277
  if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16180
16278
  const evicted = locator.heldEvicted - evictedBefore;
16181
16279
  if (evicted > 0) {
@@ -16197,17 +16295,20 @@ var FECManager = class _FECManager {
16197
16295
  * contradicts a known block is refused (counted) and must be dropped.
16198
16296
  */
16199
16297
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
16200
- 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);
16201
16301
  if (placement.status !== "placed") {
16202
16302
  this.#isoRepairRefusedCount++;
16203
16303
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
16204
16304
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
16205
16305
  }
16306
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
16206
16307
  return placement;
16207
16308
  }
16309
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
16208
16310
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
16209
16311
  if (placement.hole !== void 0) {
16210
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
16211
16312
  const hole = placement.hole.block;
16212
16313
  const previous = locator.blockEndingAt(hole.start);
16213
16314
  const loss = {
@@ -16228,17 +16329,29 @@ var FECManager = class _FECManager {
16228
16329
  * `handler`, instead of a repair-less span; null restores spans. Set by the
16229
16330
  * range repair scheduler; applies to every stream from its next gap. The
16230
16331
  * handler and the deferral are one setting, so no hole is ever created with
16231
- * 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.
16232
16336
  */
16233
- setUndelimitedLossHandler(handler) {
16337
+ setUndelimitedLossHandler(handler, onExpired = null) {
16234
16338
  this.#onUndelimitedLoss = handler;
16339
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
16235
16340
  this.#deferIsoGaps = handler !== null;
16236
16341
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
16237
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
+ }
16238
16350
  /**
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).
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).
16242
16355
  */
16243
16356
  closeIsoHoles(packetId, catalogTrackName, start, length) {
16244
16357
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -16257,7 +16370,10 @@ var FECManager = class _FECManager {
16257
16370
  */
16258
16371
  #openIsoSpan(span, packetId, catalogTrackName) {
16259
16372
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
16260
- 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
+ }
16261
16377
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
16262
16378
  }
16263
16379
  /**
@@ -16598,11 +16714,14 @@ var FECManager = class _FECManager {
16598
16714
  });
16599
16715
  client.onCleanupComplete((sbn) => {
16600
16716
  if (this.#releasedWorkerClients.has(client)) return;
16601
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16717
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16718
+ const cleanupKeys = owed.filter((key) => {
16602
16719
  const block = this.#parseBlockKey(key);
16603
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16720
+ return block.packetId === packetId && block.sbn === sbn;
16604
16721
  });
16605
- 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");
16606
16725
  });
16607
16726
  client.onError((msg, code) => {
16608
16727
  const requestedKind = code ?? msg;
@@ -16672,9 +16791,11 @@ var FECManager = class _FECManager {
16672
16791
  * fail deadline already passed, so failing it is the decision that deadline
16673
16792
  * made. A late ack then finds nothing owed and is a no-op.
16674
16793
  */
16675
- #boundOwedCleanup(key, packetId, client) {
16794
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16676
16795
  this.#clearOwedCleanupBound(key);
16677
16796
  const bound = {
16797
+ dueAtMs,
16798
+ askedAtMs: performance.now(),
16678
16799
  handle: setTimeout(() => {
16679
16800
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16680
16801
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -16844,9 +16965,15 @@ var FECManager = class _FECManager {
16844
16965
  rebuilt = /* @__PURE__ */ new Set();
16845
16966
  this.#blockRebuiltEsis.set(key, rebuilt);
16846
16967
  }
16968
+ const config = this.#trackConfigs.get(packetId);
16969
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16847
16970
  rebuilt.add(located.esi);
16848
16971
  const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16849
- if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
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
+ }
16850
16977
  return { taken: true, recovered };
16851
16978
  }
16852
16979
  /**
@@ -17113,7 +17240,10 @@ var FECManager = class _FECManager {
17113
17240
  const authoredLength = this.#blockAuthoredLengths.get(key);
17114
17241
  const rsbLength = this.#blockRsbLengths.get(key);
17115
17242
  if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17116
- 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;
17117
17247
  const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17118
17248
  if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17119
17249
  return void 0;
@@ -17236,12 +17366,13 @@ var FECManager = class _FECManager {
17236
17366
  * the inactivity deadline on each symbol, records the symbol's in-band
17237
17367
  * classification evidence, and marks the block clean when all of its
17238
17368
  * authored source symbols arrived: the SSB_length a repair symbol declared,
17239
- * otherwise K.
17369
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17370
+ * placed only now (see #noteSbnSeen).
17240
17371
  *
17241
17372
  * Throws for a packetId that was never configured, so a classified block
17242
17373
  * never reads the manager-global K or timeout.
17243
17374
  */
17244
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17375
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
17245
17376
  const trackConfig = this.#trackConfigs.get(packetId);
17246
17377
  if (trackConfig === void 0) {
17247
17378
  throw new Error(
@@ -17257,7 +17388,8 @@ var FECManager = class _FECManager {
17257
17388
  catalogTrackName,
17258
17389
  "source",
17259
17390
  evidence.packetSequenceNumber,
17260
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17391
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17392
+ held
17261
17393
  );
17262
17394
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
17263
17395
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -17312,6 +17444,7 @@ var FECManager = class _FECManager {
17312
17444
  this.#blockSourceEsis.delete(key);
17313
17445
  this.#blockRebuiltEsis.delete(key);
17314
17446
  this.#blockAuthoredLengths.delete(key);
17447
+ this.#isoSpanKeys.delete(key);
17315
17448
  this.#blockRepairRsIds.delete(key);
17316
17449
  this.#blockRsbLengths.delete(key);
17317
17450
  this.#markTerminal(key, sbn, packetId, true);
@@ -17342,6 +17475,19 @@ var FECManager = class _FECManager {
17342
17475
  #receiptNow() {
17343
17476
  return this.#receiptAtMs ?? performance.now();
17344
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
+ }
17345
17491
  /**
17346
17492
  * A held packet admitted late arms from its (earlier) receipt time; that
17347
17493
  * never pulls in a deadline later activity already armed. Outside a
@@ -17390,6 +17536,7 @@ var FECManager = class _FECManager {
17390
17536
  }
17391
17537
  #failBlock(sbn, packetId, catalogTrackName) {
17392
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;
17393
17540
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
17394
17541
  this.#clearRepairDeadline(key);
17395
17542
  this.#clearBlockTimer(key);
@@ -17409,7 +17556,8 @@ var FECManager = class _FECManager {
17409
17556
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
17410
17557
  this.#fecStats.blocksFailed++;
17411
17558
  this.#statsForPacket(packetId).blocksFailed++;
17412
- 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);
17413
17561
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
17414
17562
  }
17415
17563
  /**
@@ -17440,6 +17588,7 @@ var FECManager = class _FECManager {
17440
17588
  this.#blockSourceEsis.delete(key);
17441
17589
  this.#blockRebuiltEsis.delete(key);
17442
17590
  this.#blockAuthoredLengths.delete(key);
17591
+ this.#isoSpanKeys.delete(key);
17443
17592
  this.#blockRepairRsIds.delete(key);
17444
17593
  this.#blockRsbLengths.delete(key);
17445
17594
  this.#clearBlockTimer(key);
@@ -17532,6 +17681,7 @@ var FECManager = class _FECManager {
17532
17681
  this.#blockSourceEsis.clear();
17533
17682
  this.#blockRebuiltEsis.clear();
17534
17683
  this.#blockAuthoredLengths.clear();
17684
+ this.#isoSpanKeys.clear();
17535
17685
  this.#blockRepairRsIds.clear();
17536
17686
  this.#blockRsbLengths.clear();
17537
17687
  this.#heldLayerStates.clear();
@@ -18433,6 +18583,9 @@ function authoredGap(before, after) {
18433
18583
  function ssIdAfter(a, b) {
18434
18584
  return (a - b | 0) > 0;
18435
18585
  }
18586
+ function holdsRebuilt(stream) {
18587
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18588
+ }
18436
18589
  var FecReleaseSequencer = class {
18437
18590
  #streams = /* @__PURE__ */ new Map();
18438
18591
  #gate;
@@ -18495,6 +18648,7 @@ var FecReleaseSequencer = class {
18495
18648
  stream.lastSbn = void 0;
18496
18649
  stream.discontinuity = false;
18497
18650
  stream.rapSsId = void 0;
18651
+ stream.sourced = false;
18498
18652
  }
18499
18653
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18500
18654
  this.#recordStat("droppedStale");
@@ -18521,6 +18675,7 @@ var FecReleaseSequencer = class {
18521
18675
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18522
18676
  else hi = mid;
18523
18677
  }
18678
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18524
18679
  stream.buf.splice(lo, 0, frame);
18525
18680
  this.#drainUnlessDeferred(stream);
18526
18681
  }
@@ -18556,6 +18711,41 @@ var FecReleaseSequencer = class {
18556
18711
  }
18557
18712
  return false;
18558
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
+ }
18559
18749
  /** Re-evaluate held gaps after FEC state changed. */
18560
18750
  poke(streamKey) {
18561
18751
  if (streamKey !== void 0) {
@@ -18647,16 +18837,32 @@ var FecReleaseSequencer = class {
18647
18837
  last: null,
18648
18838
  lastSbn: void 0,
18649
18839
  discontinuity: false,
18650
- rapSsId: void 0
18840
+ rapSsId: void 0,
18841
+ sourced: false,
18842
+ draining: 0
18651
18843
  };
18652
18844
  this.#streams.set(streamKey, stream);
18653
18845
  }
18654
18846
  return stream;
18655
18847
  }
18656
- #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) {
18657
18858
  while (stream.buf.length > 0 && stream.last !== null) {
18658
18859
  const head = stream.buf[0];
18659
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
+ }
18660
18866
  if (stream.discontinuity) {
18661
18867
  stream.discontinuity = false;
18662
18868
  stream.buf.shift();
@@ -18670,9 +18876,10 @@ var FecReleaseSequencer = class {
18670
18876
  }
18671
18877
  const headSbn = head.meta.fecSbn;
18672
18878
  const from = stream.lastSbn ?? headSbn;
18673
- const to = headSbn ?? stream.lastSbn;
18879
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18674
18880
  let lost = [];
18675
18881
  const unanchored = from === void 0 && to === void 0;
18882
+ let unresolved = false;
18676
18883
  if (from !== void 0 && to !== void 0) {
18677
18884
  const resolved = this.#gate.resolvedLoss(
18678
18885
  stream.packetId,
@@ -18681,7 +18888,8 @@ var FecReleaseSequencer = class {
18681
18888
  Math.max(from, to),
18682
18889
  from
18683
18890
  );
18684
- if (resolved === null) {
18891
+ unresolved = resolved === null;
18892
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18685
18893
  const heldMs = performance.now() - head.arrivedAtMs;
18686
18894
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18687
18895
  this.#recordStat("forcedWindowFlushes");
@@ -18689,22 +18897,56 @@ var FecReleaseSequencer = class {
18689
18897
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18690
18898
  continue;
18691
18899
  }
18692
- lost = resolved;
18900
+ lost = resolved ?? [];
18693
18901
  }
18694
18902
  stream.buf.shift();
18695
18903
  this.#release(
18696
18904
  stream,
18697
18905
  head,
18698
- 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
18699
18907
  );
18700
18908
  }
18701
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
+ }
18702
18939
  #release(stream, frame, decision) {
18940
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18941
+ if (stale) this.#recordStat("droppedStale");
18703
18942
  if (decision) frame.meta.gapBefore = decision;
18704
18943
  stream.last = frame;
18705
- 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;
18706
18948
  this.#gate.onReleased?.(frame.data, frame.meta);
18707
- this.#emit(frame.data, frame.meta);
18949
+ if (!stale) this.#emit(frame.data, frame.meta);
18708
18950
  }
18709
18951
  };
18710
18952
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18965,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18965
19207
  /**
18966
19208
  * Typed view of the ISO-BMFF timing-sample budget.
18967
19209
  *
18968
- * This is the acceptance signal for the budget ratchet: `pending` walking
18969
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18970
- * and it is observable here rather than only as media silently stopping.
18971
- * Per repo policy this is a producer-owned counter -- never infer budget
18972
- * 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.
18973
19216
  */
18974
19217
  get timingBudgetStats() {
18975
19218
  return {
@@ -19029,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
19029
19272
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
19030
19273
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
19031
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
+ }
19032
19283
  /** Re-evaluate held gaps after FEC block state changed. */
19033
19284
  pokeFecRelease(packetId, catalogTrackName) {
19034
19285
  if (!this.#fecSequencer) return;
@@ -19282,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19282
19533
  if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19283
19534
  const pendingBefore = resolver2.pendingSampleCount;
19284
19535
  const evictedBefore = resolver2.evictedSampleCount;
19285
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19286
19537
  const admitted = resolver2.addMovieFragment(
19287
19538
  mpu.mpuSequenceNumber,
19288
19539
  data,
@@ -19355,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19355
19606
  info,
19356
19607
  resolver,
19357
19608
  options?.recovered === true,
19358
- options?.fecSbn
19609
+ options?.fecSbn,
19610
+ options?.fecSbnFirst
19359
19611
  );
19360
19612
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19361
19613
  }
@@ -19448,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19448
19700
  info,
19449
19701
  resolver,
19450
19702
  options?.recovered === true,
19451
- options?.fecSbn
19703
+ options?.fecSbn,
19704
+ options?.fecSbnFirst
19452
19705
  );
19453
19706
  if (outcome.record !== void 0) return outcome.record;
19454
19707
  return this.#drop(outcome.reason);
@@ -19468,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19468
19721
  * attribution exists to keep readable. Leaving the choice at the call site
19469
19722
  * is what keeps both true.
19470
19723
  */
19471
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19724
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19472
19725
  const timing = resolver.sampleTiming(
19473
19726
  combined.mpuSequenceNumber,
19474
19727
  combined.movieFragmentSequenceNumber,
@@ -19485,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19485
19738
  );
19486
19739
  if (!meta) return { reason: "combined-meta-build-failed" };
19487
19740
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19741
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19488
19742
  if (!resolver.consumeSample(
19489
19743
  combined.mpuSequenceNumber,
19490
19744
  combined.movieFragmentSequenceNumber,
@@ -23001,6 +23255,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23001
23255
  var OVERLAY_ROUTED_LIMIT = 1024;
23002
23256
  var OVERLAY_PENDING_LIMIT = 256;
23003
23257
  var OVERLAY_OFFSET_MPUS = 16;
23258
+ var OVERLAY_REBUILT_LIMIT = 256;
23004
23259
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
23005
23260
  function assertStreamIdentity(role, stream) {
23006
23261
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -23111,6 +23366,7 @@ var MmtpRouter = class {
23111
23366
  #provenance = /* @__PURE__ */ new Map();
23112
23367
  #onFlowProtected = null;
23113
23368
  #unadmittedListedDrops = 0;
23369
+ #admittedSourceDatagrams = 0;
23114
23370
  #unlistedRepairSetAside = 0;
23115
23371
  #assistBlocks = /* @__PURE__ */ new Map();
23116
23372
  #assistFailures = /* @__PURE__ */ new Map();
@@ -23171,10 +23427,22 @@ var MmtpRouter = class {
23171
23427
  * Per stream, the SS_ID offset at which a base recovery last showed the
23172
23428
  * sender's own packet_sequence_number: a recovered packet's header psn
23173
23429
  * equal to its SS_ID minus that offset. While it equals the stream's
23174
- * current offset, no hop re-stamps the stream and a packet's protected psn
23175
- * names its base SS_ID (joint decoding of an MPU no member of which arrived).
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).
23176
23433
  */
23177
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();
23178
23446
  /**
23179
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23180
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -23452,13 +23720,18 @@ var MmtpRouter = class {
23452
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23453
23721
  }
23454
23722
  };
23455
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23456
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23457
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23458
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23459
23727
  }
23460
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23461
- 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
+ }
23462
23735
  };
23463
23736
  }
23464
23737
  }
@@ -23503,8 +23776,9 @@ var MmtpRouter = class {
23503
23776
  }
23504
23777
  /**
23505
23778
  * FEC-recovered source symbols whose reconstructed header named a
23506
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23507
- * 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.
23508
23782
  */
23509
23783
  get restampedRecoveredSequenceCount() {
23510
23784
  return this.#restampedRecoveredSeqs;
@@ -23683,6 +23957,7 @@ var MmtpRouter = class {
23683
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23684
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23685
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23686
23961
  if (previous) {
23687
23962
  previous.flush();
23688
23963
  previous.dispose();
@@ -23833,7 +24108,8 @@ var MmtpRouter = class {
23833
24108
  fec,
23834
24109
  k,
23835
24110
  offsets: /* @__PURE__ */ new Map(),
23836
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23837
24113
  };
23838
24114
  this.#overlays.set(repairPacketId, inst);
23839
24115
  return () => {
@@ -23874,6 +24150,7 @@ var MmtpRouter = class {
23874
24150
  inst.last = void 0;
23875
24151
  inst.offsets.clear();
23876
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23877
24154
  }
23878
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23879
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23926,13 +24203,23 @@ var MmtpRouter = class {
23926
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23927
24204
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23928
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).
23929
24213
  */
23930
24214
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23931
24215
  if (this.#overlays.size === 0) return;
23932
24216
  const ssId = keyframeOverlaySsId(header);
23933
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23934
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23935
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23936
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23937
24224
  if (identity !== null) {
23938
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -23962,6 +24249,14 @@ var MmtpRouter = class {
23962
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23963
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23964
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
+ }
23965
24260
  }
23966
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23967
24262
  inst.moqBlocks.add(blockStart);
@@ -23995,11 +24290,16 @@ var MmtpRouter = class {
23995
24290
  }
23996
24291
  /**
23997
24292
  * Route an overlay-recovered block's packets as source media of the
23998
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23999
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
24000
- * base re-stamp, no block advance. Each packet's wire sequence is restored
24001
- * from its keyframe's own offset; without one, a stream the base never
24002
- * 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.
24003
24303
  */
24004
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
24005
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -24020,40 +24320,65 @@ var MmtpRouter = class {
24020
24320
  const restamped = this.#restampedStreams.has(streamKey);
24021
24321
  const restored = /* @__PURE__ */ new Set();
24022
24322
  const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
24023
24324
  const symbols = result.symbols.flatMap((sym) => {
24325
+ const overlaySsId = serial32Add(blockStart, sym.index);
24024
24326
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24025
24327
  if (anchor !== void 0) {
24026
- const overlaySsId = serial32Add(blockStart, sym.index);
24027
24328
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
24028
24329
  restored.add(packetSequenceNumber);
24029
24330
  const out = { ...sym, packetSequenceNumber };
24030
24331
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24031
- if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
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
+ }
24032
24339
  return [out];
24033
24340
  }
24034
24341
  if (!restamped) {
24035
- const baseSsId = unstampedBaseSsId(
24036
- this.#ssIdSeqOffsetByStream.get(streamKey),
24037
- this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24038
- sym.packetSequenceNumber
24039
- );
24040
- if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
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
+ }
24041
24352
  return [sym];
24042
24353
  }
24043
24354
  this.#overlayStats.unsequencedDropped++;
24044
24355
  return [];
24045
24356
  });
24046
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
+ }
24047
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
24048
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
24049
24373
  });
24050
24374
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24051
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24375
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24052
24376
  }
24053
24377
  /**
24054
24378
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24055
24379
  * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24056
- * from a received member of its MPU, else unstampedBaseSsId),
24380
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24381
+ * base recovery showed; `baseSsIds` holds no other),
24057
24382
  * so it is fed to the base decoder as a rebuilt source
24058
24383
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24059
24384
  * fewer to solve, and its repair can recover the block's other packets.
@@ -24079,7 +24404,7 @@ var MmtpRouter = class {
24079
24404
  const feeds = [];
24080
24405
  for (const sym of symbols) {
24081
24406
  if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24082
- const ssId = baseSsIds.get(sym);
24407
+ const ssId = baseSsIds.get(sym.index);
24083
24408
  if (ssId === void 0) {
24084
24409
  this.#overlayStats.baseUndetermined++;
24085
24410
  continue;
@@ -24266,6 +24591,10 @@ var MmtpRouter = class {
24266
24591
  get unadmittedListedDropCount() {
24267
24592
  return this.#unadmittedListedDrops;
24268
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
+ }
24269
24598
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
24270
24599
  get unlistedRepairSetAsideCount() {
24271
24600
  return this.#unlistedRepairSetAside;
@@ -24804,7 +25133,7 @@ var MmtpRouter = class {
24804
25133
  catalogTrackName
24805
25134
  )) continue;
24806
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24807
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24808
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24809
25138
  }
24810
25139
  if (payload.oversized) continue;
@@ -24892,6 +25221,7 @@ var MmtpRouter = class {
24892
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24893
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24894
25223
  this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24895
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24896
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24897
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25018,6 +25348,12 @@ var MmtpRouter = class {
25018
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
25019
25349
  * own RAP cadence, so no timer is involved.
25020
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
+ *
25021
25357
  * The commit is one synchronous transition inside the frame path: the
25022
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
25023
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -25068,6 +25404,9 @@ var MmtpRouter = class {
25068
25404
  droppedIncomingFrames: 0
25069
25405
  };
25070
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
25071
25410
  return {
25072
25411
  cancel: () => {
25073
25412
  if (this.#staged !== state) return false;
@@ -25150,7 +25489,17 @@ var MmtpRouter = class {
25150
25489
  */
25151
25490
  #routeParsedFrame(data, meta) {
25152
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
25153
- 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
+ }
25154
25503
  this.#deliverParsedFrame(data, meta);
25155
25504
  }
25156
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -25302,12 +25651,16 @@ var MmtpRouter = class {
25302
25651
  this.#parser.deferFecRelease(work);
25303
25652
  } finally {
25304
25653
  this.#fecPokeDepth--;
25305
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25306
- const pokes = [...this.#pendingFecPokes.values()];
25307
- this.#pendingFecPokes.clear();
25308
- for (const poke of pokes) {
25309
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25310
- }
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);
25311
25664
  }
25312
25665
  }
25313
25666
  }
@@ -25385,6 +25738,14 @@ var MmtpRouter = class {
25385
25738
  onRap: (packetId, catalogTrackName, ssId) => {
25386
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25387
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
+ },
25388
25749
  onReleased: (data, meta) => {
25389
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25390
25751
  const timing = meta.authoredTiming;
@@ -25526,6 +25887,7 @@ var MmtpRouter = class {
25526
25887
  const parsed = this.#parser.parsePacket(packet);
25527
25888
  if (!parsed) return;
25528
25889
  const info = parsed.info;
25890
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25529
25891
  if (source === "multicast" && !manifestAdmitted) {
25530
25892
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25531
25893
  }
@@ -25834,7 +26196,8 @@ var MmtpRouter = class {
25834
26196
  source: parserOptions?.source,
25835
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25836
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25837
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25838
26201
  };
25839
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25840
26203
  else this.#parser.feed(packet, feedOptions);
@@ -26300,7 +26663,12 @@ var MmtpRouter = class {
26300
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
26301
26664
  if (ownerPacketId == null) return;
26302
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
26303
- 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
+ };
26304
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
26305
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
26306
26674
  if (ssIdSeqOffset !== void 0) {
@@ -28096,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
28096
28464
  function isSymbolCounts(value) {
28097
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
28098
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
+ }
28099
28473
  function isNullableNonNegative(value) {
28100
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
28101
28475
  }
@@ -28115,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
28115
28489
  case "recovered":
28116
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"]);
28117
28491
  case "failed":
28118
- 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"]));
28119
28496
  case "repair-preempted":
28120
28497
  case "repair-deferred":
28121
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -28381,7 +28758,7 @@ var MulticastManager = class {
28381
28758
  onPublisherRestart: (packetId, catalogTrackName) => {
28382
28759
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
28383
28760
  },
28384
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28761
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
28385
28762
  const pathCount = this.#fec.blockPathCount(
28386
28763
  sbn,
28387
28764
  packetId,
@@ -28399,7 +28776,8 @@ var MulticastManager = class {
28399
28776
  symbols: {
28400
28777
  multicast: pathCount?.multicastSymbols ?? 0,
28401
28778
  moq: pathCount?.moqSymbols ?? 0
28402
- }
28779
+ },
28780
+ ...span === void 0 ? {} : { span }
28403
28781
  });
28404
28782
  },
28405
28783
  onFrameGap: (decision) => {
@@ -29779,10 +30157,12 @@ var MulticastManager = class {
29779
30157
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29780
30158
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29781
30159
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29782
- * 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.
29783
30163
  */
29784
- onUndelimitedLoss(cb) {
29785
- this.#fec.setUndelimitedLossHandler(cb);
30164
+ onUndelimitedLoss(cb, onExpired) {
30165
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29786
30166
  }
29787
30167
  /**
29788
30168
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -30018,7 +30398,8 @@ var MulticastManager = class {
30018
30398
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
30019
30399
  ...event,
30020
30400
  block: Object.freeze({ ...event.block }),
30021
- ..."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 }) } : {}
30022
30403
  }
30023
30404
  );
30024
30405
  if (this.#logRepairLifecycle) {
@@ -32690,6 +33071,7 @@ var RepairLayerSubscriptions = class {
32690
33071
 
32691
33072
  // src/repair-fetch.ts
32692
33073
  var MOQT_PRIORITY_MAX = 255;
33074
+ var WALK_LEAD_MS = 3;
32693
33075
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32694
33076
  function isMoqtPriority(value) {
32695
33077
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32740,8 +33122,17 @@ function wireRepairFetch(mc, opts) {
32740
33122
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32741
33123
  };
32742
33124
  const clearEntry = (entry) => {
32743
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32744
- 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);
32745
33136
  };
32746
33137
  const releaseHold = (entry) => {
32747
33138
  if (entry.range) return;
@@ -32808,21 +33199,42 @@ function wireRepairFetch(mc, opts) {
32808
33199
  const admit = (entry, now) => {
32809
33200
  const budgetMs = entry.deadlineAt - now;
32810
33201
  const rttMs = readRtt();
32811
- 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);
32812
33203
  if (reason === null) return rttMs;
32813
33204
  skip(entry, reason, rttMs, budgetMs);
32814
33205
  return null;
32815
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";
32816
33208
  const retire = (entry) => {
32817
33209
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32818
33210
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32819
33211
  };
32820
- 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) => {
32821
33220
  for (const entry of entries.values()) {
32822
- if (entry.priority < priority) return true;
33221
+ if (holdsBack(entry, priority, videoWalks)) return true;
32823
33222
  }
32824
33223
  return false;
32825
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
+ };
32826
33238
  const preemptBestEffort = () => {
32827
33239
  for (const entry of entries.values()) {
32828
33240
  if (entry.priority === 0 || !entry.running) continue;
@@ -32851,7 +33263,8 @@ function wireRepairFetch(mc, opts) {
32851
33263
  }
32852
33264
  const rttMs = admit(entry, now);
32853
33265
  if (rttMs === null) return;
32854
- if (entry.priority === 0) preemptBestEffort();
33266
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33267
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32855
33268
  if (counted) entry.attempts++;
32856
33269
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32857
33270
  rttMs,
@@ -32873,6 +33286,14 @@ function wireRepairFetch(mc, opts) {
32873
33286
  }
32874
33287
  void execute(entry, run);
32875
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
+ };
32876
33297
  const dispatch = () => {
32877
33298
  if (disposed) return;
32878
33299
  const now = performance.now();
@@ -32897,6 +33318,7 @@ function wireRepairFetch(mc, opts) {
32897
33318
  for (const entry of queued) {
32898
33319
  if (entry.priority === 0) start(entry);
32899
33320
  }
33321
+ yieldVideoWalks(queued);
32900
33322
  if (outstandingBelow(1)) return;
32901
33323
  for (const entry of queued) {
32902
33324
  if (entry.priority === 1) start(entry);
@@ -32923,6 +33345,16 @@ function wireRepairFetch(mc, opts) {
32923
33345
  }
32924
33346
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
32925
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
+ };
32926
33358
  const finish = (entry, run, fed, error) => {
32927
33359
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
32928
33360
  return;
@@ -32967,27 +33399,81 @@ function wireRepairFetch(mc, opts) {
32967
33399
  signal: run.controller.signal,
32968
33400
  subscriberPriority: entry.subscriberPriority
32969
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
+ };
32970
33410
  try {
32971
- 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({
32972
33412
  ...common,
32973
- startGroup: entry.range.startGroup,
32974
- endGroup: entry.range.endGroup,
32975
- lostSsId: entry.range.lostSsId
32976
- }) : opts.transport({ ...common, sbn: entry.sbn });
33413
+ startGroup: range.groups.startGroup,
33414
+ endGroup: range.groups.endGroup,
33415
+ lostSsId: range.lostSsId
33416
+ });
32977
33417
  for await (const packet of packets) {
32978
33418
  if (run.stopped !== null || entry.running !== run) break;
32979
33419
  fed++;
32980
- if (entry.catalogTrackName === void 0) {
32981
- mc.processRepairFromMoQ(packet, entry.packetId);
32982
- } else {
32983
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
32984
- }
33420
+ if (range === void 0) feed(packet);
33421
+ else atReport(range, () => feed(packet));
32985
33422
  }
32986
33423
  finish(entry, run, fed, void 0);
32987
33424
  } catch (error) {
32988
33425
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
32989
33426
  }
32990
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
+ }
32991
33477
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
32992
33478
  for (const entry of [...entries.values()]) {
32993
33479
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -33183,7 +33669,37 @@ function wireRepairFetch(mc, opts) {
33183
33669
  throw error;
33184
33670
  }
33185
33671
  };
33186
- 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) {
33187
33703
  mc.onUndelimitedLoss((loss) => {
33188
33704
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
33189
33705
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -33191,9 +33707,11 @@ function wireRepairFetch(mc, opts) {
33191
33707
  const blockProfile = blockProfileOf(packetId);
33192
33708
  const target = opts.resolveTarget(packetId, catalogTrackName);
33193
33709
  if (!target) return close();
33194
- const priority = repairPriorityForClass("unknown");
33195
- const range = rangeFor(loss, target.repairTrack);
33196
- 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) {
33197
33715
  close();
33198
33716
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
33199
33717
  return;
@@ -33204,8 +33722,10 @@ function wireRepairFetch(mc, opts) {
33204
33722
  packetId,
33205
33723
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
33206
33724
  };
33207
- const wire = wirePriority(identity, "unknown");
33208
- 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);
33209
33729
  if (deadline === null) {
33210
33730
  close();
33211
33731
  mc.noteRepairSkipped(
@@ -33217,6 +33737,7 @@ function wireRepairFetch(mc, opts) {
33217
33737
  );
33218
33738
  return;
33219
33739
  }
33740
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
33220
33741
  const entry = {
33221
33742
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
33222
33743
  ...identity,
@@ -33224,15 +33745,16 @@ function wireRepairFetch(mc, opts) {
33224
33745
  blockProfile,
33225
33746
  priority,
33226
33747
  subscriberPriority: wire.subscriberPriority,
33227
- deadlineAt: deadline.deadlineAt,
33228
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33748
+ deadlineAt,
33749
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
33229
33750
  anchorAt: deadline.anchorAt,
33230
33751
  delayMs: deadline.delayMs,
33231
33752
  bound: deadline.bound,
33232
33753
  order: enqueueOrder++,
33233
33754
  attempts: 0,
33234
33755
  running: null,
33235
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33756
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33757
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
33236
33758
  };
33237
33759
  const previous = entries.get(entry.key);
33238
33760
  if (previous) {
@@ -33242,7 +33764,7 @@ function wireRepairFetch(mc, opts) {
33242
33764
  entries.set(entry.key, entry);
33243
33765
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
33244
33766
  dispatch();
33245
- });
33767
+ }, onHoleExpired);
33246
33768
  }
33247
33769
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
33248
33770
  queueMicrotask(() => {