@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.75b39a2cdddf

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 (93) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1001 -215
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +222 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +58 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +105 -34
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +229 -51
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +104 -34
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +18 -1
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +323 -64
  56. package/dist/mmtp-router.js.map +2 -2
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.js +547 -72
  60. package/dist/multicast-manager.js.map +2 -2
  61. package/dist/multicast-transport.js +28 -18
  62. package/dist/multicast-transport.js.map +2 -2
  63. package/dist/multicast-unwrap.js +12 -19
  64. package/dist/multicast-unwrap.js.map +2 -2
  65. package/dist/no-modules.js +222 -48
  66. package/dist/no-modules.js.map +3 -3
  67. package/dist/repair-fetch.js.map +1 -1
  68. package/dist/repair-layer-subscriptions.d.ts +82 -0
  69. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  70. package/dist/repair-layer-subscriptions.js +192 -0
  71. package/dist/repair-layer-subscriptions.js.map +7 -0
  72. package/dist/signaling.js.map +1 -1
  73. package/dist/timestamp-codec.js +12 -19
  74. package/dist/timestamp-codec.js.map +2 -2
  75. package/dist/track-session.d.ts +1 -1
  76. package/dist/track-session.js.map +1 -1
  77. package/dist/wasm-mfu-reassembler.js.map +1 -1
  78. package/dist/wasm-mmtp-container-parser.js +104 -34
  79. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  80. package/package.json +8 -5
  81. package/schemas/catalog.schema.json +4 -0
  82. package/src/catalog-fec-params.ts +36 -1
  83. package/src/cmcd-producer.ts +189 -179
  84. package/src/fec-manager.ts +277 -6
  85. package/src/fec-worker-setup.ts +2 -2
  86. package/src/index.ts +18 -2
  87. package/src/iso-block-locator.ts +19 -0
  88. package/src/iso-bmff-timing.ts +40 -1
  89. package/src/mmtp-container-parser.ts +125 -19
  90. package/src/mmtp-fec-session.ts +115 -15
  91. package/src/mmtp-router.ts +350 -13
  92. package/src/repair-layer-subscriptions.ts +261 -0
  93. package/src/track-session.ts +1 -1
@@ -171,30 +171,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
171
171
  const authLength = view2.getUint16(2, false);
172
172
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
173
173
  }
174
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
174
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
175
175
  if (declaredEnd > payload.length)
176
176
  return null;
177
177
  if (options?.skipFecId) {
178
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
178
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
179
179
  }
180
- const overlay = options?.overlayDeclared === true && rapFlag;
181
180
  if (fecType === 1) {
182
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
181
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
183
182
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
184
183
  return null;
185
184
  }
186
- return { hasFecId: true, hasOverlayFecId: overlay };
185
+ return { hasFecId: true };
187
186
  }
188
- if (fecType !== 0 || overlay)
187
+ if (fecType !== 0)
189
188
  return null;
190
189
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
191
- return { hasFecId: false, hasOverlayFecId: false };
190
+ return { hasFecId: false };
192
191
  }
193
192
  return null;
194
193
  }
195
- function sourceFecTrailerSize(overlay) {
196
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
197
- }
198
194
  function planMfuExtraction(mmtpPacket, options, parsedHeader) {
199
195
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
200
196
  return {
@@ -247,11 +243,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
247
243
  if ("rejection" in layoutResult)
248
244
  return layoutResult;
249
245
  const { layout } = layoutResult;
250
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
246
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
251
247
  if (!trailerLayout) {
252
248
  if (!options?.skipFecId && header.fecType === 1) {
253
249
  const observed = payload.length - layout.dataEnd;
254
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
250
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
255
251
  if (observed !== expected) {
256
252
  return {
257
253
  rejection: {
@@ -283,13 +279,10 @@ function extractPlannedMfu(mmtpPacket, result) {
283
279
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
284
280
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
285
281
  const data = payload.subarray(mfuOffset, dataEnd);
286
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
287
- const ssIdAt = (offset) => ({
288
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
289
- });
290
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
291
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
292
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
282
+ const fecPayloadId = trailerLayout.hasFecId ? {
283
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
284
+ } : null;
285
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
293
286
  }
294
287
  function sourceFecPayloadIdOf(mmtpPacket, header) {
295
288
  if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
@@ -1217,6 +1210,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1217
1210
  #retentionBlocks;
1218
1211
  #heldEvicted;
1219
1212
  #newest;
1213
+ #newestSource;
1220
1214
  #holes;
1221
1215
  static #checkedRetention(retentionBlocks) {
1222
1216
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -1254,6 +1248,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
1254
1248
  observe(position) {
1255
1249
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
1256
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
+ }
1257
1266
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
1258
1267
  get heldEvicted() {
1259
1268
  return this.#heldEvicted;
@@ -1486,6 +1495,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
1486
1495
  this.#held.clear();
1487
1496
  this.#coverageEnd = void 0;
1488
1497
  this.#newest = void 0;
1498
+ this.#newestSource = void 0;
1489
1499
  }
1490
1500
  #claim(block) {
1491
1501
  const placed = [];
@@ -2074,6 +2084,7 @@ var WORKER_ERROR_LOG_CAP = 5;
2074
2084
  var WORKER_ERROR_KIND_CAP = 32;
2075
2085
  var WORKER_ERROR_OVERFLOW_KIND = "other";
2076
2086
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
2087
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
2077
2088
  function isMulticastSymbolDelivery(value) {
2078
2089
  return value === "datagram" || value === "ordered-relay";
2079
2090
  }
@@ -2171,6 +2182,10 @@ var FECManager = class _FECManager {
2171
2182
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
2172
2183
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
2173
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();
2174
2189
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
2175
2190
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
2176
2191
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -2299,6 +2314,10 @@ var FECManager = class _FECManager {
2299
2314
  // ── Keyframe-protected repair policy (block-policy.ts) ──
2300
2315
  // One classifier per physical stream, built from that stream's geometry.
2301
2316
  this.#classifiers = /* @__PURE__ */ new Map();
2317
+ // A stream without catalog timing has no classifier, so the last RAP it
2318
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
2319
+ // a second RAP is released inside one MPU (BLO-52085).
2320
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
2302
2321
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
2303
2322
  // rungs can share a packetId with a different framerate or GOP, so the
2304
2323
  // packet-level geometry is only the geometry of a packet whose streams
@@ -2456,6 +2475,7 @@ var FECManager = class _FECManager {
2456
2475
  #pendingWorkerCleanupBounds;
2457
2476
  #blockSourceCounts;
2458
2477
  #blockSourceEsis;
2478
+ #blockRebuiltEsis;
2459
2479
  #blockAuthoredLengths;
2460
2480
  #ssbLengthConflictCount;
2461
2481
  #blockRepairRsIds;
@@ -2498,6 +2518,7 @@ var FECManager = class _FECManager {
2498
2518
  #orderedFrontiers;
2499
2519
  #orderedPendingBlocks;
2500
2520
  #classifiers;
2521
+ #noSpanRaps;
2501
2522
  #streamGeometries;
2502
2523
  #failedBlocks;
2503
2524
  #resolvedBlockClasses;
@@ -3159,6 +3180,7 @@ var FECManager = class _FECManager {
3159
3180
  };
3160
3181
  const discontinuity = () => {
3161
3182
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
3183
+ this.#noSpanRaps.delete(streamKey);
3162
3184
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
3163
3185
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
3164
3186
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -3571,6 +3593,9 @@ var FECManager = class _FECManager {
3571
3593
  }
3572
3594
  if (policyChanged) {
3573
3595
  this.#bumpEpoch(packetId);
3596
+ for (const key of [...this.#noSpanRaps.keys()]) {
3597
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
3598
+ }
3574
3599
  for (const [key, classifier] of [...this.#classifiers]) {
3575
3600
  if (!this.#belongsToPacket(key, packetId)) continue;
3576
3601
  const stream = this.#parseStreamKey(key);
@@ -3773,6 +3798,7 @@ var FECManager = class _FECManager {
3773
3798
  }
3774
3799
  resetMap(this.#blockSourceCounts);
3775
3800
  resetMap(this.#blockSourceEsis);
3801
+ resetMap(this.#blockRebuiltEsis);
3776
3802
  resetMap(this.#blockAuthoredLengths);
3777
3803
  resetMap(this.#blockRepairRsIds);
3778
3804
  resetMap(this.#blockRsbLengths);
@@ -3806,6 +3832,7 @@ var FECManager = class _FECManager {
3806
3832
  resetMap(this.#isoLocators);
3807
3833
  resetMap(this.#sourceAnchors);
3808
3834
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
3835
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
3809
3836
  this.#bumpEpoch(packetId);
3810
3837
  for (const [sbn, keys] of this.#blockKeysBySbn) {
3811
3838
  for (const key of keys.keys()) {
@@ -3896,6 +3923,7 @@ var FECManager = class _FECManager {
3896
3923
  dropMap(this.#pendingWorkerCleanupClients);
3897
3924
  dropMap(this.#blockSourceCounts);
3898
3925
  dropMap(this.#blockSourceEsis);
3926
+ dropMap(this.#blockRebuiltEsis);
3899
3927
  dropMap(this.#blockAuthoredLengths);
3900
3928
  dropMap(this.#blockRepairRsIds);
3901
3929
  dropMap(this.#blockRsbLengths);
@@ -3912,6 +3940,7 @@ var FECManager = class _FECManager {
3912
3940
  dropMap(this.#isoLocators);
3913
3941
  dropMap(this.#sourceAnchors);
3914
3942
  dropMap(this.#classifiers);
3943
+ dropMap(this.#noSpanRaps);
3915
3944
  dropMap(this.#streamGeometries);
3916
3945
  dropSet(this.#pendingWorkerCleanups);
3917
3946
  dropSet(this.#completedBlocks);
@@ -4057,6 +4086,9 @@ var FECManager = class _FECManager {
4057
4086
  this.#unindexBlockKey(key);
4058
4087
  }
4059
4088
  }
4089
+ if (isSourceSymbol && source === "multicast") {
4090
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
4091
+ }
4060
4092
  return true;
4061
4093
  }
4062
4094
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -4374,6 +4406,7 @@ var FECManager = class _FECManager {
4374
4406
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
4375
4407
  const evictedBefore = locator.heldEvicted;
4376
4408
  const held = locator.hold(ssId, source);
4409
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
4377
4410
  const evicted = locator.heldEvicted - evictedBefore;
4378
4411
  if (evicted > 0) {
4379
4412
  const before = this.#isoHeldEvictedCount;
@@ -4542,11 +4575,14 @@ var FECManager = class _FECManager {
4542
4575
  /**
4543
4576
  * Whether an MPU boundary still bounds this stream's RAPs (see
4544
4577
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
4545
- * it does not. A stream with no classifier yet has shown nothing against it.
4578
+ * it does not. A stream with no classifier yet has shown nothing against it,
4579
+ * and neither has a stream without catalog timing until it releases two
4580
+ * RAPs inside one MPU.
4546
4581
  */
4547
4582
  mpuBoundsRapsFor(packetId, catalogTrackName) {
4548
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
4549
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
4583
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4584
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
4585
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
4550
4586
  }
4551
4587
  /** A block's class now, or frozen at its resolution. */
4552
4588
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -4572,7 +4608,14 @@ var FECManager = class _FECManager {
4572
4608
  * invariant (consecutive MPUs exactly one keyframe interval apart).
4573
4609
  */
4574
4610
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
4575
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
4611
+ if (!this.#trackConfigs.has(packetId)) return;
4612
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
4613
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
4614
+ const last = this.#noSpanRaps.get(streamKey);
4615
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
4616
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
4617
+ return;
4618
+ }
4576
4619
  this.#applyClassifierChanges(
4577
4620
  packetId,
4578
4621
  catalogTrackName,
@@ -4959,7 +5002,15 @@ var FECManager = class _FECManager {
4959
5002
  this.#isoSourceOversizeCount++;
4960
5003
  return null;
4961
5004
  }
4962
- 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) {
4963
5014
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
4964
5015
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
4965
5016
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -4988,6 +5039,74 @@ var FECManager = class _FECManager {
4988
5039
  }
4989
5040
  return recovered;
4990
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
+ }
4991
5110
  /**
4992
5111
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
4993
5112
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -5190,8 +5309,75 @@ var FECManager = class _FECManager {
5190
5309
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
5191
5310
  return null;
5192
5311
  }
5312
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
5193
5313
  return recovered;
5194
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
+ }
5195
5381
  /**
5196
5382
  * Record a block's authored SSB_length from a repair symbol. Returns false,
5197
5383
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -5346,6 +5532,15 @@ var FECManager = class _FECManager {
5346
5532
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
5347
5533
  return;
5348
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
+ }
5349
5544
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
5350
5545
  }
5351
5546
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -5354,6 +5549,7 @@ var FECManager = class _FECManager {
5354
5549
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
5355
5550
  this.#blockSourceCounts.delete(key);
5356
5551
  this.#blockSourceEsis.delete(key);
5552
+ this.#blockRebuiltEsis.delete(key);
5357
5553
  this.#blockAuthoredLengths.delete(key);
5358
5554
  this.#blockRepairRsIds.delete(key);
5359
5555
  this.#blockRsbLengths.delete(key);
@@ -5420,7 +5616,16 @@ var FECManager = class _FECManager {
5420
5616
  this.#repairDeadlineMs(trackConfig)
5421
5617
  );
5422
5618
  }
5423
- 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
+ );
5424
5629
  }
5425
5630
  #failBlock(sbn, packetId, catalogTrackName) {
5426
5631
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -5433,6 +5638,7 @@ var FECManager = class _FECManager {
5433
5638
  }
5434
5639
  this.#blockSourceCounts.delete(key);
5435
5640
  this.#blockSourceEsis.delete(key);
5641
+ this.#blockRebuiltEsis.delete(key);
5436
5642
  this.#blockAuthoredLengths.delete(key);
5437
5643
  this.#blockRepairRsIds.delete(key);
5438
5644
  this.#blockRsbLengths.delete(key);
@@ -5471,6 +5677,7 @@ var FECManager = class _FECManager {
5471
5677
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
5472
5678
  this.#blockSourceCounts.delete(key);
5473
5679
  this.#blockSourceEsis.delete(key);
5680
+ this.#blockRebuiltEsis.delete(key);
5474
5681
  this.#blockAuthoredLengths.delete(key);
5475
5682
  this.#blockRepairRsIds.delete(key);
5476
5683
  this.#blockRsbLengths.delete(key);
@@ -5562,6 +5769,7 @@ var FECManager = class _FECManager {
5562
5769
  this.#blockKeysBySbn.clear();
5563
5770
  this.#blockSourceCounts.clear();
5564
5771
  this.#blockSourceEsis.clear();
5772
+ this.#blockRebuiltEsis.clear();
5565
5773
  this.#blockAuthoredLengths.clear();
5566
5774
  this.#blockRepairRsIds.clear();
5567
5775
  this.#blockRsbLengths.clear();
@@ -5587,6 +5795,7 @@ var FECManager = class _FECManager {
5587
5795
  this.#blockAnchorsMs.clear();
5588
5796
  this.#blockCatalogTrackNames.clear();
5589
5797
  this.#classifiers.clear();
5798
+ this.#noSpanRaps.clear();
5590
5799
  this.#streamGeometries.clear();
5591
5800
  this.#failedBlocks.clear();
5592
5801
  this.#resolvedBlockClasses.clear();
@@ -6940,9 +7149,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
6940
7149
  } catch (error) {
6941
7150
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
6942
7151
  }
6943
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
7152
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
6944
7153
  await rx.close();
6945
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
7154
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
6946
7155
  }
6947
7156
  const events = new EventTarget();
6948
7157
  let state = "connected";
@@ -7061,14 +7270,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
7061
7270
  });
7062
7271
 
7063
7272
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
7273
+ var MAX_PENDING_PACKETS = 256;
7064
7274
  var AMTGatewayManager = class {
7065
7275
  subscriptions = /* @__PURE__ */ new Map();
7066
7276
  messageHandler;
7277
+ messageListenerRegistered = false;
7067
7278
  pendingPackets = /* @__PURE__ */ new Map();
7068
7279
  constructor() {
7069
7280
  this.messageHandler = (event) => {
7070
7281
  const data = event.data;
7071
- console.log(`[AMT] SW message received:`, data.type);
7072
7282
  if (data.type === "UDP_PACKET") {
7073
7283
  this.handlePacket(data);
7074
7284
  } else if (data.type === "SOCKET_ERROR") {
@@ -7077,12 +7287,22 @@ var AMTGatewayManager = class {
7077
7287
  console.log(`[AMT SW Debug] ${data.message}`);
7078
7288
  }
7079
7289
  };
7080
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
7081
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
7082
- console.log("[AMT] Registered service worker message handler");
7083
- } else {
7084
- console.warn("[AMT] Service worker not available");
7290
+ }
7291
+ ensureMessageListener() {
7292
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
7293
+ return;
7085
7294
  }
7295
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
7296
+ this.messageListenerRegistered = true;
7297
+ console.log("[AMT] Registered service worker message handler");
7298
+ }
7299
+ removeMessageListener() {
7300
+ if (!this.messageListenerRegistered)
7301
+ return;
7302
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7303
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7304
+ }
7305
+ this.messageListenerRegistered = false;
7086
7306
  }
7087
7307
  /**
7088
7308
  * Subscribe via AMT tunnel
@@ -7092,6 +7312,7 @@ var AMTGatewayManager = class {
7092
7312
  if (config.protected) {
7093
7313
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
7094
7314
  }
7315
+ this.ensureMessageListener();
7095
7316
  const keyStr = this.getKeyString(key);
7096
7317
  if (this.subscriptions.has(keyStr)) {
7097
7318
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -7127,6 +7348,8 @@ var AMTGatewayManager = class {
7127
7348
  this.subscriptions.delete(keyStr);
7128
7349
  this.pendingPackets.delete(subscription.subscriptionId);
7129
7350
  }
7351
+ if (this.subscriptions.size === 0)
7352
+ this.removeMessageListener();
7130
7353
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
7131
7354
  }
7132
7355
  }
@@ -7148,6 +7371,8 @@ var AMTGatewayManager = class {
7148
7371
  } finally {
7149
7372
  this.subscriptions.delete(handle);
7150
7373
  this.pendingPackets.delete(subscription.subscriptionId);
7374
+ if (this.subscriptions.size === 0)
7375
+ this.removeMessageListener();
7151
7376
  }
7152
7377
  }
7153
7378
  /**
@@ -7164,14 +7389,12 @@ var AMTGatewayManager = class {
7164
7389
  if (!subscriptionId)
7165
7390
  return;
7166
7391
  const subscription = this.subscriptions.get(subscriptionId);
7167
- if (!subscription) {
7168
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
7392
+ if (!subscription)
7169
7393
  return;
7170
- }
7171
7394
  if (!subscription.handshakeComplete) {
7172
7395
  const queue = this.pendingPackets.get(subscriptionId);
7173
7396
  const packetData2 = data;
7174
- if (queue) {
7397
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
7175
7398
  queue.push({
7176
7399
  data: packetData2.data,
7177
7400
  remoteAddress: packetData2.remoteAddress,
@@ -7197,19 +7420,15 @@ var AMTGatewayManager = class {
7197
7420
  if (!subscriptionId)
7198
7421
  return;
7199
7422
  const subscription = this.subscriptions.get(subscriptionId);
7200
- if (!subscription) {
7201
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
7423
+ if (!subscription)
7202
7424
  return;
7203
- }
7204
7425
  subscription.config.onError(new Error(error || "Unknown error"));
7205
7426
  }
7206
7427
  /**
7207
7428
  * Cleanup on destroy
7208
7429
  */
7209
7430
  destroy() {
7210
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
7211
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
7212
- }
7431
+ this.removeMessageListener();
7213
7432
  const handles = Array.from(this.subscriptions.keys());
7214
7433
  for (const handle of handles) {
7215
7434
  this.unsubscribe(handle).catch((err) => {
@@ -13118,9 +13337,21 @@ var IsoBmffTimingResolver = class {
13118
13337
  }
13119
13338
  #track = null;
13120
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();
13121
13348
  #pendingSamples = 0;
13122
13349
  #evictedSamples = 0;
13123
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
+ }
13124
13355
  get pendingSampleCount() {
13125
13356
  return this.#pendingSamples;
13126
13357
  }
@@ -13144,7 +13375,14 @@ var IsoBmffTimingResolver = class {
13144
13375
  this.#track = timing;
13145
13376
  return true;
13146
13377
  }
13147
- 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) {
13148
13386
  this.#lastMovieFragmentRejection = null;
13149
13387
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
13150
13388
  this.#lastMovieFragmentRejection = "invalid";
@@ -13170,6 +13408,8 @@ var IsoBmffTimingResolver = class {
13170
13408
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
13171
13409
  );
13172
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;
13173
13413
  let freed = 0;
13174
13414
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
13175
13415
  freed += this.#evictOldestFragment();
@@ -13183,6 +13423,7 @@ var IsoBmffTimingResolver = class {
13183
13423
  return false;
13184
13424
  }
13185
13425
  this.#fragments.set(key, [...parsed.samples]);
13426
+ this.#firstSamples.set(key, parsed.samples[0]);
13186
13427
  this.#pendingSamples += parsed.samples.length;
13187
13428
  return true;
13188
13429
  }
@@ -13240,6 +13481,7 @@ var IsoBmffTimingResolver = class {
13240
13481
  let live = 0;
13241
13482
  for (const sample of samples) if (sample !== null) live++;
13242
13483
  this.#fragments.delete(oldest);
13484
+ this.#firstSamples.delete(oldest);
13243
13485
  this.#pendingSamples -= live;
13244
13486
  this.#evictedSamples += live;
13245
13487
  return live;
@@ -13255,12 +13497,23 @@ var IsoBmffTimingResolver = class {
13255
13497
  if (!samples || !samples[index]) return false;
13256
13498
  samples[index] = null;
13257
13499
  this.#pendingSamples--;
13258
- 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
+ }
13259
13510
  return true;
13260
13511
  }
13261
13512
  reset() {
13262
13513
  this.#track = null;
13263
13514
  this.#fragments.clear();
13515
+ this.#firstSamples.clear();
13516
+ this.#delivered.clear();
13264
13517
  this.#pendingSamples = 0;
13265
13518
  }
13266
13519
  };
@@ -13742,6 +13995,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13742
13995
  this.#reclaimedTimingSamples = 0;
13743
13996
  this.#evictedTimingSamples = 0;
13744
13997
  this.#rejectedMovieFragments = 0;
13998
+ this.#recoveredHeadsAwaitingTiming = {
13999
+ held: 0,
14000
+ expired: 0,
14001
+ discarded: 0,
14002
+ superseded: 0
14003
+ };
13745
14004
  this.#sequencers = /* @__PURE__ */ new Map();
13746
14005
  this.#fecGate = null;
13747
14006
  this.#fecSequencer = null;
@@ -13850,6 +14109,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
13850
14109
  #reclaimedTimingSamples;
13851
14110
  #evictedTimingSamples;
13852
14111
  #rejectedMovieFragments;
14112
+ #recoveredHeadsAwaitingTiming;
13853
14113
  #sequencers;
13854
14114
  #fecGate;
13855
14115
  #fecSequencer;
@@ -13955,11 +14215,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
13955
14215
  /**
13956
14216
  * Typed view of the ISO-BMFF timing-sample budget.
13957
14217
  *
13958
- * This is the acceptance signal for the budget ratchet: `pending` walking
13959
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
13960
- * and it is observable here rather than only as media silently stopping.
13961
- * Per repo policy this is a producer-owned counter -- never infer budget
13962
- * state by parsing logs.
14218
+ * Each stream admits timing against its own window of `capacity` slots
14219
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
14220
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
14221
+ * timing a stream declared and never delivered shows up there. `pending`
14222
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
14223
+ * a producer-owned counter -- never infer budget state by parsing logs.
13963
14224
  */
13964
14225
  get timingBudgetStats() {
13965
14226
  return {
@@ -13970,6 +14231,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
13970
14231
  rejectedMovieFragments: this.#rejectedMovieFragments
13971
14232
  };
13972
14233
  }
14234
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
14235
+ get recoveredHeadsAwaitingTiming() {
14236
+ let pending = 0;
14237
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14238
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
14239
+ }
14240
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
14241
+ }
13973
14242
  get reorderStats() {
13974
14243
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
13975
14244
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -14143,7 +14412,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14143
14412
  const now = Date.now();
14144
14413
  for (const { awaitingTiming } of this.#timingStreams.values()) {
14145
14414
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
14146
- awaitingTiming.shift();
14415
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
14147
14416
  }
14148
14417
  }
14149
14418
  }
@@ -14261,11 +14530,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
14261
14530
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
14262
14531
  return this.#drop("fragment-unexpected-fragmentation");
14263
14532
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
14264
- if (!resolver2) return this.#drop("fragment-no-resolver");
14533
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
14265
14534
  const pendingBefore = resolver2.pendingSampleCount;
14266
14535
  const evictedBefore = resolver2.evictedSampleCount;
14267
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
14268
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
14536
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
14537
+ const admitted = resolver2.addMovieFragment(
14538
+ mpu.mpuSequenceNumber,
14539
+ data,
14540
+ remaining,
14541
+ options?.recovered === true
14542
+ );
14269
14543
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
14270
14544
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
14271
14545
  if (!admitted) {
@@ -14285,7 +14559,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14285
14559
  };
14286
14560
  }
14287
14561
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
14288
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
14562
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
14289
14563
  if (!resolver) return this.#drop("mfu-no-resolver");
14290
14564
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
14291
14565
  return this.#drop("mfu-empty-payload");
@@ -14306,6 +14580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14306
14580
  mpuSequenceNumber: mpu.mpuSequenceNumber,
14307
14581
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
14308
14582
  sampleNumber: mfuDuHeader.sampleNumber,
14583
+ sequenceNumber: info.sequenceNumber,
14309
14584
  arrivedAt: Date.now()
14310
14585
  });
14311
14586
  return this.#drop("mfu-sample-timing-missing");
@@ -14629,6 +14904,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
14629
14904
  }
14630
14905
  this.#emitFrame(data, meta);
14631
14906
  }
14907
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
14908
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
14909
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
14910
+ }
14632
14911
  /**
14633
14912
  * Claim and return the authored timing for a recovered sample. Recovery does
14634
14913
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -14690,6 +14969,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
14690
14969
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
14691
14970
  this.#sequencers.clear();
14692
14971
  this.#fecSequencer?.discard();
14972
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
14973
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14974
+ }
14693
14975
  this.#timingStreams.clear();
14694
14976
  this.#pendingTimingSamples = 0;
14695
14977
  this.#reliableMoqStartupGate.clear();
@@ -14708,10 +14990,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
14708
14990
  return resolver;
14709
14991
  }
14710
14992
  #awaitTiming(streamKey, head) {
14711
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
14712
- if (!awaitingTiming) return;
14993
+ const state = this.#timingStreams.get(streamKey);
14994
+ if (!state) return;
14995
+ if (!head.options?.recovered) {
14996
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
14997
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
14998
+ state.awaitingTiming = kept;
14999
+ }
15000
+ const awaitingTiming = state.awaitingTiming;
14713
15001
  awaitingTiming.push(head);
14714
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
15002
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
15003
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
15004
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
15005
+ }
15006
+ }
15007
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
15008
+ #leaveAwaitingTiming(head, outcome) {
15009
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
14715
15010
  }
14716
15011
  /**
14717
15012
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -14739,6 +15034,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
14739
15034
  const state = this.#timingStreams.get(streamKey);
14740
15035
  if (!state) return;
14741
15036
  this.#timingStreams.delete(streamKey);
15037
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
14742
15038
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
14743
15039
  if (this.#pendingTimingSamples < 0) {
14744
15040
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -17944,6 +18240,16 @@ function keyframeOverlaySsId(header) {
17944
18240
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
17945
18241
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
17946
18242
  }
18243
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
18244
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
18245
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
18246
+ }
18247
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
18248
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
18249
+ return headerPsn + offset >>> 0;
18250
+ }
18251
+ var OVERLAY_ROUTED_LIMIT = 1024;
18252
+ var OVERLAY_PENDING_LIMIT = 256;
17947
18253
  var OVERLAY_OFFSET_MPUS = 16;
17948
18254
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
17949
18255
  function assertStreamIdentity(role, stream) {
@@ -18055,6 +18361,7 @@ var MmtpRouter = class {
18055
18361
  #provenance = /* @__PURE__ */ new Map();
18056
18362
  #onFlowProtected = null;
18057
18363
  #unadmittedListedDrops = 0;
18364
+ #admittedSourceDatagrams = 0;
18058
18365
  #unlistedRepairSetAside = 0;
18059
18366
  #assistBlocks = /* @__PURE__ */ new Map();
18060
18367
  #assistFailures = /* @__PURE__ */ new Map();
@@ -18111,6 +18418,15 @@ var MmtpRouter = class {
18111
18418
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
18112
18419
  */
18113
18420
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18421
+ /**
18422
+ * Per stream, the SS_ID offset at which a base recovery last showed the
18423
+ * sender's own packet_sequence_number: a recovered packet's header psn
18424
+ * equal to its SS_ID minus that offset. While it equals the stream's
18425
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
18426
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
18427
+ * an MPU no member of which arrived).
18428
+ */
18429
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
18114
18430
  /**
18115
18431
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
18116
18432
  * not parsed, typically because its overlap copy was parsed first
@@ -18180,7 +18496,9 @@ var MmtpRouter = class {
18180
18496
  recovered: 0,
18181
18497
  restarts: 0,
18182
18498
  unsequencedDropped: 0,
18183
- protectedDropped: 0
18499
+ protectedDropped: 0,
18500
+ baseFed: 0,
18501
+ baseUndetermined: 0
18184
18502
  };
18185
18503
  /** Callback fired when a delta-track FEC block completes or fails. */
18186
18504
  #onDeltaBlockCompleteCb = null;
@@ -18353,7 +18671,7 @@ var MmtpRouter = class {
18353
18671
  const cb = this.#getFrameCallback(packetId);
18354
18672
  if (isVideo && !assembler) {
18355
18673
  if (frameMeta.recovered) {
18356
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
18674
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
18357
18675
  } else {
18358
18676
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
18359
18677
  }
@@ -18743,6 +19061,10 @@ var MmtpRouter = class {
18743
19061
  get keyframeOverlayStats() {
18744
19062
  return { ...this.#overlayStats };
18745
19063
  }
19064
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
19065
+ get recoveredHeadsAwaitingTiming() {
19066
+ return this.#parser.recoveredHeadsAwaitingTiming;
19067
+ }
18746
19068
  #isOverlayRepair(repairPacketId, repairTrackName) {
18747
19069
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
18748
19070
  }
@@ -18793,6 +19115,11 @@ var MmtpRouter = class {
18793
19115
  */
18794
19116
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
18795
19117
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
19118
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
19119
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
19120
+ stream.overlayToBase.routed.clear();
19121
+ stream.overlayToBase.pending.clear();
19122
+ }
18796
19123
  }
18797
19124
  #resetOverlay(inst) {
18798
19125
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -18849,9 +19176,10 @@ var MmtpRouter = class {
18849
19176
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
18850
19177
  * every overlay installed for its source, at that SS_ID, with the bytes the
18851
19178
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
18852
- * included (7.1, 7.4.3).
19179
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
19180
+ * FEC reads it.
18853
19181
  */
18854
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
19182
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
18855
19183
  if (this.#overlays.size === 0) return;
18856
19184
  const ssId = keyframeOverlaySsId(header);
18857
19185
  if (ssId === void 0) return;
@@ -18860,7 +19188,11 @@ var MmtpRouter = class {
18860
19188
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
18861
19189
  if (identity !== null) {
18862
19190
  inst.offsets.delete(identity.mpuSequenceNumber);
18863
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
19191
+ inst.offsets.set(identity.mpuSequenceNumber, {
19192
+ psnOffset: ssId - sequenceNumber >>> 0,
19193
+ overlaySsId: ssId,
19194
+ ...baseSsId === void 0 ? {} : { baseSsId }
19195
+ });
18864
19196
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
18865
19197
  }
18866
19198
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -18939,14 +19271,27 @@ var MmtpRouter = class {
18939
19271
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
18940
19272
  const restamped = this.#restampedStreams.has(streamKey);
18941
19273
  const restored = /* @__PURE__ */ new Set();
19274
+ const baseSsIds = /* @__PURE__ */ new Map();
18942
19275
  const symbols = result.symbols.flatMap((sym) => {
18943
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
18944
- if (offset !== void 0) {
18945
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
19276
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
19277
+ if (anchor !== void 0) {
19278
+ const overlaySsId = serial32Add(blockStart, sym.index);
19279
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
18946
19280
  restored.add(packetSequenceNumber);
18947
- return [{ ...sym, packetSequenceNumber }];
19281
+ const out = { ...sym, packetSequenceNumber };
19282
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
19283
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
19284
+ return [out];
19285
+ }
19286
+ if (!restamped) {
19287
+ const baseSsId = unstampedBaseSsId(
19288
+ this.#ssIdSeqOffsetByStream.get(streamKey),
19289
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
19290
+ sym.packetSequenceNumber
19291
+ );
19292
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
19293
+ return [sym];
18948
19294
  }
18949
- if (!restamped) return [sym];
18950
19295
  this.#overlayStats.unsequencedDropped++;
18951
19296
  return [];
18952
19297
  });
@@ -18955,6 +19300,85 @@ var MmtpRouter = class {
18955
19300
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
18956
19301
  });
18957
19302
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
19303
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
19304
+ }
19305
+ /**
19306
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
19307
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
19308
+ * from a received member of its MPU, else unstampedBaseSsId),
19309
+ * so it is fed to the base decoder as a rebuilt source
19310
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
19311
+ * fewer to solve, and its repair can recover the block's other packets.
19312
+ * A base block no repair has stated yet takes it when one does
19313
+ * (#feedPendingRebuilt).
19314
+ *
19315
+ * The overlay route already handled each packet fed (delivered it, or
19316
+ * held an untimed head for its FT=1), so a base recovery routes none of
19317
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
19318
+ * held head would wait in the parser for timing its first copy consumed.
19319
+ * Only an overlay block decoded from multicast symbols alone is fed: with
19320
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
19321
+ * that crossed no multicast socket, which the provenance counted from the
19322
+ * block's own arrivals cannot show.
19323
+ */
19324
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
19325
+ const fec = this.#fecManager;
19326
+ const owner = inst.leg.sourcePacketId;
19327
+ const name = inst.leg.sourceCatalogTrackName;
19328
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
19329
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
19330
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
19331
+ const feeds = [];
19332
+ for (const sym of symbols) {
19333
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
19334
+ const ssId = baseSsIds.get(sym);
19335
+ if (ssId === void 0) {
19336
+ this.#overlayStats.baseUndetermined++;
19337
+ continue;
19338
+ }
19339
+ routed.delete(ssId);
19340
+ routed.add(ssId);
19341
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
19342
+ feeds.push([ssId, sym.rawSymbol]);
19343
+ }
19344
+ for (const [ssId, symbolData] of feeds) {
19345
+ if (this.#classifyPacketId(owner) === null) return;
19346
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19347
+ }
19348
+ }
19349
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
19350
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
19351
+ const fec = this.#fecManager;
19352
+ if (!fec?.feedRebuiltSourceSymbol) return;
19353
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
19354
+ if (coords == null) {
19355
+ const pending = this.#stream(owner, name).overlayToBase.pending;
19356
+ pending.delete(ssId);
19357
+ pending.set(ssId, symbolData.slice());
19358
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
19359
+ return;
19360
+ }
19361
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
19362
+ if (!fed.taken) return;
19363
+ this.#overlayStats.baseFed++;
19364
+ if (fed.recovered === null) return;
19365
+ const sbn = coords.sbn;
19366
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
19367
+ fec.cancelBlockTimeout(sbn, owner, name);
19368
+ fec.countRecovery(owner, sbn, name);
19369
+ fec.markBlockCompleted(sbn, owner, name);
19370
+ });
19371
+ }
19372
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
19373
+ #feedPendingRebuilt(owner, name, block) {
19374
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
19375
+ if (pending === void 0 || pending.size === 0) return;
19376
+ for (const [ssId, symbolData] of [...pending]) {
19377
+ if (this.#classifyPacketId(owner) === null) return;
19378
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
19379
+ pending.delete(ssId);
19380
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
19381
+ }
18958
19382
  }
18959
19383
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
18960
19384
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -19094,6 +19518,10 @@ var MmtpRouter = class {
19094
19518
  get unadmittedListedDropCount() {
19095
19519
  return this.#unadmittedListedDrops;
19096
19520
  }
19521
+ /** 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. */
19522
+ get admittedSourceDatagramCount() {
19523
+ return this.#admittedSourceDatagrams;
19524
+ }
19097
19525
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
19098
19526
  get unlistedRepairSetAsideCount() {
19099
19527
  return this.#unlistedRepairSetAside;
@@ -19580,6 +20008,8 @@ var MmtpRouter = class {
19580
20008
  )) return;
19581
20009
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
19582
20010
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
20011
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
20012
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
19583
20013
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
19584
20014
  const recovered = fec.feedRepairSymbol(
19585
20015
  ownerPacketId,
@@ -19717,6 +20147,7 @@ var MmtpRouter = class {
19717
20147
  this.#droppedPreSyncVideo.delete(streamKey);
19718
20148
  this.#latestVideoSeqByStream.delete(streamKey);
19719
20149
  this.#ssIdSeqOffsetByStream.delete(streamKey);
20150
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
19720
20151
  this.#multicastRapTwinByStream.delete(streamKey);
19721
20152
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
19722
20153
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -20351,6 +20782,7 @@ var MmtpRouter = class {
20351
20782
  const parsed = this.#parser.parsePacket(packet);
20352
20783
  if (!parsed) return;
20353
20784
  const info = parsed.info;
20785
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
20354
20786
  if (source === "multicast" && !manifestAdmitted) {
20355
20787
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
20356
20788
  }
@@ -20632,7 +21064,8 @@ var MmtpRouter = class {
20632
21064
  parsed.header,
20633
21065
  info.sequenceNumber,
20634
21066
  info.rapFlag,
20635
- source
21067
+ source,
21068
+ fecPayloadId?.ssId
20636
21069
  );
20637
21070
  }
20638
21071
  }
@@ -20782,7 +21215,8 @@ var MmtpRouter = class {
20782
21215
  if (stream === void 0) {
20783
21216
  stream = {
20784
21217
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
20785
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
21218
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
21219
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
20786
21220
  };
20787
21221
  byName.set(catalogTrackName, stream);
20788
21222
  }
@@ -20897,8 +21331,16 @@ var MmtpRouter = class {
20897
21331
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
20898
21332
  this.#emitDirectVideo(packetId, data, meta, cb);
20899
21333
  }
20900
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
21334
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
20901
21335
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
21336
+ if (this.#isFecTrack(packetId)) {
21337
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
21338
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
21339
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
21340
+ this.#emitDirectVideo(packetId, data, meta, cb);
21341
+ return;
21342
+ }
21343
+ }
20902
21344
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
20903
21345
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
20904
21346
  this.#drainDirectRecoveredVideo(
@@ -21052,6 +21494,13 @@ var MmtpRouter = class {
21052
21494
  * frames by epoch only if pre-protection input ever matters.
21053
21495
  */
21054
21496
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
21497
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
21498
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
21499
+ result = {
21500
+ ...result,
21501
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
21502
+ };
21503
+ }
21055
21504
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
21056
21505
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
21057
21506
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -21116,7 +21565,10 @@ var MmtpRouter = class {
21116
21565
  ...result,
21117
21566
  symbols: result.symbols.map((sym) => {
21118
21567
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
21119
- if (wire === sym.packetSequenceNumber) return sym;
21568
+ if (wire === sym.packetSequenceNumber) {
21569
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
21570
+ return sym;
21571
+ }
21120
21572
  this.#restampedRecoveredSeqs++;
21121
21573
  this.#restampedStreams.add(streamKey);
21122
21574
  return { ...sym, packetSequenceNumber: wire };
@@ -21152,6 +21604,29 @@ var MmtpRouter = class {
21152
21604
  }
21153
21605
  );
21154
21606
  }
21607
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
21608
+ ownerPacketId,
21609
+ sym.fragment.mpuSequenceNumber,
21610
+ sym.fragment.movieFragmentSequenceNumber,
21611
+ sym.fragment.sampleNumber,
21612
+ catalogTrackName
21613
+ );
21614
+ for (const sym of result.symbols) {
21615
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
21616
+ this.#feedViaParser(
21617
+ sym.rawSymbol,
21618
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
21619
+ void 0,
21620
+ {
21621
+ skipFecPayloadId: true,
21622
+ recovered: true,
21623
+ deliveryProvenance,
21624
+ ...fecSbnOf(sym.index),
21625
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
21626
+ }
21627
+ );
21628
+ }
21629
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
21155
21630
  const gated = this.#isFecTrack(ownerPacketId);
21156
21631
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
21157
21632
  if (assembler) {