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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1949 -374
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +65 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +70 -12
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +403 -58
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +6 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +48 -13
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +97 -25
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +79 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +219 -45
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +342 -63
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +218 -45
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +43 -9
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +2006 -95
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +28 -5
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +16116 -15005
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +336 -59
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.d.ts +36 -8
  72. package/dist/repair-fetch.d.ts.map +1 -1
  73. package/dist/repair-fetch.js +285 -26
  74. package/dist/repair-fetch.js.map +3 -3
  75. package/dist/repair-layer-subscriptions.d.ts +82 -0
  76. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  77. package/dist/repair-layer-subscriptions.js +192 -0
  78. package/dist/repair-layer-subscriptions.js.map +7 -0
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/timestamp-codec.js +12 -19
  81. package/dist/timestamp-codec.js.map +2 -2
  82. package/dist/track-session.d.ts +1 -1
  83. package/dist/track-session.d.ts.map +1 -1
  84. package/dist/track-session.js +1 -0
  85. package/dist/track-session.js.map +2 -2
  86. package/dist/wasm-mfu-reassembler.js.map +1 -1
  87. package/dist/wasm-mmtp-container-parser.js +218 -45
  88. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  89. package/package.json +8 -5
  90. package/schemas/catalog.schema.json +4 -0
  91. package/src/catalog-fec-params.ts +36 -1
  92. package/src/cmcd-producer.ts +194 -179
  93. package/src/fec-manager.ts +433 -32
  94. package/src/fec-worker-setup.ts +2 -2
  95. package/src/index.ts +20 -2
  96. package/src/iso-block-locator.ts +133 -31
  97. package/src/iso-bmff-timing.ts +40 -1
  98. package/src/mmtp-container-parser.ts +306 -29
  99. package/src/mmtp-fec-session.ts +115 -15
  100. package/src/mmtp-router.ts +629 -54
  101. package/src/multicast-manager.ts +70 -11
  102. package/src/multicast-transport.ts +44 -20
  103. package/src/repair-fetch.ts +363 -49
  104. package/src/repair-layer-subscriptions.ts +261 -0
  105. package/src/track-session.ts +4 -1
@@ -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
@@ -348,7 +357,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
348
357
  #retentionBlocks;
349
358
  #heldEvicted;
350
359
  #newest;
360
+ #newestSource;
351
361
  #holes;
362
+ #expired;
363
+ #newlyExpired;
364
+ #closedBehind;
352
365
  static #checkedRetention(retentionBlocks) {
353
366
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
354
367
  throw new Error(
@@ -385,6 +398,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
385
398
  observe(position) {
386
399
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
387
400
  }
401
+ /**
402
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
403
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
404
+ * a source cannot run ahead of the sources sent before it except by
405
+ * reordering.
406
+ */
407
+ get newestSource() {
408
+ return this.#newestSource;
409
+ }
410
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
411
+ noteSource(ssId) {
412
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
413
+ this.#newestSource = ssId;
414
+ return true;
415
+ }
388
416
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
389
417
  get heldEvicted() {
390
418
  return this.#heldEvicted;
@@ -403,17 +431,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
403
431
  }
404
432
  /** The known block or span containing `ssId`, with its ESI. */
405
433
  locate(ssId) {
406
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
407
- const block = this.#blocks[i];
408
- if (isoBlockContains(block.start, block.length, ssId)) {
409
- 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
+ }
410
440
  }
411
441
  }
412
442
  return null;
413
443
  }
414
- /** Number of pending holes. */
444
+ /** Number of holes not closed yet, pending or past the horizon. */
415
445
  get holeCount() {
416
- 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);
417
457
  }
418
458
  /** The stated block (never a span) ending exactly at `end`. */
419
459
  blockEndingAt(end) {
@@ -469,9 +509,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
469
509
  /**
470
510
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
471
511
  * block states its range and places every held source in it; later repair
472
- * symbols of the same block change nothing. A range that contradicts a
473
- * known block (a different length, or an overlap with another block or a
474
- * 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.
475
517
  * A newly stated block retires behind its SS_Start (see the class note).
476
518
  * A block so far past `newest` that stating it would retire the position
477
519
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -490,7 +532,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
490
532
  const known = this.blockAt(start);
491
533
  if (known !== void 0) {
492
534
  if (known.span) {
493
- 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}` };
494
545
  }
495
546
  if (known.length !== length) {
496
547
  return {
@@ -558,18 +609,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
558
609
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
559
610
  }
560
611
  /**
561
- * Turn every pending hole inside `[start, start + length)` into a
562
- * repair-less span, claiming its held sources: its range FETCH ended, so
563
- * 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.
564
618
  */
565
619
  closeHolesWithin(start, length) {
566
620
  const closed = [];
567
- for (const hole of [...this.#holes]) {
568
- if (!isoBlockContains(start, length, hole.start)) continue;
569
- this.#holes.splice(this.#holes.indexOf(hole), 1);
570
- const spanBlock = { start: hole.start, length: hole.length, span: true };
571
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
572
- 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
+ }
573
634
  }
574
635
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
575
636
  }
@@ -583,15 +644,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
583
644
  return void 0;
584
645
  }
585
646
  /**
586
- * Retire every block and span whose last SS_ID, and every held source,
587
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
588
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
589
- * 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.
590
652
  */
591
653
  retireBehind(position) {
592
654
  this.observe(position);
593
655
  const horizon = this.#retentionBlocks;
594
656
  const retired = [];
657
+ this.#closedBehind.length = 0;
595
658
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
596
659
  const block = this.#blocks[i];
597
660
  const last = serial32Add(block.start, block.length - 1);
@@ -603,10 +666,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
603
666
  for (let i = this.#holes.length - 1; i >= 0; i--) {
604
667
  const hole = this.#holes[i];
605
668
  const last = serial32Add(hole.start, hole.length - 1);
606
- 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);
607
673
  }
608
674
  for (const ssId of [...this.#held.keys()]) {
609
- 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
+ }
610
678
  }
611
679
  return retired;
612
680
  }
@@ -614,9 +682,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
614
682
  reset() {
615
683
  this.#blocks.length = 0;
616
684
  this.#holes.length = 0;
685
+ this.#expired.length = 0;
686
+ this.#newlyExpired.length = 0;
687
+ this.#closedBehind.length = 0;
617
688
  this.#held.clear();
618
689
  this.#coverageEnd = void 0;
619
690
  this.#newest = void 0;
691
+ this.#newestSource = void 0;
620
692
  }
621
693
  #claim(block) {
622
694
  const placed = [];
@@ -1081,6 +1153,7 @@ var WORKER_ERROR_LOG_CAP = 5;
1081
1153
  var WORKER_ERROR_KIND_CAP = 32;
1082
1154
  var WORKER_ERROR_OVERFLOW_KIND = "other";
1083
1155
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
1156
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
1084
1157
  function isMulticastSymbolDelivery(value) {
1085
1158
  return value === "datagram" || value === "ordered-relay";
1086
1159
  }
@@ -1174,10 +1247,18 @@ var FECManager = class _FECManager {
1174
1247
  // 'pending' forever, which would park every later frame of its stream in
1175
1248
  // the in-order release gate. Each entry owns its handle; the callback
1176
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
+ */
1177
1254
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
1178
1255
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
1179
1256
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
1180
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();
1181
1262
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
1182
1263
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
1183
1264
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -1186,6 +1267,9 @@ var FECManager = class _FECManager {
1186
1267
  // SSB_length source arrivals and recoverable from SSB_length received
1187
1268
  // symbols; waiting for K fails every one of them.
1188
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();
1189
1273
  this.#ssbLengthConflictCount = 0;
1190
1274
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
1191
1275
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -1306,6 +1390,10 @@ var FECManager = class _FECManager {
1306
1390
  // ── Keyframe-protected repair policy (block-policy.ts) ──
1307
1391
  // One classifier per physical stream, built from that stream's geometry.
1308
1392
  this.#classifiers = /* @__PURE__ */ new Map();
1393
+ // A stream without catalog timing has no classifier, so the last RAP it
1394
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
1395
+ // a second RAP is released inside one MPU (BLO-52085).
1396
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
1309
1397
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
1310
1398
  // rungs can share a packetId with a different framerate or GOP, so the
1311
1399
  // packet-level geometry is only the geometry of a packet whose streams
@@ -1379,7 +1467,10 @@ var FECManager = class _FECManager {
1379
1467
  // arrives via onBlockRecovered callback.
1380
1468
  this.onBlockStarted = null;
1381
1469
  this.onBlockComplete = null;
1382
- /** 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
+ */
1383
1474
  this.onBlockFailed = null;
1384
1475
  /**
1385
1476
  * Fired once per stream when its Worker decoder stops being routable.
@@ -1421,6 +1512,8 @@ var FECManager = class _FECManager {
1421
1512
  * for it, which it must be.
1422
1513
  */
1423
1514
  this.#onUndelimitedLoss = null;
1515
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
1516
+ this.#onIsoHoleExpired = null;
1424
1517
  /** A block every symbol of which vanished was created by SBN evidence. */
1425
1518
  this.onBlockVanished = null;
1426
1519
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -1463,7 +1556,9 @@ var FECManager = class _FECManager {
1463
1556
  #pendingWorkerCleanupBounds;
1464
1557
  #blockSourceCounts;
1465
1558
  #blockSourceEsis;
1559
+ #blockRebuiltEsis;
1466
1560
  #blockAuthoredLengths;
1561
+ #isoSpanKeys;
1467
1562
  #ssbLengthConflictCount;
1468
1563
  #blockRepairRsIds;
1469
1564
  #blockRsbLengths;
@@ -1505,6 +1600,7 @@ var FECManager = class _FECManager {
1505
1600
  #orderedFrontiers;
1506
1601
  #orderedPendingBlocks;
1507
1602
  #classifiers;
1603
+ #noSpanRaps;
1508
1604
  #streamGeometries;
1509
1605
  #failedBlocks;
1510
1606
  #resolvedBlockClasses;
@@ -1529,6 +1625,7 @@ var FECManager = class _FECManager {
1529
1625
  #decoderUnavailableEvents;
1530
1626
  #repairSymbolSizeMismatchCount;
1531
1627
  #onUndelimitedLoss;
1628
+ #onIsoHoleExpired;
1532
1629
  #encodeTrackName(catalogTrackName) {
1533
1630
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
1534
1631
  if (encoded === void 0) {
@@ -2000,7 +2097,10 @@ var FECManager = class _FECManager {
2000
2097
  timer.handle = null;
2001
2098
  this.#requestRepair(sbn, packetId, catalogTrackName);
2002
2099
  };
2003
- 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
+ );
2004
2104
  this.#repairDeadlines.set(key, timer);
2005
2105
  }
2006
2106
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -2144,8 +2244,14 @@ var FECManager = class _FECManager {
2144
2244
  * Vanished blocks are not enumerated here: their starts are unknown until a
2145
2245
  * repair states them. The SS_IDs between two stated blocks become one
2146
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).
2147
2253
  */
2148
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
2254
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
2149
2255
  const streamKey = this.#streamKey(packetId, catalogTrackName);
2150
2256
  const now = performance.now();
2151
2257
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -2166,6 +2272,7 @@ var FECManager = class _FECManager {
2166
2272
  };
2167
2273
  const discontinuity = () => {
2168
2274
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
2275
+ this.#noSpanRaps.delete(streamKey);
2169
2276
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
2170
2277
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
2171
2278
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -2183,7 +2290,7 @@ var FECManager = class _FECManager {
2183
2290
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
2184
2291
  };
2185
2292
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
2186
- 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))) {
2187
2294
  discontinuity();
2188
2295
  return;
2189
2296
  }
@@ -2445,17 +2552,21 @@ var FECManager = class _FECManager {
2445
2552
  this.#blockWaitDeadlinesMs.delete(key);
2446
2553
  this.#repairHoldUntilMs.delete(key);
2447
2554
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
2448
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
2555
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
2449
2556
  };
2450
- 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
+ );
2451
2561
  this.#activeBlocks.set(key, timer);
2452
2562
  this.#indexBlockKey(key, sbn, packetId);
2453
2563
  }
2454
2564
  /**
2455
2565
  * Hand a block whose delivery is over to its decoder for finalization: the
2456
- * 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.
2457
2568
  */
2458
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
2569
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
2459
2570
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
2460
2571
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
2461
2572
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -2471,7 +2582,7 @@ var FECManager = class _FECManager {
2471
2582
  if (workerClient) {
2472
2583
  this.#pendingWorkerCleanups.add(key);
2473
2584
  this.#pendingWorkerCleanupClients.set(key, workerClient);
2474
- this.#boundOwedCleanup(key, packetId, workerClient);
2585
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
2475
2586
  workerClient.cleanup(sbn);
2476
2587
  } else {
2477
2588
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -2578,6 +2689,9 @@ var FECManager = class _FECManager {
2578
2689
  }
2579
2690
  if (policyChanged) {
2580
2691
  this.#bumpEpoch(packetId);
2692
+ for (const key of [...this.#noSpanRaps.keys()]) {
2693
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
2694
+ }
2581
2695
  for (const [key, classifier] of [...this.#classifiers]) {
2582
2696
  if (!this.#belongsToPacket(key, packetId)) continue;
2583
2697
  const stream = this.#parseStreamKey(key);
@@ -2780,7 +2894,9 @@ var FECManager = class _FECManager {
2780
2894
  }
2781
2895
  resetMap(this.#blockSourceCounts);
2782
2896
  resetMap(this.#blockSourceEsis);
2897
+ resetMap(this.#blockRebuiltEsis);
2783
2898
  resetMap(this.#blockAuthoredLengths);
2899
+ resetSet(this.#isoSpanKeys);
2784
2900
  resetMap(this.#blockRepairRsIds);
2785
2901
  resetMap(this.#blockRsbLengths);
2786
2902
  resetMap(this.#heldLayerStates);
@@ -2813,6 +2929,7 @@ var FECManager = class _FECManager {
2813
2929
  resetMap(this.#isoLocators);
2814
2930
  resetMap(this.#sourceAnchors);
2815
2931
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
2932
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
2816
2933
  this.#bumpEpoch(packetId);
2817
2934
  for (const [sbn, keys] of this.#blockKeysBySbn) {
2818
2935
  for (const key of keys.keys()) {
@@ -2903,7 +3020,9 @@ var FECManager = class _FECManager {
2903
3020
  dropMap(this.#pendingWorkerCleanupClients);
2904
3021
  dropMap(this.#blockSourceCounts);
2905
3022
  dropMap(this.#blockSourceEsis);
3023
+ dropMap(this.#blockRebuiltEsis);
2906
3024
  dropMap(this.#blockAuthoredLengths);
3025
+ dropSet(this.#isoSpanKeys);
2907
3026
  dropMap(this.#blockRepairRsIds);
2908
3027
  dropMap(this.#blockRsbLengths);
2909
3028
  dropMap(this.#blockPathCounts);
@@ -2919,6 +3038,7 @@ var FECManager = class _FECManager {
2919
3038
  dropMap(this.#isoLocators);
2920
3039
  dropMap(this.#sourceAnchors);
2921
3040
  dropMap(this.#classifiers);
3041
+ dropMap(this.#noSpanRaps);
2922
3042
  dropMap(this.#streamGeometries);
2923
3043
  dropSet(this.#pendingWorkerCleanups);
2924
3044
  dropSet(this.#completedBlocks);
@@ -3064,6 +3184,9 @@ var FECManager = class _FECManager {
3064
3184
  this.#unindexBlockKey(key);
3065
3185
  }
3066
3186
  }
3187
+ if (isSourceSymbol && source === "multicast") {
3188
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
3189
+ }
3067
3190
  return true;
3068
3191
  }
3069
3192
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -3242,8 +3365,9 @@ var FECManager = class _FECManager {
3242
3365
  return true;
3243
3366
  }
3244
3367
  /**
3245
- * Drop a block's repair hold: its FETCH ended without delivering. The block
3246
- * 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.
3247
3371
  */
3248
3372
  releaseRepairHold(sbn, packetId, catalogTrackName) {
3249
3373
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -3381,6 +3505,8 @@ var FECManager = class _FECManager {
3381
3505
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
3382
3506
  const evictedBefore = locator.heldEvicted;
3383
3507
  const held = locator.hold(ssId, source);
3508
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
3509
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
3384
3510
  const evicted = locator.heldEvicted - evictedBefore;
3385
3511
  if (evicted > 0) {
3386
3512
  const before = this.#isoHeldEvictedCount;
@@ -3401,17 +3527,20 @@ var FECManager = class _FECManager {
3401
3527
  * contradicts a known block is refused (counted) and must be dropped.
3402
3528
  */
3403
3529
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
3404
- 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);
3405
3533
  if (placement.status !== "placed") {
3406
3534
  this.#isoRepairRefusedCount++;
3407
3535
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
3408
3536
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
3409
3537
  }
3538
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
3410
3539
  return placement;
3411
3540
  }
3541
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
3412
3542
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
3413
3543
  if (placement.hole !== void 0) {
3414
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
3415
3544
  const hole = placement.hole.block;
3416
3545
  const previous = locator.blockEndingAt(hole.start);
3417
3546
  const loss = {
@@ -3432,17 +3561,29 @@ var FECManager = class _FECManager {
3432
3561
  * `handler`, instead of a repair-less span; null restores spans. Set by the
3433
3562
  * range repair scheduler; applies to every stream from its next gap. The
3434
3563
  * handler and the deferral are one setting, so no hole is ever created with
3435
- * 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.
3436
3568
  */
3437
- setUndelimitedLossHandler(handler) {
3569
+ setUndelimitedLossHandler(handler, onExpired = null) {
3438
3570
  this.#onUndelimitedLoss = handler;
3571
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
3439
3572
  this.#deferIsoGaps = handler !== null;
3440
3573
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
3441
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
+ }
3442
3582
  /**
3443
- * The range repair for a hole ended: every hole still pending inside
3444
- * `[start, start + length)` becomes a repair-less span. Returns those spans
3445
- * 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).
3446
3587
  */
3447
3588
  closeIsoHoles(packetId, catalogTrackName, start, length) {
3448
3589
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -3461,7 +3602,10 @@ var FECManager = class _FECManager {
3461
3602
  */
3462
3603
  #openIsoSpan(span, packetId, catalogTrackName) {
3463
3604
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
3464
- 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
+ }
3465
3609
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
3466
3610
  }
3467
3611
  /**
@@ -3549,11 +3693,14 @@ var FECManager = class _FECManager {
3549
3693
  /**
3550
3694
  * Whether an MPU boundary still bounds this stream's RAPs (see
3551
3695
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
3552
- * it does not. A stream with no classifier yet has shown nothing against it.
3696
+ * it does not. A stream with no classifier yet has shown nothing against it,
3697
+ * and neither has a stream without catalog timing until it releases two
3698
+ * RAPs inside one MPU.
3553
3699
  */
3554
3700
  mpuBoundsRapsFor(packetId, catalogTrackName) {
3555
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
3556
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
3701
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
3702
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
3703
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
3557
3704
  }
3558
3705
  /** A block's class now, or frozen at its resolution. */
3559
3706
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -3579,7 +3726,14 @@ var FECManager = class _FECManager {
3579
3726
  * invariant (consecutive MPUs exactly one keyframe interval apart).
3580
3727
  */
3581
3728
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
3582
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
3729
+ if (!this.#trackConfigs.has(packetId)) return;
3730
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
3731
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
3732
+ const last = this.#noSpanRaps.get(streamKey);
3733
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
3734
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
3735
+ return;
3736
+ }
3583
3737
  this.#applyClassifierChanges(
3584
3738
  packetId,
3585
3739
  catalogTrackName,
@@ -3792,11 +3946,14 @@ var FECManager = class _FECManager {
3792
3946
  });
3793
3947
  client.onCleanupComplete((sbn) => {
3794
3948
  if (this.#releasedWorkerClients.has(client)) return;
3795
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
3949
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
3950
+ const cleanupKeys = owed.filter((key) => {
3796
3951
  const block = this.#parseBlockKey(key);
3797
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
3952
+ return block.packetId === packetId && block.sbn === sbn;
3798
3953
  });
3799
- 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");
3800
3957
  });
3801
3958
  client.onError((msg, code) => {
3802
3959
  const requestedKind = code ?? msg;
@@ -3866,9 +4023,11 @@ var FECManager = class _FECManager {
3866
4023
  * fail deadline already passed, so failing it is the decision that deadline
3867
4024
  * made. A late ack then finds nothing owed and is a no-op.
3868
4025
  */
3869
- #boundOwedCleanup(key, packetId, client) {
4026
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
3870
4027
  this.#clearOwedCleanupBound(key);
3871
4028
  const bound = {
4029
+ dueAtMs,
4030
+ askedAtMs: performance.now(),
3872
4031
  handle: setTimeout(() => {
3873
4032
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
3874
4033
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -3966,7 +4125,15 @@ var FECManager = class _FECManager {
3966
4125
  this.#isoSourceOversizeCount++;
3967
4126
  return null;
3968
4127
  }
3969
- 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) {
3970
4137
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
3971
4138
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
3972
4139
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -3995,6 +4162,74 @@ var FECManager = class _FECManager {
3995
4162
  }
3996
4163
  return recovered;
3997
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
+ }
3998
4233
  /**
3999
4234
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
4000
4235
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -4197,8 +4432,75 @@ var FECManager = class _FECManager {
4197
4432
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
4198
4433
  return null;
4199
4434
  }
4435
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
4200
4436
  return recovered;
4201
4437
  }
4438
+ /**
4439
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
4440
+ * its sources in send order, so one that moves the stream's newest source
4441
+ * position past a block leaves the sources that block still misses
4442
+ * reordered behind it at best: it shortens the wait of every block it
4443
+ * passed that only a late source can complete. MoQ is left out: its streams
4444
+ * can overtake one another, so a MoQ source shows nothing about the sources
4445
+ * sent before it.
4446
+ */
4447
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
4448
+ const previous = locator.newestSource;
4449
+ if (!locator.noteSource(ssId) || previous === void 0) return;
4450
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
4451
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
4452
+ this.#shortenUnrecoverableWait(
4453
+ this.#blockKey(block.start, packetId, catalogTrackName),
4454
+ block.start,
4455
+ packetId,
4456
+ catalogTrackName
4457
+ );
4458
+ }
4459
+ }
4460
+ /**
4461
+ * The wait of a block that only a late source can complete (BLO-52085): a
4462
+ * repair stated it, its received sources plus every symbol its repair group
4463
+ * carries (RSB_length) are short of K' (the repair scheduler's
4464
+ * group-cannot-complete), and a later source of the stream has arrived, so
4465
+ * a source it misses is lost or reordered behind that one. The span, which
4466
+ * covers repair symbols paced after its sources, buys it nothing: it waits
4467
+ * the late-symbol allowance alone, the multicast wait less the span (the
4468
+ * catalog tolerance or the measured delivery gap). Undefined for any other
4469
+ * block.
4470
+ */
4471
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
4472
+ const authoredLength = this.#blockAuthoredLengths.get(key);
4473
+ const rsbLength = this.#blockRsbLengths.get(key);
4474
+ if (authoredLength === void 0 || rsbLength === void 0) 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;
4479
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
4480
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
4481
+ return void 0;
4482
+ }
4483
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
4484
+ }
4485
+ /**
4486
+ * Bring an open block's wait in to its late-source wait, when it has one.
4487
+ * Never later than it is armed: only a source can complete such a block,
4488
+ * so a repair of its group (paced across the span) must not keep it open.
4489
+ */
4490
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
4491
+ if (!this.#activeBlocks.has(key)) return;
4492
+ const config = this.#trackConfigs.get(packetId);
4493
+ if (config === void 0) return;
4494
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
4495
+ if (waitMs === void 0) return;
4496
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
4497
+ this.#armBlockTimeout(
4498
+ sbn,
4499
+ packetId,
4500
+ catalogTrackName,
4501
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
4502
+ );
4503
+ }
4202
4504
  /**
4203
4505
  * Record a block's authored SSB_length from a repair symbol. Returns false,
4204
4506
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -4296,12 +4598,13 @@ var FECManager = class _FECManager {
4296
4598
  * the inactivity deadline on each symbol, records the symbol's in-band
4297
4599
  * classification evidence, and marks the block clean when all of its
4298
4600
  * authored source symbols arrived: the SSB_length a repair symbol declared,
4299
- * otherwise K.
4601
+ * otherwise K. `held` marks a source held unplaced since it arrived and
4602
+ * placed only now (see #noteSbnSeen).
4300
4603
  *
4301
4604
  * Throws for a packetId that was never configured, so a classified block
4302
4605
  * never reads the manager-global K or timeout.
4303
4606
  */
4304
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
4607
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
4305
4608
  const trackConfig = this.#trackConfigs.get(packetId);
4306
4609
  if (trackConfig === void 0) {
4307
4610
  throw new Error(
@@ -4317,7 +4620,8 @@ var FECManager = class _FECManager {
4317
4620
  catalogTrackName,
4318
4621
  "source",
4319
4622
  evidence.packetSequenceNumber,
4320
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
4623
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
4624
+ held
4321
4625
  );
4322
4626
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
4323
4627
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -4353,6 +4657,15 @@ var FECManager = class _FECManager {
4353
4657
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
4354
4658
  return;
4355
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
+ }
4356
4669
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
4357
4670
  }
4358
4671
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -4361,7 +4674,9 @@ var FECManager = class _FECManager {
4361
4674
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
4362
4675
  this.#blockSourceCounts.delete(key);
4363
4676
  this.#blockSourceEsis.delete(key);
4677
+ this.#blockRebuiltEsis.delete(key);
4364
4678
  this.#blockAuthoredLengths.delete(key);
4679
+ this.#isoSpanKeys.delete(key);
4365
4680
  this.#blockRepairRsIds.delete(key);
4366
4681
  this.#blockRsbLengths.delete(key);
4367
4682
  this.#markTerminal(key, sbn, packetId, true);
@@ -4392,6 +4707,19 @@ var FECManager = class _FECManager {
4392
4707
  #receiptNow() {
4393
4708
  return this.#receiptAtMs ?? performance.now();
4394
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
+ }
4395
4723
  /**
4396
4724
  * A held packet admitted late arms from its (earlier) receipt time; that
4397
4725
  * never pulls in a deadline later activity already armed. Outside a
@@ -4427,10 +4755,20 @@ var FECManager = class _FECManager {
4427
4755
  this.#repairDeadlineMs(trackConfig)
4428
4756
  );
4429
4757
  }
4430
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
4758
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
4759
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
4760
+ return;
4761
+ }
4762
+ this.#armBlockTimeout(
4763
+ sbn,
4764
+ packetId,
4765
+ catalogTrackName,
4766
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
4767
+ );
4431
4768
  }
4432
4769
  #failBlock(sbn, packetId, catalogTrackName) {
4433
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;
4434
4772
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
4435
4773
  this.#clearRepairDeadline(key);
4436
4774
  this.#clearBlockTimer(key);
@@ -4440,6 +4778,7 @@ var FECManager = class _FECManager {
4440
4778
  }
4441
4779
  this.#blockSourceCounts.delete(key);
4442
4780
  this.#blockSourceEsis.delete(key);
4781
+ this.#blockRebuiltEsis.delete(key);
4443
4782
  this.#blockAuthoredLengths.delete(key);
4444
4783
  this.#blockRepairRsIds.delete(key);
4445
4784
  this.#blockRsbLengths.delete(key);
@@ -4449,7 +4788,8 @@ var FECManager = class _FECManager {
4449
4788
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
4450
4789
  this.#fecStats.blocksFailed++;
4451
4790
  this.#statsForPacket(packetId).blocksFailed++;
4452
- 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);
4453
4793
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
4454
4794
  }
4455
4795
  /**
@@ -4478,7 +4818,9 @@ var FECManager = class _FECManager {
4478
4818
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
4479
4819
  this.#blockSourceCounts.delete(key);
4480
4820
  this.#blockSourceEsis.delete(key);
4821
+ this.#blockRebuiltEsis.delete(key);
4481
4822
  this.#blockAuthoredLengths.delete(key);
4823
+ this.#isoSpanKeys.delete(key);
4482
4824
  this.#blockRepairRsIds.delete(key);
4483
4825
  this.#blockRsbLengths.delete(key);
4484
4826
  this.#clearBlockTimer(key);
@@ -4569,7 +4911,9 @@ var FECManager = class _FECManager {
4569
4911
  this.#blockKeysBySbn.clear();
4570
4912
  this.#blockSourceCounts.clear();
4571
4913
  this.#blockSourceEsis.clear();
4914
+ this.#blockRebuiltEsis.clear();
4572
4915
  this.#blockAuthoredLengths.clear();
4916
+ this.#isoSpanKeys.clear();
4573
4917
  this.#blockRepairRsIds.clear();
4574
4918
  this.#blockRsbLengths.clear();
4575
4919
  this.#heldLayerStates.clear();
@@ -4594,6 +4938,7 @@ var FECManager = class _FECManager {
4594
4938
  this.#blockAnchorsMs.clear();
4595
4939
  this.#blockCatalogTrackNames.clear();
4596
4940
  this.#classifiers.clear();
4941
+ this.#noSpanRaps.clear();
4597
4942
  this.#streamGeometries.clear();
4598
4943
  this.#failedBlocks.clear();
4599
4944
  this.#resolvedBlockClasses.clear();