@blockcast/mmt-container 2.2.0-main.75b39a2cdddf → 2.2.0-main.7e8a9d13c01c

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 (52) hide show
  1. package/dist/browser-runtime.js +634 -129
  2. package/dist/browser-runtime.js.map +3 -3
  3. package/dist/delta-fallback-config.js.map +1 -1
  4. package/dist/fec-manager.d.ts +37 -11
  5. package/dist/fec-manager.d.ts.map +1 -1
  6. package/dist/fec-manager.js +181 -52
  7. package/dist/fec-manager.js.map +2 -2
  8. package/dist/fec-worker-setup.js.map +1 -1
  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 +39 -13
  13. package/dist/iso-block-locator.d.ts.map +1 -1
  14. package/dist/iso-block-locator.js +80 -25
  15. package/dist/iso-block-locator.js.map +2 -2
  16. package/dist/media-router.js.map +1 -1
  17. package/dist/mmtp-container-parser.d.ts +21 -0
  18. package/dist/mmtp-container-parser.d.ts.map +1 -1
  19. package/dist/mmtp-container-parser.js +114 -11
  20. package/dist/mmtp-container-parser.js.map +2 -2
  21. package/dist/mmtp-container-parser.no-modules.js +113 -12
  22. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  23. package/dist/mmtp-decode-pipeline.js +114 -11
  24. package/dist/mmtp-decode-pipeline.js.map +2 -2
  25. package/dist/mmtp-router.d.ts +18 -9
  26. package/dist/mmtp-router.d.ts.map +1 -1
  27. package/dist/mmtp-router.js +254 -44
  28. package/dist/mmtp-router.js.map +2 -2
  29. package/dist/multicast-manager.d.ts +13 -3
  30. package/dist/multicast-manager.d.ts.map +1 -1
  31. package/dist/multicast-manager.js +455 -103
  32. package/dist/multicast-manager.js.map +2 -2
  33. package/dist/no-modules.js +114 -11
  34. package/dist/no-modules.js.map +2 -2
  35. package/dist/repair-fetch.d.ts +36 -8
  36. package/dist/repair-fetch.d.ts.map +1 -1
  37. package/dist/repair-fetch.js +285 -26
  38. package/dist/repair-fetch.js.map +3 -3
  39. package/dist/track-session.d.ts.map +1 -1
  40. package/dist/track-session.js +1 -0
  41. package/dist/track-session.js.map +2 -2
  42. package/dist/wasm-mmtp-container-parser.js +114 -11
  43. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  44. package/package.json +5 -5
  45. package/src/fec-manager.ts +156 -26
  46. package/src/index.ts +2 -0
  47. package/src/iso-block-locator.ts +114 -31
  48. package/src/mmtp-container-parser.ts +181 -10
  49. package/src/mmtp-router.ts +231 -58
  50. package/src/multicast-manager.ts +44 -6
  51. package/src/repair-fetch.ts +363 -49
  52. package/src/track-session.ts +3 -0
@@ -8023,6 +8023,9 @@ function authoredGap(before, after) {
8023
8023
  function ssIdAfter(a, b) {
8024
8024
  return (a - b | 0) > 0;
8025
8025
  }
8026
+ function holdsRebuilt(stream) {
8027
+ return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
8028
+ }
8026
8029
  var FecReleaseSequencer = class {
8027
8030
  #streams = /* @__PURE__ */ new Map();
8028
8031
  #gate;
@@ -8085,6 +8088,7 @@ var FecReleaseSequencer = class {
8085
8088
  stream.lastSbn = void 0;
8086
8089
  stream.discontinuity = false;
8087
8090
  stream.rapSsId = void 0;
8091
+ stream.sourced = false;
8088
8092
  }
8089
8093
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
8090
8094
  this.#recordStat("droppedStale");
@@ -8111,6 +8115,7 @@ var FecReleaseSequencer = class {
8111
8115
  if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
8112
8116
  else hi = mid;
8113
8117
  }
8118
+ if (stream.buf[lo]?.passed === true) frame.passed = true;
8114
8119
  stream.buf.splice(lo, 0, frame);
8115
8120
  this.#drainUnlessDeferred(stream);
8116
8121
  }
@@ -8146,6 +8151,41 @@ var FecReleaseSequencer = class {
8146
8151
  }
8147
8152
  return false;
8148
8153
  }
8154
+ /**
8155
+ * These streams' sink is being replaced by one that has seen no source
8156
+ * frame yet. First, from a stream that holds a frame the overlay rebuilt,
8157
+ * release to the sink being replaced the frames the unanchored path had
8158
+ * already released to it (#cursorPassed): there they waited in its queue,
8159
+ * which its replacement flushes. A stream that holds none is not drained:
8160
+ * it holds nothing the anchor kept back, and a drain would still force out
8161
+ * a frame held past the hold limit, which before the anchor waited for the
8162
+ * next push. Then treat the streams as unsourced until a source frame is
8163
+ * released.
8164
+ */
8165
+ unsource(packetId, catalogTrackName) {
8166
+ for (const stream of this.#streams.values()) {
8167
+ if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
8168
+ if (stream.draining === 0 && holdsRebuilt(stream)) {
8169
+ if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
8170
+ else this.#drain(stream, true);
8171
+ }
8172
+ stream.sourced = false;
8173
+ }
8174
+ }
8175
+ }
8176
+ /**
8177
+ * Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
8178
+ * (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
8179
+ * packetId.
8180
+ */
8181
+ holdsRebuilt(packetId, catalogTrackName) {
8182
+ for (const stream of this.#streams.values()) {
8183
+ if (stream.packetId !== packetId) continue;
8184
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
8185
+ if (holdsRebuilt(stream)) return true;
8186
+ }
8187
+ return false;
8188
+ }
8149
8189
  /** Re-evaluate held gaps after FEC state changed. */
8150
8190
  poke(streamKey) {
8151
8191
  if (streamKey !== void 0) {
@@ -8237,16 +8277,32 @@ var FecReleaseSequencer = class {
8237
8277
  last: null,
8238
8278
  lastSbn: void 0,
8239
8279
  discontinuity: false,
8240
- rapSsId: void 0
8280
+ rapSsId: void 0,
8281
+ sourced: false,
8282
+ draining: 0
8241
8283
  };
8242
8284
  this.#streams.set(streamKey, stream);
8243
8285
  }
8244
8286
  return stream;
8245
8287
  }
8246
- #drain(stream) {
8288
+ /** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
8289
+ #drain(stream, asUnanchored = false) {
8290
+ stream.draining++;
8291
+ try {
8292
+ this.#drainLoop(stream, asUnanchored);
8293
+ } finally {
8294
+ stream.draining--;
8295
+ }
8296
+ }
8297
+ #drainLoop(stream, asUnanchored) {
8247
8298
  while (stream.buf.length > 0 && stream.last !== null) {
8248
8299
  const head = stream.buf[0];
8249
8300
  const last = stream.last;
8301
+ if (compareFrameOrder(head, last) <= 0) {
8302
+ stream.buf.shift();
8303
+ this.#release(stream, head, void 0);
8304
+ continue;
8305
+ }
8250
8306
  if (stream.discontinuity) {
8251
8307
  stream.discontinuity = false;
8252
8308
  stream.buf.shift();
@@ -8260,9 +8316,10 @@ var FecReleaseSequencer = class {
8260
8316
  }
8261
8317
  const headSbn = head.meta.fecSbn;
8262
8318
  const from = stream.lastSbn ?? headSbn;
8263
- const to = headSbn ?? stream.lastSbn;
8319
+ const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
8264
8320
  let lost = [];
8265
8321
  const unanchored = from === void 0 && to === void 0;
8322
+ let unresolved = false;
8266
8323
  if (from !== void 0 && to !== void 0) {
8267
8324
  const resolved = this.#gate.resolvedLoss(
8268
8325
  stream.packetId,
@@ -8271,7 +8328,8 @@ var FecReleaseSequencer = class {
8271
8328
  Math.max(from, to),
8272
8329
  from
8273
8330
  );
8274
- if (resolved === null) {
8331
+ unresolved = resolved === null;
8332
+ if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
8275
8333
  const heldMs = performance.now() - head.arrivedAtMs;
8276
8334
  if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
8277
8335
  this.#recordStat("forcedWindowFlushes");
@@ -8279,22 +8337,56 @@ var FecReleaseSequencer = class {
8279
8337
  this.#release(stream, head, this.#gate.decide(last, head, [], true));
8280
8338
  continue;
8281
8339
  }
8282
- lost = resolved;
8340
+ lost = resolved ?? [];
8283
8341
  }
8284
8342
  stream.buf.shift();
8285
8343
  this.#release(
8286
8344
  stream,
8287
8345
  head,
8288
- lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
8346
+ lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
8289
8347
  );
8290
8348
  }
8291
8349
  }
8350
+ /**
8351
+ * An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
8352
+ * source cursor holds it unanchored, and neither does a later frame behind
8353
+ * it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
8354
+ * fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
8355
+ * like each frame that arrived in front of it since (stale there). The head
8356
+ * then goes, decided as it would be unanchored, once a frame the base path
8357
+ * delivered (a source frame, or one base FEC recovered) is buffered after a
8358
+ * marked one: the base path has passed that one, so a frame still missing
8359
+ * before it can only come from a repair. It also goes while no source frame
8360
+ * has been released, when the direct path shows a recovered frame at once.
8361
+ * And it goes at once while a staged switch decides on when frames arrive
8362
+ * (FecReleaseGate.releaseUnanchored), and when unsource releases into a
8363
+ * sink being replaced: unanchored, it had already reached the sink.
8364
+ * The scan also writes: the `passed` marks it sets stay on the frames,
8365
+ * and push marks a frame it inserts in front of a marked one from them.
8366
+ */
8367
+ #cursorPassed(stream, asUnanchored) {
8368
+ let at = -1;
8369
+ for (let i = 0; i < stream.buf.length; i++) {
8370
+ const frame = stream.buf[i];
8371
+ if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
8372
+ frame.passed = true;
8373
+ if (at < 0) at = i;
8374
+ } else if (frame.passed !== true) break;
8375
+ }
8376
+ if (at < 0) return false;
8377
+ return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
8378
+ }
8292
8379
  #release(stream, frame, decision) {
8380
+ const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
8381
+ if (stale) this.#recordStat("droppedStale");
8293
8382
  if (decision) frame.meta.gapBefore = decision;
8294
8383
  stream.last = frame;
8295
- if (frame.meta.fecSbn !== void 0) stream.lastSbn = frame.meta.fecSbn;
8384
+ if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
8385
+ stream.lastSbn = frame.meta.fecSbn;
8386
+ }
8387
+ if (frame.meta.recovered !== true) stream.sourced = true;
8296
8388
  this.#gate.onReleased?.(frame.data, frame.meta);
8297
- this.#emit(frame.data, frame.meta);
8389
+ if (!stale) this.#emit(frame.data, frame.meta);
8298
8390
  }
8299
8391
  };
8300
8392
  var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
@@ -8620,6 +8712,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
8620
8712
  fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
8621
8713
  return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
8622
8714
  }
8715
+ /** See FecReleaseSequencer.unsource. */
8716
+ unsourceFecRelease(packetId, catalogTrackName) {
8717
+ this.#fecSequencer?.unsource(packetId, catalogTrackName);
8718
+ }
8719
+ /** See FecReleaseSequencer.holdsRebuilt. */
8720
+ fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
8721
+ return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
8722
+ }
8623
8723
  /** Re-evaluate held gaps after FEC block state changed. */
8624
8724
  pokeFecRelease(packetId, catalogTrackName) {
8625
8725
  if (!this.#fecSequencer) return;
@@ -8946,7 +9046,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
8946
9046
  info,
8947
9047
  resolver,
8948
9048
  options?.recovered === true,
8949
- options?.fecSbn
9049
+ options?.fecSbn,
9050
+ options?.fecSbnFirst
8950
9051
  );
8951
9052
  if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
8952
9053
  }
@@ -9039,7 +9140,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
9039
9140
  info,
9040
9141
  resolver,
9041
9142
  options?.recovered === true,
9042
- options?.fecSbn
9143
+ options?.fecSbn,
9144
+ options?.fecSbnFirst
9043
9145
  );
9044
9146
  if (outcome.record !== void 0) return outcome.record;
9045
9147
  return this.#drop(outcome.reason);
@@ -9059,7 +9161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9059
9161
  * attribution exists to keep readable. Leaving the choice at the call site
9060
9162
  * is what keeps both true.
9061
9163
  */
9062
- #emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
9164
+ #emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
9063
9165
  const timing = resolver.sampleTiming(
9064
9166
  combined.mpuSequenceNumber,
9065
9167
  combined.movieFragmentSequenceNumber,
@@ -9076,6 +9178,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
9076
9178
  );
9077
9179
  if (!meta) return { reason: "combined-meta-build-failed" };
9078
9180
  if (fecSbn !== void 0) meta.fecSbn = fecSbn;
9181
+ if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
9079
9182
  if (!resolver.consumeSample(
9080
9183
  combined.mpuSequenceNumber,
9081
9184
  combined.movieFragmentSequenceNumber,
@@ -9926,6 +10029,7 @@ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
9926
10029
  var OVERLAY_ROUTED_LIMIT = 1024;
9927
10030
  var OVERLAY_PENDING_LIMIT = 256;
9928
10031
  var OVERLAY_OFFSET_MPUS = 16;
10032
+ var OVERLAY_REBUILT_LIMIT = 256;
9929
10033
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
9930
10034
  function assertStreamIdentity(role, stream) {
9931
10035
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
@@ -10102,6 +10206,17 @@ var MmtpRouter = class {
10102
10206
  * an MPU no member of which arrived).
10103
10207
  */
10104
10208
  #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10209
+ /**
10210
+ * The same, shown by the overlay alone, before any base recovery did: a
10211
+ * packet it rebuilt whose base copy carries its protected psn
10212
+ * (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
10213
+ * test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
10214
+ * anchors the release gate only (BLO-52085 D3): fed to the base, it would
10215
+ * make the base drop a late base copy of a keyframe the overlay never
10216
+ * finished, and lift a block's late-source cut, which nothing before
10217
+ * this offset did.
10218
+ */
10219
+ #overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
10105
10220
  /**
10106
10221
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
10107
10222
  * not parsed, typically because its overlap copy was parsed first
@@ -10379,13 +10494,18 @@ var MmtpRouter = class {
10379
10494
  this.#onDeltaBlockCompleteCb?.(packetId, false);
10380
10495
  }
10381
10496
  };
10382
- this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
10497
+ this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
10383
10498
  this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
10384
10499
  if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
10385
10500
  this.#onDeltaBlockCompleteCb?.(packetId, true);
10386
10501
  }
10387
- onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10388
- this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10502
+ if (span === void 0) {
10503
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10504
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
10505
+ } else {
10506
+ onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
10507
+ this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
10508
+ }
10389
10509
  };
10390
10510
  }
10391
10511
  }
@@ -10430,8 +10550,9 @@ var MmtpRouter = class {
10430
10550
  }
10431
10551
  /**
10432
10552
  * FEC-recovered source symbols whose reconstructed header named a
10433
- * packet_sequence_number other than the wire sequence their SS_ID maps to:
10434
- * a hop re-stamped the sequence after the sender protected the packet.
10553
+ * packet_sequence_number other than the wire sequence their SS_ID maps to,
10554
+ * or, rebuilt by the overlay, than their base copy carries: a hop
10555
+ * re-stamped the sequence after the sender protected the packet.
10435
10556
  */
10436
10557
  get restampedRecoveredSequenceCount() {
10437
10558
  return this.#restampedRecoveredSeqs;
@@ -10610,6 +10731,7 @@ var MmtpRouter = class {
10610
10731
  enableMseAssembler(packetId, config, catalogTrackName) {
10611
10732
  const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
10612
10733
  const previous = this.#mseAssemblers.get(assemblerKey);
10734
+ this.#parser.unsourceFecRelease(packetId, catalogTrackName);
10613
10735
  if (previous) {
10614
10736
  previous.flush();
10615
10737
  previous.dispose();
@@ -10760,7 +10882,8 @@ var MmtpRouter = class {
10760
10882
  fec,
10761
10883
  k,
10762
10884
  offsets: /* @__PURE__ */ new Map(),
10763
- moqBlocks: /* @__PURE__ */ new Set()
10885
+ moqBlocks: /* @__PURE__ */ new Set(),
10886
+ rebuilt: /* @__PURE__ */ new Map()
10764
10887
  };
10765
10888
  this.#overlays.set(repairPacketId, inst);
10766
10889
  return () => {
@@ -10801,6 +10924,7 @@ var MmtpRouter = class {
10801
10924
  inst.last = void 0;
10802
10925
  inst.offsets.clear();
10803
10926
  inst.moqBlocks.clear();
10927
+ inst.rebuilt.clear();
10804
10928
  }
10805
10929
  #overlaysForSource(sourcePacketId, catalogTrackName) {
10806
10930
  return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
@@ -10853,13 +10977,23 @@ var MmtpRouter = class {
10853
10977
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
10854
10978
  * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
10855
10979
  * FEC reads it.
10980
+ *
10981
+ * The base copy of a packet an overlay rebuilt with no base SS_ID shows
10982
+ * what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
10983
+ * its wire packet_sequence_number equal to the protected one rebuilt shows
10984
+ * the stream unstamped at the SS_ID offset it states, for the release gate
10985
+ * (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
10986
+ * re-stamps it, which is counted (restampedRecoveredSequenceCount).
10856
10987
  */
10857
10988
  #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
10858
10989
  if (this.#overlays.size === 0) return;
10859
10990
  const ssId = keyframeOverlaySsId(header);
10860
10991
  if (ssId === void 0) return;
10992
+ let rebuiltPsn;
10861
10993
  for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
10862
10994
  this.#noteOverlaySsId(inst, ssId, sequenceNumber);
10995
+ rebuiltPsn ??= inst.rebuilt.get(ssId);
10996
+ inst.rebuilt.delete(ssId);
10863
10997
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
10864
10998
  if (identity !== null) {
10865
10999
  inst.offsets.delete(identity.mpuSequenceNumber);
@@ -10889,6 +11023,14 @@ var MmtpRouter = class {
10889
11023
  const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
10890
11024
  if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
10891
11025
  }
11026
+ if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
11027
+ const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
11028
+ if (rebuiltPsn !== sequenceNumber) {
11029
+ this.#restampedRecoveredSeqs++;
11030
+ } else if (baseSsId !== void 0) {
11031
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11032
+ if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
11033
+ }
10892
11034
  }
10893
11035
  #noteOverlayMoqBlock(inst, blockStart) {
10894
11036
  inst.moqBlocks.add(blockStart);
@@ -10922,11 +11064,16 @@ var MmtpRouter = class {
10922
11064
  }
10923
11065
  /**
10924
11066
  * Route an overlay-recovered block's packets as source media of the
10925
- * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
10926
- * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
10927
- * base re-stamp, no block advance. Each packet's wire sequence is restored
10928
- * from its keyframe's own offset; without one, a stream the base never
10929
- * re-stamped keeps the header's sequence and any other drops the packet.
11067
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
11068
+ * packet whose base SS_ID is determined independently (overlayBaseSsId,
11069
+ * unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
11070
+ * base FEC recovered does, so it waits behind base frames still in flight,
11071
+ * though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
11072
+ * longer than unanchored; one whose base SS_ID is undetermined has no
11073
+ * release-gate anchor. No parse ledger, no base re-stamp, no block
11074
+ * advance. Each packet's wire sequence is restored from its keyframe's own
11075
+ * offset; without one, a stream the base never re-stamped keeps the
11076
+ * header's sequence and any other drops the packet.
10930
11077
  */
10931
11078
  #routeOverlayRecovered(inst, blockStart, recovered) {
10932
11079
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -10947,40 +11094,65 @@ var MmtpRouter = class {
10947
11094
  const restamped = this.#restampedStreams.has(streamKey);
10948
11095
  const restored = /* @__PURE__ */ new Set();
10949
11096
  const baseSsIds = /* @__PURE__ */ new Map();
11097
+ const gateOnly = /* @__PURE__ */ new Set();
10950
11098
  const symbols = result.symbols.flatMap((sym) => {
11099
+ const overlaySsId = serial32Add(blockStart, sym.index);
10951
11100
  const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
10952
11101
  if (anchor !== void 0) {
10953
- const overlaySsId = serial32Add(blockStart, sym.index);
10954
11102
  const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
10955
11103
  restored.add(packetSequenceNumber);
10956
11104
  const out = { ...sym, packetSequenceNumber };
10957
11105
  const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
10958
- if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
11106
+ if (baseSsId !== void 0) {
11107
+ baseSsIds.set(sym.index, baseSsId);
11108
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11109
+ if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
11110
+ this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
11111
+ }
11112
+ }
10959
11113
  return [out];
10960
11114
  }
10961
11115
  if (!restamped) {
10962
- const baseSsId = unstampedBaseSsId(
10963
- this.#ssIdSeqOffsetByStream.get(streamKey),
10964
- this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
10965
- sym.packetSequenceNumber
10966
- );
10967
- if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
11116
+ const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
11117
+ const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
11118
+ const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
11119
+ if (baseSsId !== void 0) {
11120
+ baseSsIds.set(sym.index, baseSsId);
11121
+ if (fedSsId === void 0) gateOnly.add(sym.index);
11122
+ } else {
11123
+ inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
11124
+ if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
11125
+ }
10968
11126
  return [sym];
10969
11127
  }
10970
11128
  this.#overlayStats.unsequencedDropped++;
10971
11129
  return [];
10972
11130
  });
10973
11131
  this.#overlayStats.recovered++;
11132
+ const mpuFirst = /* @__PURE__ */ new Map();
11133
+ for (const sym of symbols) {
11134
+ const ssId = baseSsIds.get(sym.index);
11135
+ const mpu = sym.fragment.mpuSequenceNumber;
11136
+ const first = mpuFirst.get(mpu);
11137
+ if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
11138
+ }
11139
+ const firstSsIds = /* @__PURE__ */ new Map();
11140
+ for (const sym of symbols) {
11141
+ if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
11142
+ }
10974
11143
  this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
10975
- deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
11144
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
11145
+ baseSsIds,
11146
+ firstSsIds
10976
11147
  });
10977
11148
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
10978
- this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
11149
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
10979
11150
  }
10980
11151
  /**
10981
11152
  * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
10982
11153
  * its base block too, once its base SS_ID is determined (overlayBaseSsId,
10983
- * from a received member of its MPU, else unstampedBaseSsId),
11154
+ * from a received member of its MPU, else unstampedBaseSsId at an offset a
11155
+ * base recovery showed; `baseSsIds` holds no other),
10984
11156
  * so it is fed to the base decoder as a rebuilt source
10985
11157
  * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
10986
11158
  * fewer to solve, and its repair can recover the block's other packets.
@@ -11006,7 +11178,7 @@ var MmtpRouter = class {
11006
11178
  const feeds = [];
11007
11179
  for (const sym of symbols) {
11008
11180
  if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
11009
- const ssId = baseSsIds.get(sym);
11181
+ const ssId = baseSsIds.get(sym.index);
11010
11182
  if (ssId === void 0) {
11011
11183
  this.#overlayStats.baseUndetermined++;
11012
11184
  continue;
@@ -11735,7 +11907,7 @@ var MmtpRouter = class {
11735
11907
  catalogTrackName
11736
11908
  )) continue;
11737
11909
  if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
11738
- fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
11910
+ fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
11739
11911
  this.#requestFecPoke(ownerPacketId, catalogTrackName);
11740
11912
  }
11741
11913
  if (payload.oversized) continue;
@@ -11823,6 +11995,7 @@ var MmtpRouter = class {
11823
11995
  this.#latestVideoSeqByStream.delete(streamKey);
11824
11996
  this.#ssIdSeqOffsetByStream.delete(streamKey);
11825
11997
  this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
11998
+ this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
11826
11999
  this.#multicastRapTwinByStream.delete(streamKey);
11827
12000
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
11828
12001
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -11949,6 +12122,12 @@ var MmtpRouter = class {
11949
12122
  * the outgoing stream stops. The hold is bounded by the incoming stream's
11950
12123
  * own RAP cadence, so no timer is involved.
11951
12124
  *
12125
+ * While the switch is staged, the FEC release gate lets a keyframe the
12126
+ * overlay rebuilt reach it, on either side, when the keyframe did before
12127
+ * the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
12128
+ * decides on what each side delivered, and the commit discards the gate's
12129
+ * frames of the outgoing stream.
12130
+ *
11952
12131
  * The commit is one synchronous transition inside the frame path: the
11953
12132
  * outgoing stream's already-admitted frames are released (its queued FEC
11954
12133
  * gap fills only up to the RAP, since recovery can queue a frame ahead of
@@ -11999,6 +12178,9 @@ var MmtpRouter = class {
11999
12178
  droppedIncomingFrames: 0
12000
12179
  };
12001
12180
  this.#staged = state;
12181
+ if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
12182
+ this.#requestFecPoke(from.packetId, from.catalogTrackName);
12183
+ }
12002
12184
  return {
12003
12185
  cancel: () => {
12004
12186
  if (this.#staged !== state) return false;
@@ -12081,7 +12263,17 @@ var MmtpRouter = class {
12081
12263
  */
12082
12264
  #routeParsedFrame(data, meta) {
12083
12265
  if (this.#classifyPacketId(meta.packetId) === null) return;
12084
- if (this.#staged !== null && this.#interceptSwitchFrame(data, meta)) return;
12266
+ if (this.#staged !== null) {
12267
+ let consumed = false;
12268
+ this.#fecPokeDepth++;
12269
+ try {
12270
+ consumed = this.#interceptSwitchFrame(data, meta);
12271
+ } finally {
12272
+ this.#fecPokeDepth--;
12273
+ this.#flushPendingFecPokes();
12274
+ }
12275
+ if (consumed) return;
12276
+ }
12085
12277
  this.#deliverParsedFrame(data, meta);
12086
12278
  }
12087
12279
  #commitStreamSwitch(state, rap, reason) {
@@ -12233,12 +12425,16 @@ var MmtpRouter = class {
12233
12425
  this.#parser.deferFecRelease(work);
12234
12426
  } finally {
12235
12427
  this.#fecPokeDepth--;
12236
- if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
12237
- const pokes = [...this.#pendingFecPokes.values()];
12238
- this.#pendingFecPokes.clear();
12239
- for (const poke of pokes) {
12240
- this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
12241
- }
12428
+ this.#flushPendingFecPokes();
12429
+ }
12430
+ }
12431
+ /** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
12432
+ #flushPendingFecPokes() {
12433
+ if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
12434
+ const pokes = [...this.#pendingFecPokes.values()];
12435
+ this.#pendingFecPokes.clear();
12436
+ for (const poke of pokes) {
12437
+ this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
12242
12438
  }
12243
12439
  }
12244
12440
  }
@@ -12316,6 +12512,14 @@ var MmtpRouter = class {
12316
12512
  onRap: (packetId, catalogTrackName, ssId) => {
12317
12513
  for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
12318
12514
  },
12515
+ // A staged switch commits on when each side's frames reach it
12516
+ // (#interceptSwitchFrame): both sides' overlay keyframes reach it when
12517
+ // they did unanchored.
12518
+ releaseUnanchored: (packetId, catalogTrackName) => {
12519
+ const staged = this.#staged;
12520
+ const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
12521
+ return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
12522
+ },
12319
12523
  onReleased: (data, meta) => {
12320
12524
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
12321
12525
  const timing = meta.authoredTiming;
@@ -12766,7 +12970,8 @@ var MmtpRouter = class {
12766
12970
  source: parserOptions?.source,
12767
12971
  deliveryProvenance: parserOptions?.deliveryProvenance,
12768
12972
  catalogTrackName: parserOptions?.catalogTrackName,
12769
- ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
12973
+ ...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
12974
+ ...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
12770
12975
  };
12771
12976
  if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
12772
12977
  else this.#parser.feed(packet, feedOptions);
@@ -13232,7 +13437,12 @@ var MmtpRouter = class {
13232
13437
  #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
13233
13438
  if (ownerPacketId == null) return;
13234
13439
  if (this.#classifyPacketId(ownerPacketId) === null) return;
13235
- const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
13440
+ const fecSbnOf = (index) => {
13441
+ if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
13442
+ const fecSbn = overlay.baseSsIds.get(index);
13443
+ const fecSbnFirst = overlay.firstSsIds.get(index);
13444
+ return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
13445
+ };
13236
13446
  const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
13237
13447
  const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
13238
13448
  if (ssIdSeqOffset !== void 0) {