@blockcast/mmt-container 2.2.0-main.226dae8b7a27 → 2.2.0-main.2c4d96d3ffc2

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 (65) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +658 -168
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +1 -1
  5. package/dist/catalog-fec-params.js.map +1 -1
  6. package/dist/cmcd-producer.d.ts +60 -47
  7. package/dist/cmcd-producer.d.ts.map +1 -1
  8. package/dist/cmcd-producer.js +138 -109
  9. package/dist/cmcd-producer.js.map +2 -2
  10. package/dist/delivery-catalog.js +17 -12
  11. package/dist/delivery-catalog.js.map +2 -2
  12. package/dist/delta-fallback-config.js.map +1 -1
  13. package/dist/fec-manager.d.ts +33 -1
  14. package/dist/fec-manager.d.ts.map +1 -1
  15. package/dist/fec-manager.js +213 -6
  16. package/dist/fec-manager.js.map +2 -2
  17. package/dist/fec-worker-setup.d.ts +1 -1
  18. package/dist/fec-worker-setup.js +1 -1
  19. package/dist/fec-worker-setup.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +2 -2
  24. package/dist/iso-block-locator.d.ts +9 -0
  25. package/dist/iso-block-locator.d.ts.map +1 -1
  26. package/dist/iso-block-locator.js +17 -0
  27. package/dist/iso-block-locator.js.map +2 -2
  28. package/dist/iso-bmff-timing.d.ts +10 -1
  29. package/dist/iso-bmff-timing.d.ts.map +1 -1
  30. package/dist/iso-bmff-timing.js +36 -2
  31. package/dist/iso-bmff-timing.js.map +2 -2
  32. package/dist/media-router.js.map +1 -1
  33. package/dist/mmtp-container-parser.d.ts +37 -2
  34. package/dist/mmtp-container-parser.d.ts.map +1 -1
  35. package/dist/mmtp-container-parser.js +86 -9
  36. package/dist/mmtp-container-parser.js.map +2 -2
  37. package/dist/mmtp-container-parser.no-modules.js +89 -11
  38. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  39. package/dist/mmtp-decode-pipeline.js +85 -9
  40. package/dist/mmtp-decode-pipeline.js.map +2 -2
  41. package/dist/mmtp-router.d.ts +16 -1
  42. package/dist/mmtp-router.d.ts.map +1 -1
  43. package/dist/mmtp-router.js +289 -35
  44. package/dist/mmtp-router.js.map +2 -2
  45. package/dist/multicast-manager.js +502 -41
  46. package/dist/multicast-manager.js.map +2 -2
  47. package/dist/multicast-transport.js +17 -12
  48. package/dist/multicast-transport.js.map +2 -2
  49. package/dist/no-modules.js +85 -9
  50. package/dist/no-modules.js.map +2 -2
  51. package/dist/track-session.d.ts +1 -1
  52. package/dist/track-session.js.map +1 -1
  53. package/dist/wasm-mmtp-container-parser.js +85 -9
  54. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  55. package/package.json +8 -5
  56. package/src/catalog-fec-params.ts +1 -1
  57. package/src/cmcd-producer.ts +189 -179
  58. package/src/fec-manager.ts +256 -6
  59. package/src/fec-worker-setup.ts +2 -2
  60. package/src/index.ts +6 -2
  61. package/src/iso-block-locator.ts +19 -0
  62. package/src/iso-bmff-timing.ts +40 -1
  63. package/src/mmtp-container-parser.ts +96 -8
  64. package/src/mmtp-router.ts +342 -13
  65. package/src/track-session.ts +1 -1
@@ -7988,8 +7988,8 @@ var MulticastSubscriptionPool = class {
7988
7988
  record.claims--;
7989
7989
  return this.acquire(gateway, config, consumer);
7990
7990
  }
7991
- const token = Symbol(key);
7992
- entry.consumers.set(token, consumer);
7991
+ const token2 = Symbol(key);
7992
+ entry.consumers.set(token2, consumer);
7993
7993
  let released = false;
7994
7994
  return {
7995
7995
  key,
@@ -7997,7 +7997,7 @@ var MulticastSubscriptionPool = class {
7997
7997
  if (released)
7998
7998
  return;
7999
7999
  released = true;
8000
- entry.consumers.delete(token);
8000
+ entry.consumers.delete(token2);
8001
8001
  record.claims--;
8002
8002
  if (record.claims > 0)
8003
8003
  return;
@@ -8746,6 +8746,7 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8746
8746
  });
8747
8747
 
8748
8748
  // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
8749
+ var MAX_PENDING_PACKETS = 256;
8749
8750
  var AMTGatewayManager = class {
8750
8751
  subscriptions = /* @__PURE__ */ new Map();
8751
8752
  messageHandler;
@@ -8754,7 +8755,6 @@ var AMTGatewayManager = class {
8754
8755
  constructor() {
8755
8756
  this.messageHandler = (event) => {
8756
8757
  const data = event.data;
8757
- console.log(`[AMT] SW message received:`, data.type);
8758
8758
  if (data.type === "UDP_PACKET") {
8759
8759
  this.handlePacket(data);
8760
8760
  } else if (data.type === "SOCKET_ERROR") {
@@ -8772,6 +8772,14 @@ var AMTGatewayManager = class {
8772
8772
  this.messageListenerRegistered = true;
8773
8773
  console.log("[AMT] Registered service worker message handler");
8774
8774
  }
8775
+ removeMessageListener() {
8776
+ if (!this.messageListenerRegistered)
8777
+ return;
8778
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8779
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8780
+ }
8781
+ this.messageListenerRegistered = false;
8782
+ }
8775
8783
  /**
8776
8784
  * Subscribe via AMT tunnel
8777
8785
  */
@@ -8816,6 +8824,8 @@ var AMTGatewayManager = class {
8816
8824
  this.subscriptions.delete(keyStr);
8817
8825
  this.pendingPackets.delete(subscription.subscriptionId);
8818
8826
  }
8827
+ if (this.subscriptions.size === 0)
8828
+ this.removeMessageListener();
8819
8829
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8820
8830
  }
8821
8831
  }
@@ -8837,6 +8847,8 @@ var AMTGatewayManager = class {
8837
8847
  } finally {
8838
8848
  this.subscriptions.delete(handle);
8839
8849
  this.pendingPackets.delete(subscription.subscriptionId);
8850
+ if (this.subscriptions.size === 0)
8851
+ this.removeMessageListener();
8840
8852
  }
8841
8853
  }
8842
8854
  /**
@@ -8853,14 +8865,12 @@ var AMTGatewayManager = class {
8853
8865
  if (!subscriptionId)
8854
8866
  return;
8855
8867
  const subscription = this.subscriptions.get(subscriptionId);
8856
- if (!subscription) {
8857
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8868
+ if (!subscription)
8858
8869
  return;
8859
- }
8860
8870
  if (!subscription.handshakeComplete) {
8861
8871
  const queue = this.pendingPackets.get(subscriptionId);
8862
8872
  const packetData2 = data;
8863
- if (queue) {
8873
+ if (queue && queue.length < MAX_PENDING_PACKETS) {
8864
8874
  queue.push({
8865
8875
  data: packetData2.data,
8866
8876
  remoteAddress: packetData2.remoteAddress,
@@ -8886,20 +8896,15 @@ var AMTGatewayManager = class {
8886
8896
  if (!subscriptionId)
8887
8897
  return;
8888
8898
  const subscription = this.subscriptions.get(subscriptionId);
8889
- if (!subscription) {
8890
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8899
+ if (!subscription)
8891
8900
  return;
8892
- }
8893
8901
  subscription.config.onError(new Error(error || "Unknown error"));
8894
8902
  }
8895
8903
  /**
8896
8904
  * Cleanup on destroy
8897
8905
  */
8898
8906
  destroy() {
8899
- if (this.messageListenerRegistered && this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8900
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8901
- this.messageListenerRegistered = false;
8902
- }
8907
+ this.removeMessageListener();
8903
8908
  const handles = Array.from(this.subscriptions.keys());
8904
8909
  for (const handle of handles) {
8905
8910
  this.unsubscribe(handle).catch((err) => {
@@ -12944,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
12944
12949
  #retentionBlocks;
12945
12950
  #heldEvicted;
12946
12951
  #newest;
12952
+ #newestSource;
12947
12953
  #holes;
12948
12954
  static #checkedRetention(retentionBlocks) {
12949
12955
  if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
@@ -12981,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
12981
12987
  observe(position) {
12982
12988
  if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
12983
12989
  }
12990
+ /**
12991
+ * The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
12992
+ * `newest`, a repair never moves it: a repair can run ahead of its sources,
12993
+ * a source cannot run ahead of the sources sent before it except by
12994
+ * reordering.
12995
+ */
12996
+ get newestSource() {
12997
+ return this.#newestSource;
12998
+ }
12999
+ /** Note a received source's SS_ID; true when it is the newest one yet. */
13000
+ noteSource(ssId) {
13001
+ if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
13002
+ this.#newestSource = ssId;
13003
+ return true;
13004
+ }
12984
13005
  /** Held sources evicted by `heldCapacity` (never by the horizon itself). */
12985
13006
  get heldEvicted() {
12986
13007
  return this.#heldEvicted;
@@ -13213,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
13213
13234
  this.#held.clear();
13214
13235
  this.#coverageEnd = void 0;
13215
13236
  this.#newest = void 0;
13237
+ this.#newestSource = void 0;
13216
13238
  }
13217
13239
  #claim(block) {
13218
13240
  const placed = [];
@@ -13832,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
13832
13854
  var WORKER_ERROR_KIND_CAP = 32;
13833
13855
  var WORKER_ERROR_OVERFLOW_KIND = "other";
13834
13856
  var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
13857
+ var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
13835
13858
  function isMulticastSymbolDelivery(value) {
13836
13859
  return value === "datagram" || value === "ordered-relay";
13837
13860
  }
@@ -13929,6 +13952,10 @@ var FECManager = class _FECManager {
13929
13952
  // Source symbol count per track-scoped block — marks complete when K symbols arrive.
13930
13953
  this.#blockSourceCounts = /* @__PURE__ */ new Map();
13931
13954
  this.#blockSourceEsis = /* @__PURE__ */ new Map();
13955
+ // Source ESIs per block that another FEC instance rebuilt and fed in
13956
+ // (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
13957
+ // not network arrivals and never make a block clean.
13958
+ this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
13932
13959
  // Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
13933
13960
  // first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
13934
13961
  // media units, usually before it holds K symbols; under iso-ssbg1-v1
@@ -14057,6 +14084,10 @@ var FECManager = class _FECManager {
14057
14084
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14058
14085
  // One classifier per physical stream, built from that stream's geometry.
14059
14086
  this.#classifiers = /* @__PURE__ */ new Map();
14087
+ // A stream without catalog timing has no classifier, so the last RAP it
14088
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14089
+ // a second RAP is released inside one MPU (BLO-52085).
14090
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14060
14091
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14061
14092
  // rungs can share a packetId with a different framerate or GOP, so the
14062
14093
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14214,6 +14245,7 @@ var FECManager = class _FECManager {
14214
14245
  #pendingWorkerCleanupBounds;
14215
14246
  #blockSourceCounts;
14216
14247
  #blockSourceEsis;
14248
+ #blockRebuiltEsis;
14217
14249
  #blockAuthoredLengths;
14218
14250
  #ssbLengthConflictCount;
14219
14251
  #blockRepairRsIds;
@@ -14256,6 +14288,7 @@ var FECManager = class _FECManager {
14256
14288
  #orderedFrontiers;
14257
14289
  #orderedPendingBlocks;
14258
14290
  #classifiers;
14291
+ #noSpanRaps;
14259
14292
  #streamGeometries;
14260
14293
  #failedBlocks;
14261
14294
  #resolvedBlockClasses;
@@ -14917,6 +14950,7 @@ var FECManager = class _FECManager {
14917
14950
  };
14918
14951
  const discontinuity = () => {
14919
14952
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14953
+ this.#noSpanRaps.delete(streamKey);
14920
14954
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14921
14955
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14922
14956
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15329,6 +15363,9 @@ var FECManager = class _FECManager {
15329
15363
  }
15330
15364
  if (policyChanged) {
15331
15365
  this.#bumpEpoch(packetId);
15366
+ for (const key of [...this.#noSpanRaps.keys()]) {
15367
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15368
+ }
15332
15369
  for (const [key, classifier] of [...this.#classifiers]) {
15333
15370
  if (!this.#belongsToPacket(key, packetId)) continue;
15334
15371
  const stream = this.#parseStreamKey(key);
@@ -15531,6 +15568,7 @@ var FECManager = class _FECManager {
15531
15568
  }
15532
15569
  resetMap(this.#blockSourceCounts);
15533
15570
  resetMap(this.#blockSourceEsis);
15571
+ resetMap(this.#blockRebuiltEsis);
15534
15572
  resetMap(this.#blockAuthoredLengths);
15535
15573
  resetMap(this.#blockRepairRsIds);
15536
15574
  resetMap(this.#blockRsbLengths);
@@ -15564,6 +15602,7 @@ var FECManager = class _FECManager {
15564
15602
  resetMap(this.#isoLocators);
15565
15603
  resetMap(this.#sourceAnchors);
15566
15604
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15605
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15567
15606
  this.#bumpEpoch(packetId);
15568
15607
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15569
15608
  for (const key of keys.keys()) {
@@ -15654,6 +15693,7 @@ var FECManager = class _FECManager {
15654
15693
  dropMap(this.#pendingWorkerCleanupClients);
15655
15694
  dropMap(this.#blockSourceCounts);
15656
15695
  dropMap(this.#blockSourceEsis);
15696
+ dropMap(this.#blockRebuiltEsis);
15657
15697
  dropMap(this.#blockAuthoredLengths);
15658
15698
  dropMap(this.#blockRepairRsIds);
15659
15699
  dropMap(this.#blockRsbLengths);
@@ -15670,6 +15710,7 @@ var FECManager = class _FECManager {
15670
15710
  dropMap(this.#isoLocators);
15671
15711
  dropMap(this.#sourceAnchors);
15672
15712
  dropMap(this.#classifiers);
15713
+ dropMap(this.#noSpanRaps);
15673
15714
  dropMap(this.#streamGeometries);
15674
15715
  dropSet(this.#pendingWorkerCleanups);
15675
15716
  dropSet(this.#completedBlocks);
@@ -15815,6 +15856,9 @@ var FECManager = class _FECManager {
15815
15856
  this.#unindexBlockKey(key);
15816
15857
  }
15817
15858
  }
15859
+ if (isSourceSymbol && source === "multicast") {
15860
+ this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
15861
+ }
15818
15862
  return true;
15819
15863
  }
15820
15864
  #observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
@@ -16132,6 +16176,7 @@ var FECManager = class _FECManager {
16132
16176
  const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
16133
16177
  const evictedBefore = locator.heldEvicted;
16134
16178
  const held = locator.hold(ssId, source);
16179
+ if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
16135
16180
  const evicted = locator.heldEvicted - evictedBefore;
16136
16181
  if (evicted > 0) {
16137
16182
  const before = this.#isoHeldEvictedCount;
@@ -16300,11 +16345,14 @@ var FECManager = class _FECManager {
16300
16345
  /**
16301
16346
  * Whether an MPU boundary still bounds this stream's RAPs (see
16302
16347
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16303
- * it does not. A stream with no classifier yet has shown nothing against it.
16348
+ * it does not. A stream with no classifier yet has shown nothing against it,
16349
+ * and neither has a stream without catalog timing until it releases two
16350
+ * RAPs inside one MPU.
16304
16351
  */
16305
16352
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16306
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16307
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16353
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16354
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16355
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16308
16356
  }
16309
16357
  /** A block's class now, or frozen at its resolution. */
16310
16358
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16330,7 +16378,14 @@ var FECManager = class _FECManager {
16330
16378
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16331
16379
  */
16332
16380
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16333
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16381
+ if (!this.#trackConfigs.has(packetId)) return;
16382
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16383
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16384
+ const last = this.#noSpanRaps.get(streamKey);
16385
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16386
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16387
+ return;
16388
+ }
16334
16389
  this.#applyClassifierChanges(
16335
16390
  packetId,
16336
16391
  catalogTrackName,
@@ -16717,7 +16772,15 @@ var FECManager = class _FECManager {
16717
16772
  this.#isoSourceOversizeCount++;
16718
16773
  return null;
16719
16774
  }
16720
- const sbn = located.block.start;
16775
+ return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
16776
+ }
16777
+ /**
16778
+ * Hand one placed source symbol to the stream's decoder: its Worker, which
16779
+ * answers through onFrame (null here), or its synchronous decoder, whose
16780
+ * recovered block is returned. `sbn` is the SS_Start of the block a repair
16781
+ * stated for `ssId`.
16782
+ */
16783
+ #feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
16721
16784
  const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
16722
16785
  const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
16723
16786
  const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
@@ -16746,6 +16809,68 @@ var FECManager = class _FECManager {
16746
16809
  }
16747
16810
  return recovered;
16748
16811
  }
16812
+ /**
16813
+ * Feed a source symbol of this stream that another FEC instance over the
16814
+ * same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
16815
+ * caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
16816
+ * BLO-52085). It joins its block's decoder as a received source would, so
16817
+ * it reduces the erasures the block still has to solve, but it is not a
16818
+ * network arrival: no transport accounting, no classification evidence,
16819
+ * no deadline refresh, and it never makes a block clean.
16820
+ *
16821
+ * Only a block a repair stated takes one (a repair-less span has no
16822
+ * decoder block). A block that is terminal, or already holds that source,
16823
+ * does not. The symbol is the exact protected packet, unpadded, as
16824
+ * feedSourceSymbol takes it.
16825
+ *
16826
+ * `recovered` is the block the synchronous decoder completed with it; the
16827
+ * caller routes it and completes the block, as for any recovery. When
16828
+ * every source of the block is then present, received or rebuilt, and no
16829
+ * decoder returned it (a Worker answers asynchronously, and reports such a
16830
+ * block as clean on its own side), the block is completed here as
16831
+ * recovered, with nothing left to route.
16832
+ */
16833
+ feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
16834
+ if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
16835
+ const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
16836
+ if (located === null || located.block.span) return NOT_TAKEN;
16837
+ if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
16838
+ const sbn = located.block.start;
16839
+ const key = this.#blockKey(sbn, packetId, catalogTrackName);
16840
+ if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
16841
+ let rebuilt = this.#blockRebuiltEsis.get(key);
16842
+ if (rebuilt?.has(located.esi)) return NOT_TAKEN;
16843
+ if (rebuilt === void 0) {
16844
+ rebuilt = /* @__PURE__ */ new Set();
16845
+ this.#blockRebuiltEsis.set(key, rebuilt);
16846
+ }
16847
+ rebuilt.add(located.esi);
16848
+ const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
16849
+ if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
16850
+ return { taken: true, recovered };
16851
+ }
16852
+ /**
16853
+ * A block that holds every one of its `length` sources, received or
16854
+ * rebuilt by another instance, with at least one rebuilt, is recovered:
16855
+ * every packet of it was delivered, by its own arrival or by the instance
16856
+ * that rebuilt it, so nothing is routed. Returns whether it completed it.
16857
+ */
16858
+ #completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
16859
+ const rebuilt = this.#blockRebuiltEsis.get(key);
16860
+ if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
16861
+ let present = rebuilt.size;
16862
+ for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
16863
+ if (present < length) return false;
16864
+ this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
16865
+ this.countRecovery(packetId, sbn, catalogTrackName);
16866
+ this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
16867
+ if (catalogTrackName === void 0) {
16868
+ this.onBlockComplete?.(sbn, packetId);
16869
+ } else {
16870
+ this.onBlockComplete?.(sbn, packetId, catalogTrackName);
16871
+ }
16872
+ return true;
16873
+ }
16749
16874
  /**
16750
16875
  * fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
16751
16876
  * exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
@@ -16948,8 +17073,72 @@ var FECManager = class _FECManager {
16948
17073
  this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
16949
17074
  return null;
16950
17075
  }
17076
+ if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
16951
17077
  return recovered;
16952
17078
  }
17079
+ /**
17080
+ * A first-arrival multicast source, held or placed. A datagram flow keeps
17081
+ * its sources in send order, so one that moves the stream's newest source
17082
+ * position past a block leaves the sources that block still misses
17083
+ * reordered behind it at best: it shortens the wait of every block it
17084
+ * passed that only a late source can complete. MoQ is left out: its streams
17085
+ * can overtake one another, so a MoQ source shows nothing about the sources
17086
+ * sent before it.
17087
+ */
17088
+ #noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
17089
+ const previous = locator.newestSource;
17090
+ if (!locator.noteSource(ssId) || previous === void 0) return;
17091
+ for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
17092
+ if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
17093
+ this.#shortenUnrecoverableWait(
17094
+ this.#blockKey(block.start, packetId, catalogTrackName),
17095
+ block.start,
17096
+ packetId,
17097
+ catalogTrackName
17098
+ );
17099
+ }
17100
+ }
17101
+ /**
17102
+ * The wait of a block that only a late source can complete (BLO-52085): a
17103
+ * repair stated it, its received sources plus every symbol its repair group
17104
+ * carries (RSB_length) are short of K' (the repair scheduler's
17105
+ * group-cannot-complete), and a later source of the stream has arrived, so
17106
+ * a source it misses is lost or reordered behind that one. The span, which
17107
+ * covers repair symbols paced after its sources, buys it nothing: it waits
17108
+ * the late-symbol allowance alone, the multicast wait less the span (the
17109
+ * catalog tolerance or the measured delivery gap). Undefined for any other
17110
+ * block.
17111
+ */
17112
+ #lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
17113
+ const authoredLength = this.#blockAuthoredLengths.get(key);
17114
+ const rsbLength = this.#blockRsbLengths.get(key);
17115
+ if (authoredLength === void 0 || rsbLength === void 0) return void 0;
17116
+ if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
17117
+ const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
17118
+ if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
17119
+ return void 0;
17120
+ }
17121
+ return this.#multicastWaitMs(config) - config.interleaveDepthMs;
17122
+ }
17123
+ /**
17124
+ * Bring an open block's wait in to its late-source wait, when it has one.
17125
+ * Never later than it is armed: only a source can complete such a block,
17126
+ * so a repair of its group (paced across the span) must not keep it open.
17127
+ */
17128
+ #shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
17129
+ if (!this.#activeBlocks.has(key)) return;
17130
+ const config = this.#trackConfigs.get(packetId);
17131
+ if (config === void 0) return;
17132
+ const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
17133
+ if (waitMs === void 0) return;
17134
+ const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
17135
+ this.#armBlockTimeout(
17136
+ sbn,
17137
+ packetId,
17138
+ catalogTrackName,
17139
+ armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
17140
+ );
17141
+ }
16953
17142
  /**
16954
17143
  * Record a block's authored SSB_length from a repair symbol. Returns false,
16955
17144
  * leaving the block unchanged, when the symbol contradicts the block: a
@@ -17104,6 +17293,15 @@ var FECManager = class _FECManager {
17104
17293
  this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
17105
17294
  return;
17106
17295
  }
17296
+ if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
17297
+ key,
17298
+ sbn,
17299
+ this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
17300
+ packetId,
17301
+ catalogTrackName
17302
+ )) {
17303
+ return;
17304
+ }
17107
17305
  this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
17108
17306
  }
17109
17307
  #completeCleanBlock(key, sbn, packetId, catalogTrackName) {
@@ -17112,6 +17310,7 @@ var FECManager = class _FECManager {
17112
17310
  this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
17113
17311
  this.#blockSourceCounts.delete(key);
17114
17312
  this.#blockSourceEsis.delete(key);
17313
+ this.#blockRebuiltEsis.delete(key);
17115
17314
  this.#blockAuthoredLengths.delete(key);
17116
17315
  this.#blockRepairRsIds.delete(key);
17117
17316
  this.#blockRsbLengths.delete(key);
@@ -17178,7 +17377,16 @@ var FECManager = class _FECManager {
17178
17377
  this.#repairDeadlineMs(trackConfig)
17179
17378
  );
17180
17379
  }
17181
- this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
17380
+ if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
17381
+ this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
17382
+ return;
17383
+ }
17384
+ this.#armBlockTimeout(
17385
+ sbn,
17386
+ packetId,
17387
+ catalogTrackName,
17388
+ this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
17389
+ );
17182
17390
  }
17183
17391
  #failBlock(sbn, packetId, catalogTrackName) {
17184
17392
  const key = this.#blockKey(sbn, packetId, catalogTrackName);
@@ -17191,6 +17399,7 @@ var FECManager = class _FECManager {
17191
17399
  }
17192
17400
  this.#blockSourceCounts.delete(key);
17193
17401
  this.#blockSourceEsis.delete(key);
17402
+ this.#blockRebuiltEsis.delete(key);
17194
17403
  this.#blockAuthoredLengths.delete(key);
17195
17404
  this.#blockRepairRsIds.delete(key);
17196
17405
  this.#blockRsbLengths.delete(key);
@@ -17229,6 +17438,7 @@ var FECManager = class _FECManager {
17229
17438
  this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
17230
17439
  this.#blockSourceCounts.delete(key);
17231
17440
  this.#blockSourceEsis.delete(key);
17441
+ this.#blockRebuiltEsis.delete(key);
17232
17442
  this.#blockAuthoredLengths.delete(key);
17233
17443
  this.#blockRepairRsIds.delete(key);
17234
17444
  this.#blockRsbLengths.delete(key);
@@ -17320,6 +17530,7 @@ var FECManager = class _FECManager {
17320
17530
  this.#blockKeysBySbn.clear();
17321
17531
  this.#blockSourceCounts.clear();
17322
17532
  this.#blockSourceEsis.clear();
17533
+ this.#blockRebuiltEsis.clear();
17323
17534
  this.#blockAuthoredLengths.clear();
17324
17535
  this.#blockRepairRsIds.clear();
17325
17536
  this.#blockRsbLengths.clear();
@@ -17345,6 +17556,7 @@ var FECManager = class _FECManager {
17345
17556
  this.#blockAnchorsMs.clear();
17346
17557
  this.#blockCatalogTrackNames.clear();
17347
17558
  this.#classifiers.clear();
17559
+ this.#noSpanRaps.clear();
17348
17560
  this.#streamGeometries.clear();
17349
17561
  this.#failedBlocks.clear();
17350
17562
  this.#resolvedBlockClasses.clear();
@@ -17875,9 +18087,21 @@ var IsoBmffTimingResolver = class {
17875
18087
  }
17876
18088
  #track = null;
17877
18089
  #fragments = /* @__PURE__ */ new Map();
18090
+ /** First sample of each pending fragment, kept in `#delivered` with its sample count. */
18091
+ #firstSamples = /* @__PURE__ */ new Map();
18092
+ /**
18093
+ * First sample and sample count of the newest fragments whose every slot was
18094
+ * cleared. Abandonment counts: `releaseSample` clears a slot too, which
18095
+ * matches the combiner, since it retires an abandoned sample as well.
18096
+ */
18097
+ #delivered = /* @__PURE__ */ new Map();
17878
18098
  #pendingSamples = 0;
17879
18099
  #evictedSamples = 0;
17880
18100
  #lastMovieFragmentRejection = null;
18101
+ /** True once an FT=0 has bound the track that FT=1 timing is read against. */
18102
+ get initialized() {
18103
+ return this.#track !== null;
18104
+ }
17881
18105
  get pendingSampleCount() {
17882
18106
  return this.#pendingSamples;
17883
18107
  }
@@ -17901,7 +18125,14 @@ var IsoBmffTimingResolver = class {
17901
18125
  this.#track = timing;
17902
18126
  return true;
17903
18127
  }
17904
- addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples) {
18128
+ /**
18129
+ * `recovered` marks an FEC-recovered copy. A recovered block hands back
18130
+ * every FT=1 it holds, the received ones included, so a recovered copy of a
18131
+ * fragment whose samples are all done is a duplicate: admitting it again
18132
+ * would charge stale timing and could displace the live MPU's. A source
18133
+ * copy is still admitted again (a rendition switch re-arms timing that way).
18134
+ */
18135
+ addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
17905
18136
  this.#lastMovieFragmentRejection = null;
17906
18137
  if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
17907
18138
  this.#lastMovieFragmentRejection = "invalid";
@@ -17927,6 +18158,8 @@ var IsoBmffTimingResolver = class {
17927
18158
  (sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
17928
18159
  );
17929
18160
  }
18161
+ const delivered = recovered ? this.#delivered.get(key) : void 0;
18162
+ if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
17930
18163
  let freed = 0;
17931
18164
  while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
17932
18165
  freed += this.#evictOldestFragment();
@@ -17940,6 +18173,7 @@ var IsoBmffTimingResolver = class {
17940
18173
  return false;
17941
18174
  }
17942
18175
  this.#fragments.set(key, [...parsed.samples]);
18176
+ this.#firstSamples.set(key, parsed.samples[0]);
17943
18177
  this.#pendingSamples += parsed.samples.length;
17944
18178
  return true;
17945
18179
  }
@@ -17997,6 +18231,7 @@ var IsoBmffTimingResolver = class {
17997
18231
  let live = 0;
17998
18232
  for (const sample of samples) if (sample !== null) live++;
17999
18233
  this.#fragments.delete(oldest);
18234
+ this.#firstSamples.delete(oldest);
18000
18235
  this.#pendingSamples -= live;
18001
18236
  this.#evictedSamples += live;
18002
18237
  return live;
@@ -18012,12 +18247,23 @@ var IsoBmffTimingResolver = class {
18012
18247
  if (!samples || !samples[index]) return false;
18013
18248
  samples[index] = null;
18014
18249
  this.#pendingSamples--;
18015
- if (samples.every((value) => value === null)) this.#fragments.delete(key);
18250
+ if (samples.every((value) => value === null)) {
18251
+ this.#fragments.delete(key);
18252
+ const first = this.#firstSamples.get(key);
18253
+ this.#firstSamples.delete(key);
18254
+ this.#delivered.delete(key);
18255
+ this.#delivered.set(key, { first, sampleCount: samples.length });
18256
+ if (this.#delivered.size > this.maxPendingSamples) {
18257
+ this.#delivered.delete(this.#delivered.keys().next().value);
18258
+ }
18259
+ }
18016
18260
  return true;
18017
18261
  }
18018
18262
  reset() {
18019
18263
  this.#track = null;
18020
18264
  this.#fragments.clear();
18265
+ this.#firstSamples.clear();
18266
+ this.#delivered.clear();
18021
18267
  this.#pendingSamples = 0;
18022
18268
  }
18023
18269
  };
@@ -18499,6 +18745,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
18499
18745
  this.#reclaimedTimingSamples = 0;
18500
18746
  this.#evictedTimingSamples = 0;
18501
18747
  this.#rejectedMovieFragments = 0;
18748
+ this.#recoveredHeadsAwaitingTiming = {
18749
+ held: 0,
18750
+ expired: 0,
18751
+ discarded: 0,
18752
+ superseded: 0
18753
+ };
18502
18754
  this.#sequencers = /* @__PURE__ */ new Map();
18503
18755
  this.#fecGate = null;
18504
18756
  this.#fecSequencer = null;
@@ -18607,6 +18859,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18607
18859
  #reclaimedTimingSamples;
18608
18860
  #evictedTimingSamples;
18609
18861
  #rejectedMovieFragments;
18862
+ #recoveredHeadsAwaitingTiming;
18610
18863
  #sequencers;
18611
18864
  #fecGate;
18612
18865
  #fecSequencer;
@@ -18727,6 +18980,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
18727
18980
  rejectedMovieFragments: this.#rejectedMovieFragments
18728
18981
  };
18729
18982
  }
18983
+ /** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
18984
+ get recoveredHeadsAwaitingTiming() {
18985
+ let pending = 0;
18986
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
18987
+ for (const head of awaitingTiming) if (head.options?.recovered) pending++;
18988
+ }
18989
+ return { ...this.#recoveredHeadsAwaitingTiming, pending };
18990
+ }
18730
18991
  get reorderStats() {
18731
18992
  let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
18732
18993
  for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
@@ -18855,10 +19116,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18855
19116
  get reassemblerDataUnitsReturned() {
18856
19117
  return this.#reassemblerDataUnitsReturned;
18857
19118
  }
18858
- #countOutcome(token) {
18859
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18860
- this.#feedOutcomes.set(token, count);
18861
- this.#lastOutcome = token;
19119
+ #countOutcome(token2) {
19120
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
19121
+ this.#feedOutcomes.set(token2, count);
19122
+ this.#lastOutcome = token2;
18862
19123
  this.#lastOutcomeCount = count;
18863
19124
  }
18864
19125
  #countDeferredOutcome(reason) {
@@ -18900,7 +19161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
18900
19161
  const now = Date.now();
18901
19162
  for (const { awaitingTiming } of this.#timingStreams.values()) {
18902
19163
  while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
18903
- awaitingTiming.shift();
19164
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
18904
19165
  }
18905
19166
  }
18906
19167
  }
@@ -19018,11 +19279,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
19018
19279
  if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
19019
19280
  return this.#drop("fragment-unexpected-fragmentation");
19020
19281
  const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
19021
- if (!resolver2) return this.#drop("fragment-no-resolver");
19282
+ if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
19022
19283
  const pendingBefore = resolver2.pendingSampleCount;
19023
19284
  const evictedBefore = resolver2.evictedSampleCount;
19024
19285
  const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
19025
- const admitted = resolver2.addMovieFragment(mpu.mpuSequenceNumber, data, remaining);
19286
+ const admitted = resolver2.addMovieFragment(
19287
+ mpu.mpuSequenceNumber,
19288
+ data,
19289
+ remaining,
19290
+ options?.recovered === true
19291
+ );
19026
19292
  this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
19027
19293
  this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
19028
19294
  if (!admitted) {
@@ -19042,7 +19308,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19042
19308
  };
19043
19309
  }
19044
19310
  if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
19045
- const resolver = this.#timingResolver(streamKey, info.packetId, false);
19311
+ const resolver = this.#timingResolver(streamKey, info.packetId, true);
19046
19312
  if (!resolver) return this.#drop("mfu-no-resolver");
19047
19313
  if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
19048
19314
  return this.#drop("mfu-empty-payload");
@@ -19063,6 +19329,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19063
19329
  mpuSequenceNumber: mpu.mpuSequenceNumber,
19064
19330
  movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
19065
19331
  sampleNumber: mfuDuHeader.sampleNumber,
19332
+ sequenceNumber: info.sequenceNumber,
19066
19333
  arrivedAt: Date.now()
19067
19334
  });
19068
19335
  return this.#drop("mfu-sample-timing-missing");
@@ -19386,6 +19653,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19386
19653
  }
19387
19654
  this.#emitFrame(data, meta);
19388
19655
  }
19656
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19657
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19658
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19659
+ }
19389
19660
  /**
19390
19661
  * Claim and return the authored timing for a recovered sample. Recovery does
19391
19662
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -19447,6 +19718,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
19447
19718
  for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
19448
19719
  this.#sequencers.clear();
19449
19720
  this.#fecSequencer?.discard();
19721
+ for (const { awaitingTiming } of this.#timingStreams.values()) {
19722
+ for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19723
+ }
19450
19724
  this.#timingStreams.clear();
19451
19725
  this.#pendingTimingSamples = 0;
19452
19726
  this.#reliableMoqStartupGate.clear();
@@ -19465,10 +19739,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
19465
19739
  return resolver;
19466
19740
  }
19467
19741
  #awaitTiming(streamKey, head) {
19468
- const awaitingTiming = this.#timingStreams.get(streamKey)?.awaitingTiming;
19469
- if (!awaitingTiming) return;
19742
+ const state = this.#timingStreams.get(streamKey);
19743
+ if (!state) return;
19744
+ if (!head.options?.recovered) {
19745
+ const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
19746
+ this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
19747
+ state.awaitingTiming = kept;
19748
+ }
19749
+ const awaitingTiming = state.awaitingTiming;
19470
19750
  awaitingTiming.push(head);
19471
- if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) awaitingTiming.shift();
19751
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
19752
+ if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
19753
+ this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
19754
+ }
19755
+ }
19756
+ /** Count a recovered head leaving the queue other than by replay or its source copy. */
19757
+ #leaveAwaitingTiming(head, outcome) {
19758
+ if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
19472
19759
  }
19473
19760
  /**
19474
19761
  * Feed again the heads whose timing the FT=1 just admitted declares. That
@@ -19496,6 +19783,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
19496
19783
  const state = this.#timingStreams.get(streamKey);
19497
19784
  if (!state) return;
19498
19785
  this.#timingStreams.delete(streamKey);
19786
+ for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
19499
19787
  this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
19500
19788
  if (this.#pendingTimingSamples < 0) {
19501
19789
  throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
@@ -22702,6 +22990,16 @@ function keyframeOverlaySsId(header) {
22702
22990
  if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22703
22991
  return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22704
22992
  }
22993
+ function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
22994
+ if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
22995
+ return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
22996
+ }
22997
+ function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
22998
+ if (offset === void 0 || offset !== unstampedOffset) return void 0;
22999
+ return headerPsn + offset >>> 0;
23000
+ }
23001
+ var OVERLAY_ROUTED_LIMIT = 1024;
23002
+ var OVERLAY_PENDING_LIMIT = 256;
22705
23003
  var OVERLAY_OFFSET_MPUS = 16;
22706
23004
  var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22707
23005
  function assertStreamIdentity(role, stream) {
@@ -22869,6 +23167,15 @@ var MmtpRouter = class {
22869
23167
  * sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
22870
23168
  */
22871
23169
  #ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
23170
+ /**
23171
+ * Per stream, the SS_ID offset at which a base recovery last showed the
23172
+ * sender's own packet_sequence_number: a recovered packet's header psn
23173
+ * equal to its SS_ID minus that offset. While it equals the stream's
23174
+ * current offset, no hop re-stamps the stream, and a packet's protected psn
23175
+ * names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
23176
+ * an MPU no member of which arrived).
23177
+ */
23178
+ #unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
22872
23179
  /**
22873
23180
  * Streams whose multicast leg delivered a RAP-flagged source packet that was
22874
23181
  * not parsed, typically because its overlap copy was parsed first
@@ -22938,7 +23245,9 @@ var MmtpRouter = class {
22938
23245
  recovered: 0,
22939
23246
  restarts: 0,
22940
23247
  unsequencedDropped: 0,
22941
- protectedDropped: 0
23248
+ protectedDropped: 0,
23249
+ baseFed: 0,
23250
+ baseUndetermined: 0
22942
23251
  };
22943
23252
  /** Callback fired when a delta-track FEC block completes or fails. */
22944
23253
  #onDeltaBlockCompleteCb = null;
@@ -23111,7 +23420,7 @@ var MmtpRouter = class {
23111
23420
  const cb = this.#getFrameCallback(packetId);
23112
23421
  if (isVideo && !assembler) {
23113
23422
  if (frameMeta.recovered) {
23114
- this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
23423
+ this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
23115
23424
  } else {
23116
23425
  this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
23117
23426
  }
@@ -23501,6 +23810,10 @@ var MmtpRouter = class {
23501
23810
  get keyframeOverlayStats() {
23502
23811
  return { ...this.#overlayStats };
23503
23812
  }
23813
+ /** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
23814
+ get recoveredHeadsAwaitingTiming() {
23815
+ return this.#parser.recoveredHeadsAwaitingTiming;
23816
+ }
23504
23817
  #isOverlayRepair(repairPacketId, repairTrackName) {
23505
23818
  return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23506
23819
  }
@@ -23551,6 +23864,11 @@ var MmtpRouter = class {
23551
23864
  */
23552
23865
  resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23553
23866
  for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23867
+ for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
23868
+ if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
23869
+ stream.overlayToBase.routed.clear();
23870
+ stream.overlayToBase.pending.clear();
23871
+ }
23554
23872
  }
23555
23873
  #resetOverlay(inst) {
23556
23874
  inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
@@ -23607,9 +23925,10 @@ var MmtpRouter = class {
23607
23925
  * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23608
23926
  * every overlay installed for its source, at that SS_ID, with the bytes the
23609
23927
  * base protects: the packet minus its 4-byte SS_ID trailer, extension
23610
- * included (7.1, 7.4.3).
23928
+ * included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
23929
+ * FEC reads it.
23611
23930
  */
23612
- #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23931
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
23613
23932
  if (this.#overlays.size === 0) return;
23614
23933
  const ssId = keyframeOverlaySsId(header);
23615
23934
  if (ssId === void 0) return;
@@ -23618,7 +23937,11 @@ var MmtpRouter = class {
23618
23937
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23619
23938
  if (identity !== null) {
23620
23939
  inst.offsets.delete(identity.mpuSequenceNumber);
23621
- inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23940
+ inst.offsets.set(identity.mpuSequenceNumber, {
23941
+ psnOffset: ssId - sequenceNumber >>> 0,
23942
+ overlaySsId: ssId,
23943
+ ...baseSsId === void 0 ? {} : { baseSsId }
23944
+ });
23622
23945
  if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23623
23946
  }
23624
23947
  const { fec, repairPacketId: pid, streamName: name } = inst;
@@ -23697,14 +24020,27 @@ var MmtpRouter = class {
23697
24020
  const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23698
24021
  const restamped = this.#restampedStreams.has(streamKey);
23699
24022
  const restored = /* @__PURE__ */ new Set();
24023
+ const baseSsIds = /* @__PURE__ */ new Map();
23700
24024
  const symbols = result.symbols.flatMap((sym) => {
23701
- const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23702
- if (offset !== void 0) {
23703
- const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
24025
+ const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
24026
+ if (anchor !== void 0) {
24027
+ const overlaySsId = serial32Add(blockStart, sym.index);
24028
+ const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
23704
24029
  restored.add(packetSequenceNumber);
23705
- return [{ ...sym, packetSequenceNumber }];
24030
+ const out = { ...sym, packetSequenceNumber };
24031
+ const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
24032
+ if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
24033
+ return [out];
24034
+ }
24035
+ 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);
24042
+ return [sym];
23706
24043
  }
23707
- if (!restamped) return [sym];
23708
24044
  this.#overlayStats.unsequencedDropped++;
23709
24045
  return [];
23710
24046
  });
@@ -23713,6 +24049,85 @@ var MmtpRouter = class {
23713
24049
  deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23714
24050
  });
23715
24051
  this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
24052
+ this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
24053
+ }
24054
+ /**
24055
+ * Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
24056
+ * its base block too, once its base SS_ID is determined (overlayBaseSsId,
24057
+ * from a received member of its MPU, else unstampedBaseSsId),
24058
+ * so it is fed to the base decoder as a rebuilt source
24059
+ * (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
24060
+ * fewer to solve, and its repair can recover the block's other packets.
24061
+ * A base block no repair has stated yet takes it when one does
24062
+ * (#feedPendingRebuilt).
24063
+ *
24064
+ * The overlay route already handled each packet fed (delivered it, or
24065
+ * held an untimed head for its FT=1), so a base recovery routes none of
24066
+ * them again (#routeVerifiedFecSymbols): a second recovered copy of a
24067
+ * held head would wait in the parser for timing its first copy consumed.
24068
+ * Only an overlay block decoded from multicast symbols alone is fed: with
24069
+ * a MoQ symbol in it, the base block's recovery would depend on bytes
24070
+ * that crossed no multicast socket, which the provenance counted from the
24071
+ * block's own arrivals cannot show.
24072
+ */
24073
+ #feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
24074
+ const fec = this.#fecManager;
24075
+ const owner = inst.leg.sourcePacketId;
24076
+ const name = inst.leg.sourceCatalogTrackName;
24077
+ if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
24078
+ if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
24079
+ const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
24080
+ const feeds = [];
24081
+ for (const sym of symbols) {
24082
+ if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
24083
+ const ssId = baseSsIds.get(sym);
24084
+ if (ssId === void 0) {
24085
+ this.#overlayStats.baseUndetermined++;
24086
+ continue;
24087
+ }
24088
+ routed.delete(ssId);
24089
+ routed.add(ssId);
24090
+ if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
24091
+ feeds.push([ssId, sym.rawSymbol]);
24092
+ }
24093
+ for (const [ssId, symbolData] of feeds) {
24094
+ if (this.#classifyPacketId(owner) === null) return;
24095
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24096
+ }
24097
+ }
24098
+ /** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
24099
+ #feedRebuiltToBase(owner, name, ssId, symbolData) {
24100
+ const fec = this.#fecManager;
24101
+ if (!fec?.feedRebuiltSourceSymbol) return;
24102
+ const coords = fec.locateIsoSource?.(owner, name, ssId);
24103
+ if (coords == null) {
24104
+ const pending = this.#stream(owner, name).overlayToBase.pending;
24105
+ pending.delete(ssId);
24106
+ pending.set(ssId, symbolData.slice());
24107
+ if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
24108
+ return;
24109
+ }
24110
+ const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
24111
+ if (!fed.taken) return;
24112
+ this.#overlayStats.baseFed++;
24113
+ if (fed.recovered === null) return;
24114
+ const sbn = coords.sbn;
24115
+ this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
24116
+ fec.cancelBlockTimeout(sbn, owner, name);
24117
+ fec.countRecovery(owner, sbn, name);
24118
+ fec.markBlockCompleted(sbn, owner, name);
24119
+ });
24120
+ }
24121
+ /** A repair stated `block`: the rebuilt sources that waited for it join it now. */
24122
+ #feedPendingRebuilt(owner, name, block) {
24123
+ const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
24124
+ if (pending === void 0 || pending.size === 0) return;
24125
+ for (const [ssId, symbolData] of [...pending]) {
24126
+ if (this.#classifyPacketId(owner) === null) return;
24127
+ if (serial32Distance(block.start, ssId) >= block.length) continue;
24128
+ pending.delete(ssId);
24129
+ this.#feedRebuiltToBase(owner, name, ssId, symbolData);
24130
+ }
23716
24131
  }
23717
24132
  /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23718
24133
  * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
@@ -24338,6 +24753,8 @@ var MmtpRouter = class {
24338
24753
  )) return;
24339
24754
  fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
24340
24755
  fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
24756
+ this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
24757
+ if (this.#classifyPacketId(ownerPacketId) === null) return;
24341
24758
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
24342
24759
  const recovered = fec.feedRepairSymbol(
24343
24760
  ownerPacketId,
@@ -24475,6 +24892,7 @@ var MmtpRouter = class {
24475
24892
  this.#droppedPreSyncVideo.delete(streamKey);
24476
24893
  this.#latestVideoSeqByStream.delete(streamKey);
24477
24894
  this.#ssIdSeqOffsetByStream.delete(streamKey);
24895
+ this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
24478
24896
  this.#multicastRapTwinByStream.delete(streamKey);
24479
24897
  this.#lastDirectSourceVideoTsByStream.delete(streamKey);
24480
24898
  this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
@@ -25390,7 +25808,8 @@ var MmtpRouter = class {
25390
25808
  parsed.header,
25391
25809
  info.sequenceNumber,
25392
25810
  info.rapFlag,
25393
- source
25811
+ source,
25812
+ fecPayloadId?.ssId
25394
25813
  );
25395
25814
  }
25396
25815
  }
@@ -25540,7 +25959,8 @@ var MmtpRouter = class {
25540
25959
  if (stream === void 0) {
25541
25960
  stream = {
25542
25961
  key: JSON.stringify([packetId, catalogTrackName ?? null]),
25543
- duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
25962
+ duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
25963
+ overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
25544
25964
  };
25545
25965
  byName.set(catalogTrackName, stream);
25546
25966
  }
@@ -25655,8 +26075,16 @@ var MmtpRouter = class {
25655
26075
  this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
25656
26076
  this.#emitDirectVideo(packetId, data, meta, cb);
25657
26077
  }
25658
- #routeDirectRecoveredVideo(packetId, data, meta, cb) {
26078
+ #routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
25659
26079
  const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
26080
+ if (this.#isFecTrack(packetId)) {
26081
+ const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
26082
+ if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
26083
+ this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
26084
+ this.#emitDirectVideo(packetId, data, meta, cb);
26085
+ return;
26086
+ }
26087
+ }
25660
26088
  this.#enqueueDirectRecoveredVideo(packetId, data, meta);
25661
26089
  const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
25662
26090
  this.#drainDirectRecoveredVideo(
@@ -25810,6 +26238,13 @@ var MmtpRouter = class {
25810
26238
  * frames by epoch only if pre-protection input ever matters.
25811
26239
  */
25812
26240
  #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
26241
+ const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
26242
+ if (overlayRouted !== void 0 && overlayRouted.size > 0) {
26243
+ result = {
26244
+ ...result,
26245
+ symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
26246
+ };
26247
+ }
25813
26248
  const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25814
26249
  const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25815
26250
  const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
@@ -25874,7 +26309,10 @@ var MmtpRouter = class {
25874
26309
  ...result,
25875
26310
  symbols: result.symbols.map((sym) => {
25876
26311
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25877
- if (wire === sym.packetSequenceNumber) return sym;
26312
+ if (wire === sym.packetSequenceNumber) {
26313
+ this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
26314
+ return sym;
26315
+ }
25878
26316
  this.#restampedRecoveredSeqs++;
25879
26317
  this.#restampedStreams.add(streamKey);
25880
26318
  return { ...sym, packetSequenceNumber: wire };
@@ -25910,6 +26348,29 @@ var MmtpRouter = class {
25910
26348
  }
25911
26349
  );
25912
26350
  }
26351
+ const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
26352
+ ownerPacketId,
26353
+ sym.fragment.mpuSequenceNumber,
26354
+ sym.fragment.movieFragmentSequenceNumber,
26355
+ sym.fragment.sampleNumber,
26356
+ catalogTrackName
26357
+ );
26358
+ for (const sym of result.symbols) {
26359
+ if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
26360
+ this.#feedViaParser(
26361
+ sym.rawSymbol,
26362
+ { packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
26363
+ void 0,
26364
+ {
26365
+ skipFecPayloadId: true,
26366
+ recovered: true,
26367
+ deliveryProvenance,
26368
+ ...fecSbnOf(sym.index),
26369
+ ...catalogTrackName === void 0 ? {} : { catalogTrackName }
26370
+ }
26371
+ );
26372
+ }
26373
+ result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
25913
26374
  const gated = this.#isFecTrack(ownerPacketId);
25914
26375
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25915
26376
  if (assembler) {
@@ -29671,7 +30132,7 @@ async function setupFecWorkerForTrack(opts) {
29671
30132
  blockProfile,
29672
30133
  k: opts.fecK,
29673
30134
  // P as this receiver joined it, not the catalog total: the worker snapshot
29674
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
30135
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29675
30136
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29676
30137
  symbolSize: opts.fecSymbolSize,
29677
30138
  interleaveDepth: interleaveDepthBlocks,
@@ -30213,24 +30674,24 @@ var MediaRouter = class {
30213
30674
  if (!this.#multicastVideoKeyed) return;
30214
30675
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30215
30676
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30216
- const token = ++this.#sourceSwitchToken;
30677
+ const token2 = ++this.#sourceSwitchToken;
30217
30678
  const maybeReady = this.#onSourceSwitch?.("multicast");
30218
30679
  if (!maybeReady || typeof maybeReady.then !== "function") {
30219
- this.#commitMulticastActivation(token);
30680
+ this.#commitMulticastActivation(token2);
30220
30681
  return;
30221
30682
  }
30222
30683
  const ready = Promise.resolve(maybeReady);
30223
- this.#pendingMulticastToken = token;
30684
+ this.#pendingMulticastToken = token2;
30224
30685
  this.#sourceSwitchReady = ready;
30225
30686
  void ready.then(
30226
- () => this.#commitMulticastActivation(token),
30227
- (err) => this.#rejectMulticastActivation(token, err)
30687
+ () => this.#commitMulticastActivation(token2),
30688
+ (err) => this.#rejectMulticastActivation(token2, err)
30228
30689
  ).finally(() => {
30229
30690
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30230
30691
  });
30231
30692
  }
30232
- #commitMulticastActivation(token) {
30233
- if (token !== this.#sourceSwitchToken) return;
30693
+ #commitMulticastActivation(token2) {
30694
+ if (token2 !== this.#sourceSwitchToken) return;
30234
30695
  this.#active = "multicast";
30235
30696
  this.#suppressed = true;
30236
30697
  this.#pendingMulticastToken = null;
@@ -30241,9 +30702,9 @@ var MediaRouter = class {
30241
30702
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30242
30703
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30243
30704
  }
30244
- #rejectMulticastActivation(token, err) {
30245
- if (token !== this.#sourceSwitchToken) return;
30246
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30705
+ #rejectMulticastActivation(token2, err) {
30706
+ if (token2 !== this.#sourceSwitchToken) return;
30707
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30247
30708
  this.#pendingMulticastVideo = null;
30248
30709
  this.#pendingMulticastAudio = null;
30249
30710
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31212,63 +31673,65 @@ var FecStarvationDetector = class {
31212
31673
 
31213
31674
  // src/cmcd-producer.ts
31214
31675
  var PLAY_MODES = ["full", "base-only", "single-track"];
31215
- function sanitize(value) {
31216
- return value.replace(/[,\r\n]/g, "");
31217
- }
31218
- function buildFecParams(stats) {
31219
- const params = {};
31220
- const cfg = stats.config;
31221
- params["x-bc-fec-algo"] = cfg.algorithm;
31222
- params["x-bc-fec-k"] = cfg.k;
31223
- params["x-bc-fec-p"] = cfg.repairCount;
31224
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
31225
- return params;
31226
- }
31227
- function buildTransportParams(snap) {
31228
- if (!snap) return {};
31229
- const params = {};
31230
- params["x-bc-transport"] = snap.transport;
31231
- if (snap.multicastGroup != null) {
31232
- params["x-bc-multicast-group"] = snap.multicastGroup;
31233
- }
31234
- if (snap.multicastSource != null) {
31235
- params["x-bc-multicast-source"] = snap.multicastSource;
31236
- }
31237
- if (snap.amtRelay != null) {
31238
- params["x-bc-amt-relay"] = snap.amtRelay;
31239
- }
31240
- return params;
31241
- }
31242
- function formatQueryString(params) {
31243
- return Object.keys(params).sort().map((key) => {
31244
- const v = params[key];
31245
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
31246
- return `${key}=${v}`;
31676
+ var DEFAULT_INTERVAL_MS = 3e4;
31677
+ var CUSTOM_PREFIX = "com.blockcast-";
31678
+ var MAX_CUSTOM_VALUE = 64;
31679
+ var TRANSPORT_TOKEN = {
31680
+ native: "ssm",
31681
+ amt: "amt",
31682
+ moq: "moq"
31683
+ };
31684
+ function sfString(value) {
31685
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31686
+ }
31687
+ function encodeCmcdRecord(record) {
31688
+ return Object.keys(record).sort().map((key) => {
31689
+ const value = record[key];
31690
+ if (typeof value === "number") return `${key}=${value}`;
31691
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31692
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31693
+ return `${key}=${value.token}`;
31247
31694
  }).join(",");
31248
31695
  }
31696
+ var token = (value) => ({ token: value });
31697
+ function putCustom(record, name, value) {
31698
+ if (value === void 0) return;
31699
+ if (typeof value === "number") value = String(value);
31700
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31701
+ record[CUSTOM_PREFIX + name] = value;
31702
+ }
31249
31703
  var CmcdProducer = class {
31250
- #onCmcdParams;
31251
31704
  #onCmcdPost;
31705
+ #intervalMs;
31706
+ #sessionId;
31707
+ #now;
31708
+ #sn = 0;
31709
+ #lastHeartbeat = null;
31252
31710
  #transport;
31253
31711
  #lastStats;
31254
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31255
31712
  #playMode = null;
31256
31713
  #deltaFailRate = null;
31257
31714
  #switchMiscount = 0;
31258
31715
  #manifestStats = null;
31259
31716
  #fecFailClosed = null;
31717
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31718
+ #lifecycle = null;
31260
31719
  constructor(options) {
31261
- this.#onCmcdParams = options.onCmcdParams;
31262
31720
  this.#onCmcdPost = options.onCmcdPost;
31721
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31722
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31723
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31724
+ }
31725
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31726
+ this.#now = options.now ?? Date.now;
31263
31727
  }
31264
31728
  /** Update the stored transport state. Called by player wiring layer. */
31265
31729
  updateTransport(snapshot) {
31266
31730
  this.#transport = snapshot;
31267
31731
  }
31268
31732
  /**
31269
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
31270
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
31271
- * (CLAUDE.md: error on bad init).
31733
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31734
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31272
31735
  */
31273
31736
  updatePlayMode(mode) {
31274
31737
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31279,9 +31742,8 @@ var CmcdProducer = class {
31279
31742
  this.#playMode = mode;
31280
31743
  }
31281
31744
  /**
31282
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
31283
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
31284
- * Omitted on single-track streams.
31745
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31746
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31285
31747
  */
31286
31748
  updateDeltaFailRate(rate) {
31287
31749
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31291,112 +31753,139 @@ var CmcdProducer = class {
31291
31753
  }
31292
31754
  this.#deltaFailRate = rate;
31293
31755
  }
31294
- /**
31295
- * Bump the cumulative switching-set misalignment counter. Emitted as
31296
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31297
- */
31756
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31298
31757
  incrementSwitchMiscount() {
31299
31758
  this.#switchMiscount += 1;
31300
31759
  }
31301
31760
  /**
31302
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
31303
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
31304
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
31305
- * stream playing without FEC is told apart from one with no loss.
31761
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31762
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31763
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31764
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31765
+ * with no loss.
31306
31766
  */
31307
31767
  updateFecFailClosed(reason) {
31768
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31308
31769
  this.#fecFailClosed = reason;
31770
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31309
31771
  }
31310
31772
  updateManifestVerification(stats) {
31311
31773
  this.#manifestStats = stats;
31312
31774
  }
31313
31775
  /**
31314
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
31315
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
31316
- * include the drift key until the next emitSwitchDrift(). Mirrors the
31317
- * existing onTransition() pattern.
31776
+ * Count a video block's resolution from the repair lifecycle: recovered in
31777
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31778
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31779
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31318
31780
  */
31781
+ onRepairLifecycle(event) {
31782
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31783
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31784
+ if (event.via === "inband") this.#lifecycle.inband++;
31785
+ else this.#lifecycle.fetch++;
31786
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31787
+ this.#lifecycle.failedKeyframe++;
31788
+ }
31789
+ }
31790
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31319
31791
  emitSwitchDrift(driftUs) {
31320
31792
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31321
31793
  throw new Error(
31322
31794
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31323
31795
  );
31324
31796
  }
31325
- const params = {};
31326
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
31327
- Object.assign(params, buildTransportParams(this.#transport));
31328
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31329
- params["x-bc-switch-drift-us"] = driftUs;
31330
- this.#emit(params);
31797
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31798
+ putCustom(record, "sdu", Math.round(driftUs));
31799
+ this.#post([record]);
31331
31800
  }
31332
- /**
31333
- * Process a FecTrackStats snapshot. Finds the video track, builds
31334
- * x-bc-* vendor keys, and fires both delivery callbacks.
31335
- */
31801
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31336
31802
  onSnapshot(data) {
31337
31803
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31338
31804
  if (!videoTrack) return;
31339
31805
  this.#lastStats = videoTrack;
31340
- const params = {
31341
- ...buildFecParams(videoTrack),
31342
- ...buildTransportParams(this.#transport)
31343
- };
31344
- if (this.#playMode !== null) {
31345
- params["x-bc-play-mode"] = this.#playMode;
31346
- }
31347
- if (this.#fecFailClosed) {
31348
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31349
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31350
- }
31351
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31352
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31353
- }
31354
- if (this.#switchMiscount > 0) {
31355
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31356
- }
31357
- if (this.#manifestStats) {
31358
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31359
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31360
- }
31361
- this.#emit(params);
31806
+ this.#heartbeat();
31362
31807
  }
31363
31808
  /**
31364
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31365
- * the caller because only the transport integration knows when state changed.
31809
+ * Take transport state for receivers that do not own FEC stats, and send the
31810
+ * heartbeat when due.
31366
31811
  */
31367
31812
  onTransportSnapshot(data) {
31368
31813
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31369
31814
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31370
31815
  }
31371
31816
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31372
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31373
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31817
+ if (data.switchMiscount !== void 0) {
31818
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31819
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31820
+ }
31821
+ this.#switchMiscount = data.switchMiscount;
31374
31822
  }
31375
31823
  this.#transport = data.transport;
31376
- const params = buildTransportParams(data.transport);
31377
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31378
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31379
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31380
- this.#emit(params);
31824
+ this.#heartbeat();
31381
31825
  }
31382
- /**
31383
- * Signal a transport transition event. Fires an immediate out-of-band
31384
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31385
- * x-bc-ct (one-shot).
31386
- */
31826
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31387
31827
  onTransition(event) {
31388
- const params = {};
31389
- if (this.#lastStats) {
31390
- Object.assign(params, buildFecParams(this.#lastStats));
31828
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31829
+ putCustom(record, "ct", token(event));
31830
+ this.#putTransport(record);
31831
+ this.#post([record]);
31832
+ }
31833
+ #base(event) {
31834
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31835
+ }
31836
+ #heartbeat() {
31837
+ const now = this.#now();
31838
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31839
+ this.#lastHeartbeat = now;
31840
+ this.#post([this.#base("t"), this.#counters()]);
31841
+ }
31842
+ #putTransport(record) {
31843
+ const snap = this.#transport;
31844
+ if (!snap) return;
31845
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31846
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31847
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31848
+ }
31849
+ putCustom(record, "ar", snap.amtRelay);
31850
+ }
31851
+ /** The `e=ce` counter report. */
31852
+ #counters() {
31853
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31854
+ const stats = this.#lastStats;
31855
+ if (stats) {
31856
+ putCustom(record, "fa", stats.config.algorithm);
31857
+ putCustom(record, "fk", stats.config.k);
31858
+ putCustom(record, "fp", stats.config.repairCount);
31859
+ putCustom(record, "pr", stats.sourceSymbols);
31860
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31861
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31862
+ putCustom(record, "fbf", stats.blocksFailed);
31863
+ }
31864
+ if (this.#lifecycle) {
31865
+ putCustom(record, "fbr", this.#lifecycle.inband);
31866
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31867
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31868
+ }
31869
+ this.#putTransport(record);
31870
+ if (this.#playMode !== null) {
31871
+ putCustom(record, "pm", token(this.#playMode));
31872
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31873
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31874
+ }
31391
31875
  }
31392
- Object.assign(params, buildTransportParams(this.#transport));
31393
- params["x-bc-ct"] = event;
31394
- this.#emit(params);
31876
+ if (this.#fecFailClosed) {
31877
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31878
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31879
+ }
31880
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31881
+ if (this.#manifestStats) {
31882
+ putCustom(record, "mv", this.#manifestStats.verified);
31883
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31884
+ }
31885
+ return record;
31395
31886
  }
31396
- /** Fire both delivery callbacks with the given params. */
31397
- #emit(params) {
31398
- this.#onCmcdParams?.(params);
31399
- this.#onCmcdPost?.(formatQueryString(params));
31887
+ #post(records) {
31888
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31400
31889
  }
31401
31890
  };
31402
31891
 
@@ -32891,6 +33380,7 @@ export {
32891
33380
  detectH264Keyframe,
32892
33381
  detectHevcKeyframe,
32893
33382
  driftCheck,
33383
+ encodeCmcdRecord,
32894
33384
  encodeDataSegment,
32895
33385
  encodeFecSourceInfo,
32896
33386
  encodeLocTimestamp,