@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.
@@ -29,6 +29,7 @@ export declare class SfuStatsReporter {
29
29
  private readonly unifiedSessionId;
30
30
  private intervalId;
31
31
  private timeoutId;
32
+ private reportCount;
32
33
  private unsubscribeDevicePermissionsSubscription?;
33
34
  private unsubscribeListDevicesSubscription?;
34
35
  private readonly sdkName;
@@ -41,8 +42,8 @@ export declare class SfuStatsReporter {
41
42
  sendReconnectionTime: (strategy: WebsocketReconnectStrategy, timeSeconds: number) => void;
42
43
  private sendTelemetryData;
43
44
  private run;
45
+ private scheduleNextReport;
44
46
  start: () => void;
45
47
  stop: () => void;
46
48
  flush: () => void;
47
- scheduleOne: (timeout: number) => void;
48
49
  }
@@ -1,4 +1,9 @@
1
1
  import { TrackType } from '../gen/video/sfu/models/models';
2
+ export type CameraStats = {
3
+ frameHeight?: number;
4
+ frameWidth?: number;
5
+ frameRate?: number;
6
+ };
2
7
  export type BaseStats = {
3
8
  audioLevel?: number;
4
9
  bytesSent?: number;
@@ -19,6 +24,7 @@ export type BaseStats = {
19
24
  concealmentEvents?: number;
20
25
  packetsReceived?: number;
21
26
  packetsLost?: number;
27
+ camera?: CameraStats;
22
28
  };
23
29
  export type StatsReport = {
24
30
  rawStats?: RTCStatsReport;
@@ -47,6 +53,7 @@ export type AggregatedStatsReport = {
47
53
  highestFrameWidth: number;
48
54
  highestFrameHeight: number;
49
55
  highestFramesPerSecond: number;
56
+ camera?: CameraStats;
50
57
  codec: string;
51
58
  codecPerTrackType: Partial<Record<TrackType, string>>;
52
59
  timestamp: number;
@@ -108,6 +115,9 @@ export interface RTCMediaSourceStats {
108
115
  kind: string;
109
116
  trackIdentifier: string;
110
117
  audioLevel?: number;
118
+ framesPerSecond?: number;
119
+ width?: number;
120
+ height?: number;
111
121
  }
112
122
  export type RTCCodecStats = {
113
123
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.41.1",
3
+ "version": "1.41.3",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -638,6 +638,8 @@ export class Call {
638
638
  this.statsReporter?.stop();
639
639
  this.statsReporter = undefined;
640
640
 
641
+ const leaveReason = message ?? reason ?? 'user is leaving the call';
642
+ this.tracer.trace('call.leaveReason', leaveReason);
641
643
  this.sfuStatsReporter?.flush();
642
644
  this.sfuStatsReporter?.stop();
643
645
  this.sfuStatsReporter = undefined;
@@ -648,9 +650,7 @@ export class Call {
648
650
  this.publisher?.dispose();
649
651
  this.publisher = undefined;
650
652
 
651
- await this.sfuClient?.leaveAndClose(
652
- message ?? reason ?? 'user is leaving the call',
653
- );
653
+ await this.sfuClient?.leaveAndClose(leaveReason);
654
654
  this.sfuClient = undefined;
655
655
  this.dynascaleManager.setSfuClient(undefined);
656
656
  await this.dynascaleManager.dispose();
@@ -854,6 +854,7 @@ export class Call {
854
854
  * Unless you are implementing a custom "ringing" flow, you should not use this method.
855
855
  */
856
856
  accept = async () => {
857
+ this.tracer.trace('call.accept', '');
857
858
  return this.streamClient.post<AcceptCallResponse>(
858
859
  `${this.streamClientBasePath}/accept`,
859
860
  );
@@ -871,6 +872,7 @@ export class Call {
871
872
  reject = async (
872
873
  reason: RejectReason = 'decline',
873
874
  ): Promise<RejectCallResponse> => {
875
+ this.tracer.trace('call.reject', reason);
874
876
  return this.streamClient.post<RejectCallResponse, RejectCallRequest>(
875
877
  `${this.streamClientBasePath}/reject`,
876
878
  { reason: reason },
@@ -1834,12 +1836,6 @@ export class Call {
1834
1836
  }
1835
1837
  }
1836
1838
 
1837
- if (track.kind === 'video') {
1838
- // schedules calibration report - the SFU will use the performance stats
1839
- // to adjust the quality thresholds as early as possible
1840
- this.sfuStatsReporter?.scheduleOne(3000);
1841
- }
1842
-
1843
1839
  await this.updateLocalStreamState(mediaStream, ...trackTypes);
1844
1840
  };
1845
1841
 
@@ -91,11 +91,7 @@ export class SpeakerManager {
91
91
  * @returns an Observable that will be updated if a device is connected or disconnected
92
92
  */
93
93
  listDevices() {
94
- if (isReactNative()) {
95
- throw new Error(
96
- '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',
97
- );
98
- }
94
+ assertUnsupportedInReactNative();
99
95
  return getAudioOutputDevices(this.call.tracer);
100
96
  }
101
97
 
@@ -107,11 +103,7 @@ export class SpeakerManager {
107
103
  * @param deviceId empty string means the system default
108
104
  */
109
105
  select(deviceId: string) {
110
- if (isReactNative()) {
111
- throw new Error(
112
- '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',
113
- );
114
- }
106
+ assertUnsupportedInReactNative();
115
107
  this.state.setDevice(deviceId);
116
108
  }
117
109
 
@@ -133,11 +125,7 @@ export class SpeakerManager {
133
125
  * Note: This method is not supported in React Native
134
126
  */
135
127
  setVolume(volume: number) {
136
- if (isReactNative()) {
137
- throw new Error(
138
- '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',
139
- );
140
- }
128
+ assertUnsupportedInReactNative();
141
129
  if (volume && (volume < 0 || volume > 1)) {
142
130
  throw new Error('Volume must be between 0 and 1');
143
131
  }
@@ -165,3 +153,11 @@ export class SpeakerManager {
165
153
  });
166
154
  }
167
155
  }
156
+
157
+ const assertUnsupportedInReactNative = () => {
158
+ if (isReactNative()) {
159
+ throw new Error(
160
+ 'Unsupported in React Native. See: https://getstream.io/video/docs/react-native/guides/camera-and-microphone/#speaker-management',
161
+ );
162
+ }
163
+ };
@@ -2,6 +2,7 @@ import type {
2
2
  AggregatedStatsReport,
3
3
  AudioAggregatedStats,
4
4
  BaseStats,
5
+ CameraStats,
5
6
  ParticipantsStatsReport,
6
7
  RTCCodecStats,
7
8
  RTCMediaSourceStats,
@@ -237,6 +238,17 @@ export type StatsTransformOpts = {
237
238
  publisher: Publisher | undefined;
238
239
  };
239
240
 
241
+ /**
242
+ * Extracts camera statistics from a media source.
243
+ *
244
+ * @param mediaSource the media source stats to extract camera info from.
245
+ */
246
+ const getCameraStats = (mediaSource: RTCMediaSourceStats): CameraStats => ({
247
+ frameRate: mediaSource.framesPerSecond,
248
+ frameWidth: mediaSource.width,
249
+ frameHeight: mediaSource.height,
250
+ });
251
+
240
252
  /**
241
253
  * Transforms raw RTC stats into a slimmer and uniform across browsers format.
242
254
  *
@@ -280,6 +292,7 @@ const transform = (
280
292
 
281
293
  let trackType: TrackType | undefined;
282
294
  let audioLevel: number | undefined;
295
+ let camera: CameraStats | undefined;
283
296
  let concealedSamples: number | undefined;
284
297
  let concealmentEvents: number | undefined;
285
298
  let packetsReceived: number | undefined;
@@ -300,6 +313,9 @@ const transform = (
300
313
  ) {
301
314
  audioLevel = mediaSource.audioLevel;
302
315
  }
316
+ if (trackKind === 'video') {
317
+ camera = getCameraStats(mediaSource);
318
+ }
303
319
  }
304
320
  } else if (kind === 'subscriber' && trackKind === 'audio') {
305
321
  const inboundStats = rtcStreamStats as RTCInboundRtpStreamStats;
@@ -333,6 +349,7 @@ const transform = (
333
349
  concealmentEvents,
334
350
  packetsReceived,
335
351
  packetsLost,
352
+ camera,
336
353
  };
337
354
  });
338
355
 
@@ -354,6 +371,7 @@ const getEmptyVideoStats = (stats?: StatsReport): AggregatedStatsReport => {
354
371
  highestFrameWidth: 0,
355
372
  highestFrameHeight: 0,
356
373
  highestFramesPerSecond: 0,
374
+ camera: {},
357
375
  codec: '',
358
376
  codecPerTrackType: {},
359
377
  timestamp: Date.now(),
@@ -404,6 +422,10 @@ const aggregate = (stats: StatsReport): AggregatedStatsReport => {
404
422
  maxArea = streamArea;
405
423
  }
406
424
 
425
+ if (stream.trackType === TrackType.VIDEO) {
426
+ acc.camera = stream.camera;
427
+ }
428
+
407
429
  qualityLimitationReasons.add(stream.qualityLimitationReason || '');
408
430
  return acc;
409
431
  }, aggregatedStats);
@@ -44,6 +44,7 @@ export class SfuStatsReporter {
44
44
 
45
45
  private intervalId: NodeJS.Timeout | undefined;
46
46
  private timeoutId: NodeJS.Timeout | undefined;
47
+ private reportCount: number = 0;
47
48
  private unsubscribeDevicePermissionsSubscription?: () => void;
48
49
  private unsubscribeListDevicesSubscription?: () => void;
49
50
  private readonly sdkName: string;
@@ -208,18 +209,33 @@ export class SfuStatsReporter {
208
209
  }
209
210
  };
210
211
 
212
+ private scheduleNextReport = () => {
213
+ const intervals = [1500, 3000, 3000, 5000];
214
+ if (this.reportCount < intervals.length) {
215
+ this.timeoutId = setTimeout(() => {
216
+ this.flush();
217
+ this.reportCount++;
218
+ this.scheduleNextReport();
219
+ }, intervals[this.reportCount]);
220
+ } else {
221
+ clearInterval(this.intervalId);
222
+ this.intervalId = setInterval(() => {
223
+ this.flush();
224
+ }, this.options.reporting_interval_ms);
225
+ }
226
+ };
227
+
211
228
  start = () => {
212
229
  if (this.options.reporting_interval_ms <= 0) return;
213
230
 
214
231
  this.observeDevice(this.microphone, 'mic');
215
232
  this.observeDevice(this.camera, 'camera');
216
233
 
234
+ this.reportCount = 0;
217
235
  clearInterval(this.intervalId);
218
- this.intervalId = setInterval(() => {
219
- this.run().catch((err) => {
220
- this.logger.warn('Failed to report stats', err);
221
- });
222
- }, this.options.reporting_interval_ms);
236
+ clearTimeout(this.timeoutId);
237
+
238
+ this.scheduleNextReport();
223
239
  };
224
240
 
225
241
  stop = () => {
@@ -233,6 +249,7 @@ export class SfuStatsReporter {
233
249
  this.intervalId = undefined;
234
250
  clearTimeout(this.timeoutId);
235
251
  this.timeoutId = undefined;
252
+ this.reportCount = 0;
236
253
  };
237
254
 
238
255
  flush = () => {
@@ -240,13 +257,4 @@ export class SfuStatsReporter {
240
257
  this.logger.warn('Failed to flush report stats', err);
241
258
  });
242
259
  };
243
-
244
- scheduleOne = (timeout: number) => {
245
- clearTimeout(this.timeoutId);
246
- this.timeoutId = setTimeout(() => {
247
- this.run().catch((err) => {
248
- this.logger.warn('Failed to report stats', err);
249
- });
250
- }, timeout);
251
- };
252
260
  }
@@ -1,5 +1,11 @@
1
1
  import { TrackType } from '../gen/video/sfu/models/models';
2
2
 
3
+ export type CameraStats = {
4
+ frameHeight?: number;
5
+ frameWidth?: number;
6
+ frameRate?: number;
7
+ };
8
+
3
9
  export type BaseStats = {
4
10
  audioLevel?: number;
5
11
  bytesSent?: number;
@@ -20,6 +26,7 @@ export type BaseStats = {
20
26
  concealmentEvents?: number;
21
27
  packetsReceived?: number;
22
28
  packetsLost?: number;
29
+ camera?: CameraStats;
23
30
  };
24
31
 
25
32
  export type StatsReport = {
@@ -51,6 +58,7 @@ export type AggregatedStatsReport = {
51
58
  highestFrameWidth: number;
52
59
  highestFrameHeight: number;
53
60
  highestFramesPerSecond: number;
61
+ camera?: CameraStats;
54
62
  codec: string;
55
63
  codecPerTrackType: Partial<Record<TrackType, string>>;
56
64
  timestamp: number;
@@ -118,6 +126,9 @@ export interface RTCMediaSourceStats {
118
126
  kind: string;
119
127
  trackIdentifier: string;
120
128
  audioLevel?: number;
129
+ framesPerSecond?: number;
130
+ width?: number;
131
+ height?: number;
121
132
  }
122
133
 
123
134
  // shim for RTCCodecStats, not yet available in the standard types