@unboundcx/video-sdk-client 2.0.11 → 2.0.12

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.
@@ -829,6 +829,10 @@ export class VideoMeetingClient extends EventEmitter {
829
829
  profile.maxSpatialLayer,
830
830
  profile.maxTemporalLayer,
831
831
  );
832
+ // Keep the send-cap flag in sync on every level change so QualityMonitor
833
+ // discounts the BWE floor while we're intentionally throttled — even on
834
+ // the mid-call path that doesn't re-produce.
835
+ this.mediasoup.setSendBitrateCap(profile.sendMaxBitrate || 0);
832
836
  }
833
837
  if (this.remoteMedia) {
834
838
  this.remoteMedia.setLayerCeiling(
@@ -38,6 +38,12 @@ export class MediasoupManager extends EventEmitter {
38
38
  // no cap. See videoQualityProfiles.js + RemoteMediaManager.setLayerCeiling.
39
39
  this.layerCeiling = { maxSpatialLayer: null, maxTemporalLayer: null };
40
40
 
41
+ // Active send bitrate cap (bps) from the data-saver profile, or 0 when
42
+ // uncapped. QualityMonitor reads this so it can ignore Chrome's BWE
43
+ // "available upload" floor while we're INTENTIONALLY sending low — a low
44
+ // estimate then means "no demand to probe higher", not "bad network".
45
+ this.sendBitrateCap = 0;
46
+
41
47
  // Initialize stats collector
42
48
  this.statsCollector = new StatsCollector(this.logger);
43
49
  this.virtualBackgroundStore = null; // Will be set externally
@@ -684,6 +690,9 @@ export class MediasoupManager extends EventEmitter {
684
690
  // gets the lion's share; lower layers get proportionally less.
685
691
  // (Screen share never reaches here.)
686
692
  const maxBitrate = Number(options.maxBitrate) || 0;
693
+ // Remember the active send cap so QualityMonitor can discount the BWE
694
+ // floor while we're intentionally throttled (data saver).
695
+ this.sendBitrateCap = maxBitrate;
687
696
  if (maxBitrate > 0 && produceOptions.encodings?.length) {
688
697
  // Relative weights per rid; we normalize over the layers actually
689
698
  // present so the allocated total always equals the budget whether
@@ -1101,6 +1110,21 @@ export class MediasoupManager extends EventEmitter {
1101
1110
  return this.producers.get("video") || this.producers.get("camera") || null;
1102
1111
  }
1103
1112
 
1113
+ /**
1114
+ * Set/clear the active send bitrate cap (bps; 0 = uncapped). Lets the
1115
+ * mid-call data-saver path (setParameters, no re-produce) keep
1116
+ * sendBitrateCap in sync so QualityMonitor discounts the BWE floor.
1117
+ * @param {number} bps
1118
+ */
1119
+ setSendBitrateCap(bps) {
1120
+ this.sendBitrateCap = Number(bps) || 0;
1121
+ }
1122
+
1123
+ /** @returns {number} active send bitrate cap in bps (0 = uncapped) */
1124
+ getSendBitrateCap() {
1125
+ return this.sendBitrateCap || 0;
1126
+ }
1127
+
1104
1128
  /**
1105
1129
  * Ask the SFU to forward a lower simulcast layer of the given peer's
1106
1130
  * video producer to us. spatialLayer 0=low, 1=mid, 2=high.
@@ -423,15 +423,27 @@ export class QualityMonitor {
423
423
  // counting that as a bad network wrongly marks audio-only participants
424
424
  // "poor" and triggers a disable-camera suggestion for a camera that's
425
425
  // already off. RTT + loss remain valid either way.
426
- // Only count availMbps once we've seen a real reading (>0); some
427
- // browsers report 0 transiently at session start.
426
+ //
427
+ // The SAME artifact happens under data saver: when we deliberately cap the
428
+ // send to e.g. 600kbps and a low-motion scene only uses ~30kbps, the BWE
429
+ // collapses toward what we're actually sending (no demand to probe higher),
430
+ // so availMbps reads ~0.1 even on a perfectly healthy link. So when a send
431
+ // cap is active we IGNORE the availMbps floor entirely and rely only on the
432
+ // encoder-bandwidth-limited ratio, which genuinely means "the encoder wants
433
+ // more bits than the link will give" — the real signal that survives a cap.
434
+ const sendCapped =
435
+ typeof this.mediasoupManager?.getSendBitrateCap === "function" &&
436
+ this.mediasoupManager.getSendBitrateCap() > 0;
437
+ const useAvailFloor = this._isSendingVideo() && !sendCapped;
428
438
  const bandwidthSeverity = !this._isSendingVideo()
429
439
  ? 0
430
- : availMbps > 0 && availMbps <= this.thresholds.availMbpsCrit
440
+ : useAvailFloor && availMbps > 0 && availMbps <= this.thresholds.availMbpsCrit
431
441
  ? 2
432
442
  : bwLimitedRatio >= this.thresholds.encoderBwLimitedCritRatio
433
443
  ? 2
434
- : availMbps > 0 && availMbps <= this.thresholds.availMbpsWarn
444
+ : useAvailFloor &&
445
+ availMbps > 0 &&
446
+ availMbps <= this.thresholds.availMbpsWarn
435
447
  ? 1
436
448
  : bwLimitedRatio >= this.thresholds.encoderBwLimitedWarnRatio
437
449
  ? 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/video-sdk-client",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "description": "Framework-agnostic WebRTC video meeting SDK powered by mediasoup",
5
5
  "type": "module",
6
6
  "main": "index.js",