@blockcast/mmt-container 0.1.0-main.f2d8d03d6809 → 0.1.0-main.f8328f38cfb6

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 (111) hide show
  1. package/dist/browser-runtime.js +231 -48
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/catalog-fec-params.d.ts +15 -0
  4. package/dist/catalog-fec-params.d.ts.map +1 -1
  5. package/dist/catalog-fec-params.js +11 -0
  6. package/dist/catalog-fec-params.js.map +2 -2
  7. package/dist/cmaf-block-assembler.js.map +1 -1
  8. package/dist/cmaf-group-batcher.js.map +1 -1
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js.map +2 -2
  11. package/dist/fec-manager.d.ts +1 -5
  12. package/dist/fec-manager.d.ts.map +1 -1
  13. package/dist/fec-manager.js +7 -19
  14. package/dist/fec-manager.js.map +2 -2
  15. package/dist/fec-worker-setup.d.ts +8 -0
  16. package/dist/fec-worker-setup.d.ts.map +1 -1
  17. package/dist/fec-worker-setup.js +5 -3
  18. package/dist/fec-worker-setup.js.map +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +2 -2
  23. package/dist/manifest-auth-plan.js +29 -11
  24. package/dist/manifest-auth-plan.js.map +2 -2
  25. package/dist/media-router.js.map +2 -2
  26. package/dist/mfu-reassembler.js +56 -0
  27. package/dist/mfu-reassembler.js.map +2 -2
  28. package/dist/mmtp-container-parser.js +56 -0
  29. package/dist/mmtp-container-parser.js.map +2 -2
  30. package/dist/mmtp-container-parser.no-modules.js +100 -15
  31. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  32. package/dist/mmtp-decode-pipeline.js +56 -0
  33. package/dist/mmtp-decode-pipeline.js.map +2 -2
  34. package/dist/mmtp-router.d.ts.map +1 -1
  35. package/dist/mmtp-router.js +72 -9
  36. package/dist/mmtp-router.js.map +2 -2
  37. package/dist/mmtp-to-cmaf.d.ts +11 -2
  38. package/dist/mmtp-to-cmaf.d.ts.map +1 -1
  39. package/dist/mmtp-to-cmaf.js +15 -4
  40. package/dist/mmtp-to-cmaf.js.map +2 -2
  41. package/dist/multicast-manager.d.ts +12 -0
  42. package/dist/multicast-manager.d.ts.map +1 -1
  43. package/dist/multicast-manager.js +117 -39
  44. package/dist/multicast-manager.js.map +2 -2
  45. package/dist/multicast-transport.js +9 -5
  46. package/dist/multicast-transport.js.map +2 -2
  47. package/dist/multicast-unwrap.js.map +2 -2
  48. package/dist/no-modules.js +100 -15
  49. package/dist/no-modules.js.map +2 -2
  50. package/dist/signaling.js.map +2 -2
  51. package/dist/source-auth.js +29 -11
  52. package/dist/source-auth.js.map +2 -2
  53. package/dist/timestamp-codec.js.map +1 -1
  54. package/dist/track-session.d.ts +74 -1
  55. package/dist/track-session.d.ts.map +1 -1
  56. package/dist/track-session.js +83 -2
  57. package/dist/track-session.js.map +2 -2
  58. package/dist/wasm-mfu-reassembler.js +56 -0
  59. package/dist/wasm-mfu-reassembler.js.map +2 -2
  60. package/dist/wasm-mmtp-container-parser.js +56 -0
  61. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  62. package/node_modules/@blockcast/fec-worker/README.md +1 -1
  63. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +2 -2
  64. package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +2 -2
  65. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +4 -4
  66. package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -1
  67. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +3 -3
  68. package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +2 -2
  69. package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +2 -2
  70. package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +4 -4
  71. package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +6 -6
  72. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +6 -5
  73. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  74. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +5 -4
  75. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  76. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +6 -5
  77. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +3 -1
  78. package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -1
  79. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +3 -1
  80. package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -1
  81. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +2 -2
  82. package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +2 -2
  83. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +5 -7
  84. package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -1
  85. package/node_modules/@blockcast/mmt-fec/dist/oti.js +4 -6
  86. package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -1
  87. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +6 -5
  88. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -1
  89. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +5 -4
  90. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -1
  91. package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +6 -5
  92. package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +3 -1
  93. package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +2 -2
  94. package/node_modules/@blockcast/mmt-fec/src/oti.ts +5 -7
  95. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +10 -1
  96. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -1
  97. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +40 -10
  98. package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -1
  99. package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +41 -8
  100. package/package.json +6 -6
  101. package/src/catalog-fec-params.ts +28 -0
  102. package/src/cmcd-producer.ts +5 -0
  103. package/src/fec-manager.ts +17 -25
  104. package/src/fec-worker-setup.ts +11 -1
  105. package/src/index.ts +2 -0
  106. package/src/mfu-reassembler.ts +95 -0
  107. package/src/mmtp-router.ts +18 -20
  108. package/src/mmtp-to-cmaf.ts +24 -2
  109. package/src/multicast-manager.ts +74 -10
  110. package/src/track-session.ts +120 -2
  111. package/test-vectors/source-contiguity-v1.json +131 -0
@@ -143,8 +143,8 @@ var init_fec_worker_client = __esm({
143
143
  * Caller must not access data after this call — it is detached.
144
144
  *
145
145
  * Carries the wire-level Repair FEC Payload ID fields (ISO 23008-1 §C.5.3):
146
- * - ssStart: SS_ID of first source symbol in the block (= SBN * SSB_length)
147
- * - ssbLength: K for this block
146
+ * - ssStart: SS_ID of first source symbol in the block (= SBN * configured K)
147
+ * - ssbLength: authored source-symbol count (1..configured K)
148
148
  * - rsId: repair symbol index within the block
149
149
  */
150
150
  feedRepair(ssStart, ssbLength, rsId, data, ts) {
@@ -1936,6 +1936,62 @@ if (void 0) {
1936
1936
  expect(out?.data).toEqual(new Uint8Array([161, 162]));
1937
1937
  expect(out?.sampleOffset).toBe(4);
1938
1938
  });
1939
+ test("a mid-data-unit packet sequence gap strands the data unit", () => {
1940
+ const reassembler = new MFUReassembler({ timeoutMs: null, maxBuffered: 10 });
1941
+ expect(
1942
+ reassembler.addFragment({
1943
+ fragmentationIndicator: 1,
1944
+ mpuSequenceNumber: 7,
1945
+ fragmentCounter: 2,
1946
+ movieFragmentSequenceNumber: 9,
1947
+ sampleNumber: 17,
1948
+ sampleOffset: 0,
1949
+ packetSequenceNumber: 100,
1950
+ data: new Uint8Array([161]),
1951
+ timestamp: 1100n,
1952
+ rapFlag: true
1953
+ })
1954
+ ).toBeNull();
1955
+ const interleaved = reassembler.addFragment({
1956
+ fragmentationIndicator: 0,
1957
+ mpuSequenceNumber: 7,
1958
+ fragmentCounter: 0,
1959
+ movieFragmentSequenceNumber: 9,
1960
+ sampleNumber: 18,
1961
+ sampleOffset: 0,
1962
+ packetSequenceNumber: 101,
1963
+ data: new Uint8Array([255]),
1964
+ timestamp: 1101n,
1965
+ rapFlag: true
1966
+ });
1967
+ expect(interleaved?.data).toEqual(new Uint8Array([255]));
1968
+ expect(
1969
+ reassembler.addFragment({
1970
+ fragmentationIndicator: 2,
1971
+ mpuSequenceNumber: 7,
1972
+ fragmentCounter: 1,
1973
+ packetSequenceNumber: 102,
1974
+ data: new Uint8Array([162]),
1975
+ timestamp: 1102n,
1976
+ rapFlag: false
1977
+ })
1978
+ ).toBeNull();
1979
+ const output = reassembler.addFragment({
1980
+ fragmentationIndicator: 3,
1981
+ mpuSequenceNumber: 7,
1982
+ fragmentCounter: 0,
1983
+ packetSequenceNumber: 103,
1984
+ data: new Uint8Array([163]),
1985
+ timestamp: 1103n,
1986
+ rapFlag: false
1987
+ });
1988
+ expect(output).toBeNull();
1989
+ expect(reassembler.stats.reassembledMFUs).toBe(1);
1990
+ expect(reassembler.stats.rejectedFragments).toBe(0);
1991
+ expect(reassembler.stats.evictedMemoryLimit).toBe(0);
1992
+ expect(reassembler.getStatus().bufferedMFUs).toBe(2);
1993
+ expect(reassembler.getStatus().pendingFragments).toBe(3);
1994
+ });
1939
1995
  test("sibling subsample DUs of one sample reassemble independently by offset", () => {
1940
1996
  const reassembler = new MFUReassembler({ timeoutMs: 1e3 });
1941
1997
  expect(
@@ -7719,16 +7775,26 @@ var MmtpToCmaf = class {
7719
7775
  return __privateGet(this, _audioInitSegment);
7720
7776
  }
7721
7777
  /** Convert a raw video sample to an fMP4 moof+mdat segment. */
7722
- wrapVideoSample(data, timestamp, keyframe, duration) {
7723
- return this.wrapVideoSampleWithTiming(data, timestamp, keyframe, duration).segment;
7778
+ wrapVideoSample(data, timestamp, keyframe, duration, compositionTimeOffset) {
7779
+ return this.wrapVideoSampleWithTiming(data, timestamp, keyframe, duration, compositionTimeOffset).segment;
7724
7780
  }
7725
7781
  /**
7726
7782
  * Convert a raw video sample and return the exact browser media interval
7727
7783
  * encoded into tfdt/trun. Consumers that correlate SourceBuffer appends with
7728
7784
  * requestVideoFrameCallback must use this rebased timestamp, not the wire or
7729
7785
  * caller-owned timestamp passed above.
7786
+ *
7787
+ * `compositionTimeOffset` is the signed authored PTS-DTS delta, in the same
7788
+ * #videoTimescale units as `timestamp` and `duration` — unlike wrapAudioSample,
7789
+ * which takes microseconds and rescales, because every other quantity on this
7790
+ * path is already denominated in #videoTimescale (see setVideoTrackParams).
7791
+ * Passing it therefore needs no conversion here; rescaling would double-convert.
7792
+ * Absent leaves the trun at version 0 with no offset field; a declared 0 is an
7793
+ * authored value and still promotes to version 1. Never reconstruct it from
7794
+ * arrival time.
7730
7795
  */
7731
- wrapVideoSampleWithTiming(data, timestamp, keyframe, duration) {
7796
+ wrapVideoSampleWithTiming(data, timestamp, keyframe, duration, compositionTimeOffset) {
7797
+ assertCompositionTimeOffsets([{ compositionTimeOffset }]);
7732
7798
  if (duration !== void 0 && Number.isFinite(duration) && duration > 0) {
7733
7799
  __privateSet(this, _frameDuration, Math.round(duration));
7734
7800
  } else if (__privateGet(this, _lastVideoTimestamp) > 0 && timestamp > __privateGet(this, _lastVideoTimestamp)) {
@@ -7746,7 +7812,8 @@ var MmtpToCmaf = class {
7746
7812
  frameDuration,
7747
7813
  keyframe,
7748
7814
  __privateWrapper(this, _videoSeq)._++,
7749
- __privateGet(this, _videoTrackId)
7815
+ __privateGet(this, _videoTrackId),
7816
+ compositionTimeOffset
7750
7817
  ),
7751
7818
  mediaTimestamp: relTs,
7752
7819
  duration: frameDuration
@@ -9255,6 +9322,24 @@ var TrackSession = class {
9255
9322
  get isFecArmed() {
9256
9323
  return __privateGet(this, _fecArmed);
9257
9324
  }
9325
+ /**
9326
+ * Start the deadline a `joining` entry deferred while the join was inactive
9327
+ * (SessionManager.joinActive). The track gets the FULL deadline from here,
9328
+ * because that is when media can first arrive. No-op outside `joining` and
9329
+ * when a deadline is already running, so it never shortens or restarts one.
9330
+ *
9331
+ * The `joinActive` guard is what makes this safe to call from anywhere: a
9332
+ * track parked in `joining` with a deferred (null) timer satisfies the other
9333
+ * two, so without it a host calling this while the join is still inactive
9334
+ * would arm the exact deadline the gate exists to defer -- silently
9335
+ * reinstating BLO-34289. It is a no-op for the internal caller, which writes
9336
+ * `#joinActive = true` before iterating the sessions.
9337
+ */
9338
+ armDeferredJoinDeadline() {
9339
+ if (__privateGet(this, _state) !== "joining" || __privateGet(this, _joinDeadlineTimer) !== null) return;
9340
+ if (!__privateGet(this, _manager).joinActive) return;
9341
+ __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, __privateGet(this, _manager).takeoverDeadlineMs);
9342
+ }
9258
9343
  dispose() {
9259
9344
  __privateMethod(this, _TrackSession_instances, clearHysteresis_fn).call(this);
9260
9345
  __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
@@ -9281,7 +9366,9 @@ transition_fn = function(to, opts) {
9281
9366
  const joinDeadlineMs = to === "joining" ? __privateGet(this, _manager).takeoverDeadlineMs : void 0;
9282
9367
  __privateSet(this, _state, to);
9283
9368
  if (from === "joining") __privateMethod(this, _TrackSession_instances, clearJoinDeadline_fn).call(this);
9284
- if (joinDeadlineMs !== void 0) __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, joinDeadlineMs);
9369
+ if (joinDeadlineMs !== void 0 && __privateGet(this, _manager).joinActive) {
9370
+ __privateMethod(this, _TrackSession_instances, armJoinDeadline_fn).call(this, joinDeadlineMs);
9371
+ }
9285
9372
  const fecActive = (s) => s === "joining" || s === "multicast" || s === "amt" || s === "failing" || s === "unicast-fallback";
9286
9373
  if (!fecActive(to) && !opts?.retainFec) __privateMethod(this, _TrackSession_instances, disarmFec_fn).call(this);
9287
9374
  __privateGet(this, _manager).callbacks?.onStateChange?.(this.config.trackName, from, to);
@@ -9333,13 +9420,16 @@ unsuppress_fn = function() {
9333
9420
  __privateGet(this, _manager).callbacks?.onSuppress?.(this.config.trackName, false);
9334
9421
  };
9335
9422
  var MAX_TAKEOVER_DEADLINE_MS = 2147483647;
9336
- var _takeoverDeadlineMs, _SessionManager_instances, requireTakeoverDeadlineMs_fn;
9423
+ var JOIN_DEFER_WARN_MS = 3e4;
9424
+ var _takeoverDeadlineMs, _joinActive, _joinDeferWarnTimer, _SessionManager_instances, armJoinDeferWarn_fn, clearJoinDeferWarn_fn, requireTakeoverDeadlineMs_fn;
9337
9425
  var SessionManager = class {
9338
9426
  constructor(poolOptions) {
9339
9427
  __privateAdd(this, _SessionManager_instances);
9340
9428
  this.sessions = /* @__PURE__ */ new Map();
9341
9429
  this.callbacks = null;
9342
9430
  __privateAdd(this, _takeoverDeadlineMs);
9431
+ __privateAdd(this, _joinActive, true);
9432
+ __privateAdd(this, _joinDeferWarnTimer, null);
9343
9433
  this.pool = new TransportPool(poolOptions);
9344
9434
  this.pool.onSubscriptionFailed((sub) => {
9345
9435
  for (const trackName of sub.consumers) {
@@ -9388,6 +9478,46 @@ var SessionManager = class {
9388
9478
  get hasTakeoverDeadline() {
9389
9479
  return __privateGet(this, _takeoverDeadlineMs) !== void 0;
9390
9480
  }
9481
+ /**
9482
+ * Whether the transport the joining tracks are waiting on is actually
9483
+ * subscribed. The takeover deadline is a media-arrival budget and media
9484
+ * cannot arrive before the join exists, so while this is false, entering
9485
+ * `joining` arms no deadline; flipping it true starts the FULL deadline for
9486
+ * every track already parked there.
9487
+ *
9488
+ * Default true, which is the arm-on-`joining` behaviour every host had
9489
+ * before: a host that only calls joinStarted() after its own join resolves
9490
+ * (moqtail-private's player, post-BLO-33339) is already at the right edge and
9491
+ * needs no gate. A host that arms joinStarted() up front — to have FEC
9492
+ * decoders ready before the first datagram — sets this false before addTrack()
9493
+ * and true when its subscribe settles. Charging a slow subscribe (bridge
9494
+ * reconnect, AMT relay handshake) against the media budget expired the
9495
+ * deadline before the subscription existed and abandoned the join on the
9496
+ * happy path. BLO-34289.
9497
+ *
9498
+ * The flag tracks the host's primary join, not per-track transport state. A
9499
+ * track that fails over to the next transport in its own chain while this is
9500
+ * false (transportFailed's failover branch also enters `joining`) has that
9501
+ * deadline gated on the primary join activating, not on its own transport.
9502
+ * Under the documented usage -- false once at setup, true once the subscribe
9503
+ * settles -- the two coincide.
9504
+ *
9505
+ * Leaving this false is not free: see JOIN_DEFER_WARN_MS.
9506
+ */
9507
+ get joinActive() {
9508
+ return __privateGet(this, _joinActive);
9509
+ }
9510
+ set joinActive(active) {
9511
+ const wasActive = __privateGet(this, _joinActive);
9512
+ __privateSet(this, _joinActive, active);
9513
+ if (!active) {
9514
+ if (wasActive) __privateMethod(this, _SessionManager_instances, armJoinDeferWarn_fn).call(this);
9515
+ return;
9516
+ }
9517
+ __privateMethod(this, _SessionManager_instances, clearJoinDeferWarn_fn).call(this);
9518
+ if (wasActive) return;
9519
+ for (const [, session] of this.sessions) session.armDeferredJoinDeadline();
9520
+ }
9391
9521
  /** Narrow to MMTP callbacks (onTrackEvent). Safe — optional chaining handles PIM-only consumers. */
9392
9522
  get mmtp() {
9393
9523
  return this.callbacks;
@@ -9541,6 +9671,7 @@ var SessionManager = class {
9541
9671
  // Cleanup
9542
9672
  // -----------------------------------------------------------------------
9543
9673
  dispose() {
9674
+ __privateMethod(this, _SessionManager_instances, clearJoinDeferWarn_fn).call(this);
9544
9675
  for (const [, session] of this.sessions) {
9545
9676
  session.dispose();
9546
9677
  }
@@ -9550,7 +9681,23 @@ var SessionManager = class {
9550
9681
  }
9551
9682
  };
9552
9683
  _takeoverDeadlineMs = new WeakMap();
9684
+ _joinActive = new WeakMap();
9685
+ _joinDeferWarnTimer = new WeakMap();
9553
9686
  _SessionManager_instances = new WeakSet();
9687
+ armJoinDeferWarn_fn = function() {
9688
+ __privateSet(this, _joinDeferWarnTimer, setTimeout(() => {
9689
+ __privateSet(this, _joinDeferWarnTimer, null);
9690
+ const deferred = [...this.sessions.values()].filter((s) => s.state === "joining").length;
9691
+ console.warn(
9692
+ `[SessionManager] joinActive has been false for ${JOIN_DEFER_WARN_MS} ms and ${deferred} track(s) sit in 'joining' with no takeover deadline. Set joinActive = true when the subscribe settles, on every exit path including failures, or these tracks never time out, never release their FEC decoders, and never advance to the next transport.`
9693
+ );
9694
+ }, JOIN_DEFER_WARN_MS));
9695
+ };
9696
+ clearJoinDeferWarn_fn = function() {
9697
+ if (__privateGet(this, _joinDeferWarnTimer) === null) return;
9698
+ clearTimeout(__privateGet(this, _joinDeferWarnTimer));
9699
+ __privateSet(this, _joinDeferWarnTimer, null);
9700
+ };
9554
9701
  // -----------------------------------------------------------------------
9555
9702
  // Track management
9556
9703
  // -----------------------------------------------------------------------
@@ -9898,6 +10045,7 @@ var FECManager = class {
9898
10045
  const k = this.kForPacket(packetId);
9899
10046
  const isSourceSymbol = esi >= 0 && esi < k;
9900
10047
  const isStartupPartial = __privateGet(this, _startupPartialBlocks).has(blockKey);
10048
+ const isReadyMulticastRepair = __privateGet(this, _startupRequireRepairReady) && source === "multicast" && !isSourceSymbol && __privateGet(this, _startupRepairReadyPackets).has(streamKey);
9901
10049
  const awaitingMulticastRepair = __privateGet(this, _startupRequireRepairReady) && source === "multicast" && !__privateGet(this, _startupRepairReadyPackets).has(streamKey) && !__privateGet(this, _startupRepairFailOpenPackets).has(streamKey);
9902
10050
  if (awaitingMulticastRepair) {
9903
10051
  if (!isSourceSymbol) {
@@ -9924,7 +10072,7 @@ var FECManager = class {
9924
10072
  if (isSourceSymbol && esi === 0) {
9925
10073
  __privateGet(this, _startupPartialBlocks).delete(blockKey);
9926
10074
  __privateGet(this, _startupAlignedPackets).add(streamKey);
9927
- } else if (isStartupPartial || !__privateGet(this, _startupAlignedPackets).has(streamKey)) {
10075
+ } else if (isStartupPartial && !isReadyMulticastRepair || !__privateGet(this, _startupAlignedPackets).has(streamKey) && !isReadyMulticastRepair) {
9928
10076
  __privateMethod(this, _FECManager_instances, markStartupPartial_fn).call(this, blockKey);
9929
10077
  return false;
9930
10078
  }
@@ -10017,7 +10165,7 @@ var FECManager = class {
10017
10165
  symbolSizeForPacket(packetId) {
10018
10166
  return __privateMethod(this, _FECManager_instances, resolveTrackConfig_fn).call(this, packetId).symbolSize;
10019
10167
  }
10020
- /** FEC interleave window in milliseconds (0 = no interleaving). */
10168
+ /** FEC block span in milliseconds (0 = no interleaving). */
10021
10169
  get interleaveDepthMs() {
10022
10170
  return __privateGet(this, _fecInterleaveDepthMs);
10023
10171
  }
@@ -10339,14 +10487,10 @@ var FECManager = class {
10339
10487
  return null;
10340
10488
  }
10341
10489
  const fecData = repairData;
10342
- if (ssbLength > 1) {
10343
- this.noteBlockActivity(
10344
- Math.floor(ssStart / ssbLength),
10345
- packetId,
10346
- catalogTrackName
10347
- );
10348
- }
10349
- const sbn = ssbLength > 0 ? Math.floor(ssStart / ssbLength) : 0;
10490
+ const k = this.kForPacket(packetId);
10491
+ if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null;
10492
+ const sbn = Math.floor(ssStart / k);
10493
+ this.noteBlockActivity(sbn, packetId, catalogTrackName);
10350
10494
  const blockKey = __privateMethod(this, _FECManager_instances, blockKey_fn).call(this, sbn, packetId, catalogTrackName);
10351
10495
  if (__privateGet(this, _completedBlocks).has(blockKey)) return null;
10352
10496
  const syncBlockOwner = __privateGet(this, _syncDecoderBlockOwners).get(blockKey);
@@ -10382,15 +10526,6 @@ var FECManager = class {
10382
10526
  if (recovered) __privateGet(this, _syncDecoderBlockOwners).delete(blockKey);
10383
10527
  return recovered;
10384
10528
  }
10385
- /**
10386
- * Unified symbol feeder. Dispatches to feedSourceSymbol or feedRepairSymbol.
10387
- */
10388
- feedSymbol(packetId, symbolId, data, isRepair) {
10389
- if (isRepair) {
10390
- return this.feedRepairSymbol(packetId, symbolId, 1, symbolId, data);
10391
- }
10392
- return this.feedSourceSymbol(packetId, symbolId, data);
10393
- }
10394
10529
  // -----------------------------------------------------------------------
10395
10530
  // Block lifecycle
10396
10531
  // -----------------------------------------------------------------------
@@ -13497,6 +13632,7 @@ var MmtpRouter = class {
13497
13632
  );
13498
13633
  }
13499
13634
  const repairSbn = __privateMethod(this, _MmtpRouter_instances, deriveRepairSbn_fn).call(this, parsed.ssStart, parsed.ssbLength, ownerPacketId);
13635
+ if (repairSbn === null) return;
13500
13636
  const repairEsi = __privateMethod(this, _MmtpRouter_instances, kForPacket_fn).call(this, ownerPacketId) + parsed.rsId;
13501
13637
  if (!__privateMethod(this, _MmtpRouter_instances, feedFecTransportSymbol_fn).call(this, ownerPacketId, repairSbn, repairEsi, parsed.repairData, "moq", catalogTrackName)) return;
13502
13638
  const recovered = catalogTrackName === void 0 ? __privateGet(this, _fecManager).feedRepairSymbol(
@@ -13527,7 +13663,7 @@ var MmtpRouter = class {
13527
13663
  } else {
13528
13664
  __privateGet(this, _fecManager).markBlockCompleted(repairSbn, ownerPacketId, catalogTrackName);
13529
13665
  }
13530
- __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, repairSbn, recovered, ownerPacketId, parsed.ssbLength, catalogTrackName);
13666
+ __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, repairSbn, recovered, ownerPacketId, catalogTrackName);
13531
13667
  }
13532
13668
  }
13533
13669
  }
@@ -13535,7 +13671,7 @@ var MmtpRouter = class {
13535
13671
  /** Route a recovered FEC block through the parser + dedup pipeline.
13536
13672
  * Sync WASM decoders return K fixed-width source symbols concatenated. */
13537
13673
  routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
13538
- __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, blockId, data, packetId, void 0, catalogTrackName);
13674
+ __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, blockId, data, packetId, catalogTrackName);
13539
13675
  }
13540
13676
  /** Route a single recovered source symbol through the parser + dedup pipeline.
13541
13677
  * Worker-backed decoders emit one padded source symbol per frame event. */
@@ -13694,8 +13830,9 @@ deriveSbnEsi_fn = function(ssId, packetId) {
13694
13830
  return { sbn: Math.floor(ssId / k), esi: ssId % k };
13695
13831
  };
13696
13832
  deriveRepairSbn_fn = function(ssStart, ssbLength, packetId) {
13697
- if (ssbLength > 0) return Math.floor(ssStart / ssbLength);
13698
- return __privateMethod(this, _MmtpRouter_instances, deriveSbnEsi_fn).call(this, ssStart, packetId).sbn;
13833
+ const k = __privateMethod(this, _MmtpRouter_instances, kForPacket_fn).call(this, packetId);
13834
+ if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null;
13835
+ return Math.floor(ssStart / k);
13699
13836
  };
13700
13837
  resolveRepairOwnerPacketId_fn = function(packetId, ingress) {
13701
13838
  if (__privateMethod(this, _MmtpRouter_instances, classifyPacketId_fn).call(this, packetId) !== null) return packetId;
@@ -13854,6 +13991,7 @@ processMMTP_fn = function(packet, source, manifestIdentity, manifestAdmitted = f
13854
13991
  );
13855
13992
  }
13856
13993
  const repairSbn = __privateMethod(this, _MmtpRouter_instances, deriveRepairSbn_fn).call(this, parsed.ssStart, parsed.ssbLength, ownerPacketId);
13994
+ if (repairSbn === null) return;
13857
13995
  const repairEsi = __privateMethod(this, _MmtpRouter_instances, kForPacket_fn).call(this, ownerPacketId) + parsed.rsId;
13858
13996
  if (!__privateMethod(this, _MmtpRouter_instances, feedFecTransportSymbol_fn).call(this, ownerPacketId, repairSbn, repairEsi, parsed.repairData, source, catalogTrackNameForPacket(ingress, ownerPacketId))) return;
13859
13997
  const repairCatalogTrackName = catalogTrackNameForPacket(ingress, ownerPacketId);
@@ -13906,7 +14044,7 @@ processMMTP_fn = function(packet, source, manifestIdentity, manifestAdmitted = f
13906
14044
  repairCatalogTrackName
13907
14045
  );
13908
14046
  }
13909
- __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, repairSbn, recovered, ownerPacketId, parsed.ssbLength, catalogTrackNameForPacket(ingress, ownerPacketId));
14047
+ __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, repairSbn, recovered, ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId));
13910
14048
  }
13911
14049
  }
13912
14050
  }
@@ -14022,7 +14160,7 @@ processMMTP_fn = function(packet, source, manifestIdentity, manifestAdmitted = f
14022
14160
  catalogTrackName
14023
14161
  );
14024
14162
  }
14025
- __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, sourceFec.sbn, recovered, info.packetId, void 0, catalogTrackName);
14163
+ __privateMethod(this, _MmtpRouter_instances, routeRecoveredBlock_fn).call(this, sourceFec.sbn, recovered, info.packetId, catalogTrackName);
14026
14164
  }
14027
14165
  }
14028
14166
  }
@@ -14370,8 +14508,8 @@ routeDirectRecoveredAudio_fn = function(packetId, data, meta, cb) {
14370
14508
  };
14371
14509
  // ── Private: FEC recovery routing ───────────────────────────────
14372
14510
  /** Route WASM-recovered block through appropriate path based on container kind. */
14373
- routeRecoveredBlock_fn = function(blockId, recovered, ownerPacketId, blockK, catalogTrackName) {
14374
- const k = blockK ?? __privateMethod(this, _MmtpRouter_instances, kForPacket_fn).call(this, ownerPacketId);
14511
+ routeRecoveredBlock_fn = function(blockId, recovered, ownerPacketId, catalogTrackName) {
14512
+ const k = __privateMethod(this, _MmtpRouter_instances, kForPacket_fn).call(this, ownerPacketId);
14375
14513
  const packetSymbolSize = __privateMethod(this, _MmtpRouter_instances, symbolSizeForPacket_fn).call(this, ownerPacketId);
14376
14514
  const symbolSize = packetSymbolSize > 0 ? packetSymbolSize : void 0;
14377
14515
  const result = processFecBlock(recovered, {
@@ -14452,7 +14590,11 @@ admitRecoveredResult_fn = function(blockId, result, ownerPacketId, catalogTrackN
14452
14590
  __privateMethod(this, _MmtpRouter_instances, routeProcessedFecSymbols_fn).call(this, blockId, { ...result, symbols: [symbol] }, ownerPacketId, catalogTrackName);
14453
14591
  }
14454
14592
  admitNext(index + 1);
14455
- }
14593
+ },
14594
+ // A stalled symbol that is later discarded instead of played must still
14595
+ // advance this loop; enforce mode never calls the release callback for
14596
+ // it (BLO-30095).
14597
+ () => admitNext(index + 1)
14456
14598
  );
14457
14599
  if (admission.action === "drop") admitNext(index + 1);
14458
14600
  };
@@ -15225,12 +15367,16 @@ var driadDiscovery = new DRIADDiscovery();
15225
15367
  function endpointKey(endpoint) {
15226
15368
  return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
15227
15369
  }
15370
+ function carriesMedia(endpoint) {
15371
+ return endpoint.carriesMedia !== false;
15372
+ }
15228
15373
  function selectMulticastTier(endpoints, estimate) {
15229
- if (endpoints.length <= 1)
15230
- return endpoints[0];
15231
- const ranked = endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
15374
+ const media = endpoints.filter(carriesMedia);
15375
+ if (media.length <= 1)
15376
+ return media[0];
15377
+ const ranked = media.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
15232
15378
  if (ranked.length < 2)
15233
- return endpoints[0];
15379
+ return media[0];
15234
15380
  if (estimate === void 0)
15235
15381
  return ranked[0];
15236
15382
  const budget = estimate * 0.8;
@@ -15479,7 +15625,7 @@ var MulticastOrchestrator = class {
15479
15625
  const currentBandwidth = __privateGet(this, _current).bandwidth;
15480
15626
  if (typeof currentBandwidth !== "number")
15481
15627
  return void 0;
15482
- const desired = this.options.endpoints.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
15628
+ const desired = this.options.endpoints.filter((endpoint) => carriesMedia(endpoint) && typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
15483
15629
  if (!desired) {
15484
15630
  __privateSet(this, _favored, void 0);
15485
15631
  return void 0;
@@ -16785,6 +16931,24 @@ function publishFecDiagnostics(packetId, stats) {
16785
16931
  function altGroupKey(track) {
16786
16932
  return track.altGroup === void 0 ? DEFAULT_ALT_GROUP : String(track.altGroup);
16787
16933
  }
16934
+ function repairTrackNamesBySource(tracks) {
16935
+ const bySource = /* @__PURE__ */ new Map();
16936
+ const add = (source, repair) => {
16937
+ if (!source || !repair) return;
16938
+ const names = bySource.get(source) ?? /* @__PURE__ */ new Set();
16939
+ names.add(repair);
16940
+ bySource.set(source, names);
16941
+ };
16942
+ for (const track of tracks) {
16943
+ if (track.fec !== void 0 && isFecCatalogConfig(track.fec) && track.fec.repairTrack) {
16944
+ add(track.name, track.fec.repairTrack);
16945
+ }
16946
+ if (track.packaging === "fec-repair" && Array.isArray(track.depends) && track.depends.length === 1) {
16947
+ add(track.depends[0], track.name);
16948
+ }
16949
+ }
16950
+ return bySource;
16951
+ }
16788
16952
  var _fec, _router, _transport, _reportedFecDiagnosticsFailure, _streamFecBaseline, _trackFecTiming, _switchingSets, _repairPacketIds, _repairInFlight, _repairLifecycleObservers, _trackResetObservers, _onRepairSubscribeCb, _MulticastManager_instances, configureTrackMapper_fn, applyStreamGlobalFecTiming_fn, _onTrackTypeDiscovered, emitRepairLifecycle_fn;
16789
16953
  var MulticastManager = class {
16790
16954
  constructor(options) {
@@ -17245,12 +17409,13 @@ var MulticastManager = class {
17245
17409
  * a factory returning a distinct decoder per stream.
17246
17410
  */
17247
17411
  configureMultiTrack(catalog, wasmDecoderSource) {
17412
+ const repairNamesBySource = repairTrackNamesBySource(catalog.tracks);
17248
17413
  const repairOwnerGroup = /* @__PURE__ */ new Map();
17249
17414
  const repairOwnerAltGroup = /* @__PURE__ */ new Map();
17250
17415
  for (const track of catalog.tracks) {
17251
- if (track.fec !== void 0 && isFecCatalogConfig(track.fec) && track.fec.repairTrack) {
17252
- repairOwnerGroup.set(track.fec.repairTrack, altGroupKey(track));
17253
- repairOwnerAltGroup.set(track.fec.repairTrack, track.altGroup);
17416
+ for (const repairName of repairNamesBySource.get(track.name) ?? []) {
17417
+ repairOwnerGroup.set(repairName, altGroupKey(track));
17418
+ repairOwnerAltGroup.set(repairName, track.altGroup);
17254
17419
  }
17255
17420
  }
17256
17421
  const groupKeyFor = (track) => repairOwnerGroup.get(track.name) ?? altGroupKey(track);
@@ -17353,9 +17518,13 @@ var MulticastManager = class {
17353
17518
  }
17354
17519
  if (track.fec !== void 0) {
17355
17520
  const fecCfg = track.fec;
17356
- const repairTrack = catalogTracksByName.get(fecCfg.repairTrack);
17357
- if (track.packetId !== void 0 && repairTrack?.packetId !== void 0) {
17358
- __privateGet(this, _router).setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
17521
+ if (track.packetId !== void 0) {
17522
+ for (const repairName of repairNamesBySource.get(track.name) ?? []) {
17523
+ const repairTrack = catalogTracksByName.get(repairName);
17524
+ if (repairTrack?.packetId !== void 0) {
17525
+ __privateGet(this, _router).setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
17526
+ }
17527
+ }
17359
17528
  }
17360
17529
  const wasmDecoder = fecDecoders.get(i);
17361
17530
  if (track.packetId !== void 0 && wasmDecoder !== void 0) {
@@ -17645,7 +17814,9 @@ async function setupFecWorkerForTrack(opts) {
17645
17814
  sampleRate: opts.sampleRate,
17646
17815
  algorithm: "raptor",
17647
17816
  k: opts.fecK,
17648
- repairCount: opts.fecRepairCount,
17817
+ // P as this receiver joined it, not the catalog total: the worker snapshot
17818
+ // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
17819
+ repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
17649
17820
  symbolSize: opts.fecSymbolSize,
17650
17821
  interleaveDepth: interleaveDepthBlocks,
17651
17822
  interleaveMs: opts.fecInterleaveMs,
@@ -19507,6 +19678,16 @@ var NO_FEC = {
19507
19678
  fecSymbolSize: 0,
19508
19679
  fecInterleaveMs: 0
19509
19680
  };
19681
+ function joinedRepairSymbolCount(params, joinedRepairTrackNames) {
19682
+ const layers = params.repairLayers;
19683
+ if (layers === void 0 || layers.length === 0) return params.fecRepairCount;
19684
+ const joined = new Set(joinedRepairTrackNames);
19685
+ let total = 0;
19686
+ for (const layer of layers) {
19687
+ if (joined.has(layer.repairTrackName)) total += layer.repairSymbols;
19688
+ }
19689
+ return total;
19690
+ }
19510
19691
  function recordOf(value) {
19511
19692
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
19512
19693
  }
@@ -19836,6 +20017,7 @@ export {
19836
20017
  FEC_SBN_MAX,
19837
20018
  FecSourceInfo,
19838
20019
  FecStarvationDetector,
20020
+ JOIN_DEFER_WARN_MS,
19839
20021
  KEYFRAME_LOST_TIMEOUT_SLACK,
19840
20022
  LOCHeaderExtensionId,
19841
20023
  MAX_ENCODING_SYMBOL_ID,
@@ -19936,6 +20118,7 @@ export {
19936
20118
  isSignalingPacket,
19937
20119
  isSourcePacket,
19938
20120
  isTrackTimingConfig,
20121
+ joinedRepairSymbolCount,
19939
20122
  mergeOverrideIntoCmsfTracks,
19940
20123
  mergeOverrideIntoHangRenditions,
19941
20124
  mfuReassemblyTimeoutMs,