@blockcast/mmt-container 2.2.0-main.a16efba1d85a → 2.2.0-main.a5b276c121d4

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 +1055 -151
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/delta-fallback-config.js.map +1 -1
  4. package/dist/fec-manager.d.ts +67 -11
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +377 -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 +48 -13
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +97 -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 +44 -8
  18. package/dist/mmtp-container-parser.d.ts.map +1 -1
  19. package/dist/mmtp-container-parser.js +122 -17
  20. package/dist/mmtp-container-parser.js.map +2 -2
  21. package/dist/mmtp-container-parser.no-modules.js +120 -18
  22. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  23. package/dist/mmtp-decode-pipeline.js +121 -17
  24. package/dist/mmtp-decode-pipeline.js.map +2 -2
  25. package/dist/mmtp-router.d.ts +28 -8
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +417 -54
  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 +876 -125
  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 +407 -28
  48. package/src/index.ts +2 -0
  49. package/src/iso-block-locator.ts +133 -31
  50. package/src/mmtp-container-parser.ts +214 -22
  51. package/src/mmtp-router.ts +502 -38
  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
@@ -12949,7 +12970,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
12949
12970
  #retentionBlocks;
12950
12971
  #heldEvicted;
12951
12972
  #newest;
12973
+ #newestSource;
12952
12974
  #holes;
12975
+ #expired;
12976
+ #newlyExpired;
12977
+ #closedBehind;
12953
12978
  static #checkedRetention(retentionBlocks) {
12954
12979
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
12955
12980
  throw new Error(
@@ -12986,6 +13011,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12986
13011
  observe(position) {
12987
13012
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12988
13013
  }
13014
+ /**
13015
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
13016
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
13017
+ * a source cannot run ahead of the sources sent before it except by
13018
+ * reordering.
13019
+ */
13020
+ get newestSource() {
13021
+ return this.#newestSource;
13022
+ }
13023
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13024
+ noteSource(ssId) {
13025
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13026
+ this.#newestSource = ssId;
13027
+ return true;
13028
+ }
12989
13029
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12990
13030
  get heldEvicted() {
12991
13031
  return this.#heldEvicted;
@@ -13004,17 +13044,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
13004
13044
  }
13005
13045
  /** The known block or span containing `ssId`, with its ESI. */
13006
13046
  locate(ssId) {
13007
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
13008
- const block = this.#blocks[i];
13009
- if (isoBlockContains(block.start, block.length, ssId)) {
13010
- 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
+ }
13011
13053
  }
13012
13054
  }
13013
13055
  return null;
13014
13056
  }
13015
- /** Number of pending holes. */
13057
+ /** Number of holes not closed yet, pending or past the horizon. */
13016
13058
  get holeCount() {
13017
- 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);
13018
13070
  }
13019
13071
  /** The stated block (never a span) ending exactly at `end`. */
13020
13072
  blockEndingAt(end) {
@@ -13070,9 +13122,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
13070
13122
  /**
13071
13123
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
13072
13124
  * block states its range and places every held source in it; later repair
13073
- * symbols of the same block change nothing. A range that contradicts a
13074
- * known block (a different length, or an overlap with another block or a
13075
- * 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.
13076
13130
  * A newly stated block retires behind its SS_Start (see the class note).
13077
13131
  * A block so far past `newest` that stating it would retire the position
13078
13132
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -13091,7 +13145,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
13091
13145
  const known = this.blockAt(start);
13092
13146
  if (known !== void 0) {
13093
13147
  if (known.span) {
13094
- 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}` };
13095
13158
  }
13096
13159
  if (known.length !== length) {
13097
13160
  return {
@@ -13159,18 +13222,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
13159
13222
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
13160
13223
  }
13161
13224
  /**
13162
- * Turn every pending hole inside `[start, start + length)` into a
13163
- * repair-less span, claiming its held sources: its range FETCH ended, so
13164
- * 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.
13165
13231
  */
13166
13232
  closeHolesWithin(start, length) {
13167
13233
  const closed = [];
13168
- for (const hole of [...this.#holes]) {
13169
- if (!isoBlockContains(start, length, hole.start)) continue;
13170
- this.#holes.splice(this.#holes.indexOf(hole), 1);
13171
- const spanBlock = { start: hole.start, length: hole.length, span: true };
13172
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
13173
- 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
+ }
13174
13247
  }
13175
13248
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
13176
13249
  }
@@ -13184,15 +13257,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
13184
13257
  return void 0;
13185
13258
  }
13186
13259
  /**
13187
- * Retire every block and span whose last SS_ID, and every held source,
13188
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
13189
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
13190
- * 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.
13191
13265
  */
13192
13266
  retireBehind(position) {
13193
13267
  this.observe(position);
13194
13268
  const horizon = this.#retentionBlocks;
13195
13269
  const retired = [];
13270
+ this.#closedBehind.length = 0;
13196
13271
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
13197
13272
  const block = this.#blocks[i];
13198
13273
  const last = serial32Add(block.start, block.length - 1);
@@ -13204,10 +13279,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
13204
13279
  for (let i = this.#holes.length - 1; i >= 0; i--) {
13205
13280
  const hole = this.#holes[i];
13206
13281
  const last = serial32Add(hole.start, hole.length - 1);
13207
- 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);
13208
13286
  }
13209
13287
  for (const ssId of [...this.#held.keys()]) {
13210
- 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
+ }
13211
13291
  }
13212
13292
  return retired;
13213
13293
  }
@@ -13215,9 +13295,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
13215
13295
  reset() {
13216
13296
  this.#blocks.length = 0;
13217
13297
  this.#holes.length = 0;
13298
+ this.#expired.length = 0;
13299
+ this.#newlyExpired.length = 0;
13300
+ this.#closedBehind.length = 0;
13218
13301
  this.#held.clear();
13219
13302
  this.#coverageEnd = void 0;
13220
13303
  this.#newest = void 0;
13304
+ this.#newestSource = void 0;
13221
13305
  }
13222
13306
  #claim(block) {
13223
13307
  const placed = [];
@@ -13837,6 +13921,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13837
13921
  var WORKER_ERROR_KIND_CAP = 32;
13838
13922
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13839
13923
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13924
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13840
13925
  function isMulticastSymbolDelivery(value) {
13841
13926
  return value === "datagram" || value === "ordered-relay";
13842
13927
  }
@@ -13930,10 +14015,18 @@ var FECManager = class _FECManager {
13930
14015
  // 'pending' forever, which would park every later frame of its stream in
13931
14016
  // the in-order release gate. Each entry owns its handle; the callback
13932
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
+ */
13933
14022
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
13934
14023
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13935
14024
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13936
14025
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
14026
+ // Source ESIs per block that another FEC instance rebuilt and fed in
14027
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
14028
+ // not network arrivals and never make a block clean.
14029
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13937
14030
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13938
14031
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13939
14032
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -13942,6 +14035,9 @@ var FECManager = class _FECManager {
13942
14035
  // SSB_length source arrivals and recoverable from SSB_length received
13943
14036
  // symbols; waiting for K fails every one of them.
13944
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();
13945
14041
  this.#ssbLengthConflictCount = 0;
13946
14042
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
13947
14043
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -14139,7 +14235,10 @@ var FECManager = class _FECManager {
14139
14235
  // arrives via onBlockRecovered callback.
14140
14236
  this.onBlockStarted = null;
14141
14237
  this.onBlockComplete = null;
14142
- /** 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
+ */
14143
14242
  this.onBlockFailed = null;
14144
14243
  /**
14145
14244
  * Fired once per stream when its Worker decoder stops being routable.
@@ -14181,6 +14280,8 @@ var FECManager = class _FECManager {
14181
14280
  * for it, which it must be.
14182
14281
  */
14183
14282
  this.#onUndelimitedLoss = null;
14283
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
14284
+ this.#onIsoHoleExpired = null;
14184
14285
  /** A block every symbol of which vanished was created by SBN evidence. */
14185
14286
  this.onBlockVanished = null;
14186
14287
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -14223,7 +14324,9 @@ var FECManager = class _FECManager {
14223
14324
  #pendingWorkerCleanupBounds;
14224
14325
  #blockSourceCounts;
14225
14326
  #blockSourceEsis;
14327
+ #blockRebuiltEsis;
14226
14328
  #blockAuthoredLengths;
14329
+ #isoSpanKeys;
14227
14330
  #ssbLengthConflictCount;
14228
14331
  #blockRepairRsIds;
14229
14332
  #blockRsbLengths;
@@ -14290,6 +14393,7 @@ var FECManager = class _FECManager {
14290
14393
  #decoderUnavailableEvents;
14291
14394
  #repairSymbolSizeMismatchCount;
14292
14395
  #onUndelimitedLoss;
14396
+ #onIsoHoleExpired;
14293
14397
  #encodeTrackName(catalogTrackName) {
14294
14398
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
14295
14399
  if (encoded === void 0) {
@@ -14761,7 +14865,10 @@ var FECManager = class _FECManager {
14761
14865
  timer.handle = null;
14762
14866
  this.#requestRepair(sbn, packetId, catalogTrackName);
14763
14867
  };
14764
- 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
+ );
14765
14872
  this.#repairDeadlines.set(key, timer);
14766
14873
  }
14767
14874
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -14905,8 +15012,14 @@ var FECManager = class _FECManager {
14905
15012
  * Vanished blocks are not enumerated here: their starts are unknown until a
14906
15013
  * repair states them. The SS_IDs between two stated blocks become one
14907
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).
14908
15021
  */
14909
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
15022
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
14910
15023
  const streamKey = this.#streamKey(packetId, catalogTrackName);
14911
15024
  const now = performance.now();
14912
15025
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -14945,7 +15058,7 @@ var FECManager = class _FECManager {
14945
15058
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
14946
15059
  };
14947
15060
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
14948
- 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))) {
14949
15062
  discontinuity();
14950
15063
  return;
14951
15064
  }
@@ -15207,17 +15320,21 @@ var FECManager = class _FECManager {
15207
15320
  this.#blockWaitDeadlinesMs.delete(key);
15208
15321
  this.#repairHoldUntilMs.delete(key);
15209
15322
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
15210
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
15323
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
15211
15324
  };
15212
- 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
+ );
15213
15329
  this.#activeBlocks.set(key, timer);
15214
15330
  this.#indexBlockKey(key, sbn, packetId);
15215
15331
  }
15216
15332
  /**
15217
15333
  * Hand a block whose delivery is over to its decoder for finalization: the
15218
- * 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.
15219
15336
  */
15220
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
15337
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
15221
15338
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
15222
15339
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
15223
15340
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -15233,7 +15350,7 @@ var FECManager = class _FECManager {
15233
15350
  if (workerClient) {
15234
15351
  this.#pendingWorkerCleanups.add(key);
15235
15352
  this.#pendingWorkerCleanupClients.set(key, workerClient);
15236
- this.#boundOwedCleanup(key, packetId, workerClient);
15353
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
15237
15354
  workerClient.cleanup(sbn);
15238
15355
  } else {
15239
15356
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -15545,7 +15662,9 @@ var FECManager = class _FECManager {
15545
15662
  }
15546
15663
  resetMap(this.#blockSourceCounts);
15547
15664
  resetMap(this.#blockSourceEsis);
15665
+ resetMap(this.#blockRebuiltEsis);
15548
15666
  resetMap(this.#blockAuthoredLengths);
15667
+ resetSet(this.#isoSpanKeys);
15549
15668
  resetMap(this.#blockRepairRsIds);
15550
15669
  resetMap(this.#blockRsbLengths);
15551
15670
  resetMap(this.#heldLayerStates);
@@ -15669,7 +15788,9 @@ var FECManager = class _FECManager {
15669
15788
  dropMap(this.#pendingWorkerCleanupClients);
15670
15789
  dropMap(this.#blockSourceCounts);
15671
15790
  dropMap(this.#blockSourceEsis);
15791
+ dropMap(this.#blockRebuiltEsis);
15672
15792
  dropMap(this.#blockAuthoredLengths);
15793
+ dropSet(this.#isoSpanKeys);
15673
15794
  dropMap(this.#blockRepairRsIds);
15674
15795
  dropMap(this.#blockRsbLengths);
15675
15796
  dropMap(this.#blockPathCounts);
@@ -15831,6 +15952,9 @@ var FECManager = class _FECManager {
15831
15952
  this.#unindexBlockKey(key);
15832
15953
  }
15833
15954
  }
15955
+ if (isSourceSymbol && source === "multicast") {
15956
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15957
+ }
15834
15958
  return true;
15835
15959
  }
15836
15960
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16009,8 +16133,9 @@ var FECManager = class _FECManager {
16009
16133
  return true;
16010
16134
  }
16011
16135
  /**
16012
- * Drop a block's repair hold: its FETCH ended without delivering. The block
16013
- * 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.
16014
16139
  */
16015
16140
  releaseRepairHold(sbn, packetId, catalogTrackName) {
16016
16141
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -16148,6 +16273,8 @@ var FECManager = class _FECManager {
16148
16273
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16149
16274
  const evictedBefore = locator.heldEvicted;
16150
16275
  const held = locator.hold(ssId, source);
16276
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
16277
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16151
16278
  const evicted = locator.heldEvicted - evictedBefore;
16152
16279
  if (evicted > 0) {
16153
16280
  const before = this.#isoHeldEvictedCount;
@@ -16168,17 +16295,20 @@ var FECManager = class _FECManager {
16168
16295
  * contradicts a known block is refused (counted) and must be dropped.
16169
16296
  */
16170
16297
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
16171
- 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);
16172
16301
  if (placement.status !== "placed") {
16173
16302
  this.#isoRepairRefusedCount++;
16174
16303
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
16175
16304
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
16176
16305
  }
16306
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
16177
16307
  return placement;
16178
16308
  }
16309
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
16179
16310
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
16180
16311
  if (placement.hole !== void 0) {
16181
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
16182
16312
  const hole = placement.hole.block;
16183
16313
  const previous = locator.blockEndingAt(hole.start);
16184
16314
  const loss = {
@@ -16199,17 +16329,29 @@ var FECManager = class _FECManager {
16199
16329
  * `handler`, instead of a repair-less span; null restores spans. Set by the
16200
16330
  * range repair scheduler; applies to every stream from its next gap. The
16201
16331
  * handler and the deferral are one setting, so no hole is ever created with
16202
- * 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.
16203
16336
  */
16204
- setUndelimitedLossHandler(handler) {
16337
+ setUndelimitedLossHandler(handler, onExpired = null) {
16205
16338
  this.#onUndelimitedLoss = handler;
16339
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
16206
16340
  this.#deferIsoGaps = handler !== null;
16207
16341
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
16208
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
+ }
16209
16350
  /**
16210
- * The range repair for a hole ended: every hole still pending inside
16211
- * `[start, start + length)` becomes a repair-less span. Returns those spans
16212
- * 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).
16213
16355
  */
16214
16356
  closeIsoHoles(packetId, catalogTrackName, start, length) {
16215
16357
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -16228,7 +16370,10 @@ var FECManager = class _FECManager {
16228
16370
  */
16229
16371
  #openIsoSpan(span, packetId, catalogTrackName) {
16230
16372
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
16231
- 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
+ }
16232
16377
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
16233
16378
  }
16234
16379
  /**
@@ -16569,11 +16714,14 @@ var FECManager = class _FECManager {
16569
16714
  });
16570
16715
  client.onCleanupComplete((sbn) => {
16571
16716
  if (this.#releasedWorkerClients.has(client)) return;
16572
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
16717
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
16718
+ const cleanupKeys = owed.filter((key) => {
16573
16719
  const block = this.#parseBlockKey(key);
16574
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
16720
+ return block.packetId === packetId && block.sbn === sbn;
16575
16721
  });
16576
- 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");
16577
16725
  });
16578
16726
  client.onError((msg, code) => {
16579
16727
  const requestedKind = code ?? msg;
@@ -16643,9 +16791,11 @@ var FECManager = class _FECManager {
16643
16791
  * fail deadline already passed, so failing it is the decision that deadline
16644
16792
  * made. A late ack then finds nothing owed and is a no-op.
16645
16793
  */
16646
- #boundOwedCleanup(key, packetId, client) {
16794
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
16647
16795
  this.#clearOwedCleanupBound(key);
16648
16796
  const bound = {
16797
+ dueAtMs,
16798
+ askedAtMs: performance.now(),
16649
16799
  handle: setTimeout(() => {
16650
16800
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
16651
16801
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -16743,7 +16893,15 @@ var FECManager = class _FECManager {
16743
16893
  this.#isoSourceOversizeCount++;
16744
16894
  return null;
16745
16895
  }
16746
- const sbn = located.block.start;
16896
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16897
+ }
16898
+ /**
16899
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16900
+ * answers through onFrame (null here), or its synchronous decoder, whose
16901
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16902
+ * stated for `ssId`.
16903
+ */
16904
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16747
16905
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16748
16906
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16749
16907
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16772,6 +16930,74 @@ var FECManager = class _FECManager {
16772
16930
  }
16773
16931
  return recovered;
16774
16932
  }
16933
+ /**
16934
+ * Feed a source symbol of this stream that another FEC instance over the
16935
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16936
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16937
+ * BLO-52085). It joins its block's decoder as a received source would, so
16938
+ * it reduces the erasures the block still has to solve, but it is not a
16939
+ * network arrival: no transport accounting, no classification evidence,
16940
+ * no deadline refresh, and it never makes a block clean.
16941
+ *
16942
+ * Only a block a repair stated takes one (a repair-less span has no
16943
+ * decoder block). A block that is terminal, or already holds that source,
16944
+ * does not. The symbol is the exact protected packet, unpadded, as
16945
+ * feedSourceSymbol takes it.
16946
+ *
16947
+ * `recovered` is the block the synchronous decoder completed with it; the
16948
+ * caller routes it and completes the block, as for any recovery. When
16949
+ * every source of the block is then present, received or rebuilt, and no
16950
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16951
+ * block as clean on its own side), the block is completed here as
16952
+ * recovered, with nothing left to route.
16953
+ */
16954
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16955
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16956
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16957
+ if (located === null || located.block.span) return NOT_TAKEN;
16958
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16959
+ const sbn = located.block.start;
16960
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16961
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16962
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16963
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16964
+ if (rebuilt === void 0) {
16965
+ rebuilt = /* @__PURE__ */ new Set();
16966
+ this.#blockRebuiltEsis.set(key, rebuilt);
16967
+ }
16968
+ const config = this.#trackConfigs.get(packetId);
16969
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16970
+ rebuilt.add(located.esi);
16971
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16972
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16973
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16974
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16975
+ }
16976
+ }
16977
+ return { taken: true, recovered };
16978
+ }
16979
+ /**
16980
+ * A block that holds every one of its `length` sources, received or
16981
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16982
+ * every packet of it was delivered, by its own arrival or by the instance
16983
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16984
+ */
16985
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16986
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16987
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16988
+ let present = rebuilt.size;
16989
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16990
+ if (present < length) return false;
16991
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16992
+ this.countRecovery(packetId, sbn, catalogTrackName);
16993
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16994
+ if (catalogTrackName === void 0) {
16995
+ this.onBlockComplete?.(sbn, packetId);
16996
+ } else {
16997
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16998
+ }
16999
+ return true;
17000
+ }
16775
17001
  /**
16776
17002
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16777
17003
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16974,8 +17200,75 @@ var FECManager = class _FECManager {
16974
17200
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16975
17201
  return null;
16976
17202
  }
17203
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16977
17204
  return recovered;
16978
17205
  }
17206
+ /**
17207
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17208
+ * its sources in send order, so one that moves the stream's newest source
17209
+ * position past a block leaves the sources that block still misses
17210
+ * reordered behind it at best: it shortens the wait of every block it
17211
+ * passed that only a late source can complete. MoQ is left out: its streams
17212
+ * can overtake one another, so a MoQ source shows nothing about the sources
17213
+ * sent before it.
17214
+ */
17215
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17216
+ const previous = locator.newestSource;
17217
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17218
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17219
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17220
+ this.#shortenUnrecoverableWait(
17221
+ this.#blockKey(block.start, packetId, catalogTrackName),
17222
+ block.start,
17223
+ packetId,
17224
+ catalogTrackName
17225
+ );
17226
+ }
17227
+ }
17228
+ /**
17229
+ * The wait of a block that only a late source can complete (BLO-52085): a
17230
+ * repair stated it, its received sources plus every symbol its repair group
17231
+ * carries (RSB_length) are short of K' (the repair scheduler's
17232
+ * group-cannot-complete), and a later source of the stream has arrived, so
17233
+ * a source it misses is lost or reordered behind that one. The span, which
17234
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17235
+ * the late-symbol allowance alone, the multicast wait less the span (the
17236
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17237
+ * block.
17238
+ */
17239
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17240
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17241
+ const rsbLength = this.#blockRsbLengths.get(key);
17242
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17243
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17244
+ const received = this.#blockSourceEsis.get(key);
17245
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17246
+ if (present + rsbLength >= authoredLength) return void 0;
17247
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17248
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17249
+ return void 0;
17250
+ }
17251
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17252
+ }
17253
+ /**
17254
+ * Bring an open block's wait in to its late-source wait, when it has one.
17255
+ * Never later than it is armed: only a source can complete such a block,
17256
+ * so a repair of its group (paced across the span) must not keep it open.
17257
+ */
17258
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17259
+ if (!this.#activeBlocks.has(key)) return;
17260
+ const config = this.#trackConfigs.get(packetId);
17261
+ if (config === void 0) return;
17262
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17263
+ if (waitMs === void 0) return;
17264
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17265
+ this.#armBlockTimeout(
17266
+ sbn,
17267
+ packetId,
17268
+ catalogTrackName,
17269
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17270
+ );
17271
+ }
16979
17272
  /**
16980
17273
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16981
17274
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17073,12 +17366,13 @@ var FECManager = class _FECManager {
17073
17366
  * the inactivity deadline on each symbol, records the symbol's in-band
17074
17367
  * classification evidence, and marks the block clean when all of its
17075
17368
  * authored source symbols arrived: the SSB_length a repair symbol declared,
17076
- * otherwise K.
17369
+ * otherwise K. `held` marks a source held unplaced since it arrived and
17370
+ * placed only now (see #noteSbnSeen).
17077
17371
  *
17078
17372
  * Throws for a packetId that was never configured, so a classified block
17079
17373
  * never reads the manager-global K or timeout.
17080
17374
  */
17081
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
17375
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
17082
17376
  const trackConfig = this.#trackConfigs.get(packetId);
17083
17377
  if (trackConfig === void 0) {
17084
17378
  throw new Error(
@@ -17094,7 +17388,8 @@ var FECManager = class _FECManager {
17094
17388
  catalogTrackName,
17095
17389
  "source",
17096
17390
  evidence.packetSequenceNumber,
17097
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
17391
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
17392
+ held
17098
17393
  );
17099
17394
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
17100
17395
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -17130,6 +17425,15 @@ var FECManager = class _FECManager {
17130
17425
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17131
17426
  return;
17132
17427
  }
17428
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17429
+ key,
17430
+ sbn,
17431
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17432
+ packetId,
17433
+ catalogTrackName
17434
+ )) {
17435
+ return;
17436
+ }
17133
17437
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17134
17438
  }
17135
17439
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17138,7 +17442,9 @@ var FECManager = class _FECManager {
17138
17442
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17139
17443
  this.#blockSourceCounts.delete(key);
17140
17444
  this.#blockSourceEsis.delete(key);
17445
+ this.#blockRebuiltEsis.delete(key);
17141
17446
  this.#blockAuthoredLengths.delete(key);
17447
+ this.#isoSpanKeys.delete(key);
17142
17448
  this.#blockRepairRsIds.delete(key);
17143
17449
  this.#blockRsbLengths.delete(key);
17144
17450
  this.#markTerminal(key, sbn, packetId, true);
@@ -17169,6 +17475,19 @@ var FECManager = class _FECManager {
17169
17475
  #receiptNow() {
17170
17476
  return this.#receiptAtMs ?? performance.now();
17171
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
+ }
17172
17491
  /**
17173
17492
  * A held packet admitted late arms from its (earlier) receipt time; that
17174
17493
  * never pulls in a deadline later activity already armed. Outside a
@@ -17204,10 +17523,20 @@ var FECManager = class _FECManager {
17204
17523
  this.#repairDeadlineMs(trackConfig)
17205
17524
  );
17206
17525
  }
17207
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17526
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17527
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17528
+ return;
17529
+ }
17530
+ this.#armBlockTimeout(
17531
+ sbn,
17532
+ packetId,
17533
+ catalogTrackName,
17534
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17535
+ );
17208
17536
  }
17209
17537
  #failBlock(sbn, packetId, catalogTrackName) {
17210
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;
17211
17540
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
17212
17541
  this.#clearRepairDeadline(key);
17213
17542
  this.#clearBlockTimer(key);
@@ -17217,6 +17546,7 @@ var FECManager = class _FECManager {
17217
17546
  }
17218
17547
  this.#blockSourceCounts.delete(key);
17219
17548
  this.#blockSourceEsis.delete(key);
17549
+ this.#blockRebuiltEsis.delete(key);
17220
17550
  this.#blockAuthoredLengths.delete(key);
17221
17551
  this.#blockRepairRsIds.delete(key);
17222
17552
  this.#blockRsbLengths.delete(key);
@@ -17226,7 +17556,8 @@ var FECManager = class _FECManager {
17226
17556
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
17227
17557
  this.#fecStats.blocksFailed++;
17228
17558
  this.#statsForPacket(packetId).blocksFailed++;
17229
- 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);
17230
17561
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
17231
17562
  }
17232
17563
  /**
@@ -17255,7 +17586,9 @@ var FECManager = class _FECManager {
17255
17586
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17256
17587
  this.#blockSourceCounts.delete(key);
17257
17588
  this.#blockSourceEsis.delete(key);
17589
+ this.#blockRebuiltEsis.delete(key);
17258
17590
  this.#blockAuthoredLengths.delete(key);
17591
+ this.#isoSpanKeys.delete(key);
17259
17592
  this.#blockRepairRsIds.delete(key);
17260
17593
  this.#blockRsbLengths.delete(key);
17261
17594
  this.#clearBlockTimer(key);
@@ -17346,7 +17679,9 @@ var FECManager = class _FECManager {
17346
17679
  this.#blockKeysBySbn.clear();
17347
17680
  this.#blockSourceCounts.clear();
17348
17681
  this.#blockSourceEsis.clear();
17682
+ this.#blockRebuiltEsis.clear();
17349
17683
  this.#blockAuthoredLengths.clear();
17684
+ this.#isoSpanKeys.clear();
17350
17685
  this.#blockRepairRsIds.clear();
17351
17686
  this.#blockRsbLengths.clear();
17352
17687
  this.#heldLayerStates.clear();
@@ -18248,6 +18583,9 @@ function authoredGap(before, after) {
18248
18583
  function ssIdAfter(a, b) {
18249
18584
  return (a - b | 0) > 0;
18250
18585
  }
18586
+ function holdsRebuilt(stream) {
18587
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18588
+ }
18251
18589
  var FecReleaseSequencer = class {
18252
18590
  #streams = /* @__PURE__ */ new Map();
18253
18591
  #gate;
@@ -18310,6 +18648,7 @@ var FecReleaseSequencer = class {
18310
18648
  stream.lastSbn = void 0;
18311
18649
  stream.discontinuity = false;
18312
18650
  stream.rapSsId = void 0;
18651
+ stream.sourced = false;
18313
18652
  }
18314
18653
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18315
18654
  this.#recordStat("droppedStale");
@@ -18336,6 +18675,7 @@ var FecReleaseSequencer = class {
18336
18675
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18337
18676
  else hi = mid;
18338
18677
  }
18678
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18339
18679
  stream.buf.splice(lo, 0, frame);
18340
18680
  this.#drainUnlessDeferred(stream);
18341
18681
  }
@@ -18371,6 +18711,41 @@ var FecReleaseSequencer = class {
18371
18711
  }
18372
18712
  return false;
18373
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
+ }
18374
18749
  /** Re-evaluate held gaps after FEC state changed. */
18375
18750
  poke(streamKey) {
18376
18751
  if (streamKey !== void 0) {
@@ -18462,16 +18837,32 @@ var FecReleaseSequencer = class {
18462
18837
  last: null,
18463
18838
  lastSbn: void 0,
18464
18839
  discontinuity: false,
18465
- rapSsId: void 0
18840
+ rapSsId: void 0,
18841
+ sourced: false,
18842
+ draining: 0
18466
18843
  };
18467
18844
  this.#streams.set(streamKey, stream);
18468
18845
  }
18469
18846
  return stream;
18470
18847
  }
18471
- #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) {
18472
18858
  while (stream.buf.length > 0 && stream.last !== null) {
18473
18859
  const head = stream.buf[0];
18474
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
+ }
18475
18866
  if (stream.discontinuity) {
18476
18867
  stream.discontinuity = false;
18477
18868
  stream.buf.shift();
@@ -18485,9 +18876,10 @@ var FecReleaseSequencer = class {
18485
18876
  }
18486
18877
  const headSbn = head.meta.fecSbn;
18487
18878
  const from = stream.lastSbn ?? headSbn;
18488
- const to = headSbn ?? stream.lastSbn;
18879
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18489
18880
  let lost = [];
18490
18881
  const unanchored = from === void 0 && to === void 0;
18882
+ let unresolved = false;
18491
18883
  if (from !== void 0 && to !== void 0) {
18492
18884
  const resolved = this.#gate.resolvedLoss(
18493
18885
  stream.packetId,
@@ -18496,7 +18888,8 @@ var FecReleaseSequencer = class {
18496
18888
  Math.max(from, to),
18497
18889
  from
18498
18890
  );
18499
- if (resolved === null) {
18891
+ unresolved = resolved === null;
18892
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18500
18893
  const heldMs = performance.now() - head.arrivedAtMs;
18501
18894
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18502
18895
  this.#recordStat("forcedWindowFlushes");
@@ -18504,22 +18897,56 @@ var FecReleaseSequencer = class {
18504
18897
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18505
18898
  continue;
18506
18899
  }
18507
- lost = resolved;
18900
+ lost = resolved ?? [];
18508
18901
  }
18509
18902
  stream.buf.shift();
18510
18903
  this.#release(
18511
18904
  stream,
18512
18905
  head,
18513
- 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
18514
18907
  );
18515
18908
  }
18516
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
+ }
18517
18939
  #release(stream, frame, decision) {
18940
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18941
+ if (stale) this.#recordStat("droppedStale");
18518
18942
  if (decision) frame.meta.gapBefore = decision;
18519
18943
  stream.last = frame;
18520
- 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;
18521
18948
  this.#gate.onReleased?.(frame.data, frame.meta);
18522
- this.#emit(frame.data, frame.meta);
18949
+ if (!stale) this.#emit(frame.data, frame.meta);
18523
18950
  }
18524
18951
  };
18525
18952
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18780,11 +19207,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18780
19207
  /**
18781
19208
  * Typed view of the ISO-BMFF timing-sample budget.
18782
19209
  *
18783
- * This is the acceptance signal for the budget ratchet: `pending` walking
18784
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18785
- * and it is observable here rather than only as media silently stopping.
18786
- * Per repo policy this is a producer-owned counter -- never infer budget
18787
- * 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.
18788
19216
  */
18789
19217
  get timingBudgetStats() {
18790
19218
  return {
@@ -18844,6 +19272,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18844
19272
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18845
19273
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18846
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
+ }
18847
19283
  /** Re-evaluate held gaps after FEC block state changed. */
18848
19284
  pokeFecRelease(packetId, catalogTrackName) {
18849
19285
  if (!this.#fecSequencer) return;
@@ -19097,7 +19533,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19097
19533
  if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19098
19534
  const pendingBefore = resolver2.pendingSampleCount;
19099
19535
  const evictedBefore = resolver2.evictedSampleCount;
19100
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19101
19537
  const admitted = resolver2.addMovieFragment(
19102
19538
  mpu.mpuSequenceNumber,
19103
19539
  data,
@@ -19170,7 +19606,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19170
19606
  info,
19171
19607
  resolver,
19172
19608
  options?.recovered === true,
19173
- options?.fecSbn
19609
+ options?.fecSbn,
19610
+ options?.fecSbnFirst
19174
19611
  );
19175
19612
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19176
19613
  }
@@ -19263,7 +19700,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19263
19700
  info,
19264
19701
  resolver,
19265
19702
  options?.recovered === true,
19266
- options?.fecSbn
19703
+ options?.fecSbn,
19704
+ options?.fecSbnFirst
19267
19705
  );
19268
19706
  if (outcome.record !== void 0) return outcome.record;
19269
19707
  return this.#drop(outcome.reason);
@@ -19283,7 +19721,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19283
19721
  * attribution exists to keep readable. Leaving the choice at the call site
19284
19722
  * is what keeps both true.
19285
19723
  */
19286
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19724
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19287
19725
  const timing = resolver.sampleTiming(
19288
19726
  combined.mpuSequenceNumber,
19289
19727
  combined.movieFragmentSequenceNumber,
@@ -19300,6 +19738,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19300
19738
  );
19301
19739
  if (!meta) return { reason: "combined-meta-build-failed" };
19302
19740
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19741
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19303
19742
  if (!resolver.consumeSample(
19304
19743
  combined.mpuSequenceNumber,
19305
19744
  combined.movieFragmentSequenceNumber,
@@ -22805,7 +23244,18 @@ function keyframeOverlaySsId(header) {
22805
23244
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22806
23245
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22807
23246
  }
23247
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
23248
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
23249
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
23250
+ }
23251
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23252
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23253
+ return headerPsn + offset >>> 0;
23254
+ }
23255
+ var OVERLAY_ROUTED_LIMIT = 1024;
23256
+ var OVERLAY_PENDING_LIMIT = 256;
22808
23257
  var OVERLAY_OFFSET_MPUS = 16;
23258
+ var OVERLAY_REBUILT_LIMIT = 256;
22809
23259
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22810
23260
  function assertStreamIdentity(role, stream) {
22811
23261
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -22916,6 +23366,7 @@ var MmtpRouter = class {
22916
23366
  #provenance = /* @__PURE__ */ new Map();
22917
23367
  #onFlowProtected = null;
22918
23368
  #unadmittedListedDrops = 0;
23369
+ #admittedSourceDatagrams = 0;
22919
23370
  #unlistedRepairSetAside = 0;
22920
23371
  #assistBlocks = /* @__PURE__ */ new Map();
22921
23372
  #assistFailures = /* @__PURE__ */ new Map();
@@ -22972,6 +23423,26 @@ var MmtpRouter = class {
22972
23423
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22973
23424
  */
22974
23425
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23426
+ /**
23427
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23428
+ * sender's own packet_sequence_number: a recovered packet's header psn
23429
+ * equal to its SS_ID minus that offset. While it equals the stream's
23430
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23431
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23432
+ * an MPU no member of which arrived).
23433
+ */
23434
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23435
+ /**
23436
+ * The same, shown by the overlay alone, before any base recovery did: a
23437
+ * packet it rebuilt whose base copy carries its protected psn
23438
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
23439
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
23440
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
23441
+ * make the base drop a late base copy of a keyframe the overlay never
23442
+ * finished, and lift a block's late-source cut, which nothing before
23443
+ * this offset did.
23444
+ */
23445
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22975
23446
  /**
22976
23447
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22977
23448
  * not parsed, typically because its overlap copy was parsed first
@@ -23041,7 +23512,9 @@ var MmtpRouter = class {
23041
23512
  recovered: 0,
23042
23513
  restarts: 0,
23043
23514
  unsequencedDropped: 0,
23044
- protectedDropped: 0
23515
+ protectedDropped: 0,
23516
+ baseFed: 0,
23517
+ baseUndetermined: 0
23045
23518
  };
23046
23519
  /** Callback fired when a delta-track FEC block completes or fails. */
23047
23520
  #onDeltaBlockCompleteCb = null;
@@ -23214,7 +23687,7 @@ var MmtpRouter = class {
23214
23687
  const cb = this.#getFrameCallback(packetId);
23215
23688
  if (isVideo && !assembler) {
23216
23689
  if (frameMeta.recovered) {
23217
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23690
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23218
23691
  } else {
23219
23692
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23220
23693
  }
@@ -23247,13 +23720,18 @@ var MmtpRouter = class {
23247
23720
  this.#onDeltaBlockCompleteCb?.(packetId, false);
23248
23721
  }
23249
23722
  };
23250
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
23723
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
23251
23724
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
23252
23725
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
23253
23726
  this.#onDeltaBlockCompleteCb?.(packetId, true);
23254
23727
  }
23255
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
23256
- 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
+ }
23257
23735
  };
23258
23736
  }
23259
23737
  }
@@ -23298,8 +23776,9 @@ var MmtpRouter = class {
23298
23776
  }
23299
23777
  /**
23300
23778
  * FEC-recovered source symbols whose reconstructed header named a
23301
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23302
- * 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.
23303
23782
  */
23304
23783
  get restampedRecoveredSequenceCount() {
23305
23784
  return this.#restampedRecoveredSeqs;
@@ -23478,6 +23957,7 @@ var MmtpRouter = class {
23478
23957
  enableMseAssembler(packetId, config, catalogTrackName) {
23479
23958
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23480
23959
  const previous = this.#mseAssemblers.get(assemblerKey);
23960
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23481
23961
  if (previous) {
23482
23962
  previous.flush();
23483
23963
  previous.dispose();
@@ -23628,7 +24108,8 @@ var MmtpRouter = class {
23628
24108
  fec,
23629
24109
  k,
23630
24110
  offsets: /* @__PURE__ */ new Map(),
23631
- moqBlocks: /* @__PURE__ */ new Set()
24111
+ moqBlocks: /* @__PURE__ */ new Set(),
24112
+ rebuilt: /* @__PURE__ */ new Map()
23632
24113
  };
23633
24114
  this.#overlays.set(repairPacketId, inst);
23634
24115
  return () => {
@@ -23658,12 +24139,18 @@ var MmtpRouter = class {
23658
24139
  */
23659
24140
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23660
24141
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
24142
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
24143
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
24144
+ stream.overlayToBase.routed.clear();
24145
+ stream.overlayToBase.pending.clear();
24146
+ }
23661
24147
  }
23662
24148
  #resetOverlay(inst) {
23663
24149
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23664
24150
  inst.last = void 0;
23665
24151
  inst.offsets.clear();
23666
24152
  inst.moqBlocks.clear();
24153
+ inst.rebuilt.clear();
23667
24154
  }
23668
24155
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23669
24156
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23714,18 +24201,33 @@ var MmtpRouter = class {
23714
24201
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23715
24202
  * every overlay installed for its source, at that SS_ID, with the bytes the
23716
24203
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23717
- * included (7.1, 7.4.3).
23718
- */
23719
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
24204
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
24205
+ * FEC reads it.
24206
+ *
24207
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24208
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24209
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24210
+ * the stream unstamped at the SS_ID offset it states, for the release gate
24211
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
24212
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
24213
+ */
24214
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23720
24215
  if (this.#overlays.size === 0) return;
23721
24216
  const ssId = keyframeOverlaySsId(header);
23722
24217
  if (ssId === void 0) return;
24218
+ let rebuiltPsn;
23723
24219
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23724
24220
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24221
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24222
+ inst.rebuilt.delete(ssId);
23725
24223
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23726
24224
  if (identity !== null) {
23727
24225
  inst.offsets.delete(identity.mpuSequenceNumber);
23728
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
24226
+ inst.offsets.set(identity.mpuSequenceNumber, {
24227
+ psnOffset: ssId - sequenceNumber >>> 0,
24228
+ overlaySsId: ssId,
24229
+ ...baseSsId === void 0 ? {} : { baseSsId }
24230
+ });
23729
24231
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23730
24232
  }
23731
24233
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23747,6 +24249,14 @@ var MmtpRouter = class {
23747
24249
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23748
24250
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23749
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
+ }
23750
24260
  }
23751
24261
  #noteOverlayMoqBlock(inst, blockStart) {
23752
24262
  inst.moqBlocks.add(blockStart);
@@ -23780,11 +24290,16 @@ var MmtpRouter = class {
23780
24290
  }
23781
24291
  /**
23782
24292
  * Route an overlay-recovered block's packets as source media of the
23783
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23784
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23785
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23786
- * from its keyframe's own offset; without one, a stream the base never
23787
- * 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.
23788
24303
  */
23789
24304
  #routeOverlayRecovered(inst, blockStart, recovered) {
23790
24305
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23804,22 +24319,139 @@ var MmtpRouter = class {
23804
24319
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23805
24320
  const restamped = this.#restampedStreams.has(streamKey);
23806
24321
  const restored = /* @__PURE__ */ new Set();
24322
+ const baseSsIds = /* @__PURE__ */ new Map();
24323
+ const gateOnly = /* @__PURE__ */ new Set();
23807
24324
  const symbols = result.symbols.flatMap((sym) => {
23808
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23809
- if (offset !== void 0) {
23810
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24325
+ const overlaySsId = serial32Add(blockStart, sym.index);
24326
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24327
+ if (anchor !== void 0) {
24328
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23811
24329
  restored.add(packetSequenceNumber);
23812
- return [{ ...sym, packetSequenceNumber }];
24330
+ const out = { ...sym, packetSequenceNumber };
24331
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24332
+ if (baseSsId !== void 0) {
24333
+ baseSsIds.set(sym.index, baseSsId);
24334
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24335
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
24336
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24337
+ }
24338
+ }
24339
+ return [out];
24340
+ }
24341
+ if (!restamped) {
24342
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24343
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24344
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24345
+ if (baseSsId !== void 0) {
24346
+ baseSsIds.set(sym.index, baseSsId);
24347
+ if (fedSsId === void 0) gateOnly.add(sym.index);
24348
+ } else {
24349
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24350
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24351
+ }
24352
+ return [sym];
23813
24353
  }
23814
- if (!restamped) return [sym];
23815
24354
  this.#overlayStats.unsequencedDropped++;
23816
24355
  return [];
23817
24356
  });
23818
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
+ }
23819
24369
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23820
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24370
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24371
+ baseSsIds,
24372
+ firstSsIds
23821
24373
  });
23822
24374
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24375
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24376
+ }
24377
+ /**
24378
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24379
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24380
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24381
+ * base recovery showed; `baseSsIds` holds no other),
24382
+ * so it is fed to the base decoder as a rebuilt source
24383
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24384
+ * fewer to solve, and its repair can recover the block's other packets.
24385
+ * A base block no repair has stated yet takes it when one does
24386
+ * (#feedPendingRebuilt).
24387
+ *
24388
+ * The overlay route already handled each packet fed (delivered it, or
24389
+ * held an untimed head for its FT=1), so a base recovery routes none of
24390
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24391
+ * held head would wait in the parser for timing its first copy consumed.
24392
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24393
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24394
+ * that crossed no multicast socket, which the provenance counted from the
24395
+ * block's own arrivals cannot show.
24396
+ */
24397
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24398
+ const fec = this.#fecManager;
24399
+ const owner = inst.leg.sourcePacketId;
24400
+ const name = inst.leg.sourceCatalogTrackName;
24401
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24402
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24403
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24404
+ const feeds = [];
24405
+ for (const sym of symbols) {
24406
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24407
+ const ssId = baseSsIds.get(sym.index);
24408
+ if (ssId === void 0) {
24409
+ this.#overlayStats.baseUndetermined++;
24410
+ continue;
24411
+ }
24412
+ routed.delete(ssId);
24413
+ routed.add(ssId);
24414
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24415
+ feeds.push([ssId, sym.rawSymbol]);
24416
+ }
24417
+ for (const [ssId, symbolData] of feeds) {
24418
+ if (this.#classifyPacketId(owner) === null) return;
24419
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24420
+ }
24421
+ }
24422
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24423
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24424
+ const fec = this.#fecManager;
24425
+ if (!fec?.feedRebuiltSourceSymbol) return;
24426
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24427
+ if (coords == null) {
24428
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24429
+ pending.delete(ssId);
24430
+ pending.set(ssId, symbolData.slice());
24431
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24432
+ return;
24433
+ }
24434
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24435
+ if (!fed.taken) return;
24436
+ this.#overlayStats.baseFed++;
24437
+ if (fed.recovered === null) return;
24438
+ const sbn = coords.sbn;
24439
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24440
+ fec.cancelBlockTimeout(sbn, owner, name);
24441
+ fec.countRecovery(owner, sbn, name);
24442
+ fec.markBlockCompleted(sbn, owner, name);
24443
+ });
24444
+ }
24445
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24446
+ #feedPendingRebuilt(owner, name, block) {
24447
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24448
+ if (pending === void 0 || pending.size === 0) return;
24449
+ for (const [ssId, symbolData] of [...pending]) {
24450
+ if (this.#classifyPacketId(owner) === null) return;
24451
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24452
+ pending.delete(ssId);
24453
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24454
+ }
23823
24455
  }
23824
24456
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23825
24457
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -23959,6 +24591,10 @@ var MmtpRouter = class {
23959
24591
  get unadmittedListedDropCount() {
23960
24592
  return this.#unadmittedListedDrops;
23961
24593
  }
24594
+ /** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
24595
+ get admittedSourceDatagramCount() {
24596
+ return this.#admittedSourceDatagrams;
24597
+ }
23962
24598
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
23963
24599
  get unlistedRepairSetAsideCount() {
23964
24600
  return this.#unlistedRepairSetAside;
@@ -24445,6 +25081,8 @@ var MmtpRouter = class {
24445
25081
  )) return;
24446
25082
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24447
25083
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
25084
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
25085
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24448
25086
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24449
25087
  const recovered = fec.feedRepairSymbol(
24450
25088
  ownerPacketId,
@@ -24495,7 +25133,7 @@ var MmtpRouter = class {
24495
25133
  catalogTrackName
24496
25134
  )) continue;
24497
25135
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
24498
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
25136
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
24499
25137
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
24500
25138
  }
24501
25139
  if (payload.oversized) continue;
@@ -24582,6 +25220,8 @@ var MmtpRouter = class {
24582
25220
  this.#droppedPreSyncVideo.delete(streamKey);
24583
25221
  this.#latestVideoSeqByStream.delete(streamKey);
24584
25222
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25223
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25224
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24585
25225
  this.#multicastRapTwinByStream.delete(streamKey);
24586
25226
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24587
25227
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24708,6 +25348,12 @@ var MmtpRouter = class {
24708
25348
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24709
25349
  * own RAP cadence, so no timer is involved.
24710
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
+ *
24711
25357
  * The commit is one synchronous transition inside the frame path: the
24712
25358
  * outgoing stream's already-admitted frames are released (its queued FEC
24713
25359
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -24758,6 +25404,9 @@ var MmtpRouter = class {
24758
25404
  droppedIncomingFrames: 0
24759
25405
  };
24760
25406
  this.#staged = state;
25407
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25408
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25409
+ }
24761
25410
  return {
24762
25411
  cancel: () => {
24763
25412
  if (this.#staged !== state) return false;
@@ -24840,7 +25489,17 @@ var MmtpRouter = class {
24840
25489
  */
24841
25490
  #routeParsedFrame(data, meta) {
24842
25491
  if (this.#classifyPacketId(meta.packetId) === null) return;
24843
- 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
+ }
24844
25503
  this.#deliverParsedFrame(data, meta);
24845
25504
  }
24846
25505
  #commitStreamSwitch(state, rap, reason) {
@@ -24992,12 +25651,16 @@ var MmtpRouter = class {
24992
25651
  this.#parser.deferFecRelease(work);
24993
25652
  } finally {
24994
25653
  this.#fecPokeDepth--;
24995
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
24996
- const pokes = [...this.#pendingFecPokes.values()];
24997
- this.#pendingFecPokes.clear();
24998
- for (const poke of pokes) {
24999
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25000
- }
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);
25001
25664
  }
25002
25665
  }
25003
25666
  }
@@ -25075,6 +25738,14 @@ var MmtpRouter = class {
25075
25738
  onRap: (packetId, catalogTrackName, ssId) => {
25076
25739
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25077
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
+ },
25078
25749
  onReleased: (data, meta) => {
25079
25750
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25080
25751
  const timing = meta.authoredTiming;
@@ -25216,6 +25887,7 @@ var MmtpRouter = class {
25216
25887
  const parsed = this.#parser.parsePacket(packet);
25217
25888
  if (!parsed) return;
25218
25889
  const info = parsed.info;
25890
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25219
25891
  if (source === "multicast" && !manifestAdmitted) {
25220
25892
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25221
25893
  }
@@ -25497,7 +26169,8 @@ var MmtpRouter = class {
25497
26169
  parsed.header,
25498
26170
  info.sequenceNumber,
25499
26171
  info.rapFlag,
25500
- source
26172
+ source,
26173
+ fecPayloadId?.ssId
25501
26174
  );
25502
26175
  }
25503
26176
  }
@@ -25523,7 +26196,8 @@ var MmtpRouter = class {
25523
26196
  source: parserOptions?.source,
25524
26197
  deliveryProvenance: parserOptions?.deliveryProvenance,
25525
26198
  catalogTrackName: parserOptions?.catalogTrackName,
25526
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26199
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26200
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25527
26201
  };
25528
26202
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25529
26203
  else this.#parser.feed(packet, feedOptions);
@@ -25647,7 +26321,8 @@ var MmtpRouter = class {
25647
26321
  if (stream === void 0) {
25648
26322
  stream = {
25649
26323
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25650
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
26324
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
26325
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25651
26326
  };
25652
26327
  byName.set(catalogTrackName, stream);
25653
26328
  }
@@ -25762,8 +26437,16 @@ var MmtpRouter = class {
25762
26437
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25763
26438
  this.#emitDirectVideo(packetId, data, meta, cb);
25764
26439
  }
25765
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26440
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25766
26441
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26442
+ if (this.#isFecTrack(packetId)) {
26443
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26444
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26445
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26446
+ this.#emitDirectVideo(packetId, data, meta, cb);
26447
+ return;
26448
+ }
26449
+ }
25767
26450
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25768
26451
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25769
26452
  this.#drainDirectRecoveredVideo(
@@ -25917,6 +26600,13 @@ var MmtpRouter = class {
25917
26600
  * frames by epoch only if pre-protection input ever matters.
25918
26601
  */
25919
26602
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26603
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26604
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26605
+ result = {
26606
+ ...result,
26607
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26608
+ };
26609
+ }
25920
26610
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25921
26611
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25922
26612
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25973,7 +26663,12 @@ var MmtpRouter = class {
25973
26663
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25974
26664
  if (ownerPacketId == null) return;
25975
26665
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25976
- 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
+ };
25977
26672
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25978
26673
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25979
26674
  if (ssIdSeqOffset !== void 0) {
@@ -25981,7 +26676,10 @@ var MmtpRouter = class {
25981
26676
  ...result,
25982
26677
  symbols: result.symbols.map((sym) => {
25983
26678
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25984
- if (wire === sym.packetSequenceNumber) return sym;
26679
+ if (wire === sym.packetSequenceNumber) {
26680
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26681
+ return sym;
26682
+ }
25985
26683
  this.#restampedRecoveredSeqs++;
25986
26684
  this.#restampedStreams.add(streamKey);
25987
26685
  return { ...sym, packetSequenceNumber: wire };
@@ -27766,6 +28464,12 @@ function isNonNegativeSafeInteger(value) {
27766
28464
  function isSymbolCounts(value) {
27767
28465
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
27768
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
+ }
27769
28473
  function isNullableNonNegative(value) {
27770
28474
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
27771
28475
  }
@@ -27785,7 +28489,10 @@ function isRepairLifecycleEvent(value) {
27785
28489
  case "recovered":
27786
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"]);
27787
28491
  case "failed":
27788
- 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"]));
27789
28496
  case "repair-preempted":
27790
28497
  case "repair-deferred":
27791
28498
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -27994,6 +28701,8 @@ var MulticastManager = class {
27994
28701
  /** Live joinRepairEndpoints() releases → the repair owners they registered,
27995
28702
  * re-applied after configureMultiTrack() clears catalog-derived owners. */
27996
28703
  #repairReleases = /* @__PURE__ */ new Map();
28704
+ /** Protected repair flows owned by each live repair-endpoint release. */
28705
+ #repairProvenanceFlows = /* @__PURE__ */ new Map();
27997
28706
  /** Each source's catalog keyframe overlay, from its `ok` binding. */
27998
28707
  #overlayBindings = /* @__PURE__ */ new Map();
27999
28708
  /** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
@@ -28051,7 +28760,7 @@ var MulticastManager = class {
28051
28760
  onPublisherRestart: (packetId, catalogTrackName) => {
28052
28761
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
28053
28762
  },
28054
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
28763
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
28055
28764
  const pathCount = this.#fec.blockPathCount(
28056
28765
  sbn,
28057
28766
  packetId,
@@ -28069,7 +28778,8 @@ var MulticastManager = class {
28069
28778
  symbols: {
28070
28779
  multicast: pathCount?.multicastSymbols ?? 0,
28071
28780
  moq: pathCount?.moqSymbols ?? 0
28072
- }
28781
+ },
28782
+ ...span === void 0 ? {} : { span }
28073
28783
  });
28074
28784
  },
28075
28785
  onFrameGap: (decision) => {
@@ -28659,6 +29369,9 @@ var MulticastManager = class {
28659
29369
  this.#transport.onFailback((reason) => {
28660
29370
  const joined = this.#provenanceJoin;
28661
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
+ }
28662
29375
  this.#onFailback?.(reason);
28663
29376
  });
28664
29377
  }
@@ -28693,6 +29406,12 @@ var MulticastManager = class {
28693
29406
  "[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port"
28694
29407
  );
28695
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
+ }
28696
29415
  const staged = this.#renditionSwitch;
28697
29416
  if (staged !== null) this.#endRenditionSwitch(staged, { reason: "superseded" }, false);
28698
29417
  this.configureFec(config);
@@ -28700,8 +29419,8 @@ var MulticastManager = class {
28700
29419
  const selectedPacketIds = config.packetIds?.length ? new Set(config.packetIds) : null;
28701
29420
  const catalogTracks = config.tracks?.filter(({ packetId }) => selectedPacketIds === null || selectedPacketIds.has(packetId));
28702
29421
  const ingressTracks = catalogTracks ? catalogTracks : selectedPacketIds === null ? void 0 : [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
28703
- this.#releaseProvenanceJoin();
28704
29422
  const provenance = config.provenance;
29423
+ this.#releaseProvenanceJoin();
28705
29424
  const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
28706
29425
  const ingress = protectedTracks === void 0 ? void 0 : {
28707
29426
  tracks: Object.freeze(
@@ -28754,8 +29473,13 @@ var MulticastManager = class {
28754
29473
  * feeds the flow's receiver; media datagrams are held until one admits them.
28755
29474
  */
28756
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) {
28757
29481
  const { flow, auth } = provenance;
28758
- const receiver = new BcProvenanceV2Receiver({
29482
+ return new BcProvenanceV2Receiver({
28759
29483
  ...provenance.receiver,
28760
29484
  // The caller's ctx when it passes one, else the catalog's: an explicit
28761
29485
  // `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
@@ -28772,9 +29496,6 @@ var MulticastManager = class {
28772
29496
  maxAuthDelayMs: flow.maxAuthDelayMs,
28773
29497
  manifestTimerMs: flow.manifestTimerMs
28774
29498
  });
28775
- this.#router.registerProvenanceFlow(ingress, receiver);
28776
- this.#hookFailback();
28777
- this.#provenanceJoin = { flow: ingress.flow };
28778
29499
  }
28779
29500
  /** Un-protect: drop the flow's hold queues (no expiry counted). */
28780
29501
  #releaseProvenanceJoin() {
@@ -28805,8 +29526,25 @@ var MulticastManager = class {
28805
29526
  * callback delivers must come from a subscription opened on
28806
29527
  * `options.gateway`. A callback that also merges another source (a MoQ or
28807
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.
28808
29535
  */
28809
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
+ }
28810
29548
  const releases = [];
28811
29549
  let released = false;
28812
29550
  const safely = (each) => {
@@ -28816,17 +29554,36 @@ var MulticastManager = class {
28816
29554
  console.warn("[MulticastManager] repair group release failed", error);
28817
29555
  }
28818
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 = [];
28819
29568
  const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
28820
29569
  ...owner,
28821
29570
  ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
28822
29571
  repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
28823
29572
  }));
28824
29573
  this.#applyRepairOwners(repairOwners);
28825
- 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);
28826
29576
  const ingress = {
28827
29577
  tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
28828
- ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
29578
+ ...flow === void 0 ? {} : { flow }
28829
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
+ }
28830
29587
  let sender;
28831
29588
  const feed = (packet, sourceAddress, ingressToFeed) => {
28832
29589
  if (released) return;
@@ -28856,7 +29613,7 @@ var MulticastManager = class {
28856
29613
  packetIds: endpoint.packetIds,
28857
29614
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
28858
29615
  // A protected repair group's records ride in band on its port (7.2.3).
28859
- ...endpoint.flow === void 0 ? {} : { protected: true }
29616
+ ...flow === void 0 ? {} : { protected: true }
28860
29617
  }, ingress).then((joined) => {
28861
29618
  if (!joined && !released) fail(transport.lastJoinOutcome);
28862
29619
  });
@@ -28865,9 +29622,12 @@ var MulticastManager = class {
28865
29622
  if (released) return;
28866
29623
  released = true;
28867
29624
  this.#repairReleases.delete(release);
29625
+ this.#repairProvenanceFlows.delete(release);
28868
29626
  for (const each of releases.splice(0)) safely(each);
28869
29627
  };
28870
29628
  this.#repairReleases.set(release, repairOwners);
29629
+ this.#repairProvenanceFlows.set(release, provenanceFlows);
29630
+ if (provenanceFlows.length) this.#hookFailback();
28871
29631
  return release;
28872
29632
  }
28873
29633
  #applyRepairOwners(owners) {
@@ -29449,10 +30209,12 @@ var MulticastManager = class {
29449
30209
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
29450
30210
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
29451
30211
  * pending hole and reported here, and `closeIsoHoles` must be called for it
29452
- * 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.
29453
30215
  */
29454
- onUndelimitedLoss(cb) {
29455
- this.#fec.setUndelimitedLossHandler(cb);
30216
+ onUndelimitedLoss(cb, onExpired) {
30217
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
29456
30218
  }
29457
30219
  /**
29458
30220
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -29688,7 +30450,8 @@ var MulticastManager = class {
29688
30450
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
29689
30451
  ...event,
29690
30452
  block: Object.freeze({ ...event.block }),
29691
- ..."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 }) } : {}
29692
30455
  }
29693
30456
  );
29694
30457
  if (this.#logRepairLifecycle) {
@@ -32360,6 +33123,7 @@ var RepairLayerSubscriptions = class {
32360
33123
 
32361
33124
  // src/repair-fetch.ts
32362
33125
  var MOQT_PRIORITY_MAX = 255;
33126
+ var WALK_LEAD_MS = 3;
32363
33127
  var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
32364
33128
  function isMoqtPriority(value) {
32365
33129
  return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX;
@@ -32410,8 +33174,17 @@ function wireRepairFetch(mc, opts) {
32410
33174
  mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName);
32411
33175
  };
32412
33176
  const clearEntry = (entry) => {
32413
- if (entry.range) mc.closeIsoHoles(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength);
32414
- 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);
32415
33188
  };
32416
33189
  const releaseHold = (entry) => {
32417
33190
  if (entry.range) return;
@@ -32478,21 +33251,42 @@ function wireRepairFetch(mc, opts) {
32478
33251
  const admit = (entry, now) => {
32479
33252
  const budgetMs = entry.deadlineAt - now;
32480
33253
  const rttMs = readRtt();
32481
- 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);
32482
33255
  if (reason === null) return rttMs;
32483
33256
  skip(entry, reason, rttMs, budgetMs);
32484
33257
  return null;
32485
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";
32486
33260
  const retire = (entry) => {
32487
33261
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
32488
33262
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
32489
33263
  };
32490
- 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) => {
32491
33272
  for (const entry of entries.values()) {
32492
- if (entry.priority < priority) return true;
33273
+ if (holdsBack(entry, priority, videoWalks)) return true;
32493
33274
  }
32494
33275
  return false;
32495
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
+ };
32496
33290
  const preemptBestEffort = () => {
32497
33291
  for (const entry of entries.values()) {
32498
33292
  if (entry.priority === 0 || !entry.running) continue;
@@ -32521,7 +33315,8 @@ function wireRepairFetch(mc, opts) {
32521
33315
  }
32522
33316
  const rttMs = admit(entry, now);
32523
33317
  if (rttMs === null) return;
32524
- if (entry.priority === 0) preemptBestEffort();
33318
+ if (entry.priority === 0 && entry.walk === void 0) preemptBestEffort();
33319
+ if (entry.range === void 0) leaveWalkedGroup(entry);
32525
33320
  if (counted) entry.attempts++;
32526
33321
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
32527
33322
  rttMs,
@@ -32543,6 +33338,14 @@ function wireRepairFetch(mc, opts) {
32543
33338
  }
32544
33339
  void execute(entry, run);
32545
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
+ };
32546
33349
  const dispatch = () => {
32547
33350
  if (disposed) return;
32548
33351
  const now = performance.now();
@@ -32567,6 +33370,7 @@ function wireRepairFetch(mc, opts) {
32567
33370
  for (const entry of queued) {
32568
33371
  if (entry.priority === 0) start(entry);
32569
33372
  }
33373
+ yieldVideoWalks(queued);
32570
33374
  if (outstandingBelow(1)) return;
32571
33375
  for (const entry of queued) {
32572
33376
  if (entry.priority === 1) start(entry);
@@ -32593,6 +33397,16 @@ function wireRepairFetch(mc, opts) {
32593
33397
  }
32594
33398
  return counts.sources + counts.repairGroupLength >= counts.authoredLength;
32595
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
+ };
32596
33410
  const finish = (entry, run, fed, error) => {
32597
33411
  if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
32598
33412
  return;
@@ -32637,27 +33451,81 @@ function wireRepairFetch(mc, opts) {
32637
33451
  signal: run.controller.signal,
32638
33452
  subscriberPriority: entry.subscriberPriority
32639
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
+ };
32640
33462
  try {
32641
- 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({
32642
33464
  ...common,
32643
- startGroup: entry.range.startGroup,
32644
- endGroup: entry.range.endGroup,
32645
- lostSsId: entry.range.lostSsId
32646
- }) : opts.transport({ ...common, sbn: entry.sbn });
33465
+ startGroup: range.groups.startGroup,
33466
+ endGroup: range.groups.endGroup,
33467
+ lostSsId: range.lostSsId
33468
+ });
32647
33469
  for await (const packet of packets) {
32648
33470
  if (run.stopped !== null || entry.running !== run) break;
32649
33471
  fed++;
32650
- if (entry.catalogTrackName === void 0) {
32651
- mc.processRepairFromMoQ(packet, entry.packetId);
32652
- } else {
32653
- mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName);
32654
- }
33472
+ if (range === void 0) feed(packet);
33473
+ else atReport(range, () => feed(packet));
32655
33474
  }
32656
33475
  finish(entry, run, fed, void 0);
32657
33476
  } catch (error) {
32658
33477
  finish(entry, run, fed, error ?? new Error("repair transport rejected"));
32659
33478
  }
32660
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
+ }
32661
33529
  const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
32662
33530
  for (const entry of [...entries.values()]) {
32663
33531
  if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue;
@@ -32853,7 +33721,37 @@ function wireRepairFetch(mc, opts) {
32853
33721
  throw error;
32854
33722
  }
32855
33723
  };
32856
- 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) {
32857
33755
  mc.onUndelimitedLoss((loss) => {
32858
33756
  const { packetId, catalogTrackName, holeStart, holeLength } = loss;
32859
33757
  const close = () => mc.closeIsoHoles(packetId, catalogTrackName, holeStart, holeLength);
@@ -32861,9 +33759,11 @@ function wireRepairFetch(mc, opts) {
32861
33759
  const blockProfile = blockProfileOf(packetId);
32862
33760
  const target = opts.resolveTarget(packetId, catalogTrackName);
32863
33761
  if (!target) return close();
32864
- const priority = repairPriorityForClass("unknown");
32865
- const range = rangeFor(loss, target.repairTrack);
32866
- 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) {
32867
33767
  close();
32868
33768
  mc.noteRepairSkipped("range-no-anchor", { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName);
32869
33769
  return;
@@ -32874,8 +33774,10 @@ function wireRepairFetch(mc, opts) {
32874
33774
  packetId,
32875
33775
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
32876
33776
  };
32877
- const wire = wirePriority(identity, "unknown");
32878
- 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);
32879
33781
  if (deadline === null) {
32880
33782
  close();
32881
33783
  mc.noteRepairSkipped(
@@ -32887,6 +33789,7 @@ function wireRepairFetch(mc, opts) {
32887
33789
  );
32888
33790
  return;
32889
33791
  }
33792
+ const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt;
32890
33793
  const entry = {
32891
33794
  key: JSON.stringify(["range", packetId, catalogTrackName ?? null, holeStart]),
32892
33795
  ...identity,
@@ -32894,15 +33797,16 @@ function wireRepairFetch(mc, opts) {
32894
33797
  blockProfile,
32895
33798
  priority,
32896
33799
  subscriberPriority: wire.subscriberPriority,
32897
- deadlineAt: deadline.deadlineAt,
32898
- playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
33800
+ deadlineAt,
33801
+ playoutDeadlineAt: deadlineAt - deadline.extensionMs,
32899
33802
  anchorAt: deadline.anchorAt,
32900
33803
  delayMs: deadline.delayMs,
32901
33804
  bound: deadline.bound,
32902
33805
  order: enqueueOrder++,
32903
33806
  attempts: 0,
32904
33807
  running: null,
32905
- range: { ...range, lostSsId: loss.lostSsId, holeLength }
33808
+ range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
33809
+ ...walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}
32906
33810
  };
32907
33811
  const previous = entries.get(entry.key);
32908
33812
  if (previous) {
@@ -32912,7 +33816,7 @@ function wireRepairFetch(mc, opts) {
32912
33816
  entries.set(entry.key, entry);
32913
33817
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
32914
33818
  dispatch();
32915
- });
33819
+ }, onHoleExpired);
32916
33820
  }
32917
33821
  const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
32918
33822
  queueMicrotask(() => {