@blockcast/mmt-container 2.2.0-main.3cc3ad1f4323 → 2.2.0-main.4ccba0f2f32a

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 +714 -147
  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 +18 -9
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +261 -50
  28. package/dist/mmtp-router.js.map +2 -2
  29. package/dist/multicast-manager.d.ts +26 -3
  30. package/dist/multicast-manager.d.ts.map +1 -1
  31. package/dist/multicast-manager.js +535 -121
  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 +231 -58
  52. package/src/multicast-manager.ts +118 -14
  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) {
@@ -23177,6 +23432,17 @@ var MmtpRouter = class {
23177
23432
  * an MPU no member of which arrived).
23178
23433
  */
23179
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();
23180
23446
  /**
23181
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23182
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -23454,13 +23720,18 @@ var MmtpRouter = class {
23454
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23455
23721
  }
23456
23722
  };
23457
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23458
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23459
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23460
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23461
23727
  }
23462
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23463
- 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
+ }
23464
23735
  };
23465
23736
  }
23466
23737
  }
@@ -23505,8 +23776,9 @@ var MmtpRouter = class {
23505
23776
  }
23506
23777
  /**
23507
23778
  * FEC-recovered source symbols whose reconstructed header named a
23508
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23509
- * 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.
23510
23782
  */
23511
23783
  get restampedRecoveredSequenceCount() {
23512
23784
  return this.#restampedRecoveredSeqs;
@@ -23685,6 +23957,7 @@ var MmtpRouter = class {
23685
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23686
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23687
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23688
23961
  if (previous) {
23689
23962
  previous.flush();
23690
23963
  previous.dispose();
@@ -23835,7 +24108,8 @@ var MmtpRouter = class {
23835
24108
  fec,
23836
24109
  k,
23837
24110
  offsets: /* @__PURE__ */ new Map(),
23838
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23839
24113
  };
23840
24114
  this.#overlays.set(repairPacketId, inst);
23841
24115
  return () => {
@@ -23876,6 +24150,7 @@ var MmtpRouter = class {
23876
24150
  inst.last = void 0;
23877
24151
  inst.offsets.clear();
23878
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23879
24154
  }
23880
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23881
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23928,13 +24203,23 @@ var MmtpRouter = class {
23928
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23929
24204
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23930
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).
23931
24213
  */
23932
24214
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23933
24215
  if (this.#overlays.size === 0) return;
23934
24216
  const ssId = keyframeOverlaySsId(header);
23935
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23936
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23937
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23938
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23939
24224
  if (identity !== null) {
23940
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -23964,6 +24249,14 @@ var MmtpRouter = class {
23964
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23965
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23966
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
+ }
23967
24260
  }
23968
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23969
24262
  inst.moqBlocks.add(blockStart);
@@ -23997,11 +24290,16 @@ var MmtpRouter = class {
23997
24290
  }
23998
24291
  /**
23999
24292
  * Route an overlay-recovered block's packets as source media of the
24000
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
24001
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
24002
- * base re-stamp, no block advance. Each packet's wire sequence is restored
24003
- * from its keyframe's own offset; without one, a stream the base never
24004
- * 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.
24005
24303
  */
24006
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
24007
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -24022,40 +24320,65 @@ var MmtpRouter = class {
24022
24320
  const restamped = this.#restampedStreams.has(streamKey);
24023
24321
  const restored = /* @__PURE__ */ new Set();
24024
24322
  const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
24025
24324
  const symbols = result.symbols.flatMap((sym) => {
24325
+ const overlaySsId = serial32Add(blockStart, sym.index);
24026
24326
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24027
24327
  if (anchor !== void 0) {
24028
- const overlaySsId = serial32Add(blockStart, sym.index);
24029
24328
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
24030
24329
  restored.add(packetSequenceNumber);
24031
24330
  const out = { ...sym, packetSequenceNumber };
24032
24331
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24033
- 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
+ }
24034
24339
  return [out];
24035
24340
  }
24036
24341
  if (!restamped) {
24037
- const baseSsId = unstampedBaseSsId(
24038
- this.#ssIdSeqOffsetByStream.get(streamKey),
24039
- this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24040
- sym.packetSequenceNumber
24041
- );
24042
- 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
+ }
24043
24352
  return [sym];
24044
24353
  }
24045
24354
  this.#overlayStats.unsequencedDropped++;
24046
24355
  return [];
24047
24356
  });
24048
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
+ }
24049
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
24050
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
24051
24373
  });
24052
24374
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24053
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24375
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24054
24376
  }
24055
24377
  /**
24056
24378
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24057
24379
  * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24058
- * 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),
24059
24382
  * so it is fed to the base decoder as a rebuilt source
24060
24383
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24061
24384
  * fewer to solve, and its repair can recover the block's other packets.
@@ -24081,7 +24404,7 @@ var MmtpRouter = class {
24081
24404
  const feeds = [];
24082
24405
  for (const sym of symbols) {
24083
24406
  if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24084
- const ssId = baseSsIds.get(sym);
24407
+ const ssId = baseSsIds.get(sym.index);
24085
24408
  if (ssId === void 0) {
24086
24409
  this.#overlayStats.baseUndetermined++;
24087
24410
  continue;
@@ -24810,7 +25133,7 @@ var MmtpRouter = class {
24810
25133
  catalogTrackName
24811
25134
  )) continue;
24812
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24813
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24814
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24815
25138
  }
24816
25139
  if (payload.oversized) continue;
@@ -24898,6 +25221,7 @@ var MmtpRouter = class {
24898
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24899
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24900
25223
  this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24901
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24902
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24903
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25024,6 +25348,12 @@ var MmtpRouter = class {
25024
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
25025
25349
  * own RAP cadence, so no timer is involved.
25026
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
+ *
25027
25357
  * The commit is one synchronous transition inside the frame path: the
25028
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
25029
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -25074,6 +25404,9 @@ var MmtpRouter = class {
25074
25404
  droppedIncomingFrames: 0
25075
25405
  };
25076
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
25077
25410
  return {
25078
25411
  cancel: () => {
25079
25412
  if (this.#staged !== state) return false;
@@ -25156,7 +25489,17 @@ var MmtpRouter = class {
25156
25489
  */
25157
25490
  #routeParsedFrame(data, meta) {
25158
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
25159
- 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
+ }
25160
25503
  this.#deliverParsedFrame(data, meta);
25161
25504
  }
25162
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -25308,12 +25651,16 @@ var MmtpRouter = class {
25308
25651
  this.#parser.deferFecRelease(work);
25309
25652
  } finally {
25310
25653
  this.#fecPokeDepth--;
25311
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25312
- const pokes = [...this.#pendingFecPokes.values()];
25313
- this.#pendingFecPokes.clear();
25314
- for (const poke of pokes) {
25315
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25316
- }
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);
25317
25664
  }
25318
25665
  }
25319
25666
  }
@@ -25391,6 +25738,14 @@ var MmtpRouter = class {
25391
25738
  onRap: (packetId, catalogTrackName, ssId) => {
25392
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25393
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
+ },
25394
25749
  onReleased: (data, meta) => {
25395
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25396
25751
  const timing = meta.authoredTiming;
@@ -25841,7 +26196,8 @@ var MmtpRouter = class {
25841
26196
  source: parserOptions?.source,
25842
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25843
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25844
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25845
26201
  };
25846
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25847
26203
  else this.#parser.feed(packet, feedOptions);
@@ -26307,7 +26663,12 @@ var MmtpRouter = class {
26307
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
26308
26664
  if (ownerPacketId == null) return;
26309
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
26310
- 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
+ };
26311
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
26312
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
26313
26674
  if (ssIdSeqOffset !== void 0) {
@@ -28103,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
28103
28464
  function isSymbolCounts(value) {
28104
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
28105
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
+ }
28106
28473
  function isNullableNonNegative(value) {
28107
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
28108
28475
  }
@@ -28122,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
28122
28489
  case "recovered":
28123
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"]);
28124
28491
  case "failed":
28125
- 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"]));
28126
28496
  case "repair-preempted":
28127
28497
  case "repair-deferred":
28128
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -28331,6 +28701,8 @@ var MulticastManager = class {
28331
28701
  /** Live joinRepairEndpoints() releases → the repair owners they registered,
28332
28702
  * re-applied after configureMultiTrack() clears catalog-derived owners. */
28333
28703
  #repairReleases = /* @__PURE__ */ new Map();
28704
+ /** Protected repair flows owned by each live repair-endpoint release. */
28705
+ #repairProvenanceFlows = /* @__PURE__ */ new Map();
28334
28706
  /** Each source's catalog keyframe overlay, from its `ok` binding. */
28335
28707
  #overlayBindings = /* @__PURE__ */ new Map();
28336
28708
  /** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
@@ -28388,7 +28760,7 @@ var MulticastManager = class {
28388
28760
  onPublisherRestart: (packetId, catalogTrackName) => {
28389
28761
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
28390
28762
  },
28391
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28763
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
28392
28764
  const pathCount = this.#fec.blockPathCount(
28393
28765
  sbn,
28394
28766
  packetId,
@@ -28406,7 +28778,8 @@ var MulticastManager = class {
28406
28778
  symbols: {
28407
28779
  multicast: pathCount?.multicastSymbols ?? 0,
28408
28780
  moq: pathCount?.moqSymbols ?? 0
28409
- }
28781
+ },
28782
+ ...span === void 0 ? {} : { span }
28410
28783
  });
28411
28784
  },
28412
28785
  onFrameGap: (decision) => {
@@ -28996,6 +29369,9 @@ var MulticastManager = class {
28996
29369
  this.#transport.onFailback((reason) => {
28997
29370
  const joined = this.#provenanceJoin;
28998
29371
  if (joined !== null) this.#router.unprotectProvenanceFlow(joined.flow);
29372
+ for (const flows of this.#repairProvenanceFlows.values()) {
29373
+ for (const flow of flows) this.#router.unprotectProvenanceFlow(flow);
29374
+ }
28999
29375
  this.#onFailback?.(reason);
29000
29376
  });
29001
29377
  }
@@ -29030,6 +29406,12 @@ var MulticastManager = class {
29030
29406
  "[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port"
29031
29407
  );
29032
29408
  }
29409
+ if (provenanceFlow !== void 0) {
29410
+ const flow = multicastProvenanceFlowKey(provenanceFlow);
29411
+ if ([...this.#repairProvenanceFlows.values()].some((flows) => flows.includes(flow))) {
29412
+ throw new TypeError(`[MulticastManager] join: provenance flow already registered by a repair endpoint: ${flow}`);
29413
+ }
29414
+ }
29033
29415
  const staged = this.#renditionSwitch;
29034
29416
  if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
29035
29417
  this.configureFec(config);
@@ -29037,8 +29419,8 @@ var MulticastManager = class {
29037
29419
  const selectedPacketIds = config.packetIds?.length ? new Set(config.packetIds) : null;
29038
29420
  const catalogTracks = config.tracks?.filter(({ packetId }) => selectedPacketIds === null || selectedPacketIds.has(packetId));
29039
29421
  const ingressTracks = catalogTracks ? catalogTracks : selectedPacketIds === null ? void 0 : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
29040
- this.#releaseProvenanceJoin();
29041
29422
  const provenance = config.provenance;
29423
+ this.#releaseProvenanceJoin();
29042
29424
  const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
29043
29425
  const ingress = protectedTracks === void 0 ? void 0 : {
29044
29426
  tracks: Object.freeze(
@@ -29091,8 +29473,13 @@ var MulticastManager = class {
29091
29473
  * feeds the flow's receiver; media datagrams are held until one admits them.
29092
29474
  */
29093
29475
  #joinProvenance(provenance, ingress) {
29476
+ this.#router.registerProvenanceFlow(ingress, this.#provenanceReceiver(provenance));
29477
+ this.#hookFailback();
29478
+ this.#provenanceJoin = { flow: ingress.flow };
29479
+ }
29480
+ #provenanceReceiver(provenance) {
29094
29481
  const { flow, auth } = provenance;
29095
- const receiver = new BcProvenanceV2Receiver({
29482
+ return new BcProvenanceV2Receiver({
29096
29483
  ...provenance.receiver,
29097
29484
  // The caller's ctx when it passes one, else the catalog's: an explicit
29098
29485
  // `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
@@ -29109,9 +29496,6 @@ var MulticastManager = class {
29109
29496
  maxAuthDelayMs: flow.maxAuthDelayMs,
29110
29497
  manifestTimerMs: flow.manifestTimerMs
29111
29498
  });
29112
- this.#router.registerProvenanceFlow(ingress, receiver);
29113
- this.#hookFailback();
29114
- this.#provenanceJoin = { flow: ingress.flow };
29115
29499
  }
29116
29500
  /** Un-protect: drop the flow's hold queues (no expiry counted). */
29117
29501
  #releaseProvenanceJoin() {
@@ -29142,8 +29526,25 @@ var MulticastManager = class {
29142
29526
  * callback delivers must come from a subscription opened on
29143
29527
  * `options.gateway`. A callback that also merges another source (a MoQ or
29144
29528
  * WebSocket repair feed) must not name a native provider here.
29529
+ *
29530
+ * An endpoint's `provenance` makes its group a protected flow of its own
29531
+ * (multicast-02 7.2.7), registered here and released with the group; its
29532
+ * records ride in band on the group's port. On the user-agent path a
29533
+ * flow-less repair group of a protected track is set aside, never read, so
29534
+ * a player joining a protected catalog's repair groups passes it.
29145
29535
  */
29146
29536
  joinRepairEndpoints(endpoints, provider, options) {
29537
+ for (const { provenance, flow, source, group, port: port2 } of endpoints) {
29538
+ if (provenance === void 0) continue;
29539
+ if (flow !== void 0) {
29540
+ throw new TypeError("[MulticastManager] joinRepairEndpoints: an endpoint names provenance or flow, never both");
29541
+ }
29542
+ if (!sameIpAddress(source, provenance.flow.source) || !sameIpAddress(group, provenance.flow.groupAddress) || port2 !== provenance.flow.port) {
29543
+ throw new RangeError(
29544
+ "[MulticastManager] joinRepairEndpoints: provenance flow (S, G, port) must match the endpoint's source, group and port"
29545
+ );
29546
+ }
29547
+ }
29147
29548
  const releases = [];
29148
29549
  let released = false;
29149
29550
  const safely = (each) => {
@@ -29153,17 +29554,36 @@ var MulticastManager = class {
29153
29554
  console.warn("[MulticastManager] repair group release failed", error);
29154
29555
  }
29155
29556
  };
29557
+ const usedFlows = /* @__PURE__ */ new Set();
29558
+ const preparedProvenance = endpoints.map((endpoint) => {
29559
+ if (endpoint.provenance === void 0) return void 0;
29560
+ const flow = multicastProvenanceFlowKey(endpoint.provenance.flow);
29561
+ if (usedFlows.has(flow) || this.#provenanceJoin?.flow === flow || [...this.#repairProvenanceFlows.values()].some((flows) => flows.includes(flow))) {
29562
+ throw new TypeError(`[MulticastManager] joinRepairEndpoints: provenance flow already registered: ${flow}`);
29563
+ }
29564
+ usedFlows.add(flow);
29565
+ return { flow, receiver: this.#provenanceReceiver(endpoint.provenance) };
29566
+ });
29567
+ const provenanceFlows = [];
29156
29568
  const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
29157
29569
  ...owner,
29158
29570
  ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
29159
29571
  repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
29160
29572
  }));
29161
29573
  this.#applyRepairOwners(repairOwners);
29162
- for (const endpoint of endpoints) {
29574
+ for (const [index, endpoint] of endpoints.entries()) {
29575
+ const flow = endpoint.provenance === void 0 ? endpoint.flow : multicastProvenanceFlowKey(endpoint.provenance.flow);
29163
29576
  const ingress = {
29164
29577
  tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
29165
- ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
29578
+ ...flow === void 0 ? {} : { flow }
29166
29579
  };
29580
+ if (endpoint.provenance !== void 0) {
29581
+ const prepared = preparedProvenance[index];
29582
+ if (prepared === void 0) throw new Error("[MulticastManager] repair provenance preparation missing");
29583
+ this.#router.registerProvenanceFlow(ingress, prepared.receiver);
29584
+ provenanceFlows.push(prepared.flow);
29585
+ releases.push(() => this.#router.releaseProvenanceFlow(flow));
29586
+ }
29167
29587
  let sender;
29168
29588
  const feed = (packet, sourceAddress, ingressToFeed) => {
29169
29589
  if (released) return;
@@ -29193,7 +29613,7 @@ var MulticastManager = class {
29193
29613
  packetIds: endpoint.packetIds,
29194
29614
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
29195
29615
  // A protected repair group's records ride in band on its port (7.2.3).
29196
- ...endpoint.flow === void 0 ? {} : { protected: true }
29616
+ ...flow === void 0 ? {} : { protected: true }
29197
29617
  }, ingress).then((joined) => {
29198
29618
  if (!joined && !released) fail(transport.lastJoinOutcome);
29199
29619
  });
@@ -29202,9 +29622,12 @@ var MulticastManager = class {
29202
29622
  if (released) return;
29203
29623
  released = true;
29204
29624
  this.#repairReleases.delete(release);
29625
+ this.#repairProvenanceFlows.delete(release);
29205
29626
  for (const each of releases.splice(0)) safely(each);
29206
29627
  };
29207
29628
  this.#repairReleases.set(release, repairOwners);
29629
+ this.#repairProvenanceFlows.set(release, provenanceFlows);
29630
+ if (provenanceFlows.length) this.#hookFailback();
29208
29631
  return release;
29209
29632
  }
29210
29633
  #applyRepairOwners(owners) {
@@ -29786,10 +30209,12 @@ var MulticastManager = class {
29786
30209
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29787
30210
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29788
30211
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29789
- * once its range FETCH ends (or none is sent).
30212
+ * once its range FETCH ends (or none is sent). `onExpired` is told of a
30213
+ * pending hole the stream runs past the locator's horizon: returning true,
30214
+ * the caller closes it (`closeIsoHoles`); otherwise it is discarded.
29790
30215
  */
29791
- onUndelimitedLoss(cb) {
29792
- this.#fec.setUndelimitedLossHandler(cb);
30216
+ onUndelimitedLoss(cb, onExpired) {
30217
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29793
30218
  }
29794
30219
  /**
29795
30220
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -30025,7 +30450,8 @@ var MulticastManager = class {
30025
30450
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
30026
30451
  ...event,
30027
30452
  block: Object.freeze({ ...event.block }),
30028
- ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
30453
+ ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
30454
+ ..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
30029
30455
  }
30030
30456
  );
30031
30457
  if (this.#logRepairLifecycle) {
@@ -32697,6 +33123,7 @@ var RepairLayerSubscriptions = class {
32697
33123
 
32698
33124
  // src/repair-fetch.ts
32699
33125
  var MOQT_PRIORITY_MAX = 255;
33126
+ var WALK_LEAD_MS = 3;
32700
33127
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32701
33128
  function isMoqtPriority(value) {
32702
33129
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32747,8 +33174,17 @@ function wireRepairFetch(mc, opts) {
32747
33174
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32748
33175
  };
32749
33176
  const clearEntry = (entry) => {
32750
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32751
- else clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33177
+ const { range } = entry;
33178
+ if (range) {
33179
+ atReport(range, () => mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength));
33180
+ } else {
33181
+ clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName);
33182
+ }
33183
+ };
33184
+ const atReport = (range, work) => {
33185
+ const fec = mc.fecManager;
33186
+ if (range.groups !== null || fec.withReceiptTime === void 0) work();
33187
+ else fec.withReceiptTime(range.reportedAt, work);
32752
33188
  };
32753
33189
  const releaseHold = (entry) => {
32754
33190
  if (entry.range) return;
@@ -32815,21 +33251,42 @@ function wireRepairFetch(mc, opts) {
32815
33251
  const admit = (entry, now) => {
32816
33252
  const budgetMs = entry.deadlineAt - now;
32817
33253
  const rttMs = readRtt();
32818
- const reason = !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < budgetMs ? null : "rtt-exceeds-budget";
33254
+ const reason = refusal(entry, now, rttMs);
32819
33255
  if (reason === null) return rttMs;
32820
33256
  skip(entry, reason, rttMs, budgetMs);
32821
33257
  return null;
32822
33258
  };
33259
+ 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";
32823
33260
  const retire = (entry) => {
32824
33261
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32825
33262
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32826
33263
  };
32827
- const outstandingBelow = (priority) => {
33264
+ const holdsBack = (entry, priority, videoWalks = true) => {
33265
+ if (entry.priority >= priority) return false;
33266
+ const { walk } = entry;
33267
+ if (walk === void 0) return true;
33268
+ if (entry.priority !== 0 || !videoWalks) return false;
33269
+ return entry.running === null || walk.group !== null && !walk.group.controller.signal.aborted;
33270
+ };
33271
+ const outstandingBelow = (priority, videoWalks = true) => {
32828
33272
  for (const entry of entries.values()) {
32829
- if (entry.priority < priority) return true;
33273
+ if (holdsBack(entry, priority, videoWalks)) return true;
32830
33274
  }
32831
33275
  return false;
32832
33276
  };
33277
+ const yieldVideoWalks = (queued) => {
33278
+ const now = performance.now();
33279
+ const rttMs = readRtt();
33280
+ const due = queued.some((entry) => entry.priority !== 0 && entry.walk === void 0 && !outstandingBelow(entry.priority, false) && refusal(entry, now, rttMs) === null);
33281
+ if (!due) return;
33282
+ for (const entry of entries.values()) {
33283
+ const walk = entry.walk;
33284
+ if (entry.priority !== 0 || walk === void 0 || walk.group === null) continue;
33285
+ if (walk.group.controller.signal.aborted) continue;
33286
+ walk.yielded = true;
33287
+ walk.group.controller.abort();
33288
+ }
33289
+ };
32833
33290
  const preemptBestEffort = () => {
32834
33291
  for (const entry of entries.values()) {
32835
33292
  if (entry.priority === 0 || !entry.running) continue;
@@ -32858,7 +33315,8 @@ function wireRepairFetch(mc, opts) {
32858
33315
  }
32859
33316
  const rttMs = admit(entry, now);
32860
33317
  if (rttMs === null) return;
32861
- if (entry.priority === 0) preemptBestEffort();
33318
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33319
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32862
33320
  if (counted) entry.attempts++;
32863
33321
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32864
33322
  rttMs,
@@ -32880,6 +33338,14 @@ function wireRepairFetch(mc, opts) {
32880
33338
  }
32881
33339
  void execute(entry, run);
32882
33340
  };
33341
+ const leaveWalkedGroup = (block) => {
33342
+ for (const entry of entries.values()) {
33343
+ const group = entry.walk?.group;
33344
+ if (group?.sbn === block.sbn && entry.packetId === block.packetId && entry.catalogTrackName === block.catalogTrackName) {
33345
+ group.controller.abort();
33346
+ }
33347
+ }
33348
+ };
32883
33349
  const dispatch = () => {
32884
33350
  if (disposed) return;
32885
33351
  const now = performance.now();
@@ -32904,6 +33370,7 @@ function wireRepairFetch(mc, opts) {
32904
33370
  for (const entry of queued) {
32905
33371
  if (entry.priority === 0) start(entry);
32906
33372
  }
33373
+ yieldVideoWalks(queued);
32907
33374
  if (outstandingBelow(1)) return;
32908
33375
  for (const entry of queued) {
32909
33376
  if (entry.priority === 1) start(entry);
@@ -32930,6 +33397,16 @@ function wireRepairFetch(mc, opts) {
32930
33397
  }
32931
33398
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
32932
33399
  };
33400
+ const groupServed = (sbn, packetId, catalogTrackName) => {
33401
+ if (mc.fecManager.blockSymbolCounts === void 0) return false;
33402
+ const counts = symbolCounts(sbn, packetId, catalogTrackName);
33403
+ if (counts === void 0 || counts.authoredLength === void 0) return true;
33404
+ return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName);
33405
+ };
33406
+ const statedEnd = (sbn, packetId, catalogTrackName) => {
33407
+ const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength;
33408
+ return length === void 0 ? void 0 : serial32Add(sbn, length);
33409
+ };
32933
33410
  const finish = (entry, run, fed, error) => {
32934
33411
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
32935
33412
  return;
@@ -32974,27 +33451,81 @@ function wireRepairFetch(mc, opts) {
32974
33451
  signal: run.controller.signal,
32975
33452
  subscriberPriority: entry.subscriberPriority
32976
33453
  };
33454
+ const { range } = entry;
33455
+ const feed = (packet) => {
33456
+ if (entry.catalogTrackName === void 0) {
33457
+ mc.processRepairFromMoQ(packet, entry.packetId);
33458
+ } else {
33459
+ mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
33460
+ }
33461
+ };
32977
33462
  try {
32978
- const packets = entry.range ? opts.rangeTransport({
33463
+ const packets = range === void 0 ? opts.transport({ ...common, sbn: entry.sbn }) : entry.walk !== void 0 ? walkHole(entry, entry.walk, range, common) : opts.rangeTransport({
32979
33464
  ...common,
32980
- startGroup: entry.range.startGroup,
32981
- endGroup: entry.range.endGroup,
32982
- lostSsId: entry.range.lostSsId
32983
- }) : opts.transport({ ...common, sbn: entry.sbn });
33465
+ startGroup: range.groups.startGroup,
33466
+ endGroup: range.groups.endGroup,
33467
+ lostSsId: range.lostSsId
33468
+ });
32984
33469
  for await (const packet of packets) {
32985
33470
  if (run.stopped !== null || entry.running !== run) break;
32986
33471
  fed++;
32987
- if (entry.catalogTrackName === void 0) {
32988
- mc.processRepairFromMoQ(packet, entry.packetId);
32989
- } else {
32990
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
32991
- }
33472
+ if (range === void 0) feed(packet);
33473
+ else atReport(range, () => feed(packet));
32992
33474
  }
32993
33475
  finish(entry, run, fed, void 0);
32994
33476
  } catch (error) {
32995
33477
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
32996
33478
  }
32997
33479
  };
33480
+ async function* walkHole(entry, walk, hole, common) {
33481
+ const lostAt = serial32Distance(entry.sbn, hole.lostSsId);
33482
+ for (let first = true; !common.signal.aborted; first = false) {
33483
+ if (!first && entry.priority !== 0 && outstandingBelow(1)) {
33484
+ stop(entry, "preempted");
33485
+ note("repair-preempted", entry);
33486
+ return;
33487
+ }
33488
+ if (!first && entry.priority === 0) {
33489
+ dispatch();
33490
+ if (common.signal.aborted) return;
33491
+ }
33492
+ const sbn = walk.next;
33493
+ let next;
33494
+ if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
33495
+ const rsIds = /* @__PURE__ */ new Set();
33496
+ const group = new AbortController();
33497
+ const cancel = () => group.abort();
33498
+ common.signal.addEventListener("abort", cancel, { once: true });
33499
+ walk.group = { sbn, controller: group };
33500
+ try {
33501
+ for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
33502
+ yield packet;
33503
+ const id = extractRepairFecPayloadId(packet);
33504
+ if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue;
33505
+ next ??= serial32Add(sbn, id.ssbLength);
33506
+ rsIds.add(id.rsId);
33507
+ if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
33508
+ group.abort();
33509
+ break;
33510
+ }
33511
+ }
33512
+ } catch (error) {
33513
+ if (!group.signal.aborted || common.signal.aborted) throw error;
33514
+ } finally {
33515
+ common.signal.removeEventListener("abort", cancel);
33516
+ walk.group = null;
33517
+ }
33518
+ if (walk.yielded) {
33519
+ walk.yielded = false;
33520
+ continue;
33521
+ }
33522
+ }
33523
+ next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName);
33524
+ if (next === void 0) return;
33525
+ if (serial32Distance(entry.sbn, next) > lostAt) return;
33526
+ walk.next = next;
33527
+ }
33528
+ }
32998
33529
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
32999
33530
  for (const entry of [...entries.values()]) {
33000
33531
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -33190,7 +33721,37 @@ function wireRepairFetch(mc, opts) {
33190
33721
  throw error;
33191
33722
  }
33192
33723
  };
33193
- if (opts.rangeTransport !== void 0) {
33724
+ const trackKind = (packetId) => {
33725
+ try {
33726
+ return mc.fecManager.trackKindFor?.(packetId);
33727
+ } catch {
33728
+ return void 0;
33729
+ }
33730
+ };
33731
+ const walkDeadlineAt = (packetId, now, deadlineAt) => {
33732
+ let repairDeadlineMs;
33733
+ try {
33734
+ repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId);
33735
+ } catch {
33736
+ repairDeadlineMs = void 0;
33737
+ }
33738
+ if (repairDeadlineMs === void 0 || !Number.isFinite(repairDeadlineMs)) return deadlineAt;
33739
+ return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS);
33740
+ };
33741
+ const onHoleExpired = (hole) => {
33742
+ if (disposed) return false;
33743
+ 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);
33744
+ if (entry === void 0) return false;
33745
+ queueMicrotask(() => {
33746
+ if (entries.get(entry.key) !== entry) return;
33747
+ retire(entry);
33748
+ stop(entry, "expired");
33749
+ clearEntry(entry);
33750
+ dispatch();
33751
+ });
33752
+ return true;
33753
+ };
33754
+ if (mc.onUndelimitedLoss !== void 0 && mc.closeIsoHoles !== void 0) {
33194
33755
  mc.onUndelimitedLoss((loss) => {
33195
33756
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
33196
33757
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -33198,9 +33759,11 @@ function wireRepairFetch(mc, opts) {
33198
33759
  const blockProfile = blockProfileOf(packetId);
33199
33760
  const target = opts.resolveTarget(packetId, catalogTrackName);
33200
33761
  if (!target) return close();
33201
- const priority = repairPriorityForClass("unknown");
33202
- const range = rangeFor(loss, target.repairTrack);
33203
- if (range === null) {
33762
+ const walked = opts.rangeTransport === void 0;
33763
+ const holeClass = walked && trackKind(packetId) === "audio" ? "audio" : "unknown";
33764
+ const priority = repairPriorityForClass(holeClass);
33765
+ const groups = walked ? null : rangeFor(loss, target.repairTrack);
33766
+ if (!walked && groups === null) {
33204
33767
  close();
33205
33768
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
33206
33769
  return;
@@ -33211,8 +33774,10 @@ function wireRepairFetch(mc, opts) {
33211
33774
  packetId,
33212
33775
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
33213
33776
  };
33214
- const wire = wirePriority(identity, "unknown");
33215
- const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, performance.now(), "unknown");
33777
+ const wire = wirePriority(identity, holeClass);
33778
+ if (wire === null) return close();
33779
+ const now = performance.now();
33780
+ const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass);
33216
33781
  if (deadline === null) {
33217
33782
  close();
33218
33783
  mc.noteRepairSkipped(
@@ -33224,6 +33789,7 @@ function wireRepairFetch(mc, opts) {
33224
33789
  );
33225
33790
  return;
33226
33791
  }
33792
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
33227
33793
  const entry = {
33228
33794
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
33229
33795
  ...identity,
@@ -33231,15 +33797,16 @@ function wireRepairFetch(mc, opts) {
33231
33797
  blockProfile,
33232
33798
  priority,
33233
33799
  subscriberPriority: wire.subscriberPriority,
33234
- deadlineAt: deadline.deadlineAt,
33235
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33800
+ deadlineAt,
33801
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
33236
33802
  anchorAt: deadline.anchorAt,
33237
33803
  delayMs: deadline.delayMs,
33238
33804
  bound: deadline.bound,
33239
33805
  order: enqueueOrder++,
33240
33806
  attempts: 0,
33241
33807
  running: null,
33242
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33808
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33809
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
33243
33810
  };
33244
33811
  const previous = entries.get(entry.key);
33245
33812
  if (previous) {
@@ -33249,7 +33816,7 @@ function wireRepairFetch(mc, opts) {
33249
33816
  entries.set(entry.key, entry);
33250
33817
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
33251
33818
  dispatch();
33252
- });
33819
+ }, onHoleExpired);
33253
33820
  }
33254
33821
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
33255
33822
  queueMicrotask(() => {