@blockcast/mmt-container 2.2.0-main.da2e7cd1dd5d → 2.2.0-main.e75bbd7576a6

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 (46) hide show
  1. package/dist/browser-runtime.js +472 -36
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/delivery-catalog.js +2 -1
  4. package/dist/delivery-catalog.js.map +2 -2
  5. package/dist/fec-manager.d.ts +30 -0
  6. package/dist/fec-manager.d.ts.map +1 -1
  7. package/dist/fec-manager.js +196 -2
  8. package/dist/fec-manager.js.map +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +2 -2
  12. package/dist/iso-block-locator.d.ts +9 -0
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +17 -0
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/iso-bmff-timing.d.ts +10 -1
  17. package/dist/iso-bmff-timing.d.ts.map +1 -1
  18. package/dist/iso-bmff-timing.js +36 -2
  19. package/dist/iso-bmff-timing.js.map +2 -2
  20. package/dist/mmtp-container-parser.d.ts +35 -2
  21. package/dist/mmtp-container-parser.d.ts.map +1 -1
  22. package/dist/mmtp-container-parser.js +82 -9
  23. package/dist/mmtp-container-parser.js.map +2 -2
  24. package/dist/mmtp-container-parser.no-modules.js +85 -11
  25. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  26. package/dist/mmtp-decode-pipeline.js +81 -9
  27. package/dist/mmtp-decode-pipeline.js.map +2 -2
  28. package/dist/mmtp-router.d.ts +18 -1
  29. package/dist/mmtp-router.d.ts.map +1 -1
  30. package/dist/mmtp-router.js +276 -34
  31. package/dist/mmtp-router.js.map +2 -2
  32. package/dist/multicast-manager.js +472 -36
  33. package/dist/multicast-manager.js.map +2 -2
  34. package/dist/multicast-transport.js +2 -1
  35. package/dist/multicast-transport.js.map +2 -2
  36. package/dist/no-modules.js +81 -9
  37. package/dist/no-modules.js.map +2 -2
  38. package/dist/wasm-mmtp-container-parser.js +81 -9
  39. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  40. package/package.json +5 -5
  41. package/src/fec-manager.ts +251 -2
  42. package/src/index.ts +1 -0
  43. package/src/iso-block-locator.ts +19 -0
  44. package/src/iso-bmff-timing.ts +40 -1
  45. package/src/mmtp-container-parser.ts +84 -8
  46. package/src/mmtp-router.ts +346 -30
@@ -1210,6 +1210,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1210
1210
  #retentionBlocks;
1211
1211
  #heldEvicted;
1212
1212
  #newest;
1213
+ #newestSource;
1213
1214
  #holes;
1214
1215
  static #checkedRetention(retentionBlocks) {
1215
1216
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -1247,6 +1248,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
1247
1248
  observe(position) {
1248
1249
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
1249
1250
  }
1251
+ /**
1252
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
1253
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
1254
+ * a source cannot run ahead of the sources sent before it except by
1255
+ * reordering.
1256
+ */
1257
+ get newestSource() {
1258
+ return this.#newestSource;
1259
+ }
1260
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
1261
+ noteSource(ssId) {
1262
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
1263
+ this.#newestSource = ssId;
1264
+ return true;
1265
+ }
1250
1266
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
1251
1267
  get heldEvicted() {
1252
1268
  return this.#heldEvicted;
@@ -1479,6 +1495,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1479
1495
  this.#held.clear();
1480
1496
  this.#coverageEnd = void 0;
1481
1497
  this.#newest = void 0;
1498
+ this.#newestSource = void 0;
1482
1499
  }
1483
1500
  #claim(block) {
1484
1501
  const placed = [];
@@ -2067,6 +2084,7 @@ var WORKER_ERROR_LOG_CAP = 5;
2067
2084
  var WORKER_ERROR_KIND_CAP = 32;
2068
2085
  var WORKER_ERROR_OVERFLOW_KIND = "other";
2069
2086
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
2087
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
2070
2088
  function isMulticastSymbolDelivery(value) {
2071
2089
  return value === "datagram" || value === "ordered-relay";
2072
2090
  }
@@ -2164,6 +2182,10 @@ var FECManager = class _FECManager {
2164
2182
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
2165
2183
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
2166
2184
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
2185
+ // Source ESIs per block that another FEC instance rebuilt and fed in
2186
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
2187
+ // not network arrivals and never make a block clean.
2188
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
2167
2189
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
2168
2190
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
2169
2191
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -2453,6 +2475,7 @@ var FECManager = class _FECManager {
2453
2475
  #pendingWorkerCleanupBounds;
2454
2476
  #blockSourceCounts;
2455
2477
  #blockSourceEsis;
2478
+ #blockRebuiltEsis;
2456
2479
  #blockAuthoredLengths;
2457
2480
  #ssbLengthConflictCount;
2458
2481
  #blockRepairRsIds;
@@ -3775,6 +3798,7 @@ var FECManager = class _FECManager {
3775
3798
  }
3776
3799
  resetMap(this.#blockSourceCounts);
3777
3800
  resetMap(this.#blockSourceEsis);
3801
+ resetMap(this.#blockRebuiltEsis);
3778
3802
  resetMap(this.#blockAuthoredLengths);
3779
3803
  resetMap(this.#blockRepairRsIds);
3780
3804
  resetMap(this.#blockRsbLengths);
@@ -3899,6 +3923,7 @@ var FECManager = class _FECManager {
3899
3923
  dropMap(this.#pendingWorkerCleanupClients);
3900
3924
  dropMap(this.#blockSourceCounts);
3901
3925
  dropMap(this.#blockSourceEsis);
3926
+ dropMap(this.#blockRebuiltEsis);
3902
3927
  dropMap(this.#blockAuthoredLengths);
3903
3928
  dropMap(this.#blockRepairRsIds);
3904
3929
  dropMap(this.#blockRsbLengths);
@@ -4061,6 +4086,9 @@ var FECManager = class _FECManager {
4061
4086
  this.#unindexBlockKey(key);
4062
4087
  }
4063
4088
  }
4089
+ if (isSourceSymbol && source === "multicast") {
4090
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
4091
+ }
4064
4092
  return true;
4065
4093
  }
4066
4094
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -4378,6 +4406,7 @@ var FECManager = class _FECManager {
4378
4406
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4379
4407
  const evictedBefore = locator.heldEvicted;
4380
4408
  const held = locator.hold(ssId, source);
4409
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
4381
4410
  const evicted = locator.heldEvicted - evictedBefore;
4382
4411
  if (evicted > 0) {
4383
4412
  const before = this.#isoHeldEvictedCount;
@@ -4973,7 +5002,15 @@ var FECManager = class _FECManager {
4973
5002
  this.#isoSourceOversizeCount++;
4974
5003
  return null;
4975
5004
  }
4976
- const sbn = located.block.start;
5005
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
5006
+ }
5007
+ /**
5008
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
5009
+ * answers through onFrame (null here), or its synchronous decoder, whose
5010
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
5011
+ * stated for `ssId`.
5012
+ */
5013
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
4977
5014
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
4978
5015
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
4979
5016
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -5002,6 +5039,74 @@ var FECManager = class _FECManager {
5002
5039
  }
5003
5040
  return recovered;
5004
5041
  }
5042
+ /**
5043
+ * Feed a source symbol of this stream that another FEC instance over the
5044
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
5045
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
5046
+ * BLO-52085). It joins its block's decoder as a received source would, so
5047
+ * it reduces the erasures the block still has to solve, but it is not a
5048
+ * network arrival: no transport accounting, no classification evidence,
5049
+ * no deadline refresh, and it never makes a block clean.
5050
+ *
5051
+ * Only a block a repair stated takes one (a repair-less span has no
5052
+ * decoder block). A block that is terminal, or already holds that source,
5053
+ * does not. The symbol is the exact protected packet, unpadded, as
5054
+ * feedSourceSymbol takes it.
5055
+ *
5056
+ * `recovered` is the block the synchronous decoder completed with it; the
5057
+ * caller routes it and completes the block, as for any recovery. When
5058
+ * every source of the block is then present, received or rebuilt, and no
5059
+ * decoder returned it (a Worker answers asynchronously, and reports such a
5060
+ * block as clean on its own side), the block is completed here as
5061
+ * recovered, with nothing left to route.
5062
+ */
5063
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
5064
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
5065
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
5066
+ if (located === null || located.block.span) return NOT_TAKEN;
5067
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
5068
+ const sbn = located.block.start;
5069
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
5070
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
5071
+ let rebuilt = this.#blockRebuiltEsis.get(key);
5072
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
5073
+ if (rebuilt === void 0) {
5074
+ rebuilt = /* @__PURE__ */ new Set();
5075
+ this.#blockRebuiltEsis.set(key, rebuilt);
5076
+ }
5077
+ const config = this.#trackConfigs.get(packetId);
5078
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
5079
+ rebuilt.add(located.esi);
5080
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
5081
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
5082
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
5083
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
5084
+ }
5085
+ }
5086
+ return { taken: true, recovered };
5087
+ }
5088
+ /**
5089
+ * A block that holds every one of its `length` sources, received or
5090
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
5091
+ * every packet of it was delivered, by its own arrival or by the instance
5092
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
5093
+ */
5094
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
5095
+ const rebuilt = this.#blockRebuiltEsis.get(key);
5096
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
5097
+ let present = rebuilt.size;
5098
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
5099
+ if (present < length) return false;
5100
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
5101
+ this.countRecovery(packetId, sbn, catalogTrackName);
5102
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5103
+ if (catalogTrackName === void 0) {
5104
+ this.onBlockComplete?.(sbn, packetId);
5105
+ } else {
5106
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
5107
+ }
5108
+ return true;
5109
+ }
5005
5110
  /**
5006
5111
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
5007
5112
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -5204,8 +5309,75 @@ var FECManager = class _FECManager {
5204
5309
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
5205
5310
  return null;
5206
5311
  }
5312
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
5207
5313
  return recovered;
5208
5314
  }
5315
+ /**
5316
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
5317
+ * its sources in send order, so one that moves the stream's newest source
5318
+ * position past a block leaves the sources that block still misses
5319
+ * reordered behind it at best: it shortens the wait of every block it
5320
+ * passed that only a late source can complete. MoQ is left out: its streams
5321
+ * can overtake one another, so a MoQ source shows nothing about the sources
5322
+ * sent before it.
5323
+ */
5324
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
5325
+ const previous = locator.newestSource;
5326
+ if (!locator.noteSource(ssId) || previous === void 0) return;
5327
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
5328
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
5329
+ this.#shortenUnrecoverableWait(
5330
+ this.#blockKey(block.start, packetId, catalogTrackName),
5331
+ block.start,
5332
+ packetId,
5333
+ catalogTrackName
5334
+ );
5335
+ }
5336
+ }
5337
+ /**
5338
+ * The wait of a block that only a late source can complete (BLO-52085): a
5339
+ * repair stated it, its received sources plus every symbol its repair group
5340
+ * carries (RSB_length) are short of K' (the repair scheduler's
5341
+ * group-cannot-complete), and a later source of the stream has arrived, so
5342
+ * a source it misses is lost or reordered behind that one. The span, which
5343
+ * covers repair symbols paced after its sources, buys it nothing: it waits
5344
+ * the late-symbol allowance alone, the multicast wait less the span (the
5345
+ * catalog tolerance or the measured delivery gap). Undefined for any other
5346
+ * block.
5347
+ */
5348
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
5349
+ const authoredLength = this.#blockAuthoredLengths.get(key);
5350
+ const rsbLength = this.#blockRsbLengths.get(key);
5351
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
5352
+ let present = this.#blockSourceCounts.get(key) ?? 0;
5353
+ const received = this.#blockSourceEsis.get(key);
5354
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
5355
+ if (present + rsbLength >= authoredLength) return void 0;
5356
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
5357
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
5358
+ return void 0;
5359
+ }
5360
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
5361
+ }
5362
+ /**
5363
+ * Bring an open block's wait in to its late-source wait, when it has one.
5364
+ * Never later than it is armed: only a source can complete such a block,
5365
+ * so a repair of its group (paced across the span) must not keep it open.
5366
+ */
5367
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
5368
+ if (!this.#activeBlocks.has(key)) return;
5369
+ const config = this.#trackConfigs.get(packetId);
5370
+ if (config === void 0) return;
5371
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
5372
+ if (waitMs === void 0) return;
5373
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
5374
+ this.#armBlockTimeout(
5375
+ sbn,
5376
+ packetId,
5377
+ catalogTrackName,
5378
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
5379
+ );
5380
+ }
5209
5381
  /**
5210
5382
  * Record a block's authored SSB_length from a repair symbol. Returns false,
5211
5383
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -5360,6 +5532,15 @@ var FECManager = class _FECManager {
5360
5532
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
5361
5533
  return;
5362
5534
  }
5535
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
5536
+ key,
5537
+ sbn,
5538
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
5539
+ packetId,
5540
+ catalogTrackName
5541
+ )) {
5542
+ return;
5543
+ }
5363
5544
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
5364
5545
  }
5365
5546
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -5368,6 +5549,7 @@ var FECManager = class _FECManager {
5368
5549
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5369
5550
  this.#blockSourceCounts.delete(key);
5370
5551
  this.#blockSourceEsis.delete(key);
5552
+ this.#blockRebuiltEsis.delete(key);
5371
5553
  this.#blockAuthoredLengths.delete(key);
5372
5554
  this.#blockRepairRsIds.delete(key);
5373
5555
  this.#blockRsbLengths.delete(key);
@@ -5434,7 +5616,16 @@ var FECManager = class _FECManager {
5434
5616
  this.#repairDeadlineMs(trackConfig)
5435
5617
  );
5436
5618
  }
5437
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
5619
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
5620
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
5621
+ return;
5622
+ }
5623
+ this.#armBlockTimeout(
5624
+ sbn,
5625
+ packetId,
5626
+ catalogTrackName,
5627
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
5628
+ );
5438
5629
  }
5439
5630
  #failBlock(sbn, packetId, catalogTrackName) {
5440
5631
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -5447,6 +5638,7 @@ var FECManager = class _FECManager {
5447
5638
  }
5448
5639
  this.#blockSourceCounts.delete(key);
5449
5640
  this.#blockSourceEsis.delete(key);
5641
+ this.#blockRebuiltEsis.delete(key);
5450
5642
  this.#blockAuthoredLengths.delete(key);
5451
5643
  this.#blockRepairRsIds.delete(key);
5452
5644
  this.#blockRsbLengths.delete(key);
@@ -5485,6 +5677,7 @@ var FECManager = class _FECManager {
5485
5677
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
5486
5678
  this.#blockSourceCounts.delete(key);
5487
5679
  this.#blockSourceEsis.delete(key);
5680
+ this.#blockRebuiltEsis.delete(key);
5488
5681
  this.#blockAuthoredLengths.delete(key);
5489
5682
  this.#blockRepairRsIds.delete(key);
5490
5683
  this.#blockRsbLengths.delete(key);
@@ -5576,6 +5769,7 @@ var FECManager = class _FECManager {
5576
5769
  this.#blockKeysBySbn.clear();
5577
5770
  this.#blockSourceCounts.clear();
5578
5771
  this.#blockSourceEsis.clear();
5772
+ this.#blockRebuiltEsis.clear();
5579
5773
  this.#blockAuthoredLengths.clear();
5580
5774
  this.#blockRepairRsIds.clear();
5581
5775
  this.#blockRsbLengths.clear();
@@ -7076,6 +7270,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7076
7270
  });
7077
7271
 
7078
7272
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7273
+ var MAX_PENDING_PACKETS = 256;
7079
7274
  var AMTGatewayManager = class {
7080
7275
  subscriptions = /* @__PURE__ */ new Map();
7081
7276
  messageHandler;
@@ -7199,7 +7394,7 @@ var AMTGatewayManager = class {
7199
7394
  if (!subscription.handshakeComplete) {
7200
7395
  const queue = this.pendingPackets.get(subscriptionId);
7201
7396
  const packetData2 = data;
7202
- if (queue) {
7397
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7203
7398
  queue.push({
7204
7399
  data: packetData2.data,
7205
7400
  remoteAddress: packetData2.remoteAddress,
@@ -13142,9 +13337,21 @@ var IsoBmffTimingResolver = class {
13142
13337
  }
13143
13338
  #track = null;
13144
13339
  #fragments = /* @__PURE__ */ new Map();
13340
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
13341
+ #firstSamples = /* @__PURE__ */ new Map();
13342
+ /**
13343
+ * First sample and sample count of the newest fragments whose every slot was
13344
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
13345
+ * matches the combiner, since it retires an abandoned sample as well.
13346
+ */
13347
+ #delivered = /* @__PURE__ */ new Map();
13145
13348
  #pendingSamples = 0;
13146
13349
  #evictedSamples = 0;
13147
13350
  #lastMovieFragmentRejection = null;
13351
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
13352
+ get initialized() {
13353
+ return this.#track !== null;
13354
+ }
13148
13355
  get pendingSampleCount() {
13149
13356
  return this.#pendingSamples;
13150
13357
  }
@@ -13168,7 +13375,14 @@ var IsoBmffTimingResolver = class {
13168
13375
  this.#track = timing;
13169
13376
  return true;
13170
13377
  }
13171
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
13378
+ /**
13379
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
13380
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
13381
+ * fragment whose samples are all done is a duplicate: admitting it again
13382
+ * would charge stale timing and could displace the live MPU's. A source
13383
+ * copy is still admitted again (a rendition switch re-arms timing that way).
13384
+ */
13385
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
13172
13386
  this.#lastMovieFragmentRejection = null;
13173
13387
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13174
13388
  this.#lastMovieFragmentRejection = "invalid";
@@ -13194,6 +13408,8 @@ var IsoBmffTimingResolver = class {
13194
13408
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13195
13409
  );
13196
13410
  }
13411
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
13412
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
13197
13413
  let freed = 0;
13198
13414
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13199
13415
  freed += this.#evictOldestFragment();
@@ -13207,6 +13423,7 @@ var IsoBmffTimingResolver = class {
13207
13423
  return false;
13208
13424
  }
13209
13425
  this.#fragments.set(key, [...parsed.samples]);
13426
+ this.#firstSamples.set(key, parsed.samples[0]);
13210
13427
  this.#pendingSamples += parsed.samples.length;
13211
13428
  return true;
13212
13429
  }
@@ -13264,6 +13481,7 @@ var IsoBmffTimingResolver = class {
13264
13481
  let live = 0;
13265
13482
  for (const sample of samples) if (sample !== null) live++;
13266
13483
  this.#fragments.delete(oldest);
13484
+ this.#firstSamples.delete(oldest);
13267
13485
  this.#pendingSamples -= live;
13268
13486
  this.#evictedSamples += live;
13269
13487
  return live;
@@ -13279,12 +13497,23 @@ var IsoBmffTimingResolver = class {
13279
13497
  if (!samples || !samples[index]) return false;
13280
13498
  samples[index] = null;
13281
13499
  this.#pendingSamples--;
13282
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
13500
+ if (samples.every((value) => value === null)) {
13501
+ this.#fragments.delete(key);
13502
+ const first = this.#firstSamples.get(key);
13503
+ this.#firstSamples.delete(key);
13504
+ this.#delivered.delete(key);
13505
+ this.#delivered.set(key, { first, sampleCount: samples.length });
13506
+ if (this.#delivered.size > this.maxPendingSamples) {
13507
+ this.#delivered.delete(this.#delivered.keys().next().value);
13508
+ }
13509
+ }
13283
13510
  return true;
13284
13511
  }
13285
13512
  reset() {
13286
13513
  this.#track = null;
13287
13514
  this.#fragments.clear();
13515
+ this.#firstSamples.clear();
13516
+ this.#delivered.clear();
13288
13517
  this.#pendingSamples = 0;
13289
13518
  }
13290
13519
  };
@@ -13766,6 +13995,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13766
13995
  this.#reclaimedTimingSamples = 0;
13767
13996
  this.#evictedTimingSamples = 0;
13768
13997
  this.#rejectedMovieFragments = 0;
13998
+ this.#recoveredHeadsAwaitingTiming = {
13999
+ held: 0,
14000
+ expired: 0,
14001
+ discarded: 0,
14002
+ superseded: 0
14003
+ };
13769
14004
  this.#sequencers = /* @__PURE__ */ new Map();
13770
14005
  this.#fecGate = null;
13771
14006
  this.#fecSequencer = null;
@@ -13874,6 +14109,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13874
14109
  #reclaimedTimingSamples;
13875
14110
  #evictedTimingSamples;
13876
14111
  #rejectedMovieFragments;
14112
+ #recoveredHeadsAwaitingTiming;
13877
14113
  #sequencers;
13878
14114
  #fecGate;
13879
14115
  #fecSequencer;
@@ -13994,6 +14230,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13994
14230
  rejectedMovieFragments: this.#rejectedMovieFragments
13995
14231
  };
13996
14232
  }
14233
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14234
+ get recoveredHeadsAwaitingTiming() {
14235
+ let pending = 0;
14236
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14237
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14238
+ }
14239
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14240
+ }
13997
14241
  get reorderStats() {
13998
14242
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13999
14243
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14167,7 +14411,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14167
14411
  const now = Date.now();
14168
14412
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14169
14413
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14170
- awaitingTiming.shift();
14414
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14171
14415
  }
14172
14416
  }
14173
14417
  }
@@ -14285,11 +14529,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14285
14529
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14286
14530
  return this.#drop("fragment-unexpected-fragmentation");
14287
14531
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14288
- if (!resolver2) return this.#drop("fragment-no-resolver");
14532
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14289
14533
  const pendingBefore = resolver2.pendingSampleCount;
14290
14534
  const evictedBefore = resolver2.evictedSampleCount;
14291
14535
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14292
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14536
+ const admitted = resolver2.addMovieFragment(
14537
+ mpu.mpuSequenceNumber,
14538
+ data,
14539
+ remaining,
14540
+ options?.recovered === true
14541
+ );
14293
14542
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14294
14543
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14295
14544
  if (!admitted) {
@@ -14309,7 +14558,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14309
14558
  };
14310
14559
  }
14311
14560
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14312
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14561
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14313
14562
  if (!resolver) return this.#drop("mfu-no-resolver");
14314
14563
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14315
14564
  return this.#drop("mfu-empty-payload");
@@ -14330,6 +14579,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14330
14579
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14331
14580
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14332
14581
  sampleNumber: mfuDuHeader.sampleNumber,
14582
+ sequenceNumber: info.sequenceNumber,
14333
14583
  arrivedAt: Date.now()
14334
14584
  });
14335
14585
  return this.#drop("mfu-sample-timing-missing");
@@ -14718,6 +14968,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14718
14968
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14719
14969
  this.#sequencers.clear();
14720
14970
  this.#fecSequencer?.discard();
14971
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14972
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14973
+ }
14721
14974
  this.#timingStreams.clear();
14722
14975
  this.#pendingTimingSamples = 0;
14723
14976
  this.#reliableMoqStartupGate.clear();
@@ -14736,10 +14989,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14736
14989
  return resolver;
14737
14990
  }
14738
14991
  #awaitTiming(streamKey, head) {
14739
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14740
- if (!awaitingTiming) return;
14992
+ const state = this.#timingStreams.get(streamKey);
14993
+ if (!state) return;
14994
+ if (!head.options?.recovered) {
14995
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
14996
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
14997
+ state.awaitingTiming = kept;
14998
+ }
14999
+ const awaitingTiming = state.awaitingTiming;
14741
15000
  awaitingTiming.push(head);
14742
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
15001
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
15002
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
15003
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
15004
+ }
15005
+ }
15006
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
15007
+ #leaveAwaitingTiming(head, outcome) {
15008
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14743
15009
  }
14744
15010
  /**
14745
15011
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14767,6 +15033,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14767
15033
  const state = this.#timingStreams.get(streamKey);
14768
15034
  if (!state) return;
14769
15035
  this.#timingStreams.delete(streamKey);
15036
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14770
15037
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14771
15038
  if (this.#pendingTimingSamples < 0) {
14772
15039
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -17972,6 +18239,16 @@ function keyframeOverlaySsId(header) {
17972
18239
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
17973
18240
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
17974
18241
  }
18242
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
18243
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
18244
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
18245
+ }
18246
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
18247
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
18248
+ return headerPsn + offset >>> 0;
18249
+ }
18250
+ var OVERLAY_ROUTED_LIMIT = 1024;
18251
+ var OVERLAY_PENDING_LIMIT = 256;
17975
18252
  var OVERLAY_OFFSET_MPUS = 16;
17976
18253
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
17977
18254
  function assertStreamIdentity(role, stream) {
@@ -18083,6 +18360,7 @@ var MmtpRouter = class {
18083
18360
  #provenance = /* @__PURE__ */ new Map();
18084
18361
  #onFlowProtected = null;
18085
18362
  #unadmittedListedDrops = 0;
18363
+ #admittedSourceDatagrams = 0;
18086
18364
  #unlistedRepairSetAside = 0;
18087
18365
  #assistBlocks = /* @__PURE__ */ new Map();
18088
18366
  #assistFailures = /* @__PURE__ */ new Map();
@@ -18139,6 +18417,15 @@ var MmtpRouter = class {
18139
18417
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
18140
18418
  */
18141
18419
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18420
+ /**
18421
+ * Per stream, the SS_ID offset at which a base recovery last showed the
18422
+ * sender's own packet_sequence_number: a recovered packet's header psn
18423
+ * equal to its SS_ID minus that offset. While it equals the stream's
18424
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
18425
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
18426
+ * an MPU no member of which arrived).
18427
+ */
18428
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18142
18429
  /**
18143
18430
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
18144
18431
  * not parsed, typically because its overlap copy was parsed first
@@ -18208,7 +18495,9 @@ var MmtpRouter = class {
18208
18495
  recovered: 0,
18209
18496
  restarts: 0,
18210
18497
  unsequencedDropped: 0,
18211
- protectedDropped: 0
18498
+ protectedDropped: 0,
18499
+ baseFed: 0,
18500
+ baseUndetermined: 0
18212
18501
  };
18213
18502
  /** Callback fired when a delta-track FEC block completes or fails. */
18214
18503
  #onDeltaBlockCompleteCb = null;
@@ -18381,7 +18670,7 @@ var MmtpRouter = class {
18381
18670
  const cb = this.#getFrameCallback(packetId);
18382
18671
  if (isVideo && !assembler) {
18383
18672
  if (frameMeta.recovered) {
18384
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
18673
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
18385
18674
  } else {
18386
18675
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
18387
18676
  }
@@ -18771,6 +19060,10 @@ var MmtpRouter = class {
18771
19060
  get keyframeOverlayStats() {
18772
19061
  return { ...this.#overlayStats };
18773
19062
  }
19063
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
19064
+ get recoveredHeadsAwaitingTiming() {
19065
+ return this.#parser.recoveredHeadsAwaitingTiming;
19066
+ }
18774
19067
  #isOverlayRepair(repairPacketId, repairTrackName) {
18775
19068
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18776
19069
  }
@@ -18821,6 +19114,11 @@ var MmtpRouter = class {
18821
19114
  */
18822
19115
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
18823
19116
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
19117
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
19118
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
19119
+ stream.overlayToBase.routed.clear();
19120
+ stream.overlayToBase.pending.clear();
19121
+ }
18824
19122
  }
18825
19123
  #resetOverlay(inst) {
18826
19124
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -18877,9 +19175,10 @@ var MmtpRouter = class {
18877
19175
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
18878
19176
  * every overlay installed for its source, at that SS_ID, with the bytes the
18879
19177
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
18880
- * included (7.1, 7.4.3).
19178
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
19179
+ * FEC reads it.
18881
19180
  */
18882
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
19181
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
18883
19182
  if (this.#overlays.size === 0) return;
18884
19183
  const ssId = keyframeOverlaySsId(header);
18885
19184
  if (ssId === void 0) return;
@@ -18888,7 +19187,11 @@ var MmtpRouter = class {
18888
19187
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
18889
19188
  if (identity !== null) {
18890
19189
  inst.offsets.delete(identity.mpuSequenceNumber);
18891
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
19190
+ inst.offsets.set(identity.mpuSequenceNumber, {
19191
+ psnOffset: ssId - sequenceNumber >>> 0,
19192
+ overlaySsId: ssId,
19193
+ ...baseSsId === void 0 ? {} : { baseSsId }
19194
+ });
18892
19195
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
18893
19196
  }
18894
19197
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -18967,14 +19270,27 @@ var MmtpRouter = class {
18967
19270
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
18968
19271
  const restamped = this.#restampedStreams.has(streamKey);
18969
19272
  const restored = /* @__PURE__ */ new Set();
19273
+ const baseSsIds = /* @__PURE__ */ new Map();
18970
19274
  const symbols = result.symbols.flatMap((sym) => {
18971
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
18972
- if (offset !== void 0) {
18973
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
19275
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
19276
+ if (anchor !== void 0) {
19277
+ const overlaySsId = serial32Add(blockStart, sym.index);
19278
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
18974
19279
  restored.add(packetSequenceNumber);
18975
- return [{ ...sym, packetSequenceNumber }];
19280
+ const out = { ...sym, packetSequenceNumber };
19281
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
19282
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
19283
+ return [out];
19284
+ }
19285
+ if (!restamped) {
19286
+ const baseSsId = unstampedBaseSsId(
19287
+ this.#ssIdSeqOffsetByStream.get(streamKey),
19288
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
19289
+ sym.packetSequenceNumber
19290
+ );
19291
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
19292
+ return [sym];
18976
19293
  }
18977
- if (!restamped) return [sym];
18978
19294
  this.#overlayStats.unsequencedDropped++;
18979
19295
  return [];
18980
19296
  });
@@ -18983,6 +19299,85 @@ var MmtpRouter = class {
18983
19299
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
18984
19300
  });
18985
19301
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
19302
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
19303
+ }
19304
+ /**
19305
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
19306
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
19307
+ * from a received member of its MPU, else unstampedBaseSsId),
19308
+ * so it is fed to the base decoder as a rebuilt source
19309
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
19310
+ * fewer to solve, and its repair can recover the block's other packets.
19311
+ * A base block no repair has stated yet takes it when one does
19312
+ * (#feedPendingRebuilt).
19313
+ *
19314
+ * The overlay route already handled each packet fed (delivered it, or
19315
+ * held an untimed head for its FT=1), so a base recovery routes none of
19316
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
19317
+ * held head would wait in the parser for timing its first copy consumed.
19318
+ * Only an overlay block decoded from multicast symbols alone is fed: with
19319
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
19320
+ * that crossed no multicast socket, which the provenance counted from the
19321
+ * block's own arrivals cannot show.
19322
+ */
19323
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
19324
+ const fec = this.#fecManager;
19325
+ const owner = inst.leg.sourcePacketId;
19326
+ const name = inst.leg.sourceCatalogTrackName;
19327
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
19328
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
19329
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
19330
+ const feeds = [];
19331
+ for (const sym of symbols) {
19332
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
19333
+ const ssId = baseSsIds.get(sym);
19334
+ if (ssId === void 0) {
19335
+ this.#overlayStats.baseUndetermined++;
19336
+ continue;
19337
+ }
19338
+ routed.delete(ssId);
19339
+ routed.add(ssId);
19340
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
19341
+ feeds.push([ssId, sym.rawSymbol]);
19342
+ }
19343
+ for (const [ssId, symbolData] of feeds) {
19344
+ if (this.#classifyPacketId(owner) === null) return;
19345
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19346
+ }
19347
+ }
19348
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
19349
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
19350
+ const fec = this.#fecManager;
19351
+ if (!fec?.feedRebuiltSourceSymbol) return;
19352
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
19353
+ if (coords == null) {
19354
+ const pending = this.#stream(owner, name).overlayToBase.pending;
19355
+ pending.delete(ssId);
19356
+ pending.set(ssId, symbolData.slice());
19357
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
19358
+ return;
19359
+ }
19360
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
19361
+ if (!fed.taken) return;
19362
+ this.#overlayStats.baseFed++;
19363
+ if (fed.recovered === null) return;
19364
+ const sbn = coords.sbn;
19365
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
19366
+ fec.cancelBlockTimeout(sbn, owner, name);
19367
+ fec.countRecovery(owner, sbn, name);
19368
+ fec.markBlockCompleted(sbn, owner, name);
19369
+ });
19370
+ }
19371
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
19372
+ #feedPendingRebuilt(owner, name, block) {
19373
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
19374
+ if (pending === void 0 || pending.size === 0) return;
19375
+ for (const [ssId, symbolData] of [...pending]) {
19376
+ if (this.#classifyPacketId(owner) === null) return;
19377
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
19378
+ pending.delete(ssId);
19379
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19380
+ }
18986
19381
  }
18987
19382
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
18988
19383
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -19122,6 +19517,10 @@ var MmtpRouter = class {
19122
19517
  get unadmittedListedDropCount() {
19123
19518
  return this.#unadmittedListedDrops;
19124
19519
  }
19520
+ /** 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. */
19521
+ get admittedSourceDatagramCount() {
19522
+ return this.#admittedSourceDatagrams;
19523
+ }
19125
19524
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
19126
19525
  get unlistedRepairSetAsideCount() {
19127
19526
  return this.#unlistedRepairSetAside;
@@ -19608,6 +20007,8 @@ var MmtpRouter = class {
19608
20007
  )) return;
19609
20008
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
19610
20009
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
20010
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
20011
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
19611
20012
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
19612
20013
  const recovered = fec.feedRepairSymbol(
19613
20014
  ownerPacketId,
@@ -19745,6 +20146,7 @@ var MmtpRouter = class {
19745
20146
  this.#droppedPreSyncVideo.delete(streamKey);
19746
20147
  this.#latestVideoSeqByStream.delete(streamKey);
19747
20148
  this.#ssIdSeqOffsetByStream.delete(streamKey);
20149
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
19748
20150
  this.#multicastRapTwinByStream.delete(streamKey);
19749
20151
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
19750
20152
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -20379,6 +20781,7 @@ var MmtpRouter = class {
20379
20781
  const parsed = this.#parser.parsePacket(packet);
20380
20782
  if (!parsed) return;
20381
20783
  const info = parsed.info;
20784
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
20382
20785
  if (source === "multicast" && !manifestAdmitted) {
20383
20786
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
20384
20787
  }
@@ -20660,7 +21063,8 @@ var MmtpRouter = class {
20660
21063
  parsed.header,
20661
21064
  info.sequenceNumber,
20662
21065
  info.rapFlag,
20663
- source
21066
+ source,
21067
+ fecPayloadId?.ssId
20664
21068
  );
20665
21069
  }
20666
21070
  }
@@ -20810,7 +21214,8 @@ var MmtpRouter = class {
20810
21214
  if (stream === void 0) {
20811
21215
  stream = {
20812
21216
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
20813
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
21217
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
21218
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
20814
21219
  };
20815
21220
  byName.set(catalogTrackName, stream);
20816
21221
  }
@@ -20925,8 +21330,16 @@ var MmtpRouter = class {
20925
21330
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
20926
21331
  this.#emitDirectVideo(packetId, data, meta, cb);
20927
21332
  }
20928
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
21333
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
20929
21334
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
21335
+ if (this.#isFecTrack(packetId)) {
21336
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
21337
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
21338
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
21339
+ this.#emitDirectVideo(packetId, data, meta, cb);
21340
+ return;
21341
+ }
21342
+ }
20930
21343
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
20931
21344
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
20932
21345
  this.#drainDirectRecoveredVideo(
@@ -21080,6 +21493,13 @@ var MmtpRouter = class {
21080
21493
  * frames by epoch only if pre-protection input ever matters.
21081
21494
  */
21082
21495
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
21496
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
21497
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
21498
+ result = {
21499
+ ...result,
21500
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
21501
+ };
21502
+ }
21083
21503
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
21084
21504
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
21085
21505
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -21144,7 +21564,10 @@ var MmtpRouter = class {
21144
21564
  ...result,
21145
21565
  symbols: result.symbols.map((sym) => {
21146
21566
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
21147
- if (wire === sym.packetSequenceNumber) return sym;
21567
+ if (wire === sym.packetSequenceNumber) {
21568
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
21569
+ return sym;
21570
+ }
21148
21571
  this.#restampedRecoveredSeqs++;
21149
21572
  this.#restampedStreams.add(streamKey);
21150
21573
  return { ...sym, packetSequenceNumber: wire };
@@ -21180,16 +21603,29 @@ var MmtpRouter = class {
21180
21603
  }
21181
21604
  );
21182
21605
  }
21183
- result = {
21184
- ...result,
21185
- symbols: result.symbols.filter((sym) => sym.fragmentType !== 2 || sym.fragment.fragmentationIndicator !== 1 || sym.fragment.movieFragmentSequenceNumber === void 0 || sym.fragment.sampleNumber === void 0 || this.#parser.hasAuthoredTimingFor(
21186
- ownerPacketId,
21187
- sym.fragment.mpuSequenceNumber,
21188
- sym.fragment.movieFragmentSequenceNumber,
21189
- sym.fragment.sampleNumber,
21190
- catalogTrackName
21191
- ))
21192
- };
21606
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21607
+ ownerPacketId,
21608
+ sym.fragment.mpuSequenceNumber,
21609
+ sym.fragment.movieFragmentSequenceNumber,
21610
+ sym.fragment.sampleNumber,
21611
+ catalogTrackName
21612
+ );
21613
+ for (const sym of result.symbols) {
21614
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21615
+ this.#feedViaParser(
21616
+ sym.rawSymbol,
21617
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21618
+ void 0,
21619
+ {
21620
+ skipFecPayloadId: true,
21621
+ recovered: true,
21622
+ deliveryProvenance,
21623
+ ...fecSbnOf(sym.index),
21624
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21625
+ }
21626
+ );
21627
+ }
21628
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21193
21629
  const gated = this.#isFecTrack(ownerPacketId);
21194
21630
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21195
21631
  if (assembler) {