@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
@@ -1610,6 +1610,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
1610
1610
  });
1611
1611
 
1612
1612
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
1613
+ var MAX_PENDING_PACKETS = 256;
1613
1614
  var AMTGatewayManager = class {
1614
1615
  subscriptions = /* @__PURE__ */ new Map();
1615
1616
  messageHandler;
@@ -1733,7 +1734,7 @@ var AMTGatewayManager = class {
1733
1734
  if (!subscription.handshakeComplete) {
1734
1735
  const queue = this.pendingPackets.get(subscriptionId);
1735
1736
  const packetData2 = data;
1736
- if (queue) {
1737
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
1737
1738
  queue.push({
1738
1739
  data: packetData2.data,
1739
1740
  remoteAddress: packetData2.remoteAddress,
@@ -7676,9 +7677,21 @@ var IsoBmffTimingResolver = class {
7676
7677
  }
7677
7678
  #track = null;
7678
7679
  #fragments = /* @__PURE__ */ new Map();
7680
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
7681
+ #firstSamples = /* @__PURE__ */ new Map();
7682
+ /**
7683
+ * First sample and sample count of the newest fragments whose every slot was
7684
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
7685
+ * matches the combiner, since it retires an abandoned sample as well.
7686
+ */
7687
+ #delivered = /* @__PURE__ */ new Map();
7679
7688
  #pendingSamples = 0;
7680
7689
  #evictedSamples = 0;
7681
7690
  #lastMovieFragmentRejection = null;
7691
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
7692
+ get initialized() {
7693
+ return this.#track !== null;
7694
+ }
7682
7695
  get pendingSampleCount() {
7683
7696
  return this.#pendingSamples;
7684
7697
  }
@@ -7702,7 +7715,14 @@ var IsoBmffTimingResolver = class {
7702
7715
  this.#track = timing;
7703
7716
  return true;
7704
7717
  }
7705
- addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples) {
7718
+ /**
7719
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
7720
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
7721
+ * fragment whose samples are all done is a duplicate: admitting it again
7722
+ * would charge stale timing and could displace the live MPU's. A source
7723
+ * copy is still admitted again (a rendition switch re-arms timing that way).
7724
+ */
7725
+ addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
7706
7726
  this.#lastMovieFragmentRejection = null;
7707
7727
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
7708
7728
  this.#lastMovieFragmentRejection = "invalid";
@@ -7728,6 +7748,8 @@ var IsoBmffTimingResolver = class {
7728
7748
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
7729
7749
  );
7730
7750
  }
7751
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
7752
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
7731
7753
  let freed = 0;
7732
7754
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
7733
7755
  freed += this.#evictOldestFragment();
@@ -7741,6 +7763,7 @@ var IsoBmffTimingResolver = class {
7741
7763
  return false;
7742
7764
  }
7743
7765
  this.#fragments.set(key, [...parsed.samples]);
7766
+ this.#firstSamples.set(key, parsed.samples[0]);
7744
7767
  this.#pendingSamples += parsed.samples.length;
7745
7768
  return true;
7746
7769
  }
@@ -7798,6 +7821,7 @@ var IsoBmffTimingResolver = class {
7798
7821
  let live = 0;
7799
7822
  for (const sample of samples) if (sample !== null) live++;
7800
7823
  this.#fragments.delete(oldest);
7824
+ this.#firstSamples.delete(oldest);
7801
7825
  this.#pendingSamples -= live;
7802
7826
  this.#evictedSamples += live;
7803
7827
  return live;
@@ -7813,12 +7837,23 @@ var IsoBmffTimingResolver = class {
7813
7837
  if (!samples || !samples[index]) return false;
7814
7838
  samples[index] = null;
7815
7839
  this.#pendingSamples--;
7816
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
7840
+ if (samples.every((value) => value === null)) {
7841
+ this.#fragments.delete(key);
7842
+ const first = this.#firstSamples.get(key);
7843
+ this.#firstSamples.delete(key);
7844
+ this.#delivered.delete(key);
7845
+ this.#delivered.set(key, { first, sampleCount: samples.length });
7846
+ if (this.#delivered.size > this.maxPendingSamples) {
7847
+ this.#delivered.delete(this.#delivered.keys().next().value);
7848
+ }
7849
+ }
7817
7850
  return true;
7818
7851
  }
7819
7852
  reset() {
7820
7853
  this.#track = null;
7821
7854
  this.#fragments.clear();
7855
+ this.#firstSamples.clear();
7856
+ this.#delivered.clear();
7822
7857
  this.#pendingSamples = 0;
7823
7858
  }
7824
7859
  };
@@ -8300,6 +8335,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
8300
8335
  this.#reclaimedTimingSamples = 0;
8301
8336
  this.#evictedTimingSamples = 0;
8302
8337
  this.#rejectedMovieFragments = 0;
8338
+ this.#recoveredHeadsAwaitingTiming = {
8339
+ held: 0,
8340
+ expired: 0,
8341
+ discarded: 0,
8342
+ superseded: 0
8343
+ };
8303
8344
  this.#sequencers = /* @__PURE__ */ new Map();
8304
8345
  this.#fecGate = null;
8305
8346
  this.#fecSequencer = null;
@@ -8408,6 +8449,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8408
8449
  #reclaimedTimingSamples;
8409
8450
  #evictedTimingSamples;
8410
8451
  #rejectedMovieFragments;
8452
+ #recoveredHeadsAwaitingTiming;
8411
8453
  #sequencers;
8412
8454
  #fecGate;
8413
8455
  #fecSequencer;
@@ -8528,6 +8570,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8528
8570
  rejectedMovieFragments: this.#rejectedMovieFragments
8529
8571
  };
8530
8572
  }
8573
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
8574
+ get recoveredHeadsAwaitingTiming() {
8575
+ let pending = 0;
8576
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
8577
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
8578
+ }
8579
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
8580
+ }
8531
8581
  get reorderStats() {
8532
8582
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
8533
8583
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -8701,7 +8751,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8701
8751
  const now = Date.now();
8702
8752
  for (const { awaitingTiming } of this.#timingStreams.values()) {
8703
8753
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
8704
- awaitingTiming.shift();
8754
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
8705
8755
  }
8706
8756
  }
8707
8757
  }
@@ -8819,11 +8869,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
8819
8869
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
8820
8870
  return this.#drop("fragment-unexpected-fragmentation");
8821
8871
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
8822
- if (!resolver2) return this.#drop("fragment-no-resolver");
8872
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
8823
8873
  const pendingBefore = resolver2.pendingSampleCount;
8824
8874
  const evictedBefore = resolver2.evictedSampleCount;
8825
8875
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
8826
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
8876
+ const admitted = resolver2.addMovieFragment(
8877
+ mpu.mpuSequenceNumber,
8878
+ data,
8879
+ remaining,
8880
+ options?.recovered === true
8881
+ );
8827
8882
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
8828
8883
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
8829
8884
  if (!admitted) {
@@ -8843,7 +8898,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8843
8898
  };
8844
8899
  }
8845
8900
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
8846
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
8901
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
8847
8902
  if (!resolver) return this.#drop("mfu-no-resolver");
8848
8903
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
8849
8904
  return this.#drop("mfu-empty-payload");
@@ -8864,6 +8919,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
8864
8919
  mpuSequenceNumber: mpu.mpuSequenceNumber,
8865
8920
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
8866
8921
  sampleNumber: mfuDuHeader.sampleNumber,
8922
+ sequenceNumber: info.sequenceNumber,
8867
8923
  arrivedAt: Date.now()
8868
8924
  });
8869
8925
  return this.#drop("mfu-sample-timing-missing");
@@ -9252,6 +9308,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
9252
9308
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
9253
9309
  this.#sequencers.clear();
9254
9310
  this.#fecSequencer?.discard();
9311
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
9312
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9313
+ }
9255
9314
  this.#timingStreams.clear();
9256
9315
  this.#pendingTimingSamples = 0;
9257
9316
  this.#reliableMoqStartupGate.clear();
@@ -9270,10 +9329,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
9270
9329
  return resolver;
9271
9330
  }
9272
9331
  #awaitTiming(streamKey, head) {
9273
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
9274
- if (!awaitingTiming) return;
9332
+ const state = this.#timingStreams.get(streamKey);
9333
+ if (!state) return;
9334
+ if (!head.options?.recovered) {
9335
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
9336
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
9337
+ state.awaitingTiming = kept;
9338
+ }
9339
+ const awaitingTiming = state.awaitingTiming;
9275
9340
  awaitingTiming.push(head);
9276
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
9341
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
9342
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
9343
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
9344
+ }
9345
+ }
9346
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
9347
+ #leaveAwaitingTiming(head, outcome) {
9348
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
9277
9349
  }
9278
9350
  /**
9279
9351
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -9301,6 +9373,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9301
9373
  const state = this.#timingStreams.get(streamKey);
9302
9374
  if (!state) return;
9303
9375
  this.#timingStreams.delete(streamKey);
9376
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
9304
9377
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
9305
9378
  if (this.#pendingTimingSamples < 0) {
9306
9379
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -9841,6 +9914,16 @@ function keyframeOverlaySsId(header) {
9841
9914
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
9842
9915
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
9843
9916
  }
9917
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
9918
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
9919
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
9920
+ }
9921
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
9922
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
9923
+ return headerPsn + offset >>> 0;
9924
+ }
9925
+ var OVERLAY_ROUTED_LIMIT = 1024;
9926
+ var OVERLAY_PENDING_LIMIT = 256;
9844
9927
  var OVERLAY_OFFSET_MPUS = 16;
9845
9928
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
9846
9929
  function assertStreamIdentity(role, stream) {
@@ -9952,6 +10035,7 @@ var MmtpRouter = class {
9952
10035
  #provenance = /* @__PURE__ */ new Map();
9953
10036
  #onFlowProtected = null;
9954
10037
  #unadmittedListedDrops = 0;
10038
+ #admittedSourceDatagrams = 0;
9955
10039
  #unlistedRepairSetAside = 0;
9956
10040
  #assistBlocks = /* @__PURE__ */ new Map();
9957
10041
  #assistFailures = /* @__PURE__ */ new Map();
@@ -10008,6 +10092,15 @@ var MmtpRouter = class {
10008
10092
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
10009
10093
  */
10010
10094
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10095
+ /**
10096
+ * Per stream, the SS_ID offset at which a base recovery last showed the
10097
+ * sender's own packet_sequence_number: a recovered packet's header psn
10098
+ * equal to its SS_ID minus that offset. While it equals the stream's
10099
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
10100
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
10101
+ * an MPU no member of which arrived).
10102
+ */
10103
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10011
10104
  /**
10012
10105
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
10013
10106
  * not parsed, typically because its overlap copy was parsed first
@@ -10077,7 +10170,9 @@ var MmtpRouter = class {
10077
10170
  recovered: 0,
10078
10171
  restarts: 0,
10079
10172
  unsequencedDropped: 0,
10080
- protectedDropped: 0
10173
+ protectedDropped: 0,
10174
+ baseFed: 0,
10175
+ baseUndetermined: 0
10081
10176
  };
10082
10177
  /** Callback fired when a delta-track FEC block completes or fails. */
10083
10178
  #onDeltaBlockCompleteCb = null;
@@ -10250,7 +10345,7 @@ var MmtpRouter = class {
10250
10345
  const cb = this.#getFrameCallback(packetId);
10251
10346
  if (isVideo && !assembler) {
10252
10347
  if (frameMeta.recovered) {
10253
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
10348
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
10254
10349
  } else {
10255
10350
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
10256
10351
  }
@@ -10640,6 +10735,10 @@ var MmtpRouter = class {
10640
10735
  get keyframeOverlayStats() {
10641
10736
  return { ...this.#overlayStats };
10642
10737
  }
10738
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
10739
+ get recoveredHeadsAwaitingTiming() {
10740
+ return this.#parser.recoveredHeadsAwaitingTiming;
10741
+ }
10643
10742
  #isOverlayRepair(repairPacketId, repairTrackName) {
10644
10743
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
10645
10744
  }
@@ -10690,6 +10789,11 @@ var MmtpRouter = class {
10690
10789
  */
10691
10790
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
10692
10791
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
10792
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
10793
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
10794
+ stream.overlayToBase.routed.clear();
10795
+ stream.overlayToBase.pending.clear();
10796
+ }
10693
10797
  }
10694
10798
  #resetOverlay(inst) {
10695
10799
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -10746,9 +10850,10 @@ var MmtpRouter = class {
10746
10850
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
10747
10851
  * every overlay installed for its source, at that SS_ID, with the bytes the
10748
10852
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
10749
- * included (7.1, 7.4.3).
10853
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
10854
+ * FEC reads it.
10750
10855
  */
10751
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
10856
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
10752
10857
  if (this.#overlays.size === 0) return;
10753
10858
  const ssId = keyframeOverlaySsId(header);
10754
10859
  if (ssId === void 0) return;
@@ -10757,7 +10862,11 @@ var MmtpRouter = class {
10757
10862
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
10758
10863
  if (identity !== null) {
10759
10864
  inst.offsets.delete(identity.mpuSequenceNumber);
10760
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
10865
+ inst.offsets.set(identity.mpuSequenceNumber, {
10866
+ psnOffset: ssId - sequenceNumber >>> 0,
10867
+ overlaySsId: ssId,
10868
+ ...baseSsId === void 0 ? {} : { baseSsId }
10869
+ });
10761
10870
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
10762
10871
  }
10763
10872
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10836,14 +10945,27 @@ var MmtpRouter = class {
10836
10945
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
10837
10946
  const restamped = this.#restampedStreams.has(streamKey);
10838
10947
  const restored = /* @__PURE__ */ new Set();
10948
+ const baseSsIds = /* @__PURE__ */ new Map();
10839
10949
  const symbols = result.symbols.flatMap((sym) => {
10840
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10841
- if (offset !== void 0) {
10842
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
10950
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10951
+ if (anchor !== void 0) {
10952
+ const overlaySsId = serial32Add(blockStart, sym.index);
10953
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
10843
10954
  restored.add(packetSequenceNumber);
10844
- return [{ ...sym, packetSequenceNumber }];
10955
+ const out = { ...sym, packetSequenceNumber };
10956
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
10957
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
10958
+ return [out];
10959
+ }
10960
+ if (!restamped) {
10961
+ const baseSsId = unstampedBaseSsId(
10962
+ this.#ssIdSeqOffsetByStream.get(streamKey),
10963
+ this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
10964
+ sym.packetSequenceNumber
10965
+ );
10966
+ if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
10967
+ return [sym];
10845
10968
  }
10846
- if (!restamped) return [sym];
10847
10969
  this.#overlayStats.unsequencedDropped++;
10848
10970
  return [];
10849
10971
  });
@@ -10852,6 +10974,85 @@ var MmtpRouter = class {
10852
10974
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
10853
10975
  });
10854
10976
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
10977
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
10978
+ }
10979
+ /**
10980
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
10981
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
10982
+ * from a received member of its MPU, else unstampedBaseSsId),
10983
+ * so it is fed to the base decoder as a rebuilt source
10984
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
10985
+ * fewer to solve, and its repair can recover the block's other packets.
10986
+ * A base block no repair has stated yet takes it when one does
10987
+ * (#feedPendingRebuilt).
10988
+ *
10989
+ * The overlay route already handled each packet fed (delivered it, or
10990
+ * held an untimed head for its FT=1), so a base recovery routes none of
10991
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
10992
+ * held head would wait in the parser for timing its first copy consumed.
10993
+ * Only an overlay block decoded from multicast symbols alone is fed: with
10994
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
10995
+ * that crossed no multicast socket, which the provenance counted from the
10996
+ * block's own arrivals cannot show.
10997
+ */
10998
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
10999
+ const fec = this.#fecManager;
11000
+ const owner = inst.leg.sourcePacketId;
11001
+ const name = inst.leg.sourceCatalogTrackName;
11002
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
11003
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
11004
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
11005
+ const feeds = [];
11006
+ for (const sym of symbols) {
11007
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
11008
+ const ssId = baseSsIds.get(sym);
11009
+ if (ssId === void 0) {
11010
+ this.#overlayStats.baseUndetermined++;
11011
+ continue;
11012
+ }
11013
+ routed.delete(ssId);
11014
+ routed.add(ssId);
11015
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
11016
+ feeds.push([ssId, sym.rawSymbol]);
11017
+ }
11018
+ for (const [ssId, symbolData] of feeds) {
11019
+ if (this.#classifyPacketId(owner) === null) return;
11020
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11021
+ }
11022
+ }
11023
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
11024
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
11025
+ const fec = this.#fecManager;
11026
+ if (!fec?.feedRebuiltSourceSymbol) return;
11027
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
11028
+ if (coords == null) {
11029
+ const pending = this.#stream(owner, name).overlayToBase.pending;
11030
+ pending.delete(ssId);
11031
+ pending.set(ssId, symbolData.slice());
11032
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
11033
+ return;
11034
+ }
11035
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
11036
+ if (!fed.taken) return;
11037
+ this.#overlayStats.baseFed++;
11038
+ if (fed.recovered === null) return;
11039
+ const sbn = coords.sbn;
11040
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
11041
+ fec.cancelBlockTimeout(sbn, owner, name);
11042
+ fec.countRecovery(owner, sbn, name);
11043
+ fec.markBlockCompleted(sbn, owner, name);
11044
+ });
11045
+ }
11046
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
11047
+ #feedPendingRebuilt(owner, name, block) {
11048
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
11049
+ if (pending === void 0 || pending.size === 0) return;
11050
+ for (const [ssId, symbolData] of [...pending]) {
11051
+ if (this.#classifyPacketId(owner) === null) return;
11052
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
11053
+ pending.delete(ssId);
11054
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
11055
+ }
10855
11056
  }
10856
11057
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
10857
11058
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -10991,6 +11192,10 @@ var MmtpRouter = class {
10991
11192
  get unadmittedListedDropCount() {
10992
11193
  return this.#unadmittedListedDrops;
10993
11194
  }
11195
+ /** 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. */
11196
+ get admittedSourceDatagramCount() {
11197
+ return this.#admittedSourceDatagrams;
11198
+ }
10994
11199
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
10995
11200
  get unlistedRepairSetAsideCount() {
10996
11201
  return this.#unlistedRepairSetAside;
@@ -11477,6 +11682,8 @@ var MmtpRouter = class {
11477
11682
  )) return;
11478
11683
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
11479
11684
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
11685
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
11686
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
11480
11687
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
11481
11688
  const recovered = fec.feedRepairSymbol(
11482
11689
  ownerPacketId,
@@ -11614,6 +11821,7 @@ var MmtpRouter = class {
11614
11821
  this.#droppedPreSyncVideo.delete(streamKey);
11615
11822
  this.#latestVideoSeqByStream.delete(streamKey);
11616
11823
  this.#ssIdSeqOffsetByStream.delete(streamKey);
11824
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
11617
11825
  this.#multicastRapTwinByStream.delete(streamKey);
11618
11826
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
11619
11827
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -12248,6 +12456,7 @@ var MmtpRouter = class {
12248
12456
  const parsed = this.#parser.parsePacket(packet);
12249
12457
  if (!parsed) return;
12250
12458
  const info = parsed.info;
12459
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
12251
12460
  if (source === "multicast" && !manifestAdmitted) {
12252
12461
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
12253
12462
  }
@@ -12529,7 +12738,8 @@ var MmtpRouter = class {
12529
12738
  parsed.header,
12530
12739
  info.sequenceNumber,
12531
12740
  info.rapFlag,
12532
- source
12741
+ source,
12742
+ fecPayloadId?.ssId
12533
12743
  );
12534
12744
  }
12535
12745
  }
@@ -12679,7 +12889,8 @@ var MmtpRouter = class {
12679
12889
  if (stream === void 0) {
12680
12890
  stream = {
12681
12891
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
12682
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
12892
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
12893
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
12683
12894
  };
12684
12895
  byName.set(catalogTrackName, stream);
12685
12896
  }
@@ -12794,8 +13005,16 @@ var MmtpRouter = class {
12794
13005
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
12795
13006
  this.#emitDirectVideo(packetId, data, meta, cb);
12796
13007
  }
12797
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
13008
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
12798
13009
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
13010
+ if (this.#isFecTrack(packetId)) {
13011
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
13012
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
13013
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
13014
+ this.#emitDirectVideo(packetId, data, meta, cb);
13015
+ return;
13016
+ }
13017
+ }
12799
13018
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
12800
13019
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
12801
13020
  this.#drainDirectRecoveredVideo(
@@ -12949,6 +13168,13 @@ var MmtpRouter = class {
12949
13168
  * frames by epoch only if pre-protection input ever matters.
12950
13169
  */
12951
13170
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
13171
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
13172
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
13173
+ result = {
13174
+ ...result,
13175
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
13176
+ };
13177
+ }
12952
13178
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12953
13179
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
12954
13180
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -13013,7 +13239,10 @@ var MmtpRouter = class {
13013
13239
  ...result,
13014
13240
  symbols: result.symbols.map((sym) => {
13015
13241
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
13016
- if (wire === sym.packetSequenceNumber) return sym;
13242
+ if (wire === sym.packetSequenceNumber) {
13243
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
13244
+ return sym;
13245
+ }
13017
13246
  this.#restampedRecoveredSeqs++;
13018
13247
  this.#restampedStreams.add(streamKey);
13019
13248
  return { ...sym, packetSequenceNumber: wire };
@@ -13049,16 +13278,29 @@ var MmtpRouter = class {
13049
13278
  }
13050
13279
  );
13051
13280
  }
13052
- result = {
13053
- ...result,
13054
- 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(
13055
- ownerPacketId,
13056
- sym.fragment.mpuSequenceNumber,
13057
- sym.fragment.movieFragmentSequenceNumber,
13058
- sym.fragment.sampleNumber,
13059
- catalogTrackName
13060
- ))
13061
- };
13281
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
13282
+ ownerPacketId,
13283
+ sym.fragment.mpuSequenceNumber,
13284
+ sym.fragment.movieFragmentSequenceNumber,
13285
+ sym.fragment.sampleNumber,
13286
+ catalogTrackName
13287
+ );
13288
+ for (const sym of result.symbols) {
13289
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
13290
+ this.#feedViaParser(
13291
+ sym.rawSymbol,
13292
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
13293
+ void 0,
13294
+ {
13295
+ skipFecPayloadId: true,
13296
+ recovered: true,
13297
+ deliveryProvenance,
13298
+ ...fecSbnOf(sym.index),
13299
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
13300
+ }
13301
+ );
13302
+ }
13303
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
13062
13304
  const gated = this.#isFecTrack(ownerPacketId);
13063
13305
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
13064
13306
  if (assembler) {