@stream-io/video-client 1.27.0 → 1.27.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.
@@ -9,33 +9,33 @@ export declare const checkIfAudioOutputChangeSupported: () => boolean;
9
9
  * Keeps track of the browser permission to use microphone. This permission also
10
10
  * affects an ability to enumerate audio devices.
11
11
  */
12
- export declare const getAudioBrowserPermission: () => BrowserPermission;
12
+ export declare const getAudioBrowserPermission: (...args: never[]) => BrowserPermission;
13
13
  /**
14
14
  * Keeps track of the browser permission to use camera. This permission also
15
15
  * affects an ability to enumerate video devices.
16
16
  */
17
- export declare const getVideoBrowserPermission: () => BrowserPermission;
17
+ export declare const getVideoBrowserPermission: (...args: never[]) => BrowserPermission;
18
18
  /**
19
19
  * Prompts the user for a permission to use audio devices (if not already granted
20
20
  * and was not prompted before) and lists the available 'audioinput' devices,
21
21
  * if devices are added/removed the list is updated, and if the permission is revoked,
22
22
  * the observable errors.
23
23
  */
24
- export declare const getAudioDevices: () => import("rxjs").Observable<MediaDeviceInfo[]>;
24
+ export declare const getAudioDevices: (...args: (Tracer | undefined)[]) => import("rxjs").Observable<MediaDeviceInfo[]>;
25
25
  /**
26
26
  * Prompts the user for a permission to use video devices (if not already granted
27
27
  * and was not prompted before) and lists the available 'videoinput' devices,
28
28
  * if devices are added/removed the list is updated, and if the permission is revoked,
29
29
  * the observable errors.
30
30
  */
31
- export declare const getVideoDevices: () => import("rxjs").Observable<MediaDeviceInfo[]>;
31
+ export declare const getVideoDevices: (...args: (Tracer | undefined)[]) => import("rxjs").Observable<MediaDeviceInfo[]>;
32
32
  /**
33
33
  * Prompts the user for a permission to use video devices (if not already granted
34
34
  * and was not prompted before) and lists the available 'audiooutput' devices,
35
35
  * if devices are added/removed the list is updated, and if the permission is revoked,
36
36
  * the observable errors.
37
37
  */
38
- export declare const getAudioOutputDevices: () => import("rxjs").Observable<MediaDeviceInfo[]>;
38
+ export declare const getAudioOutputDevices: (...args: (Tracer | undefined)[]) => import("rxjs").Observable<MediaDeviceInfo[]>;
39
39
  /**
40
40
  * Returns an audio media stream that fulfills the given constraints.
41
41
  * If no constraints are provided, it uses the browser's default ones.
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Lazily creates a value using a provided factory
3
3
  */
4
- export declare function lazy<T>(factory: () => T): () => T;
4
+ export declare function lazy<T, A>(factory: (...args: A[]) => T): (...args: A[]) => T;
@@ -1,11 +1,14 @@
1
- import type { Trace, TraceSlice } from './types';
1
+ import type { RTCStatsDataType, Trace, TraceKey, TraceSlice } from './types';
2
2
  export declare class Tracer {
3
3
  private buffer;
4
4
  private enabled;
5
5
  private readonly id;
6
+ private keys?;
6
7
  constructor(id: string | null);
7
8
  setEnabled: (enabled: boolean) => void;
8
9
  trace: Trace;
10
+ traceOnce: (key: TraceKey, tag: string, data: RTCStatsDataType) => void;
11
+ resetTrace: (key: TraceKey) => void;
9
12
  take: () => TraceSlice;
10
13
  dispose: () => void;
11
14
  }
@@ -1,5 +1,6 @@
1
1
  import type { PerformanceStats } from '../../gen/video/sfu/models/models';
2
2
  export type RTCStatsDataType = RTCConfiguration | RTCIceCandidate | RTCSignalingState | RTCIceConnectionState | RTCIceGatheringState | RTCPeerConnectionState | [number | null | string] | string | boolean | RTCOfferOptions | [string | RTCDataChannelInit | undefined] | (RTCOfferOptions | undefined) | RTCSessionDescriptionInit | (RTCIceCandidateInit | RTCIceCandidate) | object | null | undefined;
3
+ export type TraceKey = 'device-enumeration' | (string & {});
3
4
  export type Trace = (tag: string, data: RTCStatsDataType) => void;
4
5
  export type TraceRecord = [
5
6
  tag: string,
@@ -18,5 +18,5 @@ declare class TimerWorker {
18
18
  private sendMessage;
19
19
  }
20
20
  export declare const enableTimerWorker: () => void;
21
- export declare const getTimers: () => TimerWorker;
21
+ export declare const getTimers: (...args: never[]) => TimerWorker;
22
22
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.27.0",
3
+ "version": "1.27.1",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
@@ -40,7 +40,7 @@
40
40
  "@openapitools/openapi-generator-cli": "^2.13.4",
41
41
  "@rollup/plugin-replace": "^6.0.2",
42
42
  "@rollup/plugin-typescript": "^12.1.2",
43
- "@stream-io/audio-filters-web": "^0.4.2",
43
+ "@stream-io/audio-filters-web": "^0.4.3",
44
44
  "@stream-io/node-sdk": "^0.4.24",
45
45
  "@types/sdp-transform": "^2.4.9",
46
46
  "@types/ua-parser-js": "^0.7.39",
package/src/Call.ts CHANGED
@@ -1884,10 +1884,13 @@ export class Call {
1884
1884
  * @internal
1885
1885
  */
1886
1886
  notifyTrackMuteState = async (muted: boolean, ...trackTypes: TrackType[]) => {
1887
- if (!this.sfuClient) return;
1888
- await this.sfuClient.updateMuteStates(
1889
- trackTypes.map((trackType) => ({ trackType, muted })),
1890
- );
1887
+ const key = `muteState.${this.cid}.${trackTypes.join('-')}`;
1888
+ await withoutConcurrency(key, async () => {
1889
+ if (!this.sfuClient) return;
1890
+ await this.sfuClient.updateMuteStates(
1891
+ trackTypes.map((trackType) => ({ trackType, muted })),
1892
+ );
1893
+ });
1891
1894
  };
1892
1895
 
1893
1896
  /**
@@ -580,7 +580,6 @@ export class StableWSConnection {
580
580
  this.totalFailures += 1;
581
581
  this._setHealth(false);
582
582
  this.isConnecting = false;
583
- this.rejectConnectionOpen?.(new Error(`WebSocket error: ${event}`));
584
583
  this._log(`onerror() - WS connection resulted into error`, { event });
585
584
 
586
585
  this._reconnect();
@@ -149,7 +149,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
149
149
  }
150
150
 
151
151
  protected getDevices(): Observable<MediaDeviceInfo[]> {
152
- return getVideoDevices();
152
+ return getVideoDevices(this.call.tracer);
153
153
  }
154
154
 
155
155
  protected getStream(
@@ -198,6 +198,10 @@ export abstract class InputMediaDeviceManager<
198
198
  entry.stop?.();
199
199
  this.filters = this.filters.filter((f) => f !== entry);
200
200
  await this.applySettingsToStream();
201
+ this.call.tracer.trace(
202
+ `unregisterFilter.${TrackType[this.trackType]}`,
203
+ null,
204
+ );
201
205
  }),
202
206
  };
203
207
  }
@@ -219,9 +223,7 @@ export abstract class InputMediaDeviceManager<
219
223
  */
220
224
  async select(deviceId: string | undefined) {
221
225
  if (isReactNative()) {
222
- throw new Error(
223
- 'This method is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for reference.',
224
- );
226
+ throw new Error('This method is not supported in React Native.');
225
227
  }
226
228
  const prevDeviceId = this.state.selectedDevice;
227
229
  if (deviceId === prevDeviceId) {
@@ -195,6 +195,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
195
195
  this.logger('warn', 'Failed to unregister noise cancellation', err);
196
196
  });
197
197
 
198
+ this.call.tracer.trace('noiseCancellation.disabled', true);
198
199
  await this.call.notifyNoiseCancellationStopped();
199
200
  }
200
201
 
@@ -245,7 +246,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
245
246
  }
246
247
 
247
248
  protected getDevices(): Observable<MediaDeviceInfo[]> {
248
- return getAudioDevices();
249
+ return getAudioDevices(this.call.tracer);
249
250
  }
250
251
 
251
252
  protected getStream(
@@ -56,7 +56,7 @@ export class SpeakerManager {
56
56
  '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',
57
57
  );
58
58
  }
59
- return getAudioOutputDevices();
59
+ return getAudioOutputDevices(this.call.tracer);
60
60
  }
61
61
 
62
62
  /**
@@ -7,6 +7,7 @@ import {
7
7
  merge,
8
8
  shareReplay,
9
9
  startWith,
10
+ tap,
10
11
  } from 'rxjs';
11
12
  import { getLogger } from '../logger';
12
13
  import { BrowserPermission } from './BrowserPermission';
@@ -21,8 +22,13 @@ import { getCurrentValue } from '../store/rxUtils';
21
22
  *
22
23
  * @param permission a BrowserPermission instance.
23
24
  * @param kind the kind of devices to enumerate.
25
+ * @param tracer the tracer to use for tracing the device enumeration.
24
26
  */
25
- const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
27
+ const getDevices = (
28
+ permission: BrowserPermission,
29
+ kind: MediaDeviceKind,
30
+ tracer: Tracer | undefined,
31
+ ) => {
26
32
  return from(
27
33
  (async () => {
28
34
  let devices = await navigator.mediaDevices.enumerateDevices();
@@ -34,6 +40,11 @@ const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
34
40
  if (shouldPromptForBrowserPermission && (await permission.prompt())) {
35
41
  devices = await navigator.mediaDevices.enumerateDevices();
36
42
  }
43
+ tracer?.traceOnce(
44
+ 'device-enumeration',
45
+ 'navigator.mediaDevices.enumerateDevices',
46
+ devices,
47
+ );
37
48
  return devices.filter(
38
49
  (device) =>
39
50
  device.kind === kind &&
@@ -99,11 +110,12 @@ export const getVideoBrowserPermission = lazy(
99
110
  }),
100
111
  );
101
112
 
102
- const getDeviceChangeObserver = lazy(() => {
113
+ const getDeviceChangeObserver = lazy((tracer: Tracer | undefined) => {
103
114
  // 'addEventListener' is not available in React Native, returning
104
115
  // an observable that will never fire
105
116
  if (!navigator.mediaDevices.addEventListener) return from([]);
106
117
  return fromEvent(navigator.mediaDevices, 'devicechange').pipe(
118
+ tap(() => tracer?.resetTrace('device-enumeration')),
107
119
  map(() => undefined),
108
120
  debounceTime(500),
109
121
  );
@@ -115,13 +127,15 @@ const getDeviceChangeObserver = lazy(() => {
115
127
  * if devices are added/removed the list is updated, and if the permission is revoked,
116
128
  * the observable errors.
117
129
  */
118
- export const getAudioDevices = lazy(() => {
130
+ export const getAudioDevices = lazy((tracer?: Tracer) => {
119
131
  return merge(
120
- getDeviceChangeObserver(),
132
+ getDeviceChangeObserver(tracer),
121
133
  getAudioBrowserPermission().asObservable(),
122
134
  ).pipe(
123
135
  startWith(undefined),
124
- concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput')),
136
+ concatMap(() =>
137
+ getDevices(getAudioBrowserPermission(), 'audioinput', tracer),
138
+ ),
125
139
  shareReplay(1),
126
140
  );
127
141
  });
@@ -132,13 +146,15 @@ export const getAudioDevices = lazy(() => {
132
146
  * if devices are added/removed the list is updated, and if the permission is revoked,
133
147
  * the observable errors.
134
148
  */
135
- export const getVideoDevices = lazy(() => {
149
+ export const getVideoDevices = lazy((tracer?: Tracer) => {
136
150
  return merge(
137
- getDeviceChangeObserver(),
151
+ getDeviceChangeObserver(tracer),
138
152
  getVideoBrowserPermission().asObservable(),
139
153
  ).pipe(
140
154
  startWith(undefined),
141
- concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput')),
155
+ concatMap(() =>
156
+ getDevices(getVideoBrowserPermission(), 'videoinput', tracer),
157
+ ),
142
158
  shareReplay(1),
143
159
  );
144
160
  });
@@ -149,13 +165,15 @@ export const getVideoDevices = lazy(() => {
149
165
  * if devices are added/removed the list is updated, and if the permission is revoked,
150
166
  * the observable errors.
151
167
  */
152
- export const getAudioOutputDevices = lazy(() => {
168
+ export const getAudioOutputDevices = lazy((tracer?: Tracer) => {
153
169
  return merge(
154
- getDeviceChangeObserver(),
170
+ getDeviceChangeObserver(tracer),
155
171
  getAudioBrowserPermission().asObservable(),
156
172
  ).pipe(
157
173
  startWith(undefined),
158
- concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')),
174
+ concatMap(() =>
175
+ getDevices(getAudioBrowserPermission(), 'audiooutput', tracer),
176
+ ),
159
177
  shareReplay(1),
160
178
  );
161
179
  });
@@ -3,11 +3,11 @@ const uninitialized = Symbol('uninitialized');
3
3
  /**
4
4
  * Lazily creates a value using a provided factory
5
5
  */
6
- export function lazy<T>(factory: () => T): () => T {
6
+ export function lazy<T, A>(factory: (...args: A[]) => T): (...args: A[]) => T {
7
7
  let value: T | typeof uninitialized = uninitialized;
8
- return () => {
8
+ return (...args: A[]) => {
9
9
  if (value === uninitialized) {
10
- value = factory();
10
+ value = factory(...args);
11
11
  }
12
12
 
13
13
  return value;
@@ -74,24 +74,22 @@ describe('videoLayers', () => {
74
74
  });
75
75
 
76
76
  it('should use predefined bitrate values when track dimensions cant be determined', () => {
77
- const width = 0;
78
- const height = 0;
79
77
  const bitrate = 3000000;
80
78
  const track = new MediaStreamTrack();
81
- vi.spyOn(track, 'getSettings').mockReturnValue({ width, height });
79
+ vi.spyOn(track, 'getSettings').mockReturnValue({});
82
80
  const layers = computeVideoLayers(track, {
83
81
  bitrate,
84
82
  // @ts-expect-error - incomplete data
85
83
  codec: { name: 'vp8' },
86
84
  fps: 30,
87
- videoDimension: { width, height },
85
+ videoDimension: { width: 320, height: 180 },
88
86
  });
89
87
  expect(layers).toEqual([
90
88
  {
91
89
  active: true,
92
90
  rid: 'q',
93
- width: 0,
94
- height: 0,
91
+ width: 320,
92
+ height: 180,
95
93
  maxBitrate: bitrate,
96
94
  scaleResolutionDownBy: 1,
97
95
  maxFramerate: 30,
@@ -84,17 +84,17 @@ export const computeVideoLayers = (
84
84
  ): OptimalVideoLayer[] | undefined => {
85
85
  if (isAudioTrackType(publishOption.trackType)) return;
86
86
  const optimalVideoLayers: OptimalVideoLayer[] = [];
87
- const settings = videoTrack.getSettings();
88
- const { width = 0, height = 0 } = settings;
89
87
  const {
90
88
  bitrate,
91
89
  codec,
92
- fps,
90
+ fps = 30,
93
91
  maxSpatialLayers = 3,
94
92
  maxTemporalLayers = 3,
95
93
  videoDimension = { width: 1280, height: 720 },
96
94
  useSingleLayer,
97
95
  } = publishOption;
96
+ const { width = videoDimension.width, height = videoDimension.height } =
97
+ videoTrack.getSettings();
98
98
  const maxBitrate = getComputedMaxBitrate(
99
99
  videoDimension,
100
100
  width,
@@ -137,7 +137,12 @@ export const computeVideoLayers = (
137
137
 
138
138
  // for simplicity, we start with all layers enabled, then this function
139
139
  // will clear/reassign the layers that are not needed
140
- return withSimulcastConstraints(settings, optimalVideoLayers, useSingleLayer);
140
+ return withSimulcastConstraints(
141
+ width,
142
+ height,
143
+ optimalVideoLayers,
144
+ useSingleLayer,
145
+ );
141
146
  };
142
147
 
143
148
  /**
@@ -179,13 +184,14 @@ export const getComputedMaxBitrate = (
179
184
  * https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
180
185
  */
181
186
  const withSimulcastConstraints = (
182
- settings: MediaTrackSettings,
187
+ width: number,
188
+ height: number,
183
189
  optimalVideoLayers: OptimalVideoLayer[],
184
190
  useSingleLayer: boolean,
185
191
  ) => {
186
192
  let layers: OptimalVideoLayer[];
187
193
 
188
- const size = Math.max(settings.width || 0, settings.height || 0);
194
+ const size = Math.max(width, height);
189
195
  if (size <= 320) {
190
196
  // provide only one layer 320x240 (f), the one with the highest quality
191
197
  layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
@@ -1,9 +1,16 @@
1
- import type { Trace, TraceRecord, TraceSlice } from './types';
1
+ import type {
2
+ RTCStatsDataType,
3
+ Trace,
4
+ TraceKey,
5
+ TraceRecord,
6
+ TraceSlice,
7
+ } from './types';
2
8
 
3
9
  export class Tracer {
4
10
  private buffer: TraceRecord[] = [];
5
11
  private enabled = true;
6
12
  private readonly id: string | null;
13
+ private keys?: Map<TraceKey, boolean>;
7
14
 
8
15
  constructor(id: string | null) {
9
16
  this.id = id;
@@ -20,6 +27,16 @@ export class Tracer {
20
27
  this.buffer.push([tag, this.id, data, Date.now()]);
21
28
  };
22
29
 
30
+ traceOnce = (key: TraceKey, tag: string, data: RTCStatsDataType) => {
31
+ if (this.keys?.has(key)) return;
32
+ this.trace(tag, data);
33
+ (this.keys ??= new Map()).set(key, true);
34
+ };
35
+
36
+ resetTrace = (key: TraceKey) => {
37
+ this.keys?.delete(key);
38
+ };
39
+
23
40
  take = (): TraceSlice => {
24
41
  const snapshot = this.buffer;
25
42
  this.buffer = [];
@@ -33,5 +50,6 @@ export class Tracer {
33
50
 
34
51
  dispose = () => {
35
52
  this.buffer = [];
53
+ this.keys?.clear();
36
54
  };
37
55
  }
@@ -19,6 +19,7 @@ export type RTCStatsDataType =
19
19
  | null
20
20
  | undefined;
21
21
 
22
+ export type TraceKey = 'device-enumeration' | (string & {});
22
23
  export type Trace = (tag: string, data: RTCStatsDataType) => void;
23
24
 
24
25
  export type TraceRecord = [