@stream-io/video-client 1.41.1 → 1.41.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.41.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.41.2...@stream-io/video-client-1.41.3) (2026-01-30)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **react:** improve logic for calculating the lower / upper threshold for video filter degradation ([#2094](https://github.com/GetStream/stream-video-js/issues/2094)) ([5cd2d5c](https://github.com/GetStream/stream-video-js/commit/5cd2d5cb34fc7bbdfaf9529eb9f8d33a40346cab))
10
+ - **stats:** adjust send stats frequency and include "leave reason" ([#2104](https://github.com/GetStream/stream-video-js/issues/2104)) ([0182832](https://github.com/GetStream/stream-video-js/commit/018283299bebe5d5078d4006ec86b6cd56884e77))
11
+
12
+ ## [1.41.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.41.1...@stream-io/video-client-1.41.2) (2026-01-28)
13
+
14
+ - deduplicate RN compatibility assertions ([#2101](https://github.com/GetStream/stream-video-js/issues/2101)) ([5b9e6bc](https://github.com/GetStream/stream-video-js/commit/5b9e6bc227c55b067eea6345315bca015c8a7ee4))
15
+
5
16
  ## [1.41.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.41.0...@stream-io/video-client-1.41.1) (2026-01-26)
6
17
 
7
18
  ### Bug Fixes
@@ -6188,7 +6188,7 @@ const getSdkVersion = (sdk) => {
6188
6188
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6189
6189
  };
6190
6190
 
6191
- const version = "1.41.1";
6191
+ const version = "1.41.3";
6192
6192
  const [major, minor, patch] = version.split('.');
6193
6193
  let sdkInfo = {
6194
6194
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6511,6 +6511,16 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
6511
6511
  stop,
6512
6512
  };
6513
6513
  };
6514
+ /**
6515
+ * Extracts camera statistics from a media source.
6516
+ *
6517
+ * @param mediaSource the media source stats to extract camera info from.
6518
+ */
6519
+ const getCameraStats = (mediaSource) => ({
6520
+ frameRate: mediaSource.framesPerSecond,
6521
+ frameWidth: mediaSource.width,
6522
+ frameHeight: mediaSource.height,
6523
+ });
6514
6524
  /**
6515
6525
  * Transforms raw RTC stats into a slimmer and uniform across browsers format.
6516
6526
  *
@@ -6536,6 +6546,7 @@ const transform = (report, opts) => {
6536
6546
  }
6537
6547
  let trackType;
6538
6548
  let audioLevel;
6549
+ let camera;
6539
6550
  let concealedSamples;
6540
6551
  let concealmentEvents;
6541
6552
  let packetsReceived;
@@ -6551,6 +6562,9 @@ const transform = (report, opts) => {
6551
6562
  typeof mediaSource.audioLevel === 'number') {
6552
6563
  audioLevel = mediaSource.audioLevel;
6553
6564
  }
6565
+ if (trackKind === 'video') {
6566
+ camera = getCameraStats(mediaSource);
6567
+ }
6554
6568
  }
6555
6569
  }
6556
6570
  else if (kind === 'subscriber' && trackKind === 'audio') {
@@ -6584,6 +6598,7 @@ const transform = (report, opts) => {
6584
6598
  concealmentEvents,
6585
6599
  packetsReceived,
6586
6600
  packetsLost,
6601
+ camera,
6587
6602
  };
6588
6603
  });
6589
6604
  return {
@@ -6603,6 +6618,7 @@ const getEmptyVideoStats = (stats) => {
6603
6618
  highestFrameWidth: 0,
6604
6619
  highestFrameHeight: 0,
6605
6620
  highestFramesPerSecond: 0,
6621
+ camera: {},
6606
6622
  codec: '',
6607
6623
  codecPerTrackType: {},
6608
6624
  timestamp: Date.now(),
@@ -6647,6 +6663,9 @@ const aggregate = (stats) => {
6647
6663
  acc.highestFramesPerSecond = stream.framesPerSecond || 0;
6648
6664
  maxArea = streamArea;
6649
6665
  }
6666
+ if (stream.trackType === TrackType.VIDEO) {
6667
+ acc.camera = stream.camera;
6668
+ }
6650
6669
  qualityLimitationReasons.add(stream.qualityLimitationReason || '');
6651
6670
  return acc;
6652
6671
  }, aggregatedStats);
@@ -6711,6 +6730,7 @@ const aggregateAudio = (stats) => {
6711
6730
  class SfuStatsReporter {
6712
6731
  constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
6713
6732
  this.logger = videoLoggerSystem.getLogger('SfuStatsReporter');
6733
+ this.reportCount = 0;
6714
6734
  this.inputDevices = new Map();
6715
6735
  this.observeDevice = (device, kind) => {
6716
6736
  const { browserPermissionState$ } = device.state;
@@ -6812,17 +6832,31 @@ class SfuStatsReporter {
6812
6832
  throw err;
6813
6833
  }
6814
6834
  };
6835
+ this.scheduleNextReport = () => {
6836
+ const intervals = [1500, 3000, 3000, 5000];
6837
+ if (this.reportCount < intervals.length) {
6838
+ this.timeoutId = setTimeout(() => {
6839
+ this.flush();
6840
+ this.reportCount++;
6841
+ this.scheduleNextReport();
6842
+ }, intervals[this.reportCount]);
6843
+ }
6844
+ else {
6845
+ clearInterval(this.intervalId);
6846
+ this.intervalId = setInterval(() => {
6847
+ this.flush();
6848
+ }, this.options.reporting_interval_ms);
6849
+ }
6850
+ };
6815
6851
  this.start = () => {
6816
6852
  if (this.options.reporting_interval_ms <= 0)
6817
6853
  return;
6818
6854
  this.observeDevice(this.microphone, 'mic');
6819
6855
  this.observeDevice(this.camera, 'camera');
6856
+ this.reportCount = 0;
6820
6857
  clearInterval(this.intervalId);
6821
- this.intervalId = setInterval(() => {
6822
- this.run().catch((err) => {
6823
- this.logger.warn('Failed to report stats', err);
6824
- });
6825
- }, this.options.reporting_interval_ms);
6858
+ clearTimeout(this.timeoutId);
6859
+ this.scheduleNextReport();
6826
6860
  };
6827
6861
  this.stop = () => {
6828
6862
  this.unsubscribeDevicePermissionsSubscription?.();
@@ -6834,20 +6868,13 @@ class SfuStatsReporter {
6834
6868
  this.intervalId = undefined;
6835
6869
  clearTimeout(this.timeoutId);
6836
6870
  this.timeoutId = undefined;
6871
+ this.reportCount = 0;
6837
6872
  };
6838
6873
  this.flush = () => {
6839
6874
  this.run().catch((err) => {
6840
6875
  this.logger.warn('Failed to flush report stats', err);
6841
6876
  });
6842
6877
  };
6843
- this.scheduleOne = (timeout) => {
6844
- clearTimeout(this.timeoutId);
6845
- this.timeoutId = setTimeout(() => {
6846
- this.run().catch((err) => {
6847
- this.logger.warn('Failed to report stats', err);
6848
- });
6849
- }, timeout);
6850
- };
6851
6878
  this.sfuClient = sfuClient;
6852
6879
  this.options = options;
6853
6880
  this.subscriber = subscriber;
@@ -12092,9 +12119,7 @@ class SpeakerManager {
12092
12119
  * @returns an Observable that will be updated if a device is connected or disconnected
12093
12120
  */
12094
12121
  listDevices() {
12095
- if (isReactNative()) {
12096
- throw new Error('This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details');
12097
- }
12122
+ assertUnsupportedInReactNative();
12098
12123
  return getAudioOutputDevices(this.call.tracer);
12099
12124
  }
12100
12125
  /**
@@ -12105,9 +12130,7 @@ class SpeakerManager {
12105
12130
  * @param deviceId empty string means the system default
12106
12131
  */
12107
12132
  select(deviceId) {
12108
- if (isReactNative()) {
12109
- throw new Error('This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details');
12110
- }
12133
+ assertUnsupportedInReactNative();
12111
12134
  this.state.setDevice(deviceId);
12112
12135
  }
12113
12136
  /**
@@ -12117,9 +12140,7 @@ class SpeakerManager {
12117
12140
  * Note: This method is not supported in React Native
12118
12141
  */
12119
12142
  setVolume(volume) {
12120
- if (isReactNative()) {
12121
- throw new Error('This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details');
12122
- }
12143
+ assertUnsupportedInReactNative();
12123
12144
  if (volume && (volume < 0 || volume > 1)) {
12124
12145
  throw new Error('Volume must be between 0 and 1');
12125
12146
  }
@@ -12146,6 +12167,11 @@ class SpeakerManager {
12146
12167
  });
12147
12168
  }
12148
12169
  }
12170
+ const assertUnsupportedInReactNative = () => {
12171
+ if (isReactNative()) {
12172
+ throw new Error('Unsupported in React Native. See: https://getstream.io/video/docs/react-native/guides/camera-and-microphone/#speaker-management');
12173
+ }
12174
+ };
12149
12175
 
12150
12176
  /**
12151
12177
  * An object representation of a `Call`.
@@ -12448,6 +12474,8 @@ class Call {
12448
12474
  }
12449
12475
  this.statsReporter?.stop();
12450
12476
  this.statsReporter = undefined;
12477
+ const leaveReason = message ?? reason ?? 'user is leaving the call';
12478
+ this.tracer.trace('call.leaveReason', leaveReason);
12451
12479
  this.sfuStatsReporter?.flush();
12452
12480
  this.sfuStatsReporter?.stop();
12453
12481
  this.sfuStatsReporter = undefined;
@@ -12455,7 +12483,7 @@ class Call {
12455
12483
  this.subscriber = undefined;
12456
12484
  this.publisher?.dispose();
12457
12485
  this.publisher = undefined;
12458
- await this.sfuClient?.leaveAndClose(message ?? reason ?? 'user is leaving the call');
12486
+ await this.sfuClient?.leaveAndClose(leaveReason);
12459
12487
  this.sfuClient = undefined;
12460
12488
  this.dynascaleManager.setSfuClient(undefined);
12461
12489
  await this.dynascaleManager.dispose();
@@ -12597,6 +12625,7 @@ class Call {
12597
12625
  * Unless you are implementing a custom "ringing" flow, you should not use this method.
12598
12626
  */
12599
12627
  this.accept = async () => {
12628
+ this.tracer.trace('call.accept', '');
12600
12629
  return this.streamClient.post(`${this.streamClientBasePath}/accept`);
12601
12630
  };
12602
12631
  /**
@@ -12609,6 +12638,7 @@ class Call {
12609
12638
  * @param reason the reason for rejecting the call.
12610
12639
  */
12611
12640
  this.reject = async (reason = 'decline') => {
12641
+ this.tracer.trace('call.reject', reason);
12612
12642
  return this.streamClient.post(`${this.streamClientBasePath}/reject`, { reason: reason });
12613
12643
  };
12614
12644
  /**
@@ -13372,11 +13402,6 @@ class Call {
13372
13402
  trackTypes.push(screenShareAudio);
13373
13403
  }
13374
13404
  }
13375
- if (track.kind === 'video') {
13376
- // schedules calibration report - the SFU will use the performance stats
13377
- // to adjust the quality thresholds as early as possible
13378
- this.sfuStatsReporter?.scheduleOne(3000);
13379
- }
13380
13405
  await this.updateLocalStreamState(mediaStream, ...trackTypes);
13381
13406
  };
13382
13407
  /**
@@ -15321,7 +15346,7 @@ class StreamClient {
15321
15346
  this.getUserAgent = () => {
15322
15347
  if (!this.cachedUserAgent) {
15323
15348
  const { clientAppIdentifier = {} } = this.options;
15324
- const { sdkName = 'js', sdkVersion = "1.41.1", ...extras } = clientAppIdentifier;
15349
+ const { sdkName = 'js', sdkVersion = "1.41.3", ...extras } = clientAppIdentifier;
15325
15350
  this.cachedUserAgent = [
15326
15351
  `stream-video-${sdkName}-v${sdkVersion}`,
15327
15352
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),