@blockcast/mmt-container 2.2.0-main.2c4d96d3ffc2 → 2.2.0-main.34e6f78a7088

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.
@@ -8625,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
8625
8625
  } catch (error) {
8626
8626
  throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8627
8627
  }
8628
- if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8628
+ if (opened.integrityScheme !== INTEGRITY_SCHEME) {
8629
8629
  await rx.close();
8630
- throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8630
+ throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8631
8631
  }
8632
8632
  const events = new EventTarget();
8633
8633
  let state = "connected";
@@ -16844,9 +16844,15 @@ var FECManager = class _FECManager {
16844
16844
  rebuilt = /* @__PURE__ */ new Set();
16845
16845
  this.#blockRebuiltEsis.set(key, rebuilt);
16846
16846
  }
16847
+ const config = this.#trackConfigs.get(packetId);
16848
+ const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
16847
16849
  rebuilt.add(located.esi);
16848
16850
  const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16849
- if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
16851
+ if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
16852
+ if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
16853
+ this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
16854
+ }
16855
+ }
16850
16856
  return { taken: true, recovered };
16851
16857
  }
16852
16858
  /**
@@ -17113,7 +17119,10 @@ var FECManager = class _FECManager {
17113
17119
  const authoredLength = this.#blockAuthoredLengths.get(key);
17114
17120
  const rsbLength = this.#blockRsbLengths.get(key);
17115
17121
  if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17116
- if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
17122
+ let present = this.#blockSourceCounts.get(key) ?? 0;
17123
+ const received = this.#blockSourceEsis.get(key);
17124
+ for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
17125
+ if (present + rsbLength >= authoredLength) return void 0;
17117
17126
  const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17118
17127
  if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17119
17128
  return void 0;
@@ -18433,6 +18442,9 @@ function authoredGap(before, after) {
18433
18442
  function ssIdAfter(a, b) {
18434
18443
  return (a - b | 0) > 0;
18435
18444
  }
18445
+ function holdsRebuilt(stream) {
18446
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
18447
+ }
18436
18448
  var FecReleaseSequencer = class {
18437
18449
  #streams = /* @__PURE__ */ new Map();
18438
18450
  #gate;
@@ -18495,6 +18507,7 @@ var FecReleaseSequencer = class {
18495
18507
  stream.lastSbn = void 0;
18496
18508
  stream.discontinuity = false;
18497
18509
  stream.rapSsId = void 0;
18510
+ stream.sourced = false;
18498
18511
  }
18499
18512
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
18500
18513
  this.#recordStat("droppedStale");
@@ -18521,6 +18534,7 @@ var FecReleaseSequencer = class {
18521
18534
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
18522
18535
  else hi = mid;
18523
18536
  }
18537
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
18524
18538
  stream.buf.splice(lo, 0, frame);
18525
18539
  this.#drainUnlessDeferred(stream);
18526
18540
  }
@@ -18556,6 +18570,41 @@ var FecReleaseSequencer = class {
18556
18570
  }
18557
18571
  return false;
18558
18572
  }
18573
+ /**
18574
+ * These streams' sink is being replaced by one that has seen no source
18575
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
18576
+ * release to the sink being replaced the frames the unanchored path had
18577
+ * already released to it (#cursorPassed): there they waited in its queue,
18578
+ * which its replacement flushes. A stream that holds none is not drained:
18579
+ * it holds nothing the anchor kept back, and a drain would still force out
18580
+ * a frame held past the hold limit, which before the anchor waited for the
18581
+ * next push. Then treat the streams as unsourced until a source frame is
18582
+ * released.
18583
+ */
18584
+ unsource(packetId, catalogTrackName) {
18585
+ for (const stream of this.#streams.values()) {
18586
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
18587
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
18588
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
18589
+ else this.#drain(stream, true);
18590
+ }
18591
+ stream.sourced = false;
18592
+ }
18593
+ }
18594
+ }
18595
+ /**
18596
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
18597
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
18598
+ * packetId.
18599
+ */
18600
+ holdsRebuilt(packetId, catalogTrackName) {
18601
+ for (const stream of this.#streams.values()) {
18602
+ if (stream.packetId !== packetId) continue;
18603
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
18604
+ if (holdsRebuilt(stream)) return true;
18605
+ }
18606
+ return false;
18607
+ }
18559
18608
  /** Re-evaluate held gaps after FEC state changed. */
18560
18609
  poke(streamKey) {
18561
18610
  if (streamKey !== void 0) {
@@ -18647,16 +18696,32 @@ var FecReleaseSequencer = class {
18647
18696
  last: null,
18648
18697
  lastSbn: void 0,
18649
18698
  discontinuity: false,
18650
- rapSsId: void 0
18699
+ rapSsId: void 0,
18700
+ sourced: false,
18701
+ draining: 0
18651
18702
  };
18652
18703
  this.#streams.set(streamKey, stream);
18653
18704
  }
18654
18705
  return stream;
18655
18706
  }
18656
- #drain(stream) {
18707
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
18708
+ #drain(stream, asUnanchored = false) {
18709
+ stream.draining++;
18710
+ try {
18711
+ this.#drainLoop(stream, asUnanchored);
18712
+ } finally {
18713
+ stream.draining--;
18714
+ }
18715
+ }
18716
+ #drainLoop(stream, asUnanchored) {
18657
18717
  while (stream.buf.length > 0 && stream.last !== null) {
18658
18718
  const head = stream.buf[0];
18659
18719
  const last = stream.last;
18720
+ if (compareFrameOrder(head, last) <= 0) {
18721
+ stream.buf.shift();
18722
+ this.#release(stream, head, void 0);
18723
+ continue;
18724
+ }
18660
18725
  if (stream.discontinuity) {
18661
18726
  stream.discontinuity = false;
18662
18727
  stream.buf.shift();
@@ -18670,9 +18735,10 @@ var FecReleaseSequencer = class {
18670
18735
  }
18671
18736
  const headSbn = head.meta.fecSbn;
18672
18737
  const from = stream.lastSbn ?? headSbn;
18673
- const to = headSbn ?? stream.lastSbn;
18738
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
18674
18739
  let lost = [];
18675
18740
  const unanchored = from === void 0 && to === void 0;
18741
+ let unresolved = false;
18676
18742
  if (from !== void 0 && to !== void 0) {
18677
18743
  const resolved = this.#gate.resolvedLoss(
18678
18744
  stream.packetId,
@@ -18681,7 +18747,8 @@ var FecReleaseSequencer = class {
18681
18747
  Math.max(from, to),
18682
18748
  from
18683
18749
  );
18684
- if (resolved === null) {
18750
+ unresolved = resolved === null;
18751
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
18685
18752
  const heldMs = performance.now() - head.arrivedAtMs;
18686
18753
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
18687
18754
  this.#recordStat("forcedWindowFlushes");
@@ -18689,22 +18756,56 @@ var FecReleaseSequencer = class {
18689
18756
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
18690
18757
  continue;
18691
18758
  }
18692
- lost = resolved;
18759
+ lost = resolved ?? [];
18693
18760
  }
18694
18761
  stream.buf.shift();
18695
18762
  this.#release(
18696
18763
  stream,
18697
18764
  head,
18698
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18765
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
18699
18766
  );
18700
18767
  }
18701
18768
  }
18769
+ /**
18770
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
18771
+ * source cursor holds it unanchored, and neither does a later frame behind
18772
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
18773
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
18774
+ * like each frame that arrived in front of it since (stale there). The head
18775
+ * then goes, decided as it would be unanchored, once a frame the base path
18776
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
18777
+ * marked one: the base path has passed that one, so a frame still missing
18778
+ * before it can only come from a repair. It also goes while no source frame
18779
+ * has been released, when the direct path shows a recovered frame at once.
18780
+ * And it goes at once while a staged switch decides on when frames arrive
18781
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
18782
+ * sink being replaced: unanchored, it had already reached the sink.
18783
+ * The scan also writes: the `passed` marks it sets stay on the frames,
18784
+ * and push marks a frame it inserts in front of a marked one from them.
18785
+ */
18786
+ #cursorPassed(stream, asUnanchored) {
18787
+ let at2 = -1;
18788
+ for (let i = 0; i < stream.buf.length; i++) {
18789
+ const frame = stream.buf[i];
18790
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
18791
+ frame.passed = true;
18792
+ if (at2 < 0) at2 = i;
18793
+ } else if (frame.passed !== true) break;
18794
+ }
18795
+ if (at2 < 0) return false;
18796
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at2 && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
18797
+ }
18702
18798
  #release(stream, frame, decision) {
18799
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
18800
+ if (stale) this.#recordStat("droppedStale");
18703
18801
  if (decision) frame.meta.gapBefore = decision;
18704
18802
  stream.last = frame;
18705
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
18803
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
18804
+ stream.lastSbn = frame.meta.fecSbn;
18805
+ }
18806
+ if (frame.meta.recovered !== true) stream.sourced = true;
18706
18807
  this.#gate.onReleased?.(frame.data, frame.meta);
18707
- this.#emit(frame.data, frame.meta);
18808
+ if (!stale) this.#emit(frame.data, frame.meta);
18708
18809
  }
18709
18810
  };
18710
18811
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -18965,11 +19066,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18965
19066
  /**
18966
19067
  * Typed view of the ISO-BMFF timing-sample budget.
18967
19068
  *
18968
- * This is the acceptance signal for the budget ratchet: `pending` walking
18969
- * to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
18970
- * and it is observable here rather than only as media silently stopping.
18971
- * Per repo policy this is a producer-owned counter -- never infer budget
18972
- * state by parsing logs.
19069
+ * Each stream admits timing against its own window of `capacity` slots
19070
+ * (BLO-52085). A stream at capacity displaces its own oldest timing to
19071
+ * admit the moof in hand, and `evicted` counts the slots so displaced:
19072
+ * timing a stream declared and never delivered shows up there. `pending`
19073
+ * sums every stream, so it can exceed `capacity`. Per repo policy this is
19074
+ * a producer-owned counter -- never infer budget state by parsing logs.
18973
19075
  */
18974
19076
  get timingBudgetStats() {
18975
19077
  return {
@@ -19029,6 +19131,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
19029
19131
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
19030
19132
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
19031
19133
  }
19134
+ /** See FecReleaseSequencer.unsource. */
19135
+ unsourceFecRelease(packetId, catalogTrackName) {
19136
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
19137
+ }
19138
+ /** See FecReleaseSequencer.holdsRebuilt. */
19139
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
19140
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
19141
+ }
19032
19142
  /** Re-evaluate held gaps after FEC block state changed. */
19033
19143
  pokeFecRelease(packetId, catalogTrackName) {
19034
19144
  if (!this.#fecSequencer) return;
@@ -19282,7 +19392,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19282
19392
  if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19283
19393
  const pendingBefore = resolver2.pendingSampleCount;
19284
19394
  const evictedBefore = resolver2.evictedSampleCount;
19285
- const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19395
+ const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
19286
19396
  const admitted = resolver2.addMovieFragment(
19287
19397
  mpu.mpuSequenceNumber,
19288
19398
  data,
@@ -19355,7 +19465,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19355
19465
  info,
19356
19466
  resolver,
19357
19467
  options?.recovered === true,
19358
- options?.fecSbn
19468
+ options?.fecSbn,
19469
+ options?.fecSbnFirst
19359
19470
  );
19360
19471
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
19361
19472
  }
@@ -19448,7 +19559,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
19448
19559
  info,
19449
19560
  resolver,
19450
19561
  options?.recovered === true,
19451
- options?.fecSbn
19562
+ options?.fecSbn,
19563
+ options?.fecSbnFirst
19452
19564
  );
19453
19565
  if (outcome.record !== void 0) return outcome.record;
19454
19566
  return this.#drop(outcome.reason);
@@ -19468,7 +19580,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19468
19580
  * attribution exists to keep readable. Leaving the choice at the call site
19469
19581
  * is what keeps both true.
19470
19582
  */
19471
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
19583
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
19472
19584
  const timing = resolver.sampleTiming(
19473
19585
  combined.mpuSequenceNumber,
19474
19586
  combined.movieFragmentSequenceNumber,
@@ -19485,6 +19597,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19485
19597
  );
19486
19598
  if (!meta) return { reason: "combined-meta-build-failed" };
19487
19599
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
19600
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
19488
19601
  if (!resolver.consumeSample(
19489
19602
  combined.mpuSequenceNumber,
19490
19603
  combined.movieFragmentSequenceNumber,
@@ -23001,6 +23114,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23001
23114
  var OVERLAY_ROUTED_LIMIT = 1024;
23002
23115
  var OVERLAY_PENDING_LIMIT = 256;
23003
23116
  var OVERLAY_OFFSET_MPUS = 16;
23117
+ var OVERLAY_REBUILT_LIMIT = 256;
23004
23118
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
23005
23119
  function assertStreamIdentity(role, stream) {
23006
23120
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -23111,6 +23225,7 @@ var MmtpRouter = class {
23111
23225
  #provenance = /* @__PURE__ */ new Map();
23112
23226
  #onFlowProtected = null;
23113
23227
  #unadmittedListedDrops = 0;
23228
+ #admittedSourceDatagrams = 0;
23114
23229
  #unlistedRepairSetAside = 0;
23115
23230
  #assistBlocks = /* @__PURE__ */ new Map();
23116
23231
  #assistFailures = /* @__PURE__ */ new Map();
@@ -23176,6 +23291,17 @@ var MmtpRouter = class {
23176
23291
  * an MPU no member of which arrived).
23177
23292
  */
23178
23293
  #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23294
+ /**
23295
+ * The same, shown by the overlay alone, before any base recovery did: a
23296
+ * packet it rebuilt whose base copy carries its protected psn
23297
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
23298
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
23299
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
23300
+ * make the base drop a late base copy of a keyframe the overlay never
23301
+ * finished, and lift a block's late-source cut, which nothing before
23302
+ * this offset did.
23303
+ */
23304
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23179
23305
  /**
23180
23306
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23181
23307
  * not parsed, typically because its overlap copy was parsed first
@@ -23504,8 +23630,9 @@ var MmtpRouter = class {
23504
23630
  }
23505
23631
  /**
23506
23632
  * FEC-recovered source symbols whose reconstructed header named a
23507
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23508
- * a hop re-stamped the sequence after the sender protected the packet.
23633
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
23634
+ * or, rebuilt by the overlay, than their base copy carries: a hop
23635
+ * re-stamped the sequence after the sender protected the packet.
23509
23636
  */
23510
23637
  get restampedRecoveredSequenceCount() {
23511
23638
  return this.#restampedRecoveredSeqs;
@@ -23684,6 +23811,7 @@ var MmtpRouter = class {
23684
23811
  enableMseAssembler(packetId, config, catalogTrackName) {
23685
23812
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23686
23813
  const previous = this.#mseAssemblers.get(assemblerKey);
23814
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23687
23815
  if (previous) {
23688
23816
  previous.flush();
23689
23817
  previous.dispose();
@@ -23834,7 +23962,8 @@ var MmtpRouter = class {
23834
23962
  fec,
23835
23963
  k,
23836
23964
  offsets: /* @__PURE__ */ new Map(),
23837
- moqBlocks: /* @__PURE__ */ new Set()
23965
+ moqBlocks: /* @__PURE__ */ new Set(),
23966
+ rebuilt: /* @__PURE__ */ new Map()
23838
23967
  };
23839
23968
  this.#overlays.set(repairPacketId, inst);
23840
23969
  return () => {
@@ -23875,6 +24004,7 @@ var MmtpRouter = class {
23875
24004
  inst.last = void 0;
23876
24005
  inst.offsets.clear();
23877
24006
  inst.moqBlocks.clear();
24007
+ inst.rebuilt.clear();
23878
24008
  }
23879
24009
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23880
24010
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23927,13 +24057,23 @@ var MmtpRouter = class {
23927
24057
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23928
24058
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23929
24059
  * FEC reads it.
24060
+ *
24061
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
24062
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
24063
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
24064
+ * the stream unstamped at the SS_ID offset it states, for the release gate
24065
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
24066
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
23930
24067
  */
23931
24068
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23932
24069
  if (this.#overlays.size === 0) return;
23933
24070
  const ssId = keyframeOverlaySsId(header);
23934
24071
  if (ssId === void 0) return;
24072
+ let rebuiltPsn;
23935
24073
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23936
24074
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24075
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24076
+ inst.rebuilt.delete(ssId);
23937
24077
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23938
24078
  if (identity !== null) {
23939
24079
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -23963,6 +24103,14 @@ var MmtpRouter = class {
23963
24103
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23964
24104
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23965
24105
  }
24106
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
24107
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
24108
+ if (rebuiltPsn !== sequenceNumber) {
24109
+ this.#restampedRecoveredSeqs++;
24110
+ } else if (baseSsId !== void 0) {
24111
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24112
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24113
+ }
23966
24114
  }
23967
24115
  #noteOverlayMoqBlock(inst, blockStart) {
23968
24116
  inst.moqBlocks.add(blockStart);
@@ -23996,11 +24144,16 @@ var MmtpRouter = class {
23996
24144
  }
23997
24145
  /**
23998
24146
  * Route an overlay-recovered block's packets as source media of the
23999
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
24000
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
24001
- * base re-stamp, no block advance. Each packet's wire sequence is restored
24002
- * from its keyframe's own offset; without one, a stream the base never
24003
- * re-stamped keeps the header's sequence and any other drops the packet.
24147
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
24148
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
24149
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
24150
+ * base FEC recovered does, so it waits behind base frames still in flight,
24151
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
24152
+ * longer than unanchored; one whose base SS_ID is undetermined has no
24153
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
24154
+ * advance. Each packet's wire sequence is restored from its keyframe's own
24155
+ * offset; without one, a stream the base never re-stamped keeps the
24156
+ * header's sequence and any other drops the packet.
24004
24157
  */
24005
24158
  #routeOverlayRecovered(inst, blockStart, recovered) {
24006
24159
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -24021,40 +24174,65 @@ var MmtpRouter = class {
24021
24174
  const restamped = this.#restampedStreams.has(streamKey);
24022
24175
  const restored = /* @__PURE__ */ new Set();
24023
24176
  const baseSsIds = /* @__PURE__ */ new Map();
24177
+ const gateOnly = /* @__PURE__ */ new Set();
24024
24178
  const symbols = result.symbols.flatMap((sym) => {
24179
+ const overlaySsId = serial32Add(blockStart, sym.index);
24025
24180
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24026
24181
  if (anchor !== void 0) {
24027
- const overlaySsId = serial32Add(blockStart, sym.index);
24028
24182
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
24029
24183
  restored.add(packetSequenceNumber);
24030
24184
  const out = { ...sym, packetSequenceNumber };
24031
24185
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24032
- if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
24186
+ if (baseSsId !== void 0) {
24187
+ baseSsIds.set(sym.index, baseSsId);
24188
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24189
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
24190
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
24191
+ }
24192
+ }
24033
24193
  return [out];
24034
24194
  }
24035
24195
  if (!restamped) {
24036
- const baseSsId = unstampedBaseSsId(
24037
- this.#ssIdSeqOffsetByStream.get(streamKey),
24038
- this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
24039
- sym.packetSequenceNumber
24040
- );
24041
- if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
24196
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
24197
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24198
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
24199
+ if (baseSsId !== void 0) {
24200
+ baseSsIds.set(sym.index, baseSsId);
24201
+ if (fedSsId === void 0) gateOnly.add(sym.index);
24202
+ } else {
24203
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
24204
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
24205
+ }
24042
24206
  return [sym];
24043
24207
  }
24044
24208
  this.#overlayStats.unsequencedDropped++;
24045
24209
  return [];
24046
24210
  });
24047
24211
  this.#overlayStats.recovered++;
24212
+ const mpuFirst = /* @__PURE__ */ new Map();
24213
+ for (const sym of symbols) {
24214
+ const ssId = baseSsIds.get(sym.index);
24215
+ const mpu = sym.fragment.mpuSequenceNumber;
24216
+ const first = mpuFirst.get(mpu);
24217
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
24218
+ }
24219
+ const firstSsIds = /* @__PURE__ */ new Map();
24220
+ for (const sym of symbols) {
24221
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
24222
+ }
24048
24223
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
24049
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24224
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24225
+ baseSsIds,
24226
+ firstSsIds
24050
24227
  });
24051
24228
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24052
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24229
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24053
24230
  }
24054
24231
  /**
24055
24232
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24056
24233
  * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24057
- * from a received member of its MPU, else unstampedBaseSsId),
24234
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24235
+ * base recovery showed; `baseSsIds` holds no other),
24058
24236
  * so it is fed to the base decoder as a rebuilt source
24059
24237
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24060
24238
  * fewer to solve, and its repair can recover the block's other packets.
@@ -24080,7 +24258,7 @@ var MmtpRouter = class {
24080
24258
  const feeds = [];
24081
24259
  for (const sym of symbols) {
24082
24260
  if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24083
- const ssId = baseSsIds.get(sym);
24261
+ const ssId = baseSsIds.get(sym.index);
24084
24262
  if (ssId === void 0) {
24085
24263
  this.#overlayStats.baseUndetermined++;
24086
24264
  continue;
@@ -24267,6 +24445,10 @@ var MmtpRouter = class {
24267
24445
  get unadmittedListedDropCount() {
24268
24446
  return this.#unadmittedListedDrops;
24269
24447
  }
24448
+ /** 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. */
24449
+ get admittedSourceDatagramCount() {
24450
+ return this.#admittedSourceDatagrams;
24451
+ }
24270
24452
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
24271
24453
  get unlistedRepairSetAsideCount() {
24272
24454
  return this.#unlistedRepairSetAside;
@@ -24893,6 +25075,7 @@ var MmtpRouter = class {
24893
25075
  this.#latestVideoSeqByStream.delete(streamKey);
24894
25076
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24895
25077
  this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25078
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24896
25079
  this.#multicastRapTwinByStream.delete(streamKey);
24897
25080
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24898
25081
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25019,6 +25202,12 @@ var MmtpRouter = class {
25019
25202
  * the outgoing stream stops. The hold is bounded by the incoming stream's
25020
25203
  * own RAP cadence, so no timer is involved.
25021
25204
  *
25205
+ * While the switch is staged, the FEC release gate lets a keyframe the
25206
+ * overlay rebuilt reach it, on either side, when the keyframe did before
25207
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
25208
+ * decides on what each side delivered, and the commit discards the gate's
25209
+ * frames of the outgoing stream.
25210
+ *
25022
25211
  * The commit is one synchronous transition inside the frame path: the
25023
25212
  * outgoing stream's already-admitted frames are released (its queued FEC
25024
25213
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -25069,6 +25258,9 @@ var MmtpRouter = class {
25069
25258
  droppedIncomingFrames: 0
25070
25259
  };
25071
25260
  this.#staged = state;
25261
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25262
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25263
+ }
25072
25264
  return {
25073
25265
  cancel: () => {
25074
25266
  if (this.#staged !== state) return false;
@@ -25151,7 +25343,17 @@ var MmtpRouter = class {
25151
25343
  */
25152
25344
  #routeParsedFrame(data, meta) {
25153
25345
  if (this.#classifyPacketId(meta.packetId) === null) return;
25154
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
25346
+ if (this.#staged !== null) {
25347
+ let consumed = false;
25348
+ this.#fecPokeDepth++;
25349
+ try {
25350
+ consumed = this.#interceptSwitchFrame(data, meta);
25351
+ } finally {
25352
+ this.#fecPokeDepth--;
25353
+ this.#flushPendingFecPokes();
25354
+ }
25355
+ if (consumed) return;
25356
+ }
25155
25357
  this.#deliverParsedFrame(data, meta);
25156
25358
  }
25157
25359
  #commitStreamSwitch(state, rap, reason) {
@@ -25303,12 +25505,16 @@ var MmtpRouter = class {
25303
25505
  this.#parser.deferFecRelease(work);
25304
25506
  } finally {
25305
25507
  this.#fecPokeDepth--;
25306
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25307
- const pokes = [...this.#pendingFecPokes.values()];
25308
- this.#pendingFecPokes.clear();
25309
- for (const poke of pokes) {
25310
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25311
- }
25508
+ this.#flushPendingFecPokes();
25509
+ }
25510
+ }
25511
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
25512
+ #flushPendingFecPokes() {
25513
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25514
+ const pokes = [...this.#pendingFecPokes.values()];
25515
+ this.#pendingFecPokes.clear();
25516
+ for (const poke of pokes) {
25517
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25312
25518
  }
25313
25519
  }
25314
25520
  }
@@ -25386,6 +25592,14 @@ var MmtpRouter = class {
25386
25592
  onRap: (packetId, catalogTrackName, ssId) => {
25387
25593
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25388
25594
  },
25595
+ // A staged switch commits on when each side's frames reach it
25596
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
25597
+ // they did unanchored.
25598
+ releaseUnanchored: (packetId, catalogTrackName) => {
25599
+ const staged = this.#staged;
25600
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
25601
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
25602
+ },
25389
25603
  onReleased: (data, meta) => {
25390
25604
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25391
25605
  const timing = meta.authoredTiming;
@@ -25527,6 +25741,7 @@ var MmtpRouter = class {
25527
25741
  const parsed = this.#parser.parsePacket(packet);
25528
25742
  if (!parsed) return;
25529
25743
  const info = parsed.info;
25744
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25530
25745
  if (source === "multicast" && !manifestAdmitted) {
25531
25746
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25532
25747
  }
@@ -25835,7 +26050,8 @@ var MmtpRouter = class {
25835
26050
  source: parserOptions?.source,
25836
26051
  deliveryProvenance: parserOptions?.deliveryProvenance,
25837
26052
  catalogTrackName: parserOptions?.catalogTrackName,
25838
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26053
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26054
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25839
26055
  };
25840
26056
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25841
26057
  else this.#parser.feed(packet, feedOptions);
@@ -26301,7 +26517,12 @@ var MmtpRouter = class {
26301
26517
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
26302
26518
  if (ownerPacketId == null) return;
26303
26519
  if (this.#classifyPacketId(ownerPacketId) === null) return;
26304
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26520
+ const fecSbnOf = (index) => {
26521
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
26522
+ const fecSbn = overlay.baseSsIds.get(index);
26523
+ const fecSbnFirst = overlay.firstSsIds.get(index);
26524
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
26525
+ };
26305
26526
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
26306
26527
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
26307
26528
  if (ssIdSeqOffset !== void 0) {