@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 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 (82) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1336 -302
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +26 -16
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +70 -12
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +403 -58
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/iso-block-locator.d.ts +48 -13
  27. package/dist/iso-block-locator.d.ts.map +1 -1
  28. package/dist/iso-block-locator.js +97 -25
  29. package/dist/iso-block-locator.js.map +2 -2
  30. package/dist/iso-bmff-timing.d.ts +10 -1
  31. package/dist/iso-bmff-timing.d.ts.map +1 -1
  32. package/dist/iso-bmff-timing.js +36 -2
  33. package/dist/iso-bmff-timing.js.map +2 -2
  34. package/dist/media-router.js.map +1 -1
  35. package/dist/mmtp-container-parser.d.ts +79 -10
  36. package/dist/mmtp-container-parser.d.ts.map +1 -1
  37. package/dist/mmtp-container-parser.js +207 -26
  38. package/dist/mmtp-container-parser.js.map +2 -2
  39. package/dist/mmtp-container-parser.no-modules.js +209 -29
  40. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  41. package/dist/mmtp-decode-pipeline.js +206 -26
  42. package/dist/mmtp-decode-pipeline.js.map +2 -2
  43. package/dist/mmtp-router.d.ts +34 -8
  44. package/dist/mmtp-router.d.ts.map +1 -1
  45. package/dist/mmtp-router.js +555 -79
  46. package/dist/mmtp-router.js.map +2 -2
  47. package/dist/multicast-manager.d.ts +13 -3
  48. package/dist/multicast-manager.d.ts.map +1 -1
  49. package/dist/multicast-manager.js +980 -146
  50. package/dist/multicast-manager.js.map +2 -2
  51. package/dist/multicast-transport.js +28 -18
  52. package/dist/multicast-transport.js.map +2 -2
  53. package/dist/no-modules.js +206 -26
  54. package/dist/no-modules.js.map +2 -2
  55. package/dist/repair-fetch.d.ts +36 -8
  56. package/dist/repair-fetch.d.ts.map +1 -1
  57. package/dist/repair-fetch.js +285 -26
  58. package/dist/repair-fetch.js.map +3 -3
  59. package/dist/repair-layer-subscriptions.d.ts +6 -2
  60. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  61. package/dist/repair-layer-subscriptions.js +6 -3
  62. package/dist/repair-layer-subscriptions.js.map +2 -2
  63. package/dist/track-session.d.ts +1 -1
  64. package/dist/track-session.d.ts.map +1 -1
  65. package/dist/track-session.js +1 -0
  66. package/dist/track-session.js.map +2 -2
  67. package/dist/wasm-mmtp-container-parser.js +206 -26
  68. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  69. package/package.json +8 -5
  70. package/src/catalog-fec-params.ts +36 -1
  71. package/src/cmcd-producer.ts +189 -179
  72. package/src/fec-manager.ts +433 -32
  73. package/src/fec-worker-setup.ts +2 -2
  74. package/src/index.ts +9 -2
  75. package/src/iso-block-locator.ts +133 -31
  76. package/src/iso-bmff-timing.ts +40 -1
  77. package/src/mmtp-container-parser.ts +306 -29
  78. package/src/mmtp-router.ts +548 -38
  79. package/src/multicast-manager.ts +44 -6
  80. package/src/repair-fetch.ts +363 -49
  81. package/src/repair-layer-subscriptions.ts +13 -5
  82. package/src/track-session.ts +4 -1
@@ -1189,6 +1189,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
1189
1189
  this.#heldEvicted = 0;
1190
1190
  /** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
1191
1191
  this.#holes = [];
1192
+ /** Holes the horizon passed while pending: sources held until closed. */
1193
+ this.#expired = [];
1194
+ /** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
1195
+ this.#newlyExpired = [];
1196
+ /**
1197
+ * Spans closed from expired holes: behind the horizon, so kept only for
1198
+ * `locate` while their sources are fed, until the next retirement.
1199
+ */
1200
+ this.#closedBehind = [];
1192
1201
  /**
1193
1202
  * Keep a gap that is missing a source as a pending hole rather than a span,
1194
1203
  * so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
@@ -1210,7 +1219,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
1210
1219
  #retentionBlocks;
1211
1220
  #heldEvicted;
1212
1221
  #newest;
1222
+ #newestSource;
1213
1223
  #holes;
1224
+ #expired;
1225
+ #newlyExpired;
1226
+ #closedBehind;
1214
1227
  static #checkedRetention(retentionBlocks) {
1215
1228
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
1216
1229
  throw new Error(
@@ -1247,6 +1260,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
1247
1260
  observe(position) {
1248
1261
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
1249
1262
  }
1263
+ /**
1264
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
1265
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
1266
+ * a source cannot run ahead of the sources sent before it except by
1267
+ * reordering.
1268
+ */
1269
+ get newestSource() {
1270
+ return this.#newestSource;
1271
+ }
1272
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
1273
+ noteSource(ssId) {
1274
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
1275
+ this.#newestSource = ssId;
1276
+ return true;
1277
+ }
1250
1278
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
1251
1279
  get heldEvicted() {
1252
1280
  return this.#heldEvicted;
@@ -1265,17 +1293,29 @@ var IsoBlockLocator = class _IsoBlockLocator {
1265
1293
  }
1266
1294
  /** The known block or span containing `ssId`, with its ESI. */
1267
1295
  locate(ssId) {
1268
- for (let i = this.#blocks.length - 1; i >= 0; i--) {
1269
- const block = this.#blocks[i];
1270
- if (isoBlockContains(block.start, block.length, ssId)) {
1271
- return { block, esi: serial32Distance(block.start, ssId) };
1296
+ for (const blocks of [this.#blocks, this.#closedBehind]) {
1297
+ for (let i = blocks.length - 1; i >= 0; i--) {
1298
+ const block = blocks[i];
1299
+ if (isoBlockContains(block.start, block.length, ssId)) {
1300
+ return { block, esi: serial32Distance(block.start, ssId) };
1301
+ }
1272
1302
  }
1273
1303
  }
1274
1304
  return null;
1275
1305
  }
1276
- /** Number of pending holes. */
1306
+ /** Number of holes not closed yet, pending or past the horizon. */
1277
1307
  get holeCount() {
1278
- return this.#holes.length;
1308
+ return this.#holes.length + this.#expired.length;
1309
+ }
1310
+ /**
1311
+ * The pending holes the horizon has passed since the last call, in the
1312
+ * order it passed them. Each is no longer pending: no repair states a block
1313
+ * in it (that repair is `stale`, as for a span the horizon retired) and the
1314
+ * release gate does not wait for it. Its owner must close it
1315
+ * (`closeHolesWithin`), which turns it into a span that is not kept here.
1316
+ */
1317
+ takeExpiredHoles() {
1318
+ return this.#newlyExpired.splice(0);
1279
1319
  }
1280
1320
  /** The stated block (never a span) ending exactly at `end`. */
1281
1321
  blockEndingAt(end) {
@@ -1331,9 +1371,11 @@ var IsoBlockLocator = class _IsoBlockLocator {
1331
1371
  /**
1332
1372
  * Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
1333
1373
  * block states its range and places every held source in it; later repair
1334
- * symbols of the same block change nothing. A range that contradicts a
1335
- * known block (a different length, or an overlap with another block or a
1336
- * span) fails closed: the repair symbol is dropped and no block changes.
1374
+ * symbols of the same block change nothing. A repair whose range is exactly
1375
+ * a repair-less span states that span's one block. A range that contradicts
1376
+ * a known block (a different length, or an overlap with another block or
1377
+ * with part of a span) fails closed: the repair symbol is dropped and no
1378
+ * block changes.
1337
1379
  * A newly stated block retires behind its SS_Start (see the class note).
1338
1380
  * A block so far past `newest` that stating it would retire the position
1339
1381
  * `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
@@ -1352,7 +1394,16 @@ var IsoBlockLocator = class _IsoBlockLocator {
1352
1394
  const known = this.blockAt(start);
1353
1395
  if (known !== void 0) {
1354
1396
  if (known.span) {
1355
- return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
1397
+ if (known.length === length) {
1398
+ const block2 = { start, length, span: false };
1399
+ this.#blocks[this.#blocks.indexOf(known)] = block2;
1400
+ return { status: "placed", block: block2, placed: [] };
1401
+ }
1402
+ return length < known.length ? {
1403
+ status: "overlap",
1404
+ reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
1405
+ ofSpan: known
1406
+ } : { status: "overlap", reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` };
1356
1407
  }
1357
1408
  if (known.length !== length) {
1358
1409
  return {
@@ -1420,18 +1471,28 @@ var IsoBlockLocator = class _IsoBlockLocator {
1420
1471
  return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
1421
1472
  }
1422
1473
  /**
1423
- * Turn every pending hole inside `[start, start + length)` into a
1424
- * repair-less span, claiming its held sources: its range FETCH ended, so
1425
- * nothing more will state its blocks. Returns the spans, in SS_ID order.
1474
+ * Turn every hole inside `[start, start + length)` into a repair-less span,
1475
+ * claiming its held sources: its range FETCH ended, so nothing more will
1476
+ * state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
1477
+ * span that is not kept among the blocks, as the horizon would retire it at
1478
+ * once; only `locate` finds it, until the next retirement, so its sources
1479
+ * can be fed. Returns the spans, in SS_ID order.
1426
1480
  */
1427
1481
  closeHolesWithin(start, length) {
1428
1482
  const closed = [];
1429
- for (const hole of [...this.#holes]) {
1430
- if (!isoBlockContains(start, length, hole.start)) continue;
1431
- this.#holes.splice(this.#holes.indexOf(hole), 1);
1432
- const spanBlock = { start: hole.start, length: hole.length, span: true };
1433
- closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
1434
- this.#blocks.push(spanBlock);
1483
+ for (const holes of [this.#holes, this.#expired]) {
1484
+ for (const hole of [...holes]) {
1485
+ if (!isoBlockContains(start, length, hole.start)) continue;
1486
+ holes.splice(holes.indexOf(hole), 1);
1487
+ const spanBlock = { start: hole.start, length: hole.length, span: true };
1488
+ closed.push({ block: spanBlock, placed: this.#claim(spanBlock) });
1489
+ if (holes === this.#holes) {
1490
+ this.#blocks.push(spanBlock);
1491
+ } else {
1492
+ this.#closedBehind.push(spanBlock);
1493
+ if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1);
1494
+ }
1495
+ }
1435
1496
  }
1436
1497
  return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start));
1437
1498
  }
@@ -1445,15 +1506,17 @@ var IsoBlockLocator = class _IsoBlockLocator {
1445
1506
  return void 0;
1446
1507
  }
1447
1508
  /**
1448
- * Retire every block and span whose last SS_ID, and every held source,
1449
- * lies more than `retentionBlocks` blocks behind `position`, with the shared
1450
- * lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
1451
- * horizon uses. Returns the retired block starts.
1509
+ * Retire every block and span whose last SS_ID, and every held source
1510
+ * outside a hole, lies more than `retentionBlocks` blocks behind
1511
+ * `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
1512
+ * every ISO interleave horizon uses; a pending hole so far behind expires
1513
+ * (`takeExpiredHoles`). Returns the retired block starts.
1452
1514
  */
1453
1515
  retireBehind(position) {
1454
1516
  this.observe(position);
1455
1517
  const horizon = this.#retentionBlocks;
1456
1518
  const retired = [];
1519
+ this.#closedBehind.length = 0;
1457
1520
  for (let i = this.#blocks.length - 1; i >= 0; i--) {
1458
1521
  const block = this.#blocks[i];
1459
1522
  const last = serial32Add(block.start, block.length - 1);
@@ -1465,10 +1528,15 @@ var IsoBlockLocator = class _IsoBlockLocator {
1465
1528
  for (let i = this.#holes.length - 1; i >= 0; i--) {
1466
1529
  const hole = this.#holes[i];
1467
1530
  const last = serial32Add(hole.start, hole.length - 1);
1468
- if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) this.#holes.splice(i, 1);
1531
+ if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue;
1532
+ this.#holes.splice(i, 1);
1533
+ this.#expired.push(hole);
1534
+ this.#newlyExpired.push(hole);
1469
1535
  }
1470
1536
  for (const ssId of [...this.#held.keys()]) {
1471
- if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
1537
+ 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))) {
1538
+ this.#held.delete(ssId);
1539
+ }
1472
1540
  }
1473
1541
  return retired;
1474
1542
  }
@@ -1476,9 +1544,13 @@ var IsoBlockLocator = class _IsoBlockLocator {
1476
1544
  reset() {
1477
1545
  this.#blocks.length = 0;
1478
1546
  this.#holes.length = 0;
1547
+ this.#expired.length = 0;
1548
+ this.#newlyExpired.length = 0;
1549
+ this.#closedBehind.length = 0;
1479
1550
  this.#held.clear();
1480
1551
  this.#coverageEnd = void 0;
1481
1552
  this.#newest = void 0;
1553
+ this.#newestSource = void 0;
1482
1554
  }
1483
1555
  #claim(block) {
1484
1556
  const placed = [];
@@ -2067,6 +2139,7 @@ var WORKER_ERROR_LOG_CAP = 5;
2067
2139
  var WORKER_ERROR_KIND_CAP = 32;
2068
2140
  var WORKER_ERROR_OVERFLOW_KIND = "other";
2069
2141
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
2142
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
2070
2143
  function isMulticastSymbolDelivery(value) {
2071
2144
  return value === "datagram" || value === "ordered-relay";
2072
2145
  }
@@ -2160,10 +2233,18 @@ var FECManager = class _FECManager {
2160
2233
  // 'pending' forever, which would park every later frame of its stream in
2161
2234
  // the in-order release gate. Each entry owns its handle; the callback
2162
2235
  // checks it still owns the key, so a settled or reset key is never touched.
2236
+ /**
2237
+ * Per owed Worker cleanup: the bound on its acknowledgement, when its block
2238
+ * was due, and when its cleanup was asked for (see `onCleanupComplete`).
2239
+ */
2163
2240
  this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
2164
2241
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
2165
2242
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
2166
2243
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
2244
+ // Source ESIs per block that another FEC instance rebuilt and fed in
2245
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
2246
+ // not network arrivals and never make a block clean.
2247
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
2167
2248
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
2168
2249
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
2169
2250
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -2172,6 +2253,9 @@ var FECManager = class _FECManager {
2172
2253
  // SSB_length source arrivals and recoverable from SSB_length received
2173
2254
  // symbols; waiting for K fails every one of them.
2174
2255
  this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
2256
+ // Keys whose authored length is a repair-less span's (#openIsoSpan), until
2257
+ // a repair states the block there or the span resolves.
2258
+ this.#isoSpanKeys = /* @__PURE__ */ new Set();
2175
2259
  this.#ssbLengthConflictCount = 0;
2176
2260
  this.#blockRepairRsIds = /* @__PURE__ */ new Map();
2177
2261
  // RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
@@ -2292,6 +2376,10 @@ var FECManager = class _FECManager {
2292
2376
  // ── Keyframe-protected repair policy (block-policy.ts) ──
2293
2377
  // One classifier per physical stream, built from that stream's geometry.
2294
2378
  this.#classifiers = /* @__PURE__ */ new Map();
2379
+ // A stream without catalog timing has no classifier, so the last RAP it
2380
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
2381
+ // a second RAP is released inside one MPU (BLO-52085).
2382
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
2295
2383
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
2296
2384
  // rungs can share a packetId with a different framerate or GOP, so the
2297
2385
  // packet-level geometry is only the geometry of a packet whose streams
@@ -2365,7 +2453,10 @@ var FECManager = class _FECManager {
2365
2453
  // arrives via onBlockRecovered callback.
2366
2454
  this.onBlockStarted = null;
2367
2455
  this.onBlockComplete = null;
2368
- /** A block reached its fail bound. `blockClass` is its class at failure. */
2456
+ /**
2457
+ * A block reached its fail bound. `blockClass` is its class at failure.
2458
+ * `span` is set when what failed is a repair-less span starting at `sbn`.
2459
+ */
2369
2460
  this.onBlockFailed = null;
2370
2461
  /**
2371
2462
  * Fired once per stream when its Worker decoder stops being routable.
@@ -2407,6 +2498,8 @@ var FECManager = class _FECManager {
2407
2498
  * for it, which it must be.
2408
2499
  */
2409
2500
  this.#onUndelimitedLoss = null;
2501
+ /** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
2502
+ this.#onIsoHoleExpired = null;
2410
2503
  /** A block every symbol of which vanished was created by SBN evidence. */
2411
2504
  this.onBlockVanished = null;
2412
2505
  /** A block reached a terminal state (completed, recovered, or failed). */
@@ -2449,7 +2542,9 @@ var FECManager = class _FECManager {
2449
2542
  #pendingWorkerCleanupBounds;
2450
2543
  #blockSourceCounts;
2451
2544
  #blockSourceEsis;
2545
+ #blockRebuiltEsis;
2452
2546
  #blockAuthoredLengths;
2547
+ #isoSpanKeys;
2453
2548
  #ssbLengthConflictCount;
2454
2549
  #blockRepairRsIds;
2455
2550
  #blockRsbLengths;
@@ -2491,6 +2586,7 @@ var FECManager = class _FECManager {
2491
2586
  #orderedFrontiers;
2492
2587
  #orderedPendingBlocks;
2493
2588
  #classifiers;
2589
+ #noSpanRaps;
2494
2590
  #streamGeometries;
2495
2591
  #failedBlocks;
2496
2592
  #resolvedBlockClasses;
@@ -2515,6 +2611,7 @@ var FECManager = class _FECManager {
2515
2611
  #decoderUnavailableEvents;
2516
2612
  #repairSymbolSizeMismatchCount;
2517
2613
  #onUndelimitedLoss;
2614
+ #onIsoHoleExpired;
2518
2615
  #encodeTrackName(catalogTrackName) {
2519
2616
  let encoded = this.#encodedTrackNames.get(catalogTrackName);
2520
2617
  if (encoded === void 0) {
@@ -2986,7 +3083,10 @@ var FECManager = class _FECManager {
2986
3083
  timer.handle = null;
2987
3084
  this.#requestRepair(sbn, packetId, catalogTrackName);
2988
3085
  };
2989
- timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()));
3086
+ timer.handle = setTimeout(
3087
+ onFire,
3088
+ this.#receiptAtMs === void 0 ? Math.max(0, deadlineMs - performance.now()) : this.#receiptTimerDelayMs(deadlineMs, 0)
3089
+ );
2990
3090
  this.#repairDeadlines.set(key, timer);
2991
3091
  }
2992
3092
  #requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
@@ -3130,8 +3230,14 @@ var FECManager = class _FECManager {
3130
3230
  * Vanished blocks are not enumerated here: their starts are unknown until a
3131
3231
  * repair states them. The SS_IDs between two stated blocks become one
3132
3232
  * repair-less span when the later block's repair arrives (placeIsoRepair).
3233
+ *
3234
+ * A `held` source arrived before its block was known and is fed only now
3235
+ * that a repair, a span or a closed hole placed it: how far its block start
3236
+ * then lies behind the frontier measures how long the receiver held it, not
3237
+ * where the stream was, so only its SS_ID and packet_sequence_number judge
3238
+ * it (BLO-52085).
3133
3239
  */
3134
- #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn) {
3240
+ #noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber, ssId = sbn, held = false) {
3135
3241
  const streamKey = this.#streamKey(packetId, catalogTrackName);
3136
3242
  const now = performance.now();
3137
3243
  const previous = (symbol === "source" ? this.#sourceFrontiers : this.#highestSbn).get(streamKey);
@@ -3152,6 +3258,7 @@ var FECManager = class _FECManager {
3152
3258
  };
3153
3259
  const discontinuity = () => {
3154
3260
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
3261
+ this.#noSpanRaps.delete(streamKey);
3155
3262
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
3156
3263
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
3157
3264
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -3169,7 +3276,7 @@ var FECManager = class _FECManager {
3169
3276
  return sent <= 2147483647 && serial32Distance(from.ssId, to.ssId) <= sent + config.k;
3170
3277
  };
3171
3278
  if (!this.#blockPrecedes(previous.sbn, sbn)) {
3172
- if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
3279
+ if (symbol === "source" && window2 !== null && this.#blockPrecedes(sbn, previous.sbn) && (!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) || psn !== void 0 && anchor !== void 0 && serial32Precedes(ssId, anchor.ssId) && psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))) {
3173
3280
  discontinuity();
3174
3281
  return;
3175
3282
  }
@@ -3431,17 +3538,21 @@ var FECManager = class _FECManager {
3431
3538
  this.#blockWaitDeadlinesMs.delete(key);
3432
3539
  this.#repairHoldUntilMs.delete(key);
3433
3540
  this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
3434
- this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
3541
+ this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs);
3435
3542
  };
3436
- timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
3543
+ timer.handle = setTimeout(
3544
+ onFire,
3545
+ this.#receiptAtMs === void 0 ? Math.max(1, Math.ceil(deadlineMs - nowMs)) : this.#receiptTimerDelayMs(deadlineMs, 1)
3546
+ );
3437
3547
  this.#activeBlocks.set(key, timer);
3438
3548
  this.#indexBlockKey(key, sbn, packetId);
3439
3549
  }
3440
3550
  /**
3441
3551
  * Hand a block whose delivery is over to its decoder for finalization: the
3442
- * datagram deadline fired, or the ordered relay moved past it.
3552
+ * datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
3553
+ * is when it was due: its deadline, or now.
3443
3554
  */
3444
- #finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
3555
+ #finalizeExpiredBlock(sbn, packetId, catalogTrackName, dueAtMs = performance.now()) {
3445
3556
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
3446
3557
  if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
3447
3558
  this.#failBlock(sbn, packetId, catalogTrackName);
@@ -3457,7 +3568,7 @@ var FECManager = class _FECManager {
3457
3568
  if (workerClient) {
3458
3569
  this.#pendingWorkerCleanups.add(key);
3459
3570
  this.#pendingWorkerCleanupClients.set(key, workerClient);
3460
- this.#boundOwedCleanup(key, packetId, workerClient);
3571
+ this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs);
3461
3572
  workerClient.cleanup(sbn);
3462
3573
  } else {
3463
3574
  const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
@@ -3564,6 +3675,9 @@ var FECManager = class _FECManager {
3564
3675
  }
3565
3676
  if (policyChanged) {
3566
3677
  this.#bumpEpoch(packetId);
3678
+ for (const key of [...this.#noSpanRaps.keys()]) {
3679
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
3680
+ }
3567
3681
  for (const [key, classifier] of [...this.#classifiers]) {
3568
3682
  if (!this.#belongsToPacket(key, packetId)) continue;
3569
3683
  const stream = this.#parseStreamKey(key);
@@ -3766,7 +3880,9 @@ var FECManager = class _FECManager {
3766
3880
  }
3767
3881
  resetMap(this.#blockSourceCounts);
3768
3882
  resetMap(this.#blockSourceEsis);
3883
+ resetMap(this.#blockRebuiltEsis);
3769
3884
  resetMap(this.#blockAuthoredLengths);
3885
+ resetSet(this.#isoSpanKeys);
3770
3886
  resetMap(this.#blockRepairRsIds);
3771
3887
  resetMap(this.#blockRsbLengths);
3772
3888
  resetMap(this.#heldLayerStates);
@@ -3799,6 +3915,7 @@ var FECManager = class _FECManager {
3799
3915
  resetMap(this.#isoLocators);
3800
3916
  resetMap(this.#sourceAnchors);
3801
3917
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
3918
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
3802
3919
  this.#bumpEpoch(packetId);
3803
3920
  for (const [sbn, keys] of this.#blockKeysBySbn) {
3804
3921
  for (const key of keys.keys()) {
@@ -3889,7 +4006,9 @@ var FECManager = class _FECManager {
3889
4006
  dropMap(this.#pendingWorkerCleanupClients);
3890
4007
  dropMap(this.#blockSourceCounts);
3891
4008
  dropMap(this.#blockSourceEsis);
4009
+ dropMap(this.#blockRebuiltEsis);
3892
4010
  dropMap(this.#blockAuthoredLengths);
4011
+ dropSet(this.#isoSpanKeys);
3893
4012
  dropMap(this.#blockRepairRsIds);
3894
4013
  dropMap(this.#blockRsbLengths);
3895
4014
  dropMap(this.#blockPathCounts);
@@ -3905,6 +4024,7 @@ var FECManager = class _FECManager {
3905
4024
  dropMap(this.#isoLocators);
3906
4025
  dropMap(this.#sourceAnchors);
3907
4026
  dropMap(this.#classifiers);
4027
+ dropMap(this.#noSpanRaps);
3908
4028
  dropMap(this.#streamGeometries);
3909
4029
  dropSet(this.#pendingWorkerCleanups);
3910
4030
  dropSet(this.#completedBlocks);
@@ -4050,6 +4170,9 @@ var FECManager = class _FECManager {
4050
4170
  this.#unindexBlockKey(key);
4051
4171
  }
4052
4172
  }
4173
+ if (isSourceSymbol && source === "multicast") {
4174
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
4175
+ }
4053
4176
  return true;
4054
4177
  }
4055
4178
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -4228,8 +4351,9 @@ var FECManager = class _FECManager {
4228
4351
  return true;
4229
4352
  }
4230
4353
  /**
4231
- * Drop a block's repair hold: its FETCH ended without delivering. The block
4232
- * falls back to its multicast wait, which may already have passed.
4354
+ * Drop a block's repair hold: its FETCH ended without delivering, or brought
4355
+ * back only a repair that cannot complete it. The block falls back to its
4356
+ * multicast wait, which may already have passed.
4233
4357
  */
4234
4358
  releaseRepairHold(sbn, packetId, catalogTrackName) {
4235
4359
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -4367,6 +4491,8 @@ var FECManager = class _FECManager {
4367
4491
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4368
4492
  const evictedBefore = locator.heldEvicted;
4369
4493
  const held = locator.hold(ssId, source);
4494
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
4495
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
4370
4496
  const evicted = locator.heldEvicted - evictedBefore;
4371
4497
  if (evicted > 0) {
4372
4498
  const before = this.#isoHeldEvictedCount;
@@ -4387,17 +4513,20 @@ var FECManager = class _FECManager {
4387
4513
  * contradicts a known block is refused (counted) and must be dropped.
4388
4514
  */
4389
4515
  placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
4390
- const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
4516
+ const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4517
+ const placement = locator.placeRepair(ssStart, ssbLength);
4518
+ this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName);
4391
4519
  if (placement.status !== "placed") {
4392
4520
  this.#isoRepairRefusedCount++;
4393
4521
  if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
4394
4522
  console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
4395
4523
  }
4524
+ if (placement.ofSpan !== void 0) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName);
4396
4525
  return placement;
4397
4526
  }
4527
+ this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName));
4398
4528
  if (placement.span !== void 0) this.#openIsoSpan(placement.span, packetId, catalogTrackName);
4399
4529
  if (placement.hole !== void 0) {
4400
- const locator = this.#isoLocatorFor(packetId, catalogTrackName);
4401
4530
  const hole = placement.hole.block;
4402
4531
  const previous = locator.blockEndingAt(hole.start);
4403
4532
  const loss = {
@@ -4418,17 +4547,29 @@ var FECManager = class _FECManager {
4418
4547
  * `handler`, instead of a repair-less span; null restores spans. Set by the
4419
4548
  * range repair scheduler; applies to every stream from its next gap. The
4420
4549
  * handler and the deferral are one setting, so no hole is ever created with
4421
- * nobody to close it.
4550
+ * nobody to close it. `onExpired` is told of each pending hole the stream
4551
+ * runs past the locator's horizon (BLO-52085): returning true, it closes
4552
+ * the hole (`closeIsoHoles`), so the span it leaves still fails and is
4553
+ * reported; otherwise the hole is discarded with its sources.
4422
4554
  */
4423
- setUndelimitedLossHandler(handler) {
4555
+ setUndelimitedLossHandler(handler, onExpired = null) {
4424
4556
  this.#onUndelimitedLoss = handler;
4557
+ this.#onIsoHoleExpired = handler === null ? null : onExpired;
4425
4558
  this.#deferIsoGaps = handler !== null;
4426
4559
  for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps;
4427
4560
  }
4561
+ /** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
4562
+ #settleExpiredIsoHoles(locator, packetId, catalogTrackName) {
4563
+ for (const hole of locator.takeExpiredHoles()) {
4564
+ const expired = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length };
4565
+ if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length);
4566
+ }
4567
+ }
4428
4568
  /**
4429
- * The range repair for a hole ended: every hole still pending inside
4430
- * `[start, start + length)` becomes a repair-less span. Returns those spans
4431
- * with the held sources they placed, for the caller to feed (MmtpRouter).
4569
+ * The range repair for a hole ended: every hole inside `[start, start +
4570
+ * length)`, pending or expired, becomes a repair-less span. Returns those
4571
+ * spans with the held sources they placed, for the caller to feed
4572
+ * (MmtpRouter).
4432
4573
  */
4433
4574
  closeIsoHoles(packetId, catalogTrackName, start, length) {
4434
4575
  const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
@@ -4447,7 +4588,10 @@ var FECManager = class _FECManager {
4447
4588
  */
4448
4589
  #openIsoSpan(span, packetId, catalogTrackName) {
4449
4590
  const key = this.#blockKey(span.block.start, packetId, catalogTrackName);
4450
- if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, span.block.length);
4591
+ if (!this.#blockAuthoredLengths.has(key)) {
4592
+ this.#blockAuthoredLengths.set(key, span.block.length);
4593
+ this.#isoSpanKeys.add(key);
4594
+ }
4451
4595
  if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true);
4452
4596
  }
4453
4597
  /**
@@ -4535,11 +4679,14 @@ var FECManager = class _FECManager {
4535
4679
  /**
4536
4680
  * Whether an MPU boundary still bounds this stream's RAPs (see
4537
4681
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
4538
- * it does not. A stream with no classifier yet has shown nothing against it.
4682
+ * it does not. A stream with no classifier yet has shown nothing against it,
4683
+ * and neither has a stream without catalog timing until it releases two
4684
+ * RAPs inside one MPU.
4539
4685
  */
4540
4686
  mpuBoundsRapsFor(packetId, catalogTrackName) {
4541
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
4542
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
4687
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4688
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
4689
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
4543
4690
  }
4544
4691
  /** A block's class now, or frozen at its resolution. */
4545
4692
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -4565,7 +4712,14 @@ var FECManager = class _FECManager {
4565
4712
  * invariant (consecutive MPUs exactly one keyframe interval apart).
4566
4713
  */
4567
4714
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
4568
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
4715
+ if (!this.#trackConfigs.has(packetId)) return;
4716
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
4717
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4718
+ const last = this.#noSpanRaps.get(streamKey);
4719
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
4720
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
4721
+ return;
4722
+ }
4569
4723
  this.#applyClassifierChanges(
4570
4724
  packetId,
4571
4725
  catalogTrackName,
@@ -4778,11 +4932,14 @@ var FECManager = class _FECManager {
4778
4932
  });
4779
4933
  client.onCleanupComplete((sbn) => {
4780
4934
  if (this.#releasedWorkerClients.has(client)) return;
4781
- const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
4935
+ const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client);
4936
+ const cleanupKeys = owed.filter((key) => {
4782
4937
  const block = this.#parseBlockKey(key);
4783
- return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
4938
+ return block.packetId === packetId && block.sbn === sbn;
4784
4939
  });
4785
- for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
4940
+ const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity));
4941
+ const due = owed.filter((key) => !cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs);
4942
+ for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, "cleanup-ack");
4786
4943
  });
4787
4944
  client.onError((msg, code) => {
4788
4945
  const requestedKind = code ?? msg;
@@ -4852,9 +5009,11 @@ var FECManager = class _FECManager {
4852
5009
  * fail deadline already passed, so failing it is the decision that deadline
4853
5010
  * made. A late ack then finds nothing owed and is a no-op.
4854
5011
  */
4855
- #boundOwedCleanup(key, packetId, client) {
5012
+ #boundOwedCleanup(key, packetId, client, dueAtMs) {
4856
5013
  this.#clearOwedCleanupBound(key);
4857
5014
  const bound = {
5015
+ dueAtMs,
5016
+ askedAtMs: performance.now(),
4858
5017
  handle: setTimeout(() => {
4859
5018
  if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
4860
5019
  this.#pendingWorkerCleanupBounds.delete(key);
@@ -4952,7 +5111,15 @@ var FECManager = class _FECManager {
4952
5111
  this.#isoSourceOversizeCount++;
4953
5112
  return null;
4954
5113
  }
4955
- const sbn = located.block.start;
5114
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
5115
+ }
5116
+ /**
5117
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
5118
+ * answers through onFrame (null here), or its synchronous decoder, whose
5119
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
5120
+ * stated for `ssId`.
5121
+ */
5122
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
4956
5123
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
4957
5124
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
4958
5125
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -4981,6 +5148,74 @@ var FECManager = class _FECManager {
4981
5148
  }
4982
5149
  return recovered;
4983
5150
  }
5151
+ /**
5152
+ * Feed a source symbol of this stream that another FEC instance over the
5153
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
5154
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
5155
+ * BLO-52085). It joins its block's decoder as a received source would, so
5156
+ * it reduces the erasures the block still has to solve, but it is not a
5157
+ * network arrival: no transport accounting, no classification evidence,
5158
+ * no deadline refresh, and it never makes a block clean.
5159
+ *
5160
+ * Only a block a repair stated takes one (a repair-less span has no
5161
+ * decoder block). A block that is terminal, or already holds that source,
5162
+ * does not. The symbol is the exact protected packet, unpadded, as
5163
+ * feedSourceSymbol takes it.
5164
+ *
5165
+ * `recovered` is the block the synchronous decoder completed with it; the
5166
+ * caller routes it and completes the block, as for any recovery. When
5167
+ * every source of the block is then present, received or rebuilt, and no
5168
+ * decoder returned it (a Worker answers asynchronously, and reports such a
5169
+ * block as clean on its own side), the block is completed here as
5170
+ * recovered, with nothing left to route.
5171
+ */
5172
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
5173
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
5174
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
5175
+ if (located === null || located.block.span) return NOT_TAKEN;
5176
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
5177
+ const sbn = located.block.start;
5178
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
5179
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
5180
+ let rebuilt = this.#blockRebuiltEsis.get(key);
5181
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
5182
+ if (rebuilt === void 0) {
5183
+ rebuilt = /* @__PURE__ */ new Set();
5184
+ this.#blockRebuiltEsis.set(key, rebuilt);
5185
+ }
5186
+ const config = this.#trackConfigs.get(packetId);
5187
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
5188
+ rebuilt.add(located.esi);
5189
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
5190
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
5191
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
5192
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
5193
+ }
5194
+ }
5195
+ return { taken: true, recovered };
5196
+ }
5197
+ /**
5198
+ * A block that holds every one of its `length` sources, received or
5199
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
5200
+ * every packet of it was delivered, by its own arrival or by the instance
5201
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
5202
+ */
5203
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
5204
+ const rebuilt = this.#blockRebuiltEsis.get(key);
5205
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
5206
+ let present = rebuilt.size;
5207
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
5208
+ if (present < length) return false;
5209
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
5210
+ this.countRecovery(packetId, sbn, catalogTrackName);
5211
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5212
+ if (catalogTrackName === void 0) {
5213
+ this.onBlockComplete?.(sbn, packetId);
5214
+ } else {
5215
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
5216
+ }
5217
+ return true;
5218
+ }
4984
5219
  /**
4985
5220
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
4986
5221
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -5183,8 +5418,75 @@ var FECManager = class _FECManager {
5183
5418
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
5184
5419
  return null;
5185
5420
  }
5421
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
5186
5422
  return recovered;
5187
5423
  }
5424
+ /**
5425
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
5426
+ * its sources in send order, so one that moves the stream's newest source
5427
+ * position past a block leaves the sources that block still misses
5428
+ * reordered behind it at best: it shortens the wait of every block it
5429
+ * passed that only a late source can complete. MoQ is left out: its streams
5430
+ * can overtake one another, so a MoQ source shows nothing about the sources
5431
+ * sent before it.
5432
+ */
5433
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
5434
+ const previous = locator.newestSource;
5435
+ if (!locator.noteSource(ssId) || previous === void 0) return;
5436
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
5437
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
5438
+ this.#shortenUnrecoverableWait(
5439
+ this.#blockKey(block.start, packetId, catalogTrackName),
5440
+ block.start,
5441
+ packetId,
5442
+ catalogTrackName
5443
+ );
5444
+ }
5445
+ }
5446
+ /**
5447
+ * The wait of a block that only a late source can complete (BLO-52085): a
5448
+ * repair stated it, its received sources plus every symbol its repair group
5449
+ * carries (RSB_length) are short of K' (the repair scheduler's
5450
+ * group-cannot-complete), and a later source of the stream has arrived, so
5451
+ * a source it misses is lost or reordered behind that one. The span, which
5452
+ * covers repair symbols paced after its sources, buys it nothing: it waits
5453
+ * the late-symbol allowance alone, the multicast wait less the span (the
5454
+ * catalog tolerance or the measured delivery gap). Undefined for any other
5455
+ * block.
5456
+ */
5457
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
5458
+ const authoredLength = this.#blockAuthoredLengths.get(key);
5459
+ const rsbLength = this.#blockRsbLengths.get(key);
5460
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
5461
+ let present = this.#blockSourceCounts.get(key) ?? 0;
5462
+ const received = this.#blockSourceEsis.get(key);
5463
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
5464
+ if (present + rsbLength >= authoredLength) return void 0;
5465
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
5466
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
5467
+ return void 0;
5468
+ }
5469
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
5470
+ }
5471
+ /**
5472
+ * Bring an open block's wait in to its late-source wait, when it has one.
5473
+ * Never later than it is armed: only a source can complete such a block,
5474
+ * so a repair of its group (paced across the span) must not keep it open.
5475
+ */
5476
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
5477
+ if (!this.#activeBlocks.has(key)) return;
5478
+ const config = this.#trackConfigs.get(packetId);
5479
+ if (config === void 0) return;
5480
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
5481
+ if (waitMs === void 0) return;
5482
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
5483
+ this.#armBlockTimeout(
5484
+ sbn,
5485
+ packetId,
5486
+ catalogTrackName,
5487
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
5488
+ );
5489
+ }
5188
5490
  /**
5189
5491
  * Record a block's authored SSB_length from a repair symbol. Returns false,
5190
5492
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -5282,12 +5584,13 @@ var FECManager = class _FECManager {
5282
5584
  * the inactivity deadline on each symbol, records the symbol's in-band
5283
5585
  * classification evidence, and marks the block clean when all of its
5284
5586
  * authored source symbols arrived: the SSB_length a repair symbol declared,
5285
- * otherwise K.
5587
+ * otherwise K. `held` marks a source held unplaced since it arrived and
5588
+ * placed only now (see #noteSbnSeen).
5286
5589
  *
5287
5590
  * Throws for a packetId that was never configured, so a classified block
5288
5591
  * never reads the manager-global K or timeout.
5289
5592
  */
5290
- trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
5593
+ trackBlock(sbn, packetId, catalogTrackName, esi, evidence, held = false) {
5291
5594
  const trackConfig = this.#trackConfigs.get(packetId);
5292
5595
  if (trackConfig === void 0) {
5293
5596
  throw new Error(
@@ -5303,7 +5606,8 @@ var FECManager = class _FECManager {
5303
5606
  catalogTrackName,
5304
5607
  "source",
5305
5608
  evidence.packetSequenceNumber,
5306
- Number.isInteger(esi) ? sbn + esi >>> 0 : sbn
5609
+ Number.isInteger(esi) ? sbn + esi >>> 0 : sbn,
5610
+ held
5307
5611
  );
5308
5612
  const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
5309
5613
  if (Number.isInteger(esi) && esi >= authoredLength) {
@@ -5339,6 +5643,15 @@ var FECManager = class _FECManager {
5339
5643
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
5340
5644
  return;
5341
5645
  }
5646
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
5647
+ key,
5648
+ sbn,
5649
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
5650
+ packetId,
5651
+ catalogTrackName
5652
+ )) {
5653
+ return;
5654
+ }
5342
5655
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
5343
5656
  }
5344
5657
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -5347,7 +5660,9 @@ var FECManager = class _FECManager {
5347
5660
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5348
5661
  this.#blockSourceCounts.delete(key);
5349
5662
  this.#blockSourceEsis.delete(key);
5663
+ this.#blockRebuiltEsis.delete(key);
5350
5664
  this.#blockAuthoredLengths.delete(key);
5665
+ this.#isoSpanKeys.delete(key);
5351
5666
  this.#blockRepairRsIds.delete(key);
5352
5667
  this.#blockRsbLengths.delete(key);
5353
5668
  this.#markTerminal(key, sbn, packetId, true);
@@ -5378,6 +5693,19 @@ var FECManager = class _FECManager {
5378
5693
  #receiptNow() {
5379
5694
  return this.#receiptAtMs ?? performance.now();
5380
5695
  }
5696
+ /**
5697
+ * The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
5698
+ * scope: counted from the receipt time, as the timer of a symbol fed when
5699
+ * it was received was, less the whole ms since. So it fires on the tick
5700
+ * that timer would have, never later than a delay counted from now, and
5701
+ * its handler still re-arms a timer that fires before its deadline.
5702
+ * Counted from now, a deadline set from the receipt time is rounded up
5703
+ * from another fraction of a ms (BLO-52085).
5704
+ */
5705
+ #receiptTimerDelayMs(deadlineMs, minMs) {
5706
+ const receivedAtMs = this.#receiptAtMs;
5707
+ return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs));
5708
+ }
5381
5709
  /**
5382
5710
  * A held packet admitted late arms from its (earlier) receipt time; that
5383
5711
  * never pulls in a deadline later activity already armed. Outside a
@@ -5413,10 +5741,20 @@ var FECManager = class _FECManager {
5413
5741
  this.#repairDeadlineMs(trackConfig)
5414
5742
  );
5415
5743
  }
5416
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
5744
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
5745
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
5746
+ return;
5747
+ }
5748
+ this.#armBlockTimeout(
5749
+ sbn,
5750
+ packetId,
5751
+ catalogTrackName,
5752
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
5753
+ );
5417
5754
  }
5418
5755
  #failBlock(sbn, packetId, catalogTrackName) {
5419
5756
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
5757
+ const span = this.#isoSpanKeys.delete(key) ? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 } : void 0;
5420
5758
  this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
5421
5759
  this.#clearRepairDeadline(key);
5422
5760
  this.#clearBlockTimer(key);
@@ -5426,6 +5764,7 @@ var FECManager = class _FECManager {
5426
5764
  }
5427
5765
  this.#blockSourceCounts.delete(key);
5428
5766
  this.#blockSourceEsis.delete(key);
5767
+ this.#blockRebuiltEsis.delete(key);
5429
5768
  this.#blockAuthoredLengths.delete(key);
5430
5769
  this.#blockRepairRsIds.delete(key);
5431
5770
  this.#blockRsbLengths.delete(key);
@@ -5435,7 +5774,8 @@ var FECManager = class _FECManager {
5435
5774
  this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
5436
5775
  this.#fecStats.blocksFailed++;
5437
5776
  this.#statsForPacket(packetId).blocksFailed++;
5438
- this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
5777
+ if (span === void 0) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
5778
+ else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
5439
5779
  this.onBlockResolved?.(sbn, packetId, catalogTrackName);
5440
5780
  }
5441
5781
  /**
@@ -5464,7 +5804,9 @@ var FECManager = class _FECManager {
5464
5804
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
5465
5805
  this.#blockSourceCounts.delete(key);
5466
5806
  this.#blockSourceEsis.delete(key);
5807
+ this.#blockRebuiltEsis.delete(key);
5467
5808
  this.#blockAuthoredLengths.delete(key);
5809
+ this.#isoSpanKeys.delete(key);
5468
5810
  this.#blockRepairRsIds.delete(key);
5469
5811
  this.#blockRsbLengths.delete(key);
5470
5812
  this.#clearBlockTimer(key);
@@ -5555,7 +5897,9 @@ var FECManager = class _FECManager {
5555
5897
  this.#blockKeysBySbn.clear();
5556
5898
  this.#blockSourceCounts.clear();
5557
5899
  this.#blockSourceEsis.clear();
5900
+ this.#blockRebuiltEsis.clear();
5558
5901
  this.#blockAuthoredLengths.clear();
5902
+ this.#isoSpanKeys.clear();
5559
5903
  this.#blockRepairRsIds.clear();
5560
5904
  this.#blockRsbLengths.clear();
5561
5905
  this.#heldLayerStates.clear();
@@ -5580,6 +5924,7 @@ var FECManager = class _FECManager {
5580
5924
  this.#blockAnchorsMs.clear();
5581
5925
  this.#blockCatalogTrackNames.clear();
5582
5926
  this.#classifiers.clear();
5927
+ this.#noSpanRaps.clear();
5583
5928
  this.#streamGeometries.clear();
5584
5929
  this.#failedBlocks.clear();
5585
5930
  this.#resolvedBlockClasses.clear();
@@ -6933,9 +7278,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
6933
7278
  } catch (error) {
6934
7279
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
6935
7280
  }
6936
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
7281
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
6937
7282
  await rx.close();
6938
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
7283
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
6939
7284
  }
6940
7285
  const events = new EventTarget();
6941
7286
  let state = "connected";
@@ -7054,14 +7399,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7054
7399
  });
7055
7400
 
7056
7401
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7402
+ var MAX_PENDING_PACKETS = 256;
7057
7403
  var AMTGatewayManager = class {
7058
7404
  subscriptions = /* @__PURE__ */ new Map();
7059
7405
  messageHandler;
7406
+ messageListenerRegistered = false;
7060
7407
  pendingPackets = /* @__PURE__ */ new Map();
7061
7408
  constructor() {
7062
7409
  this.messageHandler = (event) => {
7063
7410
  const data = event.data;
7064
- console.log(`[AMT] SW message received:`, data.type);
7065
7411
  if (data.type === "UDP_PACKET") {
7066
7412
  this.handlePacket(data);
7067
7413
  } else if (data.type === "SOCKET_ERROR") {
@@ -7070,12 +7416,22 @@ var AMTGatewayManager = class {
7070
7416
  console.log(`[AMT SW Debug] ${data.message}`);
7071
7417
  }
7072
7418
  };
7073
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
7074
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
7075
- console.log("[AMT] Registered service worker message handler");
7076
- } else {
7077
- console.warn("[AMT] Service worker not available");
7419
+ }
7420
+ ensureMessageListener() {
7421
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
7422
+ return;
7078
7423
  }
7424
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
7425
+ this.messageListenerRegistered = true;
7426
+ console.log("[AMT] Registered service worker message handler");
7427
+ }
7428
+ removeMessageListener() {
7429
+ if (!this.messageListenerRegistered)
7430
+ return;
7431
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7432
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7433
+ }
7434
+ this.messageListenerRegistered = false;
7079
7435
  }
7080
7436
  /**
7081
7437
  * Subscribe via AMT tunnel
@@ -7085,6 +7441,7 @@ var AMTGatewayManager = class {
7085
7441
  if (config.protected) {
7086
7442
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
7087
7443
  }
7444
+ this.ensureMessageListener();
7088
7445
  const keyStr = this.getKeyString(key);
7089
7446
  if (this.subscriptions.has(keyStr)) {
7090
7447
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -7120,6 +7477,8 @@ var AMTGatewayManager = class {
7120
7477
  this.subscriptions.delete(keyStr);
7121
7478
  this.pendingPackets.delete(subscription.subscriptionId);
7122
7479
  }
7480
+ if (this.subscriptions.size === 0)
7481
+ this.removeMessageListener();
7123
7482
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
7124
7483
  }
7125
7484
  }
@@ -7141,6 +7500,8 @@ var AMTGatewayManager = class {
7141
7500
  } finally {
7142
7501
  this.subscriptions.delete(handle);
7143
7502
  this.pendingPackets.delete(subscription.subscriptionId);
7503
+ if (this.subscriptions.size === 0)
7504
+ this.removeMessageListener();
7144
7505
  }
7145
7506
  }
7146
7507
  /**
@@ -7157,14 +7518,12 @@ var AMTGatewayManager = class {
7157
7518
  if (!subscriptionId)
7158
7519
  return;
7159
7520
  const subscription = this.subscriptions.get(subscriptionId);
7160
- if (!subscription) {
7161
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
7521
+ if (!subscription)
7162
7522
  return;
7163
- }
7164
7523
  if (!subscription.handshakeComplete) {
7165
7524
  const queue = this.pendingPackets.get(subscriptionId);
7166
7525
  const packetData2 = data;
7167
- if (queue) {
7526
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7168
7527
  queue.push({
7169
7528
  data: packetData2.data,
7170
7529
  remoteAddress: packetData2.remoteAddress,
@@ -7190,19 +7549,15 @@ var AMTGatewayManager = class {
7190
7549
  if (!subscriptionId)
7191
7550
  return;
7192
7551
  const subscription = this.subscriptions.get(subscriptionId);
7193
- if (!subscription) {
7194
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
7552
+ if (!subscription)
7195
7553
  return;
7196
- }
7197
7554
  subscription.config.onError(new Error(error || "Unknown error"));
7198
7555
  }
7199
7556
  /**
7200
7557
  * Cleanup on destroy
7201
7558
  */
7202
7559
  destroy() {
7203
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7204
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7205
- }
7560
+ this.removeMessageListener();
7206
7561
  const handles = Array.from(this.subscriptions.keys());
7207
7562
  for (const handle of handles) {
7208
7563
  this.unsubscribe(handle).catch((err) => {
@@ -13111,9 +13466,21 @@ var IsoBmffTimingResolver = class {
13111
13466
  }
13112
13467
  #track = null;
13113
13468
  #fragments = /* @__PURE__ */ new Map();
13469
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
13470
+ #firstSamples = /* @__PURE__ */ new Map();
13471
+ /**
13472
+ * First sample and sample count of the newest fragments whose every slot was
13473
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
13474
+ * matches the combiner, since it retires an abandoned sample as well.
13475
+ */
13476
+ #delivered = /* @__PURE__ */ new Map();
13114
13477
  #pendingSamples = 0;
13115
13478
  #evictedSamples = 0;
13116
13479
  #lastMovieFragmentRejection = null;
13480
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
13481
+ get initialized() {
13482
+ return this.#track !== null;
13483
+ }
13117
13484
  get pendingSampleCount() {
13118
13485
  return this.#pendingSamples;
13119
13486
  }
@@ -13137,7 +13504,14 @@ var IsoBmffTimingResolver = class {
13137
13504
  this.#track = timing;
13138
13505
  return true;
13139
13506
  }
13140
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
13507
+ /**
13508
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
13509
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
13510
+ * fragment whose samples are all done is a duplicate: admitting it again
13511
+ * would charge stale timing and could displace the live MPU's. A source
13512
+ * copy is still admitted again (a rendition switch re-arms timing that way).
13513
+ */
13514
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
13141
13515
  this.#lastMovieFragmentRejection = null;
13142
13516
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13143
13517
  this.#lastMovieFragmentRejection = "invalid";
@@ -13163,6 +13537,8 @@ var IsoBmffTimingResolver = class {
13163
13537
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13164
13538
  );
13165
13539
  }
13540
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
13541
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
13166
13542
  let freed = 0;
13167
13543
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13168
13544
  freed += this.#evictOldestFragment();
@@ -13176,6 +13552,7 @@ var IsoBmffTimingResolver = class {
13176
13552
  return false;
13177
13553
  }
13178
13554
  this.#fragments.set(key, [...parsed.samples]);
13555
+ this.#firstSamples.set(key, parsed.samples[0]);
13179
13556
  this.#pendingSamples += parsed.samples.length;
13180
13557
  return true;
13181
13558
  }
@@ -13233,6 +13610,7 @@ var IsoBmffTimingResolver = class {
13233
13610
  let live = 0;
13234
13611
  for (const sample of samples) if (sample !== null) live++;
13235
13612
  this.#fragments.delete(oldest);
13613
+ this.#firstSamples.delete(oldest);
13236
13614
  this.#pendingSamples -= live;
13237
13615
  this.#evictedSamples += live;
13238
13616
  return live;
@@ -13248,12 +13626,23 @@ var IsoBmffTimingResolver = class {
13248
13626
  if (!samples || !samples[index]) return false;
13249
13627
  samples[index] = null;
13250
13628
  this.#pendingSamples--;
13251
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
13629
+ if (samples.every((value) => value === null)) {
13630
+ this.#fragments.delete(key);
13631
+ const first = this.#firstSamples.get(key);
13632
+ this.#firstSamples.delete(key);
13633
+ this.#delivered.delete(key);
13634
+ this.#delivered.set(key, { first, sampleCount: samples.length });
13635
+ if (this.#delivered.size > this.maxPendingSamples) {
13636
+ this.#delivered.delete(this.#delivered.keys().next().value);
13637
+ }
13638
+ }
13252
13639
  return true;
13253
13640
  }
13254
13641
  reset() {
13255
13642
  this.#track = null;
13256
13643
  this.#fragments.clear();
13644
+ this.#firstSamples.clear();
13645
+ this.#delivered.clear();
13257
13646
  this.#pendingSamples = 0;
13258
13647
  }
13259
13648
  };
@@ -13423,6 +13812,9 @@ function authoredGap(before, after) {
13423
13812
  function ssIdAfter(a, b) {
13424
13813
  return (a - b | 0) > 0;
13425
13814
  }
13815
+ function holdsRebuilt(stream) {
13816
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
13817
+ }
13426
13818
  var FecReleaseSequencer = class {
13427
13819
  #streams = /* @__PURE__ */ new Map();
13428
13820
  #gate;
@@ -13485,6 +13877,7 @@ var FecReleaseSequencer = class {
13485
13877
  stream.lastSbn = void 0;
13486
13878
  stream.discontinuity = false;
13487
13879
  stream.rapSsId = void 0;
13880
+ stream.sourced = false;
13488
13881
  }
13489
13882
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
13490
13883
  this.#recordStat("droppedStale");
@@ -13511,6 +13904,7 @@ var FecReleaseSequencer = class {
13511
13904
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
13512
13905
  else hi = mid;
13513
13906
  }
13907
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
13514
13908
  stream.buf.splice(lo, 0, frame);
13515
13909
  this.#drainUnlessDeferred(stream);
13516
13910
  }
@@ -13546,6 +13940,41 @@ var FecReleaseSequencer = class {
13546
13940
  }
13547
13941
  return false;
13548
13942
  }
13943
+ /**
13944
+ * These streams' sink is being replaced by one that has seen no source
13945
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
13946
+ * release to the sink being replaced the frames the unanchored path had
13947
+ * already released to it (#cursorPassed): there they waited in its queue,
13948
+ * which its replacement flushes. A stream that holds none is not drained:
13949
+ * it holds nothing the anchor kept back, and a drain would still force out
13950
+ * a frame held past the hold limit, which before the anchor waited for the
13951
+ * next push. Then treat the streams as unsourced until a source frame is
13952
+ * released.
13953
+ */
13954
+ unsource(packetId, catalogTrackName) {
13955
+ for (const stream of this.#streams.values()) {
13956
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
13957
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
13958
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
13959
+ else this.#drain(stream, true);
13960
+ }
13961
+ stream.sourced = false;
13962
+ }
13963
+ }
13964
+ }
13965
+ /**
13966
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
13967
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
13968
+ * packetId.
13969
+ */
13970
+ holdsRebuilt(packetId, catalogTrackName) {
13971
+ for (const stream of this.#streams.values()) {
13972
+ if (stream.packetId !== packetId) continue;
13973
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
13974
+ if (holdsRebuilt(stream)) return true;
13975
+ }
13976
+ return false;
13977
+ }
13549
13978
  /** Re-evaluate held gaps after FEC state changed. */
13550
13979
  poke(streamKey) {
13551
13980
  if (streamKey !== void 0) {
@@ -13637,16 +14066,32 @@ var FecReleaseSequencer = class {
13637
14066
  last: null,
13638
14067
  lastSbn: void 0,
13639
14068
  discontinuity: false,
13640
- rapSsId: void 0
14069
+ rapSsId: void 0,
14070
+ sourced: false,
14071
+ draining: 0
13641
14072
  };
13642
14073
  this.#streams.set(streamKey, stream);
13643
14074
  }
13644
14075
  return stream;
13645
14076
  }
13646
- #drain(stream) {
14077
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
14078
+ #drain(stream, asUnanchored = false) {
14079
+ stream.draining++;
14080
+ try {
14081
+ this.#drainLoop(stream, asUnanchored);
14082
+ } finally {
14083
+ stream.draining--;
14084
+ }
14085
+ }
14086
+ #drainLoop(stream, asUnanchored) {
13647
14087
  while (stream.buf.length > 0 && stream.last !== null) {
13648
14088
  const head = stream.buf[0];
13649
14089
  const last = stream.last;
14090
+ if (compareFrameOrder(head, last) <= 0) {
14091
+ stream.buf.shift();
14092
+ this.#release(stream, head, void 0);
14093
+ continue;
14094
+ }
13650
14095
  if (stream.discontinuity) {
13651
14096
  stream.discontinuity = false;
13652
14097
  stream.buf.shift();
@@ -13660,9 +14105,10 @@ var FecReleaseSequencer = class {
13660
14105
  }
13661
14106
  const headSbn = head.meta.fecSbn;
13662
14107
  const from = stream.lastSbn ?? headSbn;
13663
- const to = headSbn ?? stream.lastSbn;
14108
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
13664
14109
  let lost = [];
13665
14110
  const unanchored = from === void 0 && to === void 0;
14111
+ let unresolved = false;
13666
14112
  if (from !== void 0 && to !== void 0) {
13667
14113
  const resolved = this.#gate.resolvedLoss(
13668
14114
  stream.packetId,
@@ -13671,7 +14117,8 @@ var FecReleaseSequencer = class {
13671
14117
  Math.max(from, to),
13672
14118
  from
13673
14119
  );
13674
- if (resolved === null) {
14120
+ unresolved = resolved === null;
14121
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
13675
14122
  const heldMs = performance.now() - head.arrivedAtMs;
13676
14123
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
13677
14124
  this.#recordStat("forcedWindowFlushes");
@@ -13679,22 +14126,56 @@ var FecReleaseSequencer = class {
13679
14126
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
13680
14127
  continue;
13681
14128
  }
13682
- lost = resolved;
14129
+ lost = resolved ?? [];
13683
14130
  }
13684
14131
  stream.buf.shift();
13685
14132
  this.#release(
13686
14133
  stream,
13687
14134
  head,
13688
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
14135
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
13689
14136
  );
13690
14137
  }
13691
14138
  }
14139
+ /**
14140
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
14141
+ * source cursor holds it unanchored, and neither does a later frame behind
14142
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
14143
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
14144
+ * like each frame that arrived in front of it since (stale there). The head
14145
+ * then goes, decided as it would be unanchored, once a frame the base path
14146
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
14147
+ * marked one: the base path has passed that one, so a frame still missing
14148
+ * before it can only come from a repair. It also goes while no source frame
14149
+ * has been released, when the direct path shows a recovered frame at once.
14150
+ * And it goes at once while a staged switch decides on when frames arrive
14151
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
14152
+ * sink being replaced: unanchored, it had already reached the sink.
14153
+ * The scan also writes: the `passed` marks it sets stay on the frames,
14154
+ * and push marks a frame it inserts in front of a marked one from them.
14155
+ */
14156
+ #cursorPassed(stream, asUnanchored) {
14157
+ let at = -1;
14158
+ for (let i = 0; i < stream.buf.length; i++) {
14159
+ const frame = stream.buf[i];
14160
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
14161
+ frame.passed = true;
14162
+ if (at < 0) at = i;
14163
+ } else if (frame.passed !== true) break;
14164
+ }
14165
+ if (at < 0) return false;
14166
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
14167
+ }
13692
14168
  #release(stream, frame, decision) {
14169
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
14170
+ if (stale) this.#recordStat("droppedStale");
13693
14171
  if (decision) frame.meta.gapBefore = decision;
13694
14172
  stream.last = frame;
13695
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
14173
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
14174
+ stream.lastSbn = frame.meta.fecSbn;
14175
+ }
14176
+ if (frame.meta.recovered !== true) stream.sourced = true;
13696
14177
  this.#gate.onReleased?.(frame.data, frame.meta);
13697
- this.#emit(frame.data, frame.meta);
14178
+ if (!stale) this.#emit(frame.data, frame.meta);
13698
14179
  }
13699
14180
  };
13700
14181
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -13735,6 +14216,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13735
14216
  this.#reclaimedTimingSamples = 0;
13736
14217
  this.#evictedTimingSamples = 0;
13737
14218
  this.#rejectedMovieFragments = 0;
14219
+ this.#recoveredHeadsAwaitingTiming = {
14220
+ held: 0,
14221
+ expired: 0,
14222
+ discarded: 0,
14223
+ superseded: 0
14224
+ };
13738
14225
  this.#sequencers = /* @__PURE__ */ new Map();
13739
14226
  this.#fecGate = null;
13740
14227
  this.#fecSequencer = null;
@@ -13843,6 +14330,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13843
14330
  #reclaimedTimingSamples;
13844
14331
  #evictedTimingSamples;
13845
14332
  #rejectedMovieFragments;
14333
+ #recoveredHeadsAwaitingTiming;
13846
14334
  #sequencers;
13847
14335
  #fecGate;
13848
14336
  #fecSequencer;
@@ -13948,11 +14436,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13948
14436
  /**
13949
14437
  * Typed view of the ISO-BMFF timing-sample budget.
13950
14438
  *
13951
- * This is the acceptance signal for the budget ratchet: `pending` walking
13952
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
13953
- * and it is observable here rather than only as media silently stopping.
13954
- * Per repo policy this is a producer-owned counter -- never infer budget
13955
- * state by parsing logs.
14439
+ * Each stream admits timing against its own window of `capacity` slots
14440
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
14441
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
14442
+ * timing a stream declared and never delivered shows up there. `pending`
14443
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
14444
+ * a producer-owned counter -- never infer budget state by parsing logs.
13956
14445
  */
13957
14446
  get timingBudgetStats() {
13958
14447
  return {
@@ -13963,6 +14452,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13963
14452
  rejectedMovieFragments: this.#rejectedMovieFragments
13964
14453
  };
13965
14454
  }
14455
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14456
+ get recoveredHeadsAwaitingTiming() {
14457
+ let pending = 0;
14458
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14459
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14460
+ }
14461
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14462
+ }
13966
14463
  get reorderStats() {
13967
14464
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13968
14465
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14004,6 +14501,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
14004
14501
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
14005
14502
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
14006
14503
  }
14504
+ /** See FecReleaseSequencer.unsource. */
14505
+ unsourceFecRelease(packetId, catalogTrackName) {
14506
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
14507
+ }
14508
+ /** See FecReleaseSequencer.holdsRebuilt. */
14509
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
14510
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
14511
+ }
14007
14512
  /** Re-evaluate held gaps after FEC block state changed. */
14008
14513
  pokeFecRelease(packetId, catalogTrackName) {
14009
14514
  if (!this.#fecSequencer) return;
@@ -14136,7 +14641,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14136
14641
  const now = Date.now();
14137
14642
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14138
14643
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14139
- awaitingTiming.shift();
14644
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14140
14645
  }
14141
14646
  }
14142
14647
  }
@@ -14254,11 +14759,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14254
14759
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14255
14760
  return this.#drop("fragment-unexpected-fragmentation");
14256
14761
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14257
- if (!resolver2) return this.#drop("fragment-no-resolver");
14762
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14258
14763
  const pendingBefore = resolver2.pendingSampleCount;
14259
14764
  const evictedBefore = resolver2.evictedSampleCount;
14260
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14261
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14765
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
14766
+ const admitted = resolver2.addMovieFragment(
14767
+ mpu.mpuSequenceNumber,
14768
+ data,
14769
+ remaining,
14770
+ options?.recovered === true
14771
+ );
14262
14772
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14263
14773
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14264
14774
  if (!admitted) {
@@ -14278,7 +14788,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14278
14788
  };
14279
14789
  }
14280
14790
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14281
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14791
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14282
14792
  if (!resolver) return this.#drop("mfu-no-resolver");
14283
14793
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14284
14794
  return this.#drop("mfu-empty-payload");
@@ -14299,6 +14809,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14299
14809
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14300
14810
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14301
14811
  sampleNumber: mfuDuHeader.sampleNumber,
14812
+ sequenceNumber: info.sequenceNumber,
14302
14813
  arrivedAt: Date.now()
14303
14814
  });
14304
14815
  return this.#drop("mfu-sample-timing-missing");
@@ -14324,7 +14835,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
14324
14835
  info,
14325
14836
  resolver,
14326
14837
  options?.recovered === true,
14327
- options?.fecSbn
14838
+ options?.fecSbn,
14839
+ options?.fecSbnFirst
14328
14840
  );
14329
14841
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
14330
14842
  }
@@ -14417,7 +14929,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
14417
14929
  info,
14418
14930
  resolver,
14419
14931
  options?.recovered === true,
14420
- options?.fecSbn
14932
+ options?.fecSbn,
14933
+ options?.fecSbnFirst
14421
14934
  );
14422
14935
  if (outcome.record !== void 0) return outcome.record;
14423
14936
  return this.#drop(outcome.reason);
@@ -14437,7 +14950,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14437
14950
  * attribution exists to keep readable. Leaving the choice at the call site
14438
14951
  * is what keeps both true.
14439
14952
  */
14440
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
14953
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
14441
14954
  const timing = resolver.sampleTiming(
14442
14955
  combined.mpuSequenceNumber,
14443
14956
  combined.movieFragmentSequenceNumber,
@@ -14454,6 +14967,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14454
14967
  );
14455
14968
  if (!meta) return { reason: "combined-meta-build-failed" };
14456
14969
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
14970
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
14457
14971
  if (!resolver.consumeSample(
14458
14972
  combined.mpuSequenceNumber,
14459
14973
  combined.movieFragmentSequenceNumber,
@@ -14622,6 +15136,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
14622
15136
  }
14623
15137
  this.#emitFrame(data, meta);
14624
15138
  }
15139
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
15140
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
15141
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
15142
+ }
14625
15143
  /**
14626
15144
  * Claim and return the authored timing for a recovered sample. Recovery does
14627
15145
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -14683,6 +15201,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14683
15201
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14684
15202
  this.#sequencers.clear();
14685
15203
  this.#fecSequencer?.discard();
15204
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
15205
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
15206
+ }
14686
15207
  this.#timingStreams.clear();
14687
15208
  this.#pendingTimingSamples = 0;
14688
15209
  this.#reliableMoqStartupGate.clear();
@@ -14701,10 +15222,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14701
15222
  return resolver;
14702
15223
  }
14703
15224
  #awaitTiming(streamKey, head) {
14704
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14705
- if (!awaitingTiming) return;
15225
+ const state = this.#timingStreams.get(streamKey);
15226
+ if (!state) return;
15227
+ if (!head.options?.recovered) {
15228
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
15229
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
15230
+ state.awaitingTiming = kept;
15231
+ }
15232
+ const awaitingTiming = state.awaitingTiming;
14706
15233
  awaitingTiming.push(head);
14707
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
15234
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
15235
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
15236
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
15237
+ }
15238
+ }
15239
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
15240
+ #leaveAwaitingTiming(head, outcome) {
15241
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14708
15242
  }
14709
15243
  /**
14710
15244
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14732,6 +15266,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14732
15266
  const state = this.#timingStreams.get(streamKey);
14733
15267
  if (!state) return;
14734
15268
  this.#timingStreams.delete(streamKey);
15269
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14735
15270
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14736
15271
  if (this.#pendingTimingSamples < 0) {
14737
15272
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -17937,7 +18472,18 @@ function keyframeOverlaySsId(header) {
17937
18472
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
17938
18473
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
17939
18474
  }
18475
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
18476
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
18477
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
18478
+ }
18479
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
18480
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
18481
+ return headerPsn + offset >>> 0;
18482
+ }
18483
+ var OVERLAY_ROUTED_LIMIT = 1024;
18484
+ var OVERLAY_PENDING_LIMIT = 256;
17940
18485
  var OVERLAY_OFFSET_MPUS = 16;
18486
+ var OVERLAY_REBUILT_LIMIT = 256;
17941
18487
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
17942
18488
  function assertStreamIdentity(role, stream) {
17943
18489
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -18048,6 +18594,7 @@ var MmtpRouter = class {
18048
18594
  #provenance = /* @__PURE__ */ new Map();
18049
18595
  #onFlowProtected = null;
18050
18596
  #unadmittedListedDrops = 0;
18597
+ #admittedSourceDatagrams = 0;
18051
18598
  #unlistedRepairSetAside = 0;
18052
18599
  #assistBlocks = /* @__PURE__ */ new Map();
18053
18600
  #assistFailures = /* @__PURE__ */ new Map();
@@ -18104,6 +18651,26 @@ var MmtpRouter = class {
18104
18651
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
18105
18652
  */
18106
18653
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18654
+ /**
18655
+ * Per stream, the SS_ID offset at which a base recovery last showed the
18656
+ * sender's own packet_sequence_number: a recovered packet's header psn
18657
+ * equal to its SS_ID minus that offset. While it equals the stream's
18658
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
18659
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
18660
+ * an MPU no member of which arrived).
18661
+ */
18662
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18663
+ /**
18664
+ * The same, shown by the overlay alone, before any base recovery did: a
18665
+ * packet it rebuilt whose base copy carries its protected psn
18666
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
18667
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
18668
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
18669
+ * make the base drop a late base copy of a keyframe the overlay never
18670
+ * finished, and lift a block's late-source cut, which nothing before
18671
+ * this offset did.
18672
+ */
18673
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18107
18674
  /**
18108
18675
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
18109
18676
  * not parsed, typically because its overlap copy was parsed first
@@ -18173,7 +18740,9 @@ var MmtpRouter = class {
18173
18740
  recovered: 0,
18174
18741
  restarts: 0,
18175
18742
  unsequencedDropped: 0,
18176
- protectedDropped: 0
18743
+ protectedDropped: 0,
18744
+ baseFed: 0,
18745
+ baseUndetermined: 0
18177
18746
  };
18178
18747
  /** Callback fired when a delta-track FEC block completes or fails. */
18179
18748
  #onDeltaBlockCompleteCb = null;
@@ -18346,7 +18915,7 @@ var MmtpRouter = class {
18346
18915
  const cb = this.#getFrameCallback(packetId);
18347
18916
  if (isVideo && !assembler) {
18348
18917
  if (frameMeta.recovered) {
18349
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
18918
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
18350
18919
  } else {
18351
18920
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
18352
18921
  }
@@ -18379,13 +18948,18 @@ var MmtpRouter = class {
18379
18948
  this.#onDeltaBlockCompleteCb?.(packetId, false);
18380
18949
  }
18381
18950
  };
18382
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
18951
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
18383
18952
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
18384
18953
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
18385
18954
  this.#onDeltaBlockCompleteCb?.(packetId, true);
18386
18955
  }
18387
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
18388
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
18956
+ if (span === void 0) {
18957
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
18958
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
18959
+ } else {
18960
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
18961
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
18962
+ }
18389
18963
  };
18390
18964
  }
18391
18965
  }
@@ -18430,8 +19004,9 @@ var MmtpRouter = class {
18430
19004
  }
18431
19005
  /**
18432
19006
  * FEC-recovered source symbols whose reconstructed header named a
18433
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
18434
- * a hop re-stamped the sequence after the sender protected the packet.
19007
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
19008
+ * or, rebuilt by the overlay, than their base copy carries: a hop
19009
+ * re-stamped the sequence after the sender protected the packet.
18435
19010
  */
18436
19011
  get restampedRecoveredSequenceCount() {
18437
19012
  return this.#restampedRecoveredSeqs;
@@ -18610,6 +19185,7 @@ var MmtpRouter = class {
18610
19185
  enableMseAssembler(packetId, config, catalogTrackName) {
18611
19186
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
18612
19187
  const previous = this.#mseAssemblers.get(assemblerKey);
19188
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
18613
19189
  if (previous) {
18614
19190
  previous.flush();
18615
19191
  previous.dispose();
@@ -18736,6 +19312,10 @@ var MmtpRouter = class {
18736
19312
  get keyframeOverlayStats() {
18737
19313
  return { ...this.#overlayStats };
18738
19314
  }
19315
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
19316
+ get recoveredHeadsAwaitingTiming() {
19317
+ return this.#parser.recoveredHeadsAwaitingTiming;
19318
+ }
18739
19319
  #isOverlayRepair(repairPacketId, repairTrackName) {
18740
19320
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18741
19321
  }
@@ -18756,7 +19336,8 @@ var MmtpRouter = class {
18756
19336
  fec,
18757
19337
  k,
18758
19338
  offsets: /* @__PURE__ */ new Map(),
18759
- moqBlocks: /* @__PURE__ */ new Set()
19339
+ moqBlocks: /* @__PURE__ */ new Set(),
19340
+ rebuilt: /* @__PURE__ */ new Map()
18760
19341
  };
18761
19342
  this.#overlays.set(repairPacketId, inst);
18762
19343
  return () => {
@@ -18786,12 +19367,18 @@ var MmtpRouter = class {
18786
19367
  */
18787
19368
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
18788
19369
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
19370
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
19371
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
19372
+ stream.overlayToBase.routed.clear();
19373
+ stream.overlayToBase.pending.clear();
19374
+ }
18789
19375
  }
18790
19376
  #resetOverlay(inst) {
18791
19377
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
18792
19378
  inst.last = void 0;
18793
19379
  inst.offsets.clear();
18794
19380
  inst.moqBlocks.clear();
19381
+ inst.rebuilt.clear();
18795
19382
  }
18796
19383
  #overlaysForSource(sourcePacketId, catalogTrackName) {
18797
19384
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -18842,18 +19429,33 @@ var MmtpRouter = class {
18842
19429
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
18843
19430
  * every overlay installed for its source, at that SS_ID, with the bytes the
18844
19431
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
18845
- * included (7.1, 7.4.3).
18846
- */
18847
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
19432
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
19433
+ * FEC reads it.
19434
+ *
19435
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
19436
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
19437
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
19438
+ * the stream unstamped at the SS_ID offset it states, for the release gate
19439
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
19440
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
19441
+ */
19442
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
18848
19443
  if (this.#overlays.size === 0) return;
18849
19444
  const ssId = keyframeOverlaySsId(header);
18850
19445
  if (ssId === void 0) return;
19446
+ let rebuiltPsn;
18851
19447
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
18852
19448
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
19449
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
19450
+ inst.rebuilt.delete(ssId);
18853
19451
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
18854
19452
  if (identity !== null) {
18855
19453
  inst.offsets.delete(identity.mpuSequenceNumber);
18856
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
19454
+ inst.offsets.set(identity.mpuSequenceNumber, {
19455
+ psnOffset: ssId - sequenceNumber >>> 0,
19456
+ overlaySsId: ssId,
19457
+ ...baseSsId === void 0 ? {} : { baseSsId }
19458
+ });
18857
19459
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
18858
19460
  }
18859
19461
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -18875,6 +19477,14 @@ var MmtpRouter = class {
18875
19477
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
18876
19478
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
18877
19479
  }
19480
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
19481
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
19482
+ if (rebuiltPsn !== sequenceNumber) {
19483
+ this.#restampedRecoveredSeqs++;
19484
+ } else if (baseSsId !== void 0) {
19485
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
19486
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
19487
+ }
18878
19488
  }
18879
19489
  #noteOverlayMoqBlock(inst, blockStart) {
18880
19490
  inst.moqBlocks.add(blockStart);
@@ -18908,11 +19518,16 @@ var MmtpRouter = class {
18908
19518
  }
18909
19519
  /**
18910
19520
  * Route an overlay-recovered block's packets as source media of the
18911
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
18912
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
18913
- * base re-stamp, no block advance. Each packet's wire sequence is restored
18914
- * from its keyframe's own offset; without one, a stream the base never
18915
- * re-stamped keeps the header's sequence and any other drops the packet.
19521
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
19522
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
19523
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
19524
+ * base FEC recovered does, so it waits behind base frames still in flight,
19525
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
19526
+ * longer than unanchored; one whose base SS_ID is undetermined has no
19527
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
19528
+ * advance. Each packet's wire sequence is restored from its keyframe's own
19529
+ * offset; without one, a stream the base never re-stamped keeps the
19530
+ * header's sequence and any other drops the packet.
18916
19531
  */
18917
19532
  #routeOverlayRecovered(inst, blockStart, recovered) {
18918
19533
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -18932,22 +19547,139 @@ var MmtpRouter = class {
18932
19547
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
18933
19548
  const restamped = this.#restampedStreams.has(streamKey);
18934
19549
  const restored = /* @__PURE__ */ new Set();
19550
+ const baseSsIds = /* @__PURE__ */ new Map();
19551
+ const gateOnly = /* @__PURE__ */ new Set();
18935
19552
  const symbols = result.symbols.flatMap((sym) => {
18936
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
18937
- if (offset !== void 0) {
18938
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
19553
+ const overlaySsId = serial32Add(blockStart, sym.index);
19554
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
19555
+ if (anchor !== void 0) {
19556
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
18939
19557
  restored.add(packetSequenceNumber);
18940
- return [{ ...sym, packetSequenceNumber }];
19558
+ const out = { ...sym, packetSequenceNumber };
19559
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
19560
+ if (baseSsId !== void 0) {
19561
+ baseSsIds.set(sym.index, baseSsId);
19562
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
19563
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
19564
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
19565
+ }
19566
+ }
19567
+ return [out];
19568
+ }
19569
+ if (!restamped) {
19570
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
19571
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
19572
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
19573
+ if (baseSsId !== void 0) {
19574
+ baseSsIds.set(sym.index, baseSsId);
19575
+ if (fedSsId === void 0) gateOnly.add(sym.index);
19576
+ } else {
19577
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
19578
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
19579
+ }
19580
+ return [sym];
18941
19581
  }
18942
- if (!restamped) return [sym];
18943
19582
  this.#overlayStats.unsequencedDropped++;
18944
19583
  return [];
18945
19584
  });
18946
19585
  this.#overlayStats.recovered++;
19586
+ const mpuFirst = /* @__PURE__ */ new Map();
19587
+ for (const sym of symbols) {
19588
+ const ssId = baseSsIds.get(sym.index);
19589
+ const mpu = sym.fragment.mpuSequenceNumber;
19590
+ const first = mpuFirst.get(mpu);
19591
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
19592
+ }
19593
+ const firstSsIds = /* @__PURE__ */ new Map();
19594
+ for (const sym of symbols) {
19595
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
19596
+ }
18947
19597
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
18948
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
19598
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
19599
+ baseSsIds,
19600
+ firstSsIds
18949
19601
  });
18950
19602
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
19603
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
19604
+ }
19605
+ /**
19606
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
19607
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
19608
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
19609
+ * base recovery showed; `baseSsIds` holds no other),
19610
+ * so it is fed to the base decoder as a rebuilt source
19611
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
19612
+ * fewer to solve, and its repair can recover the block's other packets.
19613
+ * A base block no repair has stated yet takes it when one does
19614
+ * (#feedPendingRebuilt).
19615
+ *
19616
+ * The overlay route already handled each packet fed (delivered it, or
19617
+ * held an untimed head for its FT=1), so a base recovery routes none of
19618
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
19619
+ * held head would wait in the parser for timing its first copy consumed.
19620
+ * Only an overlay block decoded from multicast symbols alone is fed: with
19621
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
19622
+ * that crossed no multicast socket, which the provenance counted from the
19623
+ * block's own arrivals cannot show.
19624
+ */
19625
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
19626
+ const fec = this.#fecManager;
19627
+ const owner = inst.leg.sourcePacketId;
19628
+ const name = inst.leg.sourceCatalogTrackName;
19629
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
19630
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
19631
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
19632
+ const feeds = [];
19633
+ for (const sym of symbols) {
19634
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
19635
+ const ssId = baseSsIds.get(sym.index);
19636
+ if (ssId === void 0) {
19637
+ this.#overlayStats.baseUndetermined++;
19638
+ continue;
19639
+ }
19640
+ routed.delete(ssId);
19641
+ routed.add(ssId);
19642
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
19643
+ feeds.push([ssId, sym.rawSymbol]);
19644
+ }
19645
+ for (const [ssId, symbolData] of feeds) {
19646
+ if (this.#classifyPacketId(owner) === null) return;
19647
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19648
+ }
19649
+ }
19650
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
19651
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
19652
+ const fec = this.#fecManager;
19653
+ if (!fec?.feedRebuiltSourceSymbol) return;
19654
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
19655
+ if (coords == null) {
19656
+ const pending = this.#stream(owner, name).overlayToBase.pending;
19657
+ pending.delete(ssId);
19658
+ pending.set(ssId, symbolData.slice());
19659
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
19660
+ return;
19661
+ }
19662
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
19663
+ if (!fed.taken) return;
19664
+ this.#overlayStats.baseFed++;
19665
+ if (fed.recovered === null) return;
19666
+ const sbn = coords.sbn;
19667
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
19668
+ fec.cancelBlockTimeout(sbn, owner, name);
19669
+ fec.countRecovery(owner, sbn, name);
19670
+ fec.markBlockCompleted(sbn, owner, name);
19671
+ });
19672
+ }
19673
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
19674
+ #feedPendingRebuilt(owner, name, block) {
19675
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
19676
+ if (pending === void 0 || pending.size === 0) return;
19677
+ for (const [ssId, symbolData] of [...pending]) {
19678
+ if (this.#classifyPacketId(owner) === null) return;
19679
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
19680
+ pending.delete(ssId);
19681
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19682
+ }
18951
19683
  }
18952
19684
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
18953
19685
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -19087,6 +19819,10 @@ var MmtpRouter = class {
19087
19819
  get unadmittedListedDropCount() {
19088
19820
  return this.#unadmittedListedDrops;
19089
19821
  }
19822
+ /** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
19823
+ get admittedSourceDatagramCount() {
19824
+ return this.#admittedSourceDatagrams;
19825
+ }
19090
19826
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
19091
19827
  get unlistedRepairSetAsideCount() {
19092
19828
  return this.#unlistedRepairSetAside;
@@ -19573,6 +20309,8 @@ var MmtpRouter = class {
19573
20309
  )) return;
19574
20310
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
19575
20311
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
20312
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
20313
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
19576
20314
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
19577
20315
  const recovered = fec.feedRepairSymbol(
19578
20316
  ownerPacketId,
@@ -19623,7 +20361,7 @@ var MmtpRouter = class {
19623
20361
  catalogTrackName
19624
20362
  )) continue;
19625
20363
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
19626
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
20364
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
19627
20365
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
19628
20366
  }
19629
20367
  if (payload.oversized) continue;
@@ -19710,6 +20448,8 @@ var MmtpRouter = class {
19710
20448
  this.#droppedPreSyncVideo.delete(streamKey);
19711
20449
  this.#latestVideoSeqByStream.delete(streamKey);
19712
20450
  this.#ssIdSeqOffsetByStream.delete(streamKey);
20451
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
20452
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
19713
20453
  this.#multicastRapTwinByStream.delete(streamKey);
19714
20454
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
19715
20455
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -19836,6 +20576,12 @@ var MmtpRouter = class {
19836
20576
  * the outgoing stream stops. The hold is bounded by the incoming stream's
19837
20577
  * own RAP cadence, so no timer is involved.
19838
20578
  *
20579
+ * While the switch is staged, the FEC release gate lets a keyframe the
20580
+ * overlay rebuilt reach it, on either side, when the keyframe did before
20581
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
20582
+ * decides on what each side delivered, and the commit discards the gate's
20583
+ * frames of the outgoing stream.
20584
+ *
19839
20585
  * The commit is one synchronous transition inside the frame path: the
19840
20586
  * outgoing stream's already-admitted frames are released (its queued FEC
19841
20587
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -19886,6 +20632,9 @@ var MmtpRouter = class {
19886
20632
  droppedIncomingFrames: 0
19887
20633
  };
19888
20634
  this.#staged = state;
20635
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
20636
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
20637
+ }
19889
20638
  return {
19890
20639
  cancel: () => {
19891
20640
  if (this.#staged !== state) return false;
@@ -19968,7 +20717,17 @@ var MmtpRouter = class {
19968
20717
  */
19969
20718
  #routeParsedFrame(data, meta) {
19970
20719
  if (this.#classifyPacketId(meta.packetId) === null) return;
19971
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
20720
+ if (this.#staged !== null) {
20721
+ let consumed = false;
20722
+ this.#fecPokeDepth++;
20723
+ try {
20724
+ consumed = this.#interceptSwitchFrame(data, meta);
20725
+ } finally {
20726
+ this.#fecPokeDepth--;
20727
+ this.#flushPendingFecPokes();
20728
+ }
20729
+ if (consumed) return;
20730
+ }
19972
20731
  this.#deliverParsedFrame(data, meta);
19973
20732
  }
19974
20733
  #commitStreamSwitch(state, rap, reason) {
@@ -20120,12 +20879,16 @@ var MmtpRouter = class {
20120
20879
  this.#parser.deferFecRelease(work);
20121
20880
  } finally {
20122
20881
  this.#fecPokeDepth--;
20123
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
20124
- const pokes = [...this.#pendingFecPokes.values()];
20125
- this.#pendingFecPokes.clear();
20126
- for (const poke of pokes) {
20127
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
20128
- }
20882
+ this.#flushPendingFecPokes();
20883
+ }
20884
+ }
20885
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
20886
+ #flushPendingFecPokes() {
20887
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
20888
+ const pokes = [...this.#pendingFecPokes.values()];
20889
+ this.#pendingFecPokes.clear();
20890
+ for (const poke of pokes) {
20891
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
20129
20892
  }
20130
20893
  }
20131
20894
  }
@@ -20203,6 +20966,14 @@ var MmtpRouter = class {
20203
20966
  onRap: (packetId, catalogTrackName, ssId) => {
20204
20967
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
20205
20968
  },
20969
+ // A staged switch commits on when each side's frames reach it
20970
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
20971
+ // they did unanchored.
20972
+ releaseUnanchored: (packetId, catalogTrackName) => {
20973
+ const staged = this.#staged;
20974
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
20975
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
20976
+ },
20206
20977
  onReleased: (data, meta) => {
20207
20978
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
20208
20979
  const timing = meta.authoredTiming;
@@ -20344,6 +21115,7 @@ var MmtpRouter = class {
20344
21115
  const parsed = this.#parser.parsePacket(packet);
20345
21116
  if (!parsed) return;
20346
21117
  const info = parsed.info;
21118
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
20347
21119
  if (source === "multicast" && !manifestAdmitted) {
20348
21120
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
20349
21121
  }
@@ -20625,7 +21397,8 @@ var MmtpRouter = class {
20625
21397
  parsed.header,
20626
21398
  info.sequenceNumber,
20627
21399
  info.rapFlag,
20628
- source
21400
+ source,
21401
+ fecPayloadId?.ssId
20629
21402
  );
20630
21403
  }
20631
21404
  }
@@ -20651,7 +21424,8 @@ var MmtpRouter = class {
20651
21424
  source: parserOptions?.source,
20652
21425
  deliveryProvenance: parserOptions?.deliveryProvenance,
20653
21426
  catalogTrackName: parserOptions?.catalogTrackName,
20654
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
21427
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
21428
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
20655
21429
  };
20656
21430
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
20657
21431
  else this.#parser.feed(packet, feedOptions);
@@ -20775,7 +21549,8 @@ var MmtpRouter = class {
20775
21549
  if (stream === void 0) {
20776
21550
  stream = {
20777
21551
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
20778
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
21552
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
21553
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
20779
21554
  };
20780
21555
  byName.set(catalogTrackName, stream);
20781
21556
  }
@@ -20890,8 +21665,16 @@ var MmtpRouter = class {
20890
21665
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
20891
21666
  this.#emitDirectVideo(packetId, data, meta, cb);
20892
21667
  }
20893
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
21668
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
20894
21669
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
21670
+ if (this.#isFecTrack(packetId)) {
21671
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
21672
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
21673
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
21674
+ this.#emitDirectVideo(packetId, data, meta, cb);
21675
+ return;
21676
+ }
21677
+ }
20895
21678
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
20896
21679
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
20897
21680
  this.#drainDirectRecoveredVideo(
@@ -21045,6 +21828,13 @@ var MmtpRouter = class {
21045
21828
  * frames by epoch only if pre-protection input ever matters.
21046
21829
  */
21047
21830
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
21831
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
21832
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
21833
+ result = {
21834
+ ...result,
21835
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
21836
+ };
21837
+ }
21048
21838
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
21049
21839
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
21050
21840
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -21101,7 +21891,12 @@ var MmtpRouter = class {
21101
21891
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
21102
21892
  if (ownerPacketId == null) return;
21103
21893
  if (this.#classifyPacketId(ownerPacketId) === null) return;
21104
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
21894
+ const fecSbnOf = (index) => {
21895
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
21896
+ const fecSbn = overlay.baseSsIds.get(index);
21897
+ const fecSbnFirst = overlay.firstSsIds.get(index);
21898
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
21899
+ };
21105
21900
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
21106
21901
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
21107
21902
  if (ssIdSeqOffset !== void 0) {
@@ -21109,7 +21904,10 @@ var MmtpRouter = class {
21109
21904
  ...result,
21110
21905
  symbols: result.symbols.map((sym) => {
21111
21906
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
21112
- if (wire === sym.packetSequenceNumber) return sym;
21907
+ if (wire === sym.packetSequenceNumber) {
21908
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
21909
+ return sym;
21910
+ }
21113
21911
  this.#restampedRecoveredSeqs++;
21114
21912
  this.#restampedStreams.add(streamKey);
21115
21913
  return { ...sym, packetSequenceNumber: wire };
@@ -21145,6 +21943,29 @@ var MmtpRouter = class {
21145
21943
  }
21146
21944
  );
21147
21945
  }
21946
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21947
+ ownerPacketId,
21948
+ sym.fragment.mpuSequenceNumber,
21949
+ sym.fragment.movieFragmentSequenceNumber,
21950
+ sym.fragment.sampleNumber,
21951
+ catalogTrackName
21952
+ );
21953
+ for (const sym of result.symbols) {
21954
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21955
+ this.#feedViaParser(
21956
+ sym.rawSymbol,
21957
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21958
+ void 0,
21959
+ {
21960
+ skipFecPayloadId: true,
21961
+ recovered: true,
21962
+ deliveryProvenance,
21963
+ ...fecSbnOf(sym.index),
21964
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21965
+ }
21966
+ );
21967
+ }
21968
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21148
21969
  const gated = this.#isFecTrack(ownerPacketId);
21149
21970
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21150
21971
  if (assembler) {
@@ -22621,6 +23442,12 @@ function isNonNegativeSafeInteger(value) {
22621
23442
  function isSymbolCounts(value) {
22622
23443
  return isRecord(value) && hasExactKeys(value, ["multicast", "moq"]) && isNonNegativeSafeInteger(value["multicast"]) && isNonNegativeSafeInteger(value["moq"]);
22623
23444
  }
23445
+ function isSpanFailure(value) {
23446
+ if (!isRecord(value) || !hasExactKeys(value, ["length", "sources"])) return false;
23447
+ const length = value["length"];
23448
+ const sources = value["sources"];
23449
+ return Number.isSafeInteger(length) && length > 0 && isNonNegativeSafeInteger(sources) && sources < length;
23450
+ }
22624
23451
  function isNullableNonNegative(value) {
22625
23452
  return value === null || typeof value === "number" && Number.isFinite(value) && value >= 0;
22626
23453
  }
@@ -22640,7 +23467,10 @@ function isRepairLifecycleEvent(value) {
22640
23467
  case "recovered":
22641
23468
  return hasExactKeys(value, ["version", "type", "block", "blockClass", "via", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && (value["via"] === "inband" || value["via"] === "quic") && isSymbolCounts(value["symbols"]);
22642
23469
  case "failed":
22643
- return hasExactKeys(value, ["version", "type", "block", "blockClass", "symbols"]) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]);
23470
+ return hasExactKeys(
23471
+ value,
23472
+ "span" in value ? ["version", "type", "block", "blockClass", "symbols", "span"] : ["version", "type", "block", "blockClass", "symbols"]
23473
+ ) && isRepairBlockIdentity(block) && BLOCK_CLASSES.has(value["blockClass"]) && isSymbolCounts(value["symbols"]) && (!("span" in value) || isSpanFailure(value["span"]));
22644
23474
  case "repair-preempted":
22645
23475
  case "repair-deferred":
22646
23476
  return hasExactKeys(value, ["version", "type", "block"]) && isRepairBlockIdentity(block);
@@ -22906,7 +23736,7 @@ var MulticastManager = class {
22906
23736
  onPublisherRestart: (packetId, catalogTrackName) => {
22907
23737
  this.#resetStreamOutsideRouter(packetId, catalogTrackName);
22908
23738
  },
22909
- onBlockFailed: (sbn, packetId, catalogTrackName, blockClass) => {
23739
+ onBlockFailed: (sbn, packetId, catalogTrackName, blockClass, span) => {
22910
23740
  const pathCount = this.#fec.blockPathCount(
22911
23741
  sbn,
22912
23742
  packetId,
@@ -22924,7 +23754,8 @@ var MulticastManager = class {
22924
23754
  symbols: {
22925
23755
  multicast: pathCount?.multicastSymbols ?? 0,
22926
23756
  moq: pathCount?.moqSymbols ?? 0
22927
- }
23757
+ },
23758
+ ...span === void 0 ? {} : { span }
22928
23759
  });
22929
23760
  },
22930
23761
  onFrameGap: (decision) => {
@@ -24304,10 +25135,12 @@ var MulticastManager = class {
24304
25135
  * Register the range repair scheduler (draft-ramadan-moq-fec §7.5): from now
24305
25136
  * on an iso-ssbg1-v1 gap missing a source and every repair is kept as a
24306
25137
  * pending hole and reported here, and `closeIsoHoles` must be called for it
24307
- * once its range FETCH ends (or none is sent).
25138
+ * once its range FETCH ends (or none is sent). `onExpired` is told of a
25139
+ * pending hole the stream runs past the locator's horizon: returning true,
25140
+ * the caller closes it (`closeIsoHoles`); otherwise it is discarded.
24308
25141
  */
24309
- onUndelimitedLoss(cb) {
24310
- this.#fec.setUndelimitedLossHandler(cb);
25142
+ onUndelimitedLoss(cb, onExpired) {
25143
+ this.#fec.setUndelimitedLossHandler(cb, onExpired ?? null);
24311
25144
  }
24312
25145
  /**
24313
25146
  * Report each stream whose FEC fails closed at runtime (fec-01 6.3.9), e.g.
@@ -24543,7 +25376,8 @@ var MulticastManager = class {
24543
25376
  "stream" in event ? { ...event, stream: Object.freeze({ ...event.stream }) } : {
24544
25377
  ...event,
24545
25378
  block: Object.freeze({ ...event.block }),
24546
- ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {}
25379
+ ..."symbols" in event ? { symbols: Object.freeze({ ...event.symbols }) } : {},
25380
+ ..."span" in event && event.span !== void 0 ? { span: Object.freeze({ ...event.span }) } : {}
24547
25381
  }
24548
25382
  );
24549
25383
  if (this.#logRepairLifecycle) {