@unboundcx/video-sdk-client 1.1.0 → 2.0.1

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.
@@ -474,21 +474,23 @@ export class RemoteMediaManager extends EventEmitter {
474
474
  * @returns {number} - Optimal spatial layer (0-2)
475
475
  */
476
476
  _calculateOptimalSpatialLayer(width, height) {
477
- // Use the larger dimension to determine quality
478
- const maxDimension = Math.max(width, height);
479
-
480
- // Spatial layer mapping (assumes standard simulcast layers):
481
- // Layer 0: 320x180 - for thumbnails < 240px
482
- // Layer 1: 960x540 - for medium tiles 240px - 540px
483
- // Layer 2: 1920x1080 - for large tiles/fullscreen > 540px
484
-
485
- if (maxDimension <= 240) {
486
- return 0; // Low quality for small thumbnails
487
- } else if (maxDimension <= 540) {
488
- return 1; // Medium quality for smaller grid tiles
489
- } else {
490
- return 2; // High quality (1080p) for anything larger
491
- }
477
+ // Pick the smallest simulcast layer whose source resolution is >=
478
+ // the number of pixels the browser will actually paint for this
479
+ // tile. Browsers render at CSS pixels × devicePixelRatio; on
480
+ // Retina (DPR=2) a 540px CSS tile actually displays 1080 device
481
+ // pixels and *does* benefit from the 1080p source, while on a
482
+ // standard (DPR=1) display that same 540px tile can be served
483
+ // from the 540p source without any visible difference.
484
+ //
485
+ // Source resolutions (rid l/m/h in MediasoupManager produce
486
+ // options): 480×270, 960×540, 1920×1080.
487
+ const dpr =
488
+ (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
489
+ const maxPx = Math.max(width || 0, height || 0) * dpr;
490
+
491
+ if (maxPx <= 480) return 0; // ≤480 px → 270p source suffices
492
+ if (maxPx <= 960) return 1; // ≤960 px → 540p source suffices
493
+ return 2; // larger → need 1080p source
492
494
  }
493
495
 
494
496
  /**
@@ -9,16 +9,35 @@ export class StatsCollector {
9
9
  this.sendTransportStatsTimeout = null;
10
10
  this.recvTransportStatsInterval = null;
11
11
  this.tracks = new Map(); // trackId -> { participantId, kind }
12
- this.onStatsCallback = null; // Callback for local stats updates
12
+ this.onStatsCallback = null; // Host-app callback (UI display)
13
+ this._internalCallbacks = []; // SDK-internal subscribers (QualityMonitor)
13
14
  }
14
15
 
15
16
  /**
16
- * Set callback for stats updates (for local UI display)
17
+ * Set callback for stats updates (for local UI display). Calling this
18
+ * replaces the prior host callback. SDK-internal subscribers (like
19
+ * QualityMonitor) use addInternalCallback() instead so they can't be
20
+ * accidentally clobbered by the host calling setStatsCallback.
17
21
  */
18
22
  setStatsCallback(callback) {
19
23
  this.onStatsCallback = callback;
20
24
  }
21
25
 
26
+ /**
27
+ * SDK-internal: append a stats subscriber that survives host
28
+ * setStatsCallback() calls. Used by QualityMonitor so it can keep
29
+ * receiving stats even when the meet UI installs its own callback.
30
+ * Returns an unsubscribe function.
31
+ */
32
+ addInternalCallback(callback) {
33
+ if (typeof callback !== 'function') return () => {};
34
+ this._internalCallbacks.push(callback);
35
+ return () => {
36
+ const i = this._internalCallbacks.indexOf(callback);
37
+ if (i >= 0) this._internalCallbacks.splice(i, 1);
38
+ };
39
+ }
40
+
22
41
  /**
23
42
  * Register a track for stats collection
24
43
  */
@@ -676,9 +695,21 @@ export class StatsCollector {
676
695
  bandwidth,
677
696
  };
678
697
 
679
- // Call local callback first (for UI display)
698
+ // Fan out to host callback (UI display) AND SDK-internal subscribers
699
+ // (e.g. QualityMonitor). Internal subscribers go first so monitor
700
+ // state updates BEFORE the UI sees the sample — the host callback
701
+ // may read monitor state to render badges. Each callback is wrapped
702
+ // so one throwing subscriber can't starve the others (or the server
703
+ // emit below).
704
+ for (const cb of this._internalCallbacks || []) {
705
+ try { cb(statData); } catch (err) {
706
+ this.logger.warn('StatsCollector :: internal callback error', err);
707
+ }
708
+ }
680
709
  if (this.onStatsCallback) {
681
- this.onStatsCallback(statData);
710
+ try { this.onStatsCallback(statData); } catch (err) {
711
+ this.logger.warn('StatsCollector :: host callback error', err);
712
+ }
682
713
  }
683
714
 
684
715
  // Emit to server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/video-sdk-client",
3
- "version": "1.1.0",
3
+ "version": "2.0.1",
4
4
  "description": "Framework-agnostic WebRTC video meeting SDK powered by mediasoup",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,7 +11,7 @@
11
11
  "./utils/*": "./utils/*.js"
12
12
  },
13
13
  "scripts": {
14
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "test": "node --test 'test/*.test.js'"
15
15
  },
16
16
  "keywords": [
17
17
  "webrtc",