@blockcast/mmt-container 2.2.0-main.079cd7664c0b → 2.2.0-main.0a7228501631

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,
@@ -22994,9 +23107,14 @@ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
22994
23107
  if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
22995
23108
  return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
22996
23109
  }
23110
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
23111
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
23112
+ return headerPsn + offset >>> 0;
23113
+ }
22997
23114
  var OVERLAY_ROUTED_LIMIT = 1024;
22998
23115
  var OVERLAY_PENDING_LIMIT = 256;
22999
23116
  var OVERLAY_OFFSET_MPUS = 16;
23117
+ var OVERLAY_REBUILT_LIMIT = 256;
23000
23118
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
23001
23119
  function assertStreamIdentity(role, stream) {
23002
23120
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -23107,6 +23225,7 @@ var MmtpRouter = class {
23107
23225
  #provenance = /* @__PURE__ */ new Map();
23108
23226
  #onFlowProtected = null;
23109
23227
  #unadmittedListedDrops = 0;
23228
+ #admittedSourceDatagrams = 0;
23110
23229
  #unlistedRepairSetAside = 0;
23111
23230
  #assistBlocks = /* @__PURE__ */ new Map();
23112
23231
  #assistFailures = /* @__PURE__ */ new Map();
@@ -23163,6 +23282,26 @@ var MmtpRouter = class {
23163
23282
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
23164
23283
  */
23165
23284
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23285
+ /**
23286
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23287
+ * sender's own packet_sequence_number: a recovered packet's header psn
23288
+ * equal to its SS_ID minus that offset. While it equals the stream's
23289
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23290
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23291
+ * an MPU no member of which arrived).
23292
+ */
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();
23166
23305
  /**
23167
23306
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
23168
23307
  * not parsed, typically because its overlap copy was parsed first
@@ -23491,8 +23630,9 @@ var MmtpRouter = class {
23491
23630
  }
23492
23631
  /**
23493
23632
  * FEC-recovered source symbols whose reconstructed header named a
23494
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
23495
- * 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.
23496
23636
  */
23497
23637
  get restampedRecoveredSequenceCount() {
23498
23638
  return this.#restampedRecoveredSeqs;
@@ -23671,6 +23811,7 @@ var MmtpRouter = class {
23671
23811
  enableMseAssembler(packetId, config, catalogTrackName) {
23672
23812
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
23673
23813
  const previous = this.#mseAssemblers.get(assemblerKey);
23814
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
23674
23815
  if (previous) {
23675
23816
  previous.flush();
23676
23817
  previous.dispose();
@@ -23821,7 +23962,8 @@ var MmtpRouter = class {
23821
23962
  fec,
23822
23963
  k,
23823
23964
  offsets: /* @__PURE__ */ new Map(),
23824
- moqBlocks: /* @__PURE__ */ new Set()
23965
+ moqBlocks: /* @__PURE__ */ new Set(),
23966
+ rebuilt: /* @__PURE__ */ new Map()
23825
23967
  };
23826
23968
  this.#overlays.set(repairPacketId, inst);
23827
23969
  return () => {
@@ -23862,6 +24004,7 @@ var MmtpRouter = class {
23862
24004
  inst.last = void 0;
23863
24005
  inst.offsets.clear();
23864
24006
  inst.moqBlocks.clear();
24007
+ inst.rebuilt.clear();
23865
24008
  }
23866
24009
  #overlaysForSource(sourcePacketId, catalogTrackName) {
23867
24010
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -23914,13 +24057,23 @@ var MmtpRouter = class {
23914
24057
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23915
24058
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23916
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).
23917
24067
  */
23918
24068
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23919
24069
  if (this.#overlays.size === 0) return;
23920
24070
  const ssId = keyframeOverlaySsId(header);
23921
24071
  if (ssId === void 0) return;
24072
+ let rebuiltPsn;
23922
24073
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23923
24074
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
24075
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
24076
+ inst.rebuilt.delete(ssId);
23924
24077
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23925
24078
  if (identity !== null) {
23926
24079
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -23950,6 +24103,14 @@ var MmtpRouter = class {
23950
24103
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23951
24104
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23952
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
+ }
23953
24114
  }
23954
24115
  #noteOverlayMoqBlock(inst, blockStart) {
23955
24116
  inst.moqBlocks.add(blockStart);
@@ -23983,11 +24144,16 @@ var MmtpRouter = class {
23983
24144
  }
23984
24145
  /**
23985
24146
  * Route an overlay-recovered block's packets as source media of the
23986
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23987
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23988
- * base re-stamp, no block advance. Each packet's wire sequence is restored
23989
- * from its keyframe's own offset; without one, a stream the base never
23990
- * 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.
23991
24157
  */
23992
24158
  #routeOverlayRecovered(inst, blockStart, recovered) {
23993
24159
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -24008,31 +24174,65 @@ var MmtpRouter = class {
24008
24174
  const restamped = this.#restampedStreams.has(streamKey);
24009
24175
  const restored = /* @__PURE__ */ new Set();
24010
24176
  const baseSsIds = /* @__PURE__ */ new Map();
24177
+ const gateOnly = /* @__PURE__ */ new Set();
24011
24178
  const symbols = result.symbols.flatMap((sym) => {
24179
+ const overlaySsId = serial32Add(blockStart, sym.index);
24012
24180
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24013
24181
  if (anchor !== void 0) {
24014
- const overlaySsId = serial32Add(blockStart, sym.index);
24015
24182
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
24016
24183
  restored.add(packetSequenceNumber);
24017
24184
  const out = { ...sym, packetSequenceNumber };
24018
24185
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24019
- 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
+ }
24020
24193
  return [out];
24021
24194
  }
24022
- if (!restamped) return [sym];
24195
+ if (!restamped) {
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
+ }
24206
+ return [sym];
24207
+ }
24023
24208
  this.#overlayStats.unsequencedDropped++;
24024
24209
  return [];
24025
24210
  });
24026
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
+ }
24027
24223
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
24028
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
24224
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
24225
+ baseSsIds,
24226
+ firstSsIds
24029
24227
  });
24030
24228
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24031
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24229
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
24032
24230
  }
24033
24231
  /**
24034
24232
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24035
- * its base block too, once its base SS_ID is determined (overlayBaseSsId),
24233
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24234
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
24235
+ * base recovery showed; `baseSsIds` holds no other),
24036
24236
  * so it is fed to the base decoder as a rebuilt source
24037
24237
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24038
24238
  * fewer to solve, and its repair can recover the block's other packets.
@@ -24058,7 +24258,7 @@ var MmtpRouter = class {
24058
24258
  const feeds = [];
24059
24259
  for (const sym of symbols) {
24060
24260
  if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24061
- const ssId = baseSsIds.get(sym);
24261
+ const ssId = baseSsIds.get(sym.index);
24062
24262
  if (ssId === void 0) {
24063
24263
  this.#overlayStats.baseUndetermined++;
24064
24264
  continue;
@@ -24068,7 +24268,10 @@ var MmtpRouter = class {
24068
24268
  if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24069
24269
  feeds.push([ssId, sym.rawSymbol]);
24070
24270
  }
24071
- for (const [ssId, symbolData] of feeds) this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24271
+ for (const [ssId, symbolData] of feeds) {
24272
+ if (this.#classifyPacketId(owner) === null) return;
24273
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24274
+ }
24072
24275
  }
24073
24276
  /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24074
24277
  #feedRebuiltToBase(owner, name, ssId, symbolData) {
@@ -24078,7 +24281,7 @@ var MmtpRouter = class {
24078
24281
  if (coords == null) {
24079
24282
  const pending = this.#stream(owner, name).overlayToBase.pending;
24080
24283
  pending.delete(ssId);
24081
- pending.set(ssId, symbolData);
24284
+ pending.set(ssId, symbolData.slice());
24082
24285
  if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24083
24286
  return;
24084
24287
  }
@@ -24098,6 +24301,7 @@ var MmtpRouter = class {
24098
24301
  const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24099
24302
  if (pending === void 0 || pending.size === 0) return;
24100
24303
  for (const [ssId, symbolData] of [...pending]) {
24304
+ if (this.#classifyPacketId(owner) === null) return;
24101
24305
  if (serial32Distance(block.start, ssId) >= block.length) continue;
24102
24306
  pending.delete(ssId);
24103
24307
  this.#feedRebuiltToBase(owner, name, ssId, symbolData);
@@ -24241,6 +24445,10 @@ var MmtpRouter = class {
24241
24445
  get unadmittedListedDropCount() {
24242
24446
  return this.#unadmittedListedDrops;
24243
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
+ }
24244
24452
  /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
24245
24453
  get unlistedRepairSetAsideCount() {
24246
24454
  return this.#unlistedRepairSetAside;
@@ -24717,8 +24925,6 @@ var MmtpRouter = class {
24717
24925
  }
24718
24926
  this.#replayIsoSources(ownerPacketId, catalogTrackName, blockStart, placement.placed);
24719
24927
  if (this.#classifyPacketId(ownerPacketId) === null) return;
24720
- this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24721
- if (this.#classifyPacketId(ownerPacketId) === null) return;
24722
24928
  if (!this.#feedFecTransportSymbol(
24723
24929
  ownerPacketId,
24724
24930
  blockStart,
@@ -24729,6 +24935,8 @@ var MmtpRouter = class {
24729
24935
  )) return;
24730
24936
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24731
24937
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24938
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24939
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24732
24940
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24733
24941
  const recovered = fec.feedRepairSymbol(
24734
24942
  ownerPacketId,
@@ -24866,6 +25074,8 @@ var MmtpRouter = class {
24866
25074
  this.#droppedPreSyncVideo.delete(streamKey);
24867
25075
  this.#latestVideoSeqByStream.delete(streamKey);
24868
25076
  this.#ssIdSeqOffsetByStream.delete(streamKey);
25077
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
25078
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
24869
25079
  this.#multicastRapTwinByStream.delete(streamKey);
24870
25080
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24871
25081
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -24992,6 +25202,12 @@ var MmtpRouter = class {
24992
25202
  * the outgoing stream stops. The hold is bounded by the incoming stream's
24993
25203
  * own RAP cadence, so no timer is involved.
24994
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
+ *
24995
25211
  * The commit is one synchronous transition inside the frame path: the
24996
25212
  * outgoing stream's already-admitted frames are released (its queued FEC
24997
25213
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -25042,6 +25258,9 @@ var MmtpRouter = class {
25042
25258
  droppedIncomingFrames: 0
25043
25259
  };
25044
25260
  this.#staged = state;
25261
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
25262
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
25263
+ }
25045
25264
  return {
25046
25265
  cancel: () => {
25047
25266
  if (this.#staged !== state) return false;
@@ -25124,7 +25343,17 @@ var MmtpRouter = class {
25124
25343
  */
25125
25344
  #routeParsedFrame(data, meta) {
25126
25345
  if (this.#classifyPacketId(meta.packetId) === null) return;
25127
- 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
+ }
25128
25357
  this.#deliverParsedFrame(data, meta);
25129
25358
  }
25130
25359
  #commitStreamSwitch(state, rap, reason) {
@@ -25276,12 +25505,16 @@ var MmtpRouter = class {
25276
25505
  this.#parser.deferFecRelease(work);
25277
25506
  } finally {
25278
25507
  this.#fecPokeDepth--;
25279
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
25280
- const pokes = [...this.#pendingFecPokes.values()];
25281
- this.#pendingFecPokes.clear();
25282
- for (const poke of pokes) {
25283
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
25284
- }
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);
25285
25518
  }
25286
25519
  }
25287
25520
  }
@@ -25359,6 +25592,14 @@ var MmtpRouter = class {
25359
25592
  onRap: (packetId, catalogTrackName, ssId) => {
25360
25593
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
25361
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
+ },
25362
25603
  onReleased: (data, meta) => {
25363
25604
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
25364
25605
  const timing = meta.authoredTiming;
@@ -25500,6 +25741,7 @@ var MmtpRouter = class {
25500
25741
  const parsed = this.#parser.parsePacket(packet);
25501
25742
  if (!parsed) return;
25502
25743
  const info = parsed.info;
25744
+ if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
25503
25745
  if (source === "multicast" && !manifestAdmitted) {
25504
25746
  this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
25505
25747
  }
@@ -25808,7 +26050,8 @@ var MmtpRouter = class {
25808
26050
  source: parserOptions?.source,
25809
26051
  deliveryProvenance: parserOptions?.deliveryProvenance,
25810
26052
  catalogTrackName: parserOptions?.catalogTrackName,
25811
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
26053
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
26054
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
25812
26055
  };
25813
26056
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
25814
26057
  else this.#parser.feed(packet, feedOptions);
@@ -26274,7 +26517,12 @@ var MmtpRouter = class {
26274
26517
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
26275
26518
  if (ownerPacketId == null) return;
26276
26519
  if (this.#classifyPacketId(ownerPacketId) === null) return;
26277
- 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
+ };
26278
26526
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
26279
26527
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
26280
26528
  if (ssIdSeqOffset !== void 0) {
@@ -26282,7 +26530,10 @@ var MmtpRouter = class {
26282
26530
  ...result,
26283
26531
  symbols: result.symbols.map((sym) => {
26284
26532
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
26285
- if (wire === sym.packetSequenceNumber) return sym;
26533
+ if (wire === sym.packetSequenceNumber) {
26534
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26535
+ return sym;
26536
+ }
26286
26537
  this.#restampedRecoveredSeqs++;
26287
26538
  this.#restampedStreams.add(streamKey);
26288
26539
  return { ...sym, packetSequenceNumber: wire };