@blockcast/mmt-container 2.2.0-main.f480a7ede8b1 → 2.2.0-main.fb5c7dfbe2de

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/browser-runtime.js +891 -141
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/delta-fallback-config.js.map +1 -1
  4. package/dist/fec-manager.d.ts +67 -11
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +283 -54
  7. package/dist/fec-manager.js.map +2 -2
  8. package/dist/fec-worker-setup.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +2 -2
  12. package/dist/iso-block-locator.d.ts +39 -13
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +80 -25
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/media-router.js.map +1 -1
  17. package/dist/mmtp-container-parser.d.ts +42 -8
  18. package/dist/mmtp-container-parser.d.ts.map +1 -1
  19. package/dist/mmtp-container-parser.js +121 -17
  20. package/dist/mmtp-container-parser.js.map +2 -2
  21. package/dist/mmtp-container-parser.no-modules.js +120 -18
  22. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  23. package/dist/mmtp-decode-pipeline.js +121 -17
  24. package/dist/mmtp-decode-pipeline.js.map +2 -2
  25. package/dist/mmtp-router.d.ts +28 -8
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +407 -52
  28. package/dist/mmtp-router.js.map +2 -2
  29. package/dist/multicast-manager.d.ts +13 -3
  30. package/dist/multicast-manager.d.ts.map +1 -1
  31. package/dist/multicast-manager.js +712 -115
  32. package/dist/multicast-manager.js.map +2 -2
  33. package/dist/multicast-transport.js +2 -2
  34. package/dist/multicast-transport.js.map +2 -2
  35. package/dist/no-modules.js +121 -17
  36. package/dist/no-modules.js.map +2 -2
  37. package/dist/repair-fetch.d.ts +36 -8
  38. package/dist/repair-fetch.d.ts.map +1 -1
  39. package/dist/repair-fetch.js +285 -26
  40. package/dist/repair-fetch.js.map +3 -3
  41. package/dist/track-session.d.ts.map +1 -1
  42. package/dist/track-session.js +1 -0
  43. package/dist/track-session.js.map +2 -2
  44. package/dist/wasm-mmtp-container-parser.js +121 -17
  45. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  46. package/package.json +5 -5
  47. package/src/fec-manager.ts +304 -28
  48. package/src/index.ts +2 -0
  49. package/src/iso-block-locator.ts +114 -31
  50. package/src/mmtp-container-parser.ts +210 -21
  51. package/src/mmtp-router.ts +488 -45
  52. package/src/multicast-manager.ts +44 -6
  53. package/src/repair-fetch.ts +363 -49
  54. package/src/track-session.ts +3 -0
@@ -327,6 +327,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
327
327
  this.#heldEvicted = 0;
328
328
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
329
329
  this.#holes = [];
330
+ /** Holes the horizon passed while pending: sources held until closed. */
331
+ this.#expired = [];
332
+ /** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
333
+ this.#newlyExpired = [];
334
+ /**
335
+ * Spans closed from expired holes: behind the horizon, so kept only for
336
+ * `locate` while their sources are fed, until the next retirement.
337
+ */
338
+ this.#closedBehind = [];
330
339
  /**
331
340
  * Keep a gap that is missing a source as a pending hole rather than a span,
332
341
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -350,6 +359,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
350
359
  #newest;
351
360
  #newestSource;
352
361
  #holes;
362
+ #expired;
363
+ #newlyExpired;
364
+ #closedBehind;
353
365
  static #checkedRetention(retentionBlocks) {
354
366
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
355
367
  throw new Error(
@@ -419,17 +431,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
419
431
  }
420
432
  /** The known block or span containing `ssId`, with its ESI. */
421
433
  locate(ssId) {
422
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
423
- const block = this.#blocks[i];
424
- if (isoBlockContains(block.start, block.length, ssId)) {
425
- return { block, esi: serial32Distance(block.start, ssId) };
434
+ for (const blocks of [this.#blocks, this.#closedBehind]) {
435
+ for (let i = blocks.length - 1; i >= 0; i--) {
436
+ const block = blocks[i];
437
+ if (isoBlockContains(block.start, block.length, ssId)) {
438
+ return { block, esi: serial32Distance(block.start, ssId) };
439
+ }
426
440
  }
427
441
  }
428
442
  return null;
429
443
  }
430
- /** Number of pending holes. */
444
+ /** Number of holes not closed yet, pending or past the horizon. */
431
445
  get holeCount() {
432
- return this.#holes.length;
446
+ return this.#holes.length + this.#expired.length;
447
+ }
448
+ /**
449
+ * The pending holes the horizon has passed since the last call, in the
450
+ * order it passed them. Each is no longer pending: no repair states a block
451
+ * in it (that repair is `stale`, as for a span the horizon retired) and the
452
+ * release gate does not wait for it. Its owner must close it
453
+ * (`closeHolesWithin`), which turns it into a span that is not kept here.
454
+ */
455
+ takeExpiredHoles() {
456
+ return this.#newlyExpired.splice(0);
433
457
  }
434
458
  /** The stated block (never a span) ending exactly at `end`. */
435
459
  blockEndingAt(end) {
@@ -485,9 +509,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
485
509
  /**
486
510
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
487
511
  * block states its range and places every held source in it; later repair
488
- * symbols of the same block change nothing. A range that contradicts a
489
- * known block (a different length, or an overlap with another block or a
490
- * span) fails closed: the repair symbol is dropped and no block changes.
512
+ * symbols of the same block change nothing. A repair whose range is exactly
513
+ * a repair-less span states that span's one block. A range that contradicts
514
+ * a known block (a different length, or an overlap with another block or
515
+ * with part of a span) fails closed: the repair symbol is dropped and no
516
+ * block changes.
491
517
  * A newly stated block retires behind its SS_Start (see the class note).
492
518
  * A block so far past `newest` that stating it would retire the position
493
519
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -506,7 +532,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
506
532
  const known = this.blockAt(start);
507
533
  if (known !== void 0) {
508
534
  if (known.span) {
509
- return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
535
+ if (known.length === length) {
536
+ const block2 = { start, length, span: false };
537
+ this.#blocks[this.#blocks.indexOf(known)] = block2;
538
+ return { status: "placed", block: block2, placed: [] };
539
+ }
540
+ return length < known.length ? {
541
+ status: "overlap",
542
+ reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
543
+ ofSpan: known
544
+ } : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
510
545
  }
511
546
  if (known.length !== length) {
512
547
  return {
@@ -574,18 +609,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
574
609
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
575
610
  }
576
611
  /**
577
- * Turn every pending hole inside `[start, start + length)` into a
578
- * repair-less span, claiming its held sources: its range FETCH ended, so
579
- * nothing more will state its blocks. Returns the spans, in SS_ID order.
612
+ * Turn every hole inside `[start, start + length)` into a repair-less span,
613
+ * claiming its held sources: its range FETCH ended, so nothing more will
614
+ * state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
615
+ * span that is not kept among the blocks, as the horizon would retire it at
616
+ * once; only `locate` finds it, until the next retirement, so its sources
617
+ * can be fed. Returns the spans, in SS_ID order.
580
618
  */
581
619
  closeHolesWithin(start, length) {
582
620
  const closed = [];
583
- for (const hole of [...this.#holes]) {
584
- if (!isoBlockContains(start, length, hole.start)) continue;
585
- this.#holes.splice(this.#holes.indexOf(hole), 1);
586
- const spanBlock = { start: hole.start, length: hole.length, span: true };
587
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
588
- this.#blocks.push(spanBlock);
621
+ for (const holes of [this.#holes, this.#expired]) {
622
+ for (const hole of [...holes]) {
623
+ if (!isoBlockContains(start, length, hole.start)) continue;
624
+ holes.splice(holes.indexOf(hole), 1);
625
+ const spanBlock = { start: hole.start, length: hole.length, span: true };
626
+ closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
627
+ if (holes === this.#holes) {
628
+ this.#blocks.push(spanBlock);
629
+ } else {
630
+ this.#closedBehind.push(spanBlock);
631
+ if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
632
+ }
633
+ }
589
634
  }
590
635
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
591
636
  }
@@ -599,15 +644,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
599
644
  return void 0;
600
645
  }
601
646
  /**
602
- * Retire every block and span whose last SS_ID, and every held source,
603
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
604
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
605
- * horizon uses. Returns the retired block starts.
647
+ * Retire every block and span whose last SS_ID, and every held source
648
+ * outside a hole, lies more than `retentionBlocks` blocks behind
649
+ * `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
650
+ * every ISO interleave horizon uses; a pending hole so far behind expires
651
+ * (`takeExpiredHoles`). Returns the retired block starts.
606
652
  */
607
653
  retireBehind(position) {
608
654
  this.observe(position);
609
655
  const horizon = this.#retentionBlocks;
610
656
  const retired = [];
657
+ this.#closedBehind.length = 0;
611
658
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
612
659
  const block = this.#blocks[i];
613
660
  const last = serial32Add(block.start, block.length - 1);
@@ -619,10 +666,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
619
666
  for (let i = this.#holes.length - 1; i >= 0; i--) {
620
667
  const hole = this.#holes[i];
621
668
  const last = serial32Add(hole.start, hole.length - 1);
622
- if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) this.#holes.splice(i, 1);
669
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
670
+ this.#holes.splice(i, 1);
671
+ this.#expired.push(hole);
672
+ this.#newlyExpired.push(hole);
623
673
  }
624
674
  for (const ssId of [...this.#held.keys()]) {
625
- if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
675
+ 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))) {
676
+ this.#held.delete(ssId);
677
+ }
626
678
  }
627
679
  return retired;
628
680
  }
@@ -630,6 +682,9 @@ var IsoBlockLocator = class _IsoBlockLocator {
630
682
  reset() {
631
683
  this.#blocks.length = 0;
632
684
  this.#holes.length = 0;
685
+ this.#expired.length = 0;
686
+ this.#newlyExpired.length = 0;
687
+ this.#closedBehind.length = 0;
633
688
  this.#held.clear();
634
689
  this.#coverageEnd = void 0;
635
690
  this.#newest = void 0;
@@ -1098,6 +1153,7 @@ var WORKER_ERROR_LOG_CAP = 5;
1098
1153
  var WORKER_ERROR_KIND_CAP = 32;
1099
1154
  var WORKER_ERROR_OVERFLOW_KIND = "other";
1100
1155
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
1156
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
1101
1157
  function isMulticastSymbolDelivery(value) {
1102
1158
  return value === "datagram" || value === "ordered-relay";
1103
1159
  }
@@ -1191,10 +1247,18 @@ var FECManager = class _FECManager {
1191
1247
  // 'pending' forever, which would park every later frame of its stream in
1192
1248
  // the in-order release gate. Each entry owns its handle; the callback
1193
1249
  // checks it still owns the key, so a settled or reset key is never touched.
1250
+ /**
1251
+ * Per owed Worker cleanup: the bound on its acknowledgement, when its block
1252
+ * was due, and when its cleanup was asked for (see `onCleanupComplete`).
1253
+ */
1194
1254
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
1195
1255
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
1196
1256
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
1197
1257
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
1258
+ // Source ESIs per block that another FEC instance rebuilt and fed in
1259
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
1260
+ // not network arrivals and never make a block clean.
1261
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
1198
1262
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
1199
1263
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
1200
1264
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -1203,6 +1267,9 @@ var FECManager = class _FECManager {
1203
1267
  // SSB_length source arrivals and recoverable from SSB_length received
1204
1268
  // symbols; waiting for K fails every one of them.
1205
1269
  this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
1270
+ // Keys whose authored length is a repair-less span's (#openIsoSpan), until
1271
+ // a repair states the block there or the span resolves.
1272
+ this.#isoSpanKeys = /* @__PURE__ */ new Set();
1206
1273
  this.#ssbLengthConflictCount = 0;
1207
1274
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
1208
1275
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -1400,7 +1467,10 @@ var FECManager = class _FECManager {
1400
1467
  // arrives via onBlockRecovered callback.
1401
1468
  this.onBlockStarted = null;
1402
1469
  this.onBlockComplete = null;
1403
- /** A block reached its fail bound. `blockClass` is its class at failure. */
1470
+ /**
1471
+ * A block reached its fail bound. `blockClass` is its class at failure.
1472
+ * `span` is set when what failed is a repair-less span starting at `sbn`.
1473
+ */
1404
1474
  this.onBlockFailed = null;
1405
1475
  /**
1406
1476
  * Fired once per stream when its Worker decoder stops being routable.
@@ -1442,6 +1512,8 @@ var FECManager = class _FECManager {
1442
1512
  * for it, which it must be.
1443
1513
  */
1444
1514
  this.#onUndelimitedLoss = null;
1515
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
1516
+ this.#onIsoHoleExpired = null;
1445
1517
  /** A block every symbol of which vanished was created by SBN evidence. */
1446
1518
  this.onBlockVanished = null;
1447
1519
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -1484,7 +1556,9 @@ var FECManager = class _FECManager {
1484
1556
  #pendingWorkerCleanupBounds;
1485
1557
  #blockSourceCounts;
1486
1558
  #blockSourceEsis;
1559
+ #blockRebuiltEsis;
1487
1560
  #blockAuthoredLengths;
1561
+ #isoSpanKeys;
1488
1562
  #ssbLengthConflictCount;
1489
1563
  #blockRepairRsIds;
1490
1564
  #blockRsbLengths;
@@ -1551,6 +1625,7 @@ var FECManager = class _FECManager {
1551
1625
  #decoderUnavailableEvents;
1552
1626
  #repairSymbolSizeMismatchCount;
1553
1627
  #onUndelimitedLoss;
1628
+ #onIsoHoleExpired;
1554
1629
  #encodeTrackName(catalogTrackName) {
1555
1630
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
1556
1631
  if (encoded === void 0) {
@@ -2022,7 +2097,10 @@ var FECManager = class _FECManager {
2022
2097
  timer.handle = null;
2023
2098
  this.#requestRepair(sbn, packetId, catalogTrackName);
2024
2099
  };
2025
- timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()));
2100
+ timer.handle = setTimeout(
2101
+ onFire,
2102
+ this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
2103
+ );
2026
2104
  this.#repairDeadlines.set(key, timer);
2027
2105
  }
2028
2106
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -2166,8 +2244,14 @@ var FECManager = class _FECManager {
2166
2244
  * Vanished blocks are not enumerated here: their starts are unknown until a
2167
2245
  * repair states them. The SS_IDs between two stated blocks become one
2168
2246
  * repair-less span when the later block's repair arrives (placeIsoRepair).
2247
+ *
2248
+ * A `held` source arrived before its block was known and is fed only now
2249
+ * that a repair, a span or a closed hole placed it: how far its block start
2250
+ * then lies behind the frontier measures how long the receiver held it, not
2251
+ * where the stream was, so only its SS_ID and packet_sequence_number judge
2252
+ * it (BLO-52085).
2169
2253
  */
2170
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
2254
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
2171
2255
  const streamKey = this.#streamKey(packetId, catalogTrackName);
2172
2256
  const now = performance.now();
2173
2257
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -2206,7 +2290,7 @@ var FECManager = class _FECManager {
2206
2290
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
2207
2291
  };
2208
2292
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
2209
- if (symbol === "source" && window !== 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))) {
2293
+ if (symbol === "source" && window !== 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))) {
2210
2294
  discontinuity();
2211
2295
  return;
2212
2296
  }
@@ -2468,17 +2552,21 @@ var FECManager = class _FECManager {
2468
2552
  this.#blockWaitDeadlinesMs.delete(key);
2469
2553
  this.#repairHoldUntilMs.delete(key);
2470
2554
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
2471
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
2555
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
2472
2556
  };
2473
- timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
2557
+ timer.handle = setTimeout(
2558
+ onFire,
2559
+ this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
2560
+ );
2474
2561
  this.#activeBlocks.set(key, timer);
2475
2562
  this.#indexBlockKey(key, sbn, packetId);
2476
2563
  }
2477
2564
  /**
2478
2565
  * Hand a block whose delivery is over to its decoder for finalization: the
2479
- * datagram deadline fired, or the ordered relay moved past it.
2566
+ * datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
2567
+ * is when it was due: its deadline, or now.
2480
2568
  */
2481
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
2569
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
2482
2570
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
2483
2571
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
2484
2572
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -2494,7 +2582,7 @@ var FECManager = class _FECManager {
2494
2582
  if (workerClient) {
2495
2583
  this.#pendingWorkerCleanups.add(key);
2496
2584
  this.#pendingWorkerCleanupClients.set(key, workerClient);
2497
- this.#boundOwedCleanup(key, packetId, workerClient);
2585
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
2498
2586
  workerClient.cleanup(sbn);
2499
2587
  } else {
2500
2588
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -2806,7 +2894,9 @@ var FECManager = class _FECManager {
2806
2894
  }
2807
2895
  resetMap(this.#blockSourceCounts);
2808
2896
  resetMap(this.#blockSourceEsis);
2897
+ resetMap(this.#blockRebuiltEsis);
2809
2898
  resetMap(this.#blockAuthoredLengths);
2899
+ resetSet(this.#isoSpanKeys);
2810
2900
  resetMap(this.#blockRepairRsIds);
2811
2901
  resetMap(this.#blockRsbLengths);
2812
2902
  resetMap(this.#heldLayerStates);
@@ -2930,7 +3020,9 @@ var FECManager = class _FECManager {
2930
3020
  dropMap(this.#pendingWorkerCleanupClients);
2931
3021
  dropMap(this.#blockSourceCounts);
2932
3022
  dropMap(this.#blockSourceEsis);
3023
+ dropMap(this.#blockRebuiltEsis);
2933
3024
  dropMap(this.#blockAuthoredLengths);
3025
+ dropSet(this.#isoSpanKeys);
2934
3026
  dropMap(this.#blockRepairRsIds);
2935
3027
  dropMap(this.#blockRsbLengths);
2936
3028
  dropMap(this.#blockPathCounts);
@@ -3273,8 +3365,9 @@ var FECManager = class _FECManager {
3273
3365
  return true;
3274
3366
  }
3275
3367
  /**
3276
- * Drop a block's repair hold: its FETCH ended without delivering. The block
3277
- * falls back to its multicast wait, which may already have passed.
3368
+ * Drop a block's repair hold: its FETCH ended without delivering, or brought
3369
+ * back only a repair that cannot complete it. The block falls back to its
3370
+ * multicast wait, which may already have passed.
3278
3371
  */
3279
3372
  releaseRepairHold(sbn, packetId, catalogTrackName) {
3280
3373
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -3412,6 +3505,7 @@ var FECManager = class _FECManager {
3412
3505
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
3413
3506
  const evictedBefore = locator.heldEvicted;
3414
3507
  const held = locator.hold(ssId, source);
3508
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
3415
3509
  if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
3416
3510
  const evicted = locator.heldEvicted - evictedBefore;
3417
3511
  if (evicted > 0) {
@@ -3433,17 +3527,20 @@ var FECManager = class _FECManager {
3433
3527
  * contradicts a known block is refused (counted) and must be dropped.
3434
3528
  */
3435
3529
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
3436
- const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
3530
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
3531
+ const placement = locator.placeRepair(ssStart, ssbLength);
3532
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
3437
3533
  if (placement.status !== "placed") {
3438
3534
  this.#isoRepairRefusedCount++;
3439
3535
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
3440
3536
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
3441
3537
  }
3538
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
3442
3539
  return placement;
3443
3540
  }
3541
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
3444
3542
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
3445
3543
  if (placement.hole !== void 0) {
3446
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
3447
3544
  const hole = placement.hole.block;
3448
3545
  const previous = locator.blockEndingAt(hole.start);
3449
3546
  const loss = {
@@ -3464,17 +3561,29 @@ var FECManager = class _FECManager {
3464
3561
  * `handler`, instead of a repair-less span; null restores spans. Set by the
3465
3562
  * range repair scheduler; applies to every stream from its next gap. The
3466
3563
  * handler and the deferral are one setting, so no hole is ever created with
3467
- * nobody to close it.
3564
+ * nobody to close it. `onExpired` is told of each pending hole the stream
3565
+ * runs past the locator's horizon (BLO-52085): returning true, it closes
3566
+ * the hole (`closeIsoHoles`), so the span it leaves still fails and is
3567
+ * reported; otherwise the hole is discarded with its sources.
3468
3568
  */
3469
- setUndelimitedLossHandler(handler) {
3569
+ setUndelimitedLossHandler(handler, onExpired = null) {
3470
3570
  this.#onUndelimitedLoss = handler;
3571
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
3471
3572
  this.#deferIsoGaps = handler !== null;
3472
3573
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
3473
3574
  }
3575
+ /** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
3576
+ #settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
3577
+ for (const hole of locator.takeExpiredHoles()) {
3578
+ const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
3579
+ if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
3580
+ }
3581
+ }
3474
3582
  /**
3475
- * The range repair for a hole ended: every hole still pending inside
3476
- * `[start, start + length)` becomes a repair-less span. Returns those spans
3477
- * with the held sources they placed, for the caller to feed (MmtpRouter).
3583
+ * The range repair for a hole ended: every hole inside `[start, start +
3584
+ * length)`, pending or expired, becomes a repair-less span. Returns those
3585
+ * spans with the held sources they placed, for the caller to feed
3586
+ * (MmtpRouter).
3478
3587
  */
3479
3588
  closeIsoHoles(packetId, catalogTrackName, start, length) {
3480
3589
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -3493,7 +3602,10 @@ var FECManager = class _FECManager {
3493
3602
  */
3494
3603
  #openIsoSpan(span, packetId, catalogTrackName) {
3495
3604
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
3496
- if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, span.block.length);
3605
+ if (!this.#blockAuthoredLengths.has(key)) {
3606
+ this.#blockAuthoredLengths.set(key, span.block.length);
3607
+ this.#isoSpanKeys.add(key);
3608
+ }
3497
3609
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
3498
3610
  }
3499
3611
  /**
@@ -3834,11 +3946,14 @@ var FECManager = class _FECManager {
3834
3946
  });
3835
3947
  client.onCleanupComplete((sbn) => {
3836
3948
  if (this.#releasedWorkerClients.has(client)) return;
3837
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
3949
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
3950
+ const cleanupKeys = owed.filter((key) => {
3838
3951
  const block = this.#parseBlockKey(key);
3839
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
3952
+ return block.packetId === packetId && block.sbn === sbn;
3840
3953
  });
3841
- for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
3954
+ const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
3955
+ const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
3956
+ for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
3842
3957
  });
3843
3958
  client.onError((msg, code) => {
3844
3959
  const requestedKind = code ?? msg;
@@ -3908,9 +4023,11 @@ var FECManager = class _FECManager {
3908
4023
  * fail deadline already passed, so failing it is the decision that deadline
3909
4024
  * made. A late ack then finds nothing owed and is a no-op.
3910
4025
  */
3911
- #boundOwedCleanup(key, packetId, client) {
4026
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
3912
4027
  this.#clearOwedCleanupBound(key);
3913
4028
  const bound = {
4029
+ dueAtMs,
4030
+ askedAtMs: performance.now(),
3914
4031
  handle: setTimeout(() => {
3915
4032
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
3916
4033
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -4008,7 +4125,15 @@ var FECManager = class _FECManager {
4008
4125
  this.#isoSourceOversizeCount++;
4009
4126
  return null;
4010
4127
  }
4011
- const sbn = located.block.start;
4128
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
4129
+ }
4130
+ /**
4131
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
4132
+ * answers through onFrame (null here), or its synchronous decoder, whose
4133
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
4134
+ * stated for `ssId`.
4135
+ */
4136
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
4012
4137
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
4013
4138
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
4014
4139
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -4037,6 +4162,74 @@ var FECManager = class _FECManager {
4037
4162
  }
4038
4163
  return recovered;
4039
4164
  }
4165
+ /**
4166
+ * Feed a source symbol of this stream that another FEC instance over the
4167
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
4168
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
4169
+ * BLO-52085). It joins its block's decoder as a received source would, so
4170
+ * it reduces the erasures the block still has to solve, but it is not a
4171
+ * network arrival: no transport accounting, no classification evidence,
4172
+ * no deadline refresh, and it never makes a block clean.
4173
+ *
4174
+ * Only a block a repair stated takes one (a repair-less span has no
4175
+ * decoder block). A block that is terminal, or already holds that source,
4176
+ * does not. The symbol is the exact protected packet, unpadded, as
4177
+ * feedSourceSymbol takes it.
4178
+ *
4179
+ * `recovered` is the block the synchronous decoder completed with it; the
4180
+ * caller routes it and completes the block, as for any recovery. When
4181
+ * every source of the block is then present, received or rebuilt, and no
4182
+ * decoder returned it (a Worker answers asynchronously, and reports such a
4183
+ * block as clean on its own side), the block is completed here as
4184
+ * recovered, with nothing left to route.
4185
+ */
4186
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
4187
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
4188
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
4189
+ if (located === null || located.block.span) return NOT_TAKEN;
4190
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
4191
+ const sbn = located.block.start;
4192
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
4193
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
4194
+ let rebuilt = this.#blockRebuiltEsis.get(key);
4195
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
4196
+ if (rebuilt === void 0) {
4197
+ rebuilt = /* @__PURE__ */ new Set();
4198
+ this.#blockRebuiltEsis.set(key, rebuilt);
4199
+ }
4200
+ const config = this.#trackConfigs.get(packetId);
4201
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
4202
+ rebuilt.add(located.esi);
4203
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
4204
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
4205
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
4206
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
4207
+ }
4208
+ }
4209
+ return { taken: true, recovered };
4210
+ }
4211
+ /**
4212
+ * A block that holds every one of its `length` sources, received or
4213
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
4214
+ * every packet of it was delivered, by its own arrival or by the instance
4215
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
4216
+ */
4217
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
4218
+ const rebuilt = this.#blockRebuiltEsis.get(key);
4219
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
4220
+ let present = rebuilt.size;
4221
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
4222
+ if (present < length) return false;
4223
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
4224
+ this.countRecovery(packetId, sbn, catalogTrackName);
4225
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
4226
+ if (catalogTrackName === void 0) {
4227
+ this.onBlockComplete?.(sbn, packetId);
4228
+ } else {
4229
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
4230
+ }
4231
+ return true;
4232
+ }
4040
4233
  /**
4041
4234
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
4042
4235
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -4279,7 +4472,10 @@ var FECManager = class _FECManager {
4279
4472
  const authoredLength = this.#blockAuthoredLengths.get(key);
4280
4473
  const rsbLength = this.#blockRsbLengths.get(key);
4281
4474
  if (authoredLength === void 0 || rsbLength === void 0) return void 0;
4282
- if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
4475
+ let present = this.#blockSourceCounts.get(key) ?? 0;
4476
+ const received = this.#blockSourceEsis.get(key);
4477
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
4478
+ if (present + rsbLength >= authoredLength) return void 0;
4283
4479
  const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
4284
4480
  if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
4285
4481
  return void 0;
@@ -4402,12 +4598,13 @@ var FECManager = class _FECManager {
4402
4598
  * the inactivity deadline on each symbol, records the symbol's in-band
4403
4599
  * classification evidence, and marks the block clean when all of its
4404
4600
  * authored source symbols arrived: the SSB_length a repair symbol declared,
4405
- * otherwise K.
4601
+ * otherwise K. `held` marks a source held unplaced since it arrived and
4602
+ * placed only now (see #noteSbnSeen).
4406
4603
  *
4407
4604
  * Throws for a packetId that was never configured, so a classified block
4408
4605
  * never reads the manager-global K or timeout.
4409
4606
  */
4410
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
4607
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
4411
4608
  const trackConfig = this.#trackConfigs.get(packetId);
4412
4609
  if (trackConfig === void 0) {
4413
4610
  throw new Error(
@@ -4423,7 +4620,8 @@ var FECManager = class _FECManager {
4423
4620
  catalogTrackName,
4424
4621
  "source",
4425
4622
  evidence.packetSequenceNumber,
4426
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
4623
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
4624
+ held
4427
4625
  );
4428
4626
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
4429
4627
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -4459,6 +4657,15 @@ var FECManager = class _FECManager {
4459
4657
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
4460
4658
  return;
4461
4659
  }
4660
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
4661
+ key,
4662
+ sbn,
4663
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
4664
+ packetId,
4665
+ catalogTrackName
4666
+ )) {
4667
+ return;
4668
+ }
4462
4669
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
4463
4670
  }
4464
4671
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -4467,7 +4674,9 @@ var FECManager = class _FECManager {
4467
4674
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
4468
4675
  this.#blockSourceCounts.delete(key);
4469
4676
  this.#blockSourceEsis.delete(key);
4677
+ this.#blockRebuiltEsis.delete(key);
4470
4678
  this.#blockAuthoredLengths.delete(key);
4679
+ this.#isoSpanKeys.delete(key);
4471
4680
  this.#blockRepairRsIds.delete(key);
4472
4681
  this.#blockRsbLengths.delete(key);
4473
4682
  this.#markTerminal(key, sbn, packetId, true);
@@ -4498,6 +4707,19 @@ var FECManager = class _FECManager {
4498
4707
  #receiptNow() {
4499
4708
  return this.#receiptAtMs ?? performance.now();
4500
4709
  }
4710
+ /**
4711
+ * The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
4712
+ * scope: counted from the receipt time, as the timer of a symbol fed when
4713
+ * it was received was, less the whole ms since. So it fires on the tick
4714
+ * that timer would have, never later than a delay counted from now, and
4715
+ * its handler still re-arms a timer that fires before its deadline.
4716
+ * Counted from now, a deadline set from the receipt time is rounded up
4717
+ * from another fraction of a ms (BLO-52085).
4718
+ */
4719
+ #receiptTimerDelayMs(deadlineMs, minMs) {
4720
+ const receivedAtMs = this.#receiptAtMs;
4721
+ return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
4722
+ }
4501
4723
  /**
4502
4724
  * A held packet admitted late arms from its (earlier) receipt time; that
4503
4725
  * never pulls in a deadline later activity already armed. Outside a
@@ -4546,6 +4768,7 @@ var FECManager = class _FECManager {
4546
4768
  }
4547
4769
  #failBlock(sbn, packetId, catalogTrackName) {
4548
4770
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
4771
+ const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
4549
4772
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
4550
4773
  this.#clearRepairDeadline(key);
4551
4774
  this.#clearBlockTimer(key);
@@ -4555,6 +4778,7 @@ var FECManager = class _FECManager {
4555
4778
  }
4556
4779
  this.#blockSourceCounts.delete(key);
4557
4780
  this.#blockSourceEsis.delete(key);
4781
+ this.#blockRebuiltEsis.delete(key);
4558
4782
  this.#blockAuthoredLengths.delete(key);
4559
4783
  this.#blockRepairRsIds.delete(key);
4560
4784
  this.#blockRsbLengths.delete(key);
@@ -4564,7 +4788,8 @@ var FECManager = class _FECManager {
4564
4788
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
4565
4789
  this.#fecStats.blocksFailed++;
4566
4790
  this.#statsForPacket(packetId).blocksFailed++;
4567
- this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
4791
+ if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
4792
+ else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
4568
4793
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
4569
4794
  }
4570
4795
  /**
@@ -4593,7 +4818,9 @@ var FECManager = class _FECManager {
4593
4818
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
4594
4819
  this.#blockSourceCounts.delete(key);
4595
4820
  this.#blockSourceEsis.delete(key);
4821
+ this.#blockRebuiltEsis.delete(key);
4596
4822
  this.#blockAuthoredLengths.delete(key);
4823
+ this.#isoSpanKeys.delete(key);
4597
4824
  this.#blockRepairRsIds.delete(key);
4598
4825
  this.#blockRsbLengths.delete(key);
4599
4826
  this.#clearBlockTimer(key);
@@ -4684,7 +4911,9 @@ var FECManager = class _FECManager {
4684
4911
  this.#blockKeysBySbn.clear();
4685
4912
  this.#blockSourceCounts.clear();
4686
4913
  this.#blockSourceEsis.clear();
4914
+ this.#blockRebuiltEsis.clear();
4687
4915
  this.#blockAuthoredLengths.clear();
4916
+ this.#isoSpanKeys.clear();
4688
4917
  this.#blockRepairRsIds.clear();
4689
4918
  this.#blockRsbLengths.clear();
4690
4919
  this.#heldLayerStates.clear();