@stream-io/video-client 1.4.8 → 1.5.0-0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/dist/index.browser.es.js +1976 -1476
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1974 -1473
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1976 -1476
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +93 -9
  9. package/dist/src/StreamSfuClient.d.ts +72 -56
  10. package/dist/src/StreamVideoClient.d.ts +2 -2
  11. package/dist/src/coordinator/connection/client.d.ts +3 -4
  12. package/dist/src/coordinator/connection/types.d.ts +5 -1
  13. package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
  14. package/dist/src/devices/MicrophoneManager.d.ts +1 -1
  15. package/dist/src/events/callEventHandlers.d.ts +1 -3
  16. package/dist/src/events/internal.d.ts +4 -0
  17. package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
  18. package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
  19. package/dist/src/helpers/ensureExhausted.d.ts +1 -0
  20. package/dist/src/helpers/withResolvers.d.ts +14 -0
  21. package/dist/src/logger.d.ts +1 -0
  22. package/dist/src/rpc/createClient.d.ts +2 -0
  23. package/dist/src/rpc/index.d.ts +1 -0
  24. package/dist/src/rpc/retryable.d.ts +23 -0
  25. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  26. package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
  27. package/dist/src/rtc/Publisher.d.ts +24 -25
  28. package/dist/src/rtc/Subscriber.d.ts +12 -11
  29. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -0
  30. package/dist/src/rtc/helpers/tracks.d.ts +3 -3
  31. package/dist/src/rtc/signal.d.ts +1 -1
  32. package/dist/src/store/CallState.d.ts +46 -2
  33. package/package.json +5 -5
  34. package/src/Call.ts +618 -563
  35. package/src/StreamSfuClient.ts +277 -246
  36. package/src/StreamVideoClient.ts +15 -16
  37. package/src/__tests__/Call.test.ts +145 -2
  38. package/src/__tests__/StreamVideoClient.api.test.ts +168 -0
  39. package/src/coordinator/connection/client.ts +25 -8
  40. package/src/coordinator/connection/connection.ts +2 -1
  41. package/src/coordinator/connection/types.ts +6 -0
  42. package/src/devices/BrowserPermission.ts +1 -1
  43. package/src/devices/CameraManager.ts +1 -1
  44. package/src/devices/InputMediaDeviceManager.ts +12 -3
  45. package/src/devices/MicrophoneManager.ts +3 -3
  46. package/src/devices/devices.ts +1 -1
  47. package/src/events/__tests__/mutes.test.ts +10 -13
  48. package/src/events/__tests__/participant.test.ts +75 -0
  49. package/src/events/callEventHandlers.ts +4 -7
  50. package/src/events/internal.ts +20 -3
  51. package/src/events/mutes.ts +5 -3
  52. package/src/events/participant.ts +48 -15
  53. package/src/gen/video/sfu/event/events.ts +451 -8
  54. package/src/gen/video/sfu/models/models.ts +211 -204
  55. package/src/helpers/ensureExhausted.ts +5 -0
  56. package/src/helpers/withResolvers.ts +43 -0
  57. package/src/logger.ts +3 -1
  58. package/src/rpc/__tests__/retryable.test.ts +72 -0
  59. package/src/rpc/createClient.ts +21 -0
  60. package/src/rpc/index.ts +1 -0
  61. package/src/rpc/retryable.ts +57 -0
  62. package/src/rtc/Dispatcher.ts +6 -2
  63. package/src/rtc/IceTrickleBuffer.ts +2 -2
  64. package/src/rtc/Publisher.ts +127 -163
  65. package/src/rtc/Subscriber.ts +94 -155
  66. package/src/rtc/__tests__/Publisher.test.ts +18 -95
  67. package/src/rtc/__tests__/Subscriber.test.ts +63 -99
  68. package/src/rtc/__tests__/videoLayers.test.ts +2 -2
  69. package/src/rtc/helpers/rtcConfiguration.ts +11 -0
  70. package/src/rtc/helpers/tracks.ts +27 -7
  71. package/src/rtc/signal.ts +3 -3
  72. package/src/rtc/videoLayers.ts +1 -10
  73. package/src/stats/SfuStatsReporter.ts +1 -0
  74. package/src/store/CallState.ts +109 -2
  75. package/src/store/__tests__/CallState.test.ts +48 -37
  76. package/dist/src/rtc/flows/join.d.ts +0 -20
  77. package/src/rtc/flows/join.ts +0 -65
@@ -84,29 +84,30 @@ export class StreamVideoClient {
84
84
 
85
85
  setLogger(logger, logLevel);
86
86
  this.logger = getLogger(['client']);
87
+ const coordinatorLogger = getLogger(['coordinator']);
87
88
 
88
89
  if (typeof apiKeyOrArgs === 'string') {
89
90
  this.streamClient = new StreamClient(apiKeyOrArgs, {
90
91
  persistUserOnConnectionFailure: true,
91
92
  ...opts,
92
93
  logLevel,
93
- logger: this.logger,
94
+ logger: coordinatorLogger,
94
95
  });
95
96
  } else {
96
97
  this.streamClient = new StreamClient(apiKeyOrArgs.apiKey, {
97
98
  persistUserOnConnectionFailure: true,
98
99
  ...apiKeyOrArgs.options,
99
100
  logLevel,
100
- logger: this.logger,
101
+ logger: coordinatorLogger,
101
102
  });
102
103
 
103
104
  const sdkInfo = getSdkInfo();
104
105
  if (sdkInfo) {
106
+ const sdkName = SdkType[sdkInfo.type].toLowerCase();
107
+ const sdkVersion = `${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`;
108
+ const userAgent = this.streamClient.getUserAgent();
105
109
  this.streamClient.setUserAgent(
106
- this.streamClient.getUserAgent() +
107
- `-video-${SdkType[sdkInfo.type].toLowerCase()}-sdk-${
108
- sdkInfo.major
109
- }.${sdkInfo.minor}.${sdkInfo.patch}`,
110
+ `${userAgent}-video-${sdkName}-sdk-${sdkVersion}`,
110
111
  );
111
112
  }
112
113
  }
@@ -142,10 +143,10 @@ export class StreamVideoClient {
142
143
  * @param user the user to connect.
143
144
  * @param token a token or a function that returns a token.
144
145
  */
145
- async connectUser(
146
+ connectUser = async (
146
147
  user: User,
147
148
  token?: TokenOrProvider,
148
- ): Promise<void | ConnectedEvent> {
149
+ ): Promise<void | ConnectedEvent> => {
149
150
  if (user.type === 'anonymous') {
150
151
  user.id = '!anon';
151
152
  return this.connectAnonymousUser(user as UserWithId, token);
@@ -254,7 +255,7 @@ export class StreamVideoClient {
254
255
  );
255
256
 
256
257
  return connectUserResponse;
257
- }
258
+ };
258
259
 
259
260
  /**
260
261
  * Disconnects the currently connected user from the client.
@@ -359,7 +360,7 @@ export class StreamVideoClient {
359
360
  clientStore: this.writeableStateStore,
360
361
  });
361
362
  call.state.updateFromCallResponse(c.call);
362
- await call.applyDeviceConfig();
363
+ await call.applyDeviceConfig(false);
363
364
  if (data.watch) {
364
365
  this.writeableStateStore.registerCall(call);
365
366
  }
@@ -424,12 +425,12 @@ export class StreamVideoClient {
424
425
  * @param {string} push_provider_name user provided push provider name
425
426
  * @param {string} [userID] the user id (defaults to current user)
426
427
  */
427
- async addVoipDevice(
428
+ addVoipDevice = async (
428
429
  id: string,
429
430
  push_provider: string,
430
431
  push_provider_name: string,
431
432
  userID?: string,
432
- ) {
433
+ ) => {
433
434
  return await this.addDevice(
434
435
  id,
435
436
  push_provider,
@@ -437,7 +438,7 @@ export class StreamVideoClient {
437
438
  userID,
438
439
  true,
439
440
  );
440
- }
441
+ };
441
442
 
442
443
  /**
443
444
  * getDevices - Returns the devices associated with a current user
@@ -471,9 +472,7 @@ export class StreamVideoClient {
471
472
  onRingingCall = async (call_cid: string) => {
472
473
  // if we find the call and is already ringing, we don't need to create a new call
473
474
  // as client would have received the call.ring state because the app had WS alive when receiving push notifications
474
- let call = this.readOnlyStateStore.calls.find(
475
- (c) => c.cid === call_cid && c.ringing,
476
- );
475
+ let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
477
476
  if (!call) {
478
477
  // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
479
478
  const [callType, callId] = call_cid.split(':');
@@ -1,10 +1,21 @@
1
- import { afterEach, beforeEach, expect, it, vi } from 'vitest';
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ MockInstance,
8
+ vi,
9
+ } from 'vitest';
2
10
  import { StreamVideoClient } from '../StreamVideoClient';
3
11
  import 'dotenv/config';
4
12
  import { StreamClient } from '@stream-io/node-sdk';
5
13
  import { generateUUIDv4 } from '../coordinator/connection/utils';
6
14
  import { CallingState } from '../store';
7
15
  import { Dispatcher } from '../rtc';
16
+ import { Call } from '../Call';
17
+ import { StreamVideoParticipant } from '../types';
18
+ import { TrackType } from '../gen/video/sfu/models/models';
8
19
 
9
20
  const apiKey = process.env.STREAM_API_KEY!;
10
21
  const secret = process.env.STREAM_SECRET!;
@@ -15,13 +26,15 @@ const tokenProvider = async () =>
15
26
 
16
27
  let client: StreamVideoClient;
17
28
 
18
- beforeEach(() => {
29
+ beforeEach(async () => {
19
30
  client = new StreamVideoClient({
20
31
  apiKey,
21
32
  options: { browser: true },
22
33
  tokenProvider,
23
34
  user: { id: 'jane' },
24
35
  });
36
+
37
+ await client.streamClient.wsPromise;
25
38
  });
26
39
 
27
40
  it('can get a call', async () => {
@@ -109,6 +122,136 @@ it("doesn't break when joining and leaving the same instance in quick succession
109
122
  ]);
110
123
  });
111
124
 
125
+ describe('state updates in reponse to coordinator API', () => {
126
+ let call: Call;
127
+
128
+ beforeEach(() => {
129
+ call = client.call('default', generateUUIDv4());
130
+ });
131
+
132
+ it('should create and update state', async () => {
133
+ await call.create({
134
+ data: {
135
+ members: [{ user_id: 'sara' }],
136
+ settings_override: {
137
+ screensharing: {
138
+ enabled: false,
139
+ },
140
+ audio: {
141
+ default_device: 'earpiece',
142
+ },
143
+ },
144
+ },
145
+ });
146
+
147
+ expect(call.state.settings?.screensharing.enabled).toBe(false);
148
+ expect(call.state.settings?.audio.default_device).toBe('earpiece');
149
+ expect(call.state.members.length).toBe(1);
150
+ expect(call.state.members[0].user_id).toBe('sara');
151
+ expect(call.isCreatedByMe).toBe(true);
152
+ });
153
+
154
+ it('should get or create and update state', async () => {
155
+ await call.create({
156
+ data: {
157
+ members: [{ user_id: 'sara', role: 'admin' }],
158
+ settings_override: {
159
+ limits: {
160
+ max_participants: 5,
161
+ },
162
+ },
163
+ },
164
+ });
165
+
166
+ expect(call.state.settings?.limits.max_participants).toBe(5);
167
+ expect(call.state.members.length).toBe(1);
168
+ expect(call.state.members[0].user_id).toBe('sara');
169
+ expect(call.state.members[0].role).toBe('admin');
170
+ });
171
+
172
+ it('should get or create and update state', async () => {
173
+ await call.getOrCreate({
174
+ data: {
175
+ members: [{ user_id: 'sara', role: 'admin' }],
176
+ settings_override: {
177
+ limits: {
178
+ max_participants: 5,
179
+ },
180
+ },
181
+ },
182
+ });
183
+
184
+ expect(call.state.settings?.limits.max_participants).toBe(5);
185
+ expect(call.state.members.length).toBe(1);
186
+ expect(call.state.members[0].user_id).toBe('sara');
187
+ expect(call.state.members[0].role).toBe('admin');
188
+ });
189
+
190
+ it('should get and update state', async () => {
191
+ await serverClient.video.call(call.type, call.id).create({
192
+ data: {
193
+ settings_override: {
194
+ limits: { max_duration_seconds: 180 },
195
+ },
196
+ created_by_id: userId,
197
+ },
198
+ });
199
+
200
+ await call.get();
201
+
202
+ expect(call.state.settings?.limits.max_duration_seconds).toBe(180);
203
+ });
204
+
205
+ it('should ring', async () => {
206
+ await call.getOrCreate({ ring: true });
207
+
208
+ expect(call.ringing).toBe(true);
209
+
210
+ await call.leave();
211
+ });
212
+
213
+ afterEach(async () => {
214
+ await serverClient.video.call(call.type, call.id).delete({ hard: true });
215
+ });
216
+ });
217
+
218
+ describe('muting logic', () => {
219
+ let call: Call;
220
+ let spy: MockInstance;
221
+
222
+ beforeEach(async () => {
223
+ call = client.call('default', generateUUIDv4());
224
+ call.state.updateOrAddParticipant('1', {
225
+ userId: 'sara',
226
+ publishedTracks: [TrackType.AUDIO],
227
+ } as StreamVideoParticipant);
228
+
229
+ spy = vi
230
+ .spyOn(call, 'muteUser')
231
+ .mockImplementation(() => Promise.resolve({ duration: '0ms' }));
232
+ });
233
+
234
+ it('should mute self', () => {
235
+ call.muteSelf('audio');
236
+
237
+ expect(spy).toHaveBeenCalledWith(userId, 'audio');
238
+ });
239
+
240
+ it('should mute others', () => {
241
+ call.muteOthers('video');
242
+
243
+ expect(spy).not.toHaveBeenCalled();
244
+
245
+ call.muteOthers('audio');
246
+
247
+ expect(spy).toHaveBeenCalledWith(['sara'], 'audio');
248
+ });
249
+
250
+ afterEach(() => {
251
+ vi.restoreAllMocks();
252
+ });
253
+ });
254
+
112
255
  afterEach(() => {
113
256
  client.disconnectUser();
114
257
  });
@@ -0,0 +1,168 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { StreamVideoClient } from '../StreamVideoClient';
3
+ import 'dotenv/config';
4
+ import { generateUUIDv4 } from '../coordinator/connection/utils';
5
+ import { StreamClient } from '@stream-io/node-sdk';
6
+ import { CreateDeviceRequest } from '../gen/coordinator';
7
+
8
+ const apiKey = process.env.STREAM_API_KEY!;
9
+ const secret = process.env.STREAM_SECRET!;
10
+
11
+ const serverClient = new StreamClient(apiKey, secret);
12
+
13
+ const tokenProvider = (userId: string) => {
14
+ return async () => {
15
+ return new Promise<string>((resolve) => {
16
+ setTimeout(() => {
17
+ const token = serverClient.createToken(
18
+ userId,
19
+ undefined,
20
+ Math.round(Date.now() / 1000 - 10),
21
+ );
22
+ resolve(token);
23
+ }, 100);
24
+ });
25
+ };
26
+ };
27
+
28
+ describe('StreamVideoClient - coordinator API', () => {
29
+ let client: StreamVideoClient;
30
+ const user = {
31
+ id: 'sara',
32
+ };
33
+
34
+ beforeAll(() => {
35
+ client = new StreamVideoClient(apiKey, {
36
+ // tests run in node, so we have to fake being in browser env
37
+ browser: true,
38
+ });
39
+ client.connectUser(user, tokenProvider(user.id));
40
+ });
41
+
42
+ it('query calls', async () => {
43
+ let response = await client.queryCalls();
44
+
45
+ let calls = response.calls;
46
+ expect(calls.length).toBeGreaterThanOrEqual(1);
47
+
48
+ const queryCallsReq = {
49
+ sort: [{ field: 'starts_at', direction: -1 }],
50
+ limit: 2,
51
+ };
52
+ response = await client.queryCalls(queryCallsReq);
53
+
54
+ calls = response.calls;
55
+ expect(calls.length).toBe(2);
56
+
57
+ response = await client.queryCalls({
58
+ ...queryCallsReq,
59
+ next: response.next,
60
+ });
61
+
62
+ expect(response.calls.length).toBeLessThanOrEqual(2);
63
+
64
+ response = await client.queryCalls({
65
+ filter_conditions: { backstage: { $eq: false } },
66
+ });
67
+
68
+ expect(response.calls.length).toBeGreaterThanOrEqual(1);
69
+ });
70
+
71
+ it('query calls - ongoing', async () => {
72
+ const response = await client.queryCalls({
73
+ filter_conditions: { ongoing: { $eq: true } },
74
+ });
75
+
76
+ // Dummy test
77
+ expect(response.calls).toBeDefined();
78
+ });
79
+
80
+ it('query calls - upcoming', async () => {
81
+ const mins30 = 1000 * 60 * 60 * 30;
82
+ const inNext30mins = new Date(Date.now() + mins30);
83
+ const response = await client.queryCalls({
84
+ filter_conditions: {
85
+ starts_at: { $gt: inNext30mins.toISOString() },
86
+ },
87
+ });
88
+
89
+ // Dummy test
90
+ expect(response.calls).toBeDefined();
91
+ });
92
+
93
+ it('query call stats', async () => {
94
+ const response = await client.queryCallStats({
95
+ filter_conditions: { call_cid: 'default:test' },
96
+ });
97
+
98
+ expect(response.reports).toBeDefined();
99
+ });
100
+
101
+ it('edges', async () => {
102
+ const response = await client.edges();
103
+
104
+ expect(response.edges).toBeDefined();
105
+ });
106
+
107
+ describe('devices', () => {
108
+ const device: CreateDeviceRequest = {
109
+ id: generateUUIDv4(),
110
+ push_provider: 'firebase',
111
+ push_provider_name: 'firebase',
112
+ };
113
+
114
+ it('add device', async () => {
115
+ expect(
116
+ async () =>
117
+ await client.addDevice(
118
+ device.id,
119
+ device.push_provider,
120
+ device.push_provider_name,
121
+ ),
122
+ ).not.toThrowError();
123
+ });
124
+
125
+ it('add voip device', async () => {
126
+ expect(
127
+ async () =>
128
+ await client.addVoipDevice(
129
+ device.id + 'voip',
130
+ device.push_provider,
131
+ device.push_provider_name!,
132
+ ),
133
+ ).not.toThrowError();
134
+ });
135
+
136
+ it('get devices', { retry: 3, timeout: 15000 }, async () => {
137
+ // Wait a little bit, because if we query devices too soon backend will return 404
138
+ await new Promise<void>((resolve) => {
139
+ setTimeout(resolve, 5000);
140
+ });
141
+
142
+ const response = await client.getDevices();
143
+
144
+ expect(response.devices.find((d) => d.id === device.id)).toBeDefined();
145
+ expect(
146
+ response.devices.find((d) => d.id === device.id + 'voip'),
147
+ ).toBeDefined();
148
+ });
149
+
150
+ it('remove device', { retry: 3, timeout: 15000 }, async () => {
151
+ // Wait a little bit, because if we query devices too soon backend will return 404
152
+ await new Promise<void>((resolve) => {
153
+ setTimeout(resolve, 5000);
154
+ });
155
+
156
+ expect(
157
+ async () => await client.removeDevice(device.id),
158
+ ).not.toThrowError();
159
+ expect(
160
+ async () => await client.removeDevice(device.id + 'void'),
161
+ ).not.toThrowError();
162
+ });
163
+ });
164
+
165
+ afterAll(() => {
166
+ client.disconnectUser();
167
+ });
168
+ });
@@ -12,10 +12,12 @@ import { TokenManager } from './token_manager';
12
12
  import { WSConnectionFallback } from './connection_fallback';
13
13
  import { isErrorResponse, isWSFailure } from './errors';
14
14
  import {
15
+ addConnectionEventListeners,
15
16
  isFunction,
16
17
  isOnline,
17
18
  KnownCodes,
18
19
  randomId,
20
+ removeConnectionEventListeners,
19
21
  retryInterval,
20
22
  sleep,
21
23
  } from './utils';
@@ -131,11 +133,19 @@ export class StreamClient {
131
133
  this.options.baseURL || 'https://video.stream-io-api.com/video',
132
134
  );
133
135
 
134
- if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) {
136
+ if (
137
+ typeof process !== 'undefined' &&
138
+ 'env' in process &&
139
+ process.env.STREAM_LOCAL_TEST_RUN
140
+ ) {
135
141
  this.setBaseURL('http://localhost:3030/video');
136
142
  }
137
143
 
138
- if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) {
144
+ if (
145
+ typeof process !== 'undefined' &&
146
+ 'env' in process &&
147
+ process.env.STREAM_LOCAL_TEST_HOST
148
+ ) {
139
149
  this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
140
150
  }
141
151
 
@@ -265,6 +275,7 @@ export class StreamClient {
265
275
  );
266
276
 
267
277
  try {
278
+ addConnectionEventListeners(this.updateNetworkConnectionStatus);
268
279
  return await this.setUserPromise;
269
280
  } catch (err) {
270
281
  if (this.persistUserOnConnectionFailure) {
@@ -398,6 +409,7 @@ export class StreamClient {
398
409
  this.anonymous = false;
399
410
 
400
411
  await this.closeConnection(timeout);
412
+ removeConnectionEventListeners(this.updateNetworkConnectionStatus);
401
413
 
402
414
  this.tokenManager.reset();
403
415
 
@@ -436,6 +448,7 @@ export class StreamClient {
436
448
  user: UserWithId,
437
449
  tokenOrProvider: TokenOrProvider,
438
450
  ) => {
451
+ addConnectionEventListeners(this.updateNetworkConnectionStatus);
439
452
  this.connectionIdPromise = new Promise<string | undefined>(
440
453
  (resolve, reject) => {
441
454
  this.resolveConnectionId = resolve;
@@ -661,7 +674,6 @@ export class StreamClient {
661
674
  };
662
675
 
663
676
  dispatchEvent = (event: StreamVideoEvent) => {
664
- if (!event.received_at) event.received_at = new Date();
665
677
  this.logger('debug', `Dispatching event: ${event.type}`, event);
666
678
  if (!this.listeners) return;
667
679
 
@@ -855,10 +867,15 @@ export class StreamClient {
855
867
  });
856
868
  };
857
869
 
858
- /**
859
- * creates an abort controller that will be used by the next HTTP Request.
860
- */
861
- createAbortControllerForNextRequest = () => {
862
- return (this.nextRequestAbortController = new AbortController());
870
+ updateNetworkConnectionStatus = (
871
+ event: { type: 'online' | 'offline' } | Event,
872
+ ) => {
873
+ if (event.type === 'offline') {
874
+ this.logger('debug', 'device went offline');
875
+ this.dispatchEvent({ type: 'network.changed', online: false });
876
+ } else if (event.type === 'online') {
877
+ this.logger('debug', 'device went online');
878
+ this.dispatchEvent({ type: 'network.changed', online: true });
879
+ }
863
880
  };
864
881
  }
@@ -382,7 +382,7 @@ export class StableWSConnection {
382
382
  );
383
383
  postInsights?.('ws_fatal', insights);
384
384
  }
385
- this.client.rejectConnectionId?.();
385
+ this.client.rejectConnectionId?.(err);
386
386
  throw err;
387
387
  }
388
388
  }
@@ -578,6 +578,7 @@ export class StableWSConnection {
578
578
  }
579
579
 
580
580
  if (data) {
581
+ data.received_at = new Date();
581
582
  this.client.dispatchEvent(data);
582
583
  }
583
584
  this.scheduleConnectionCheck();
@@ -52,6 +52,11 @@ export type ConnectionChangedEvent = {
52
52
  online: boolean;
53
53
  };
54
54
 
55
+ export type NetworkChangedEvent = {
56
+ type: 'network.changed';
57
+ online: boolean;
58
+ };
59
+
55
60
  export type TransportChangedEvent = {
56
61
  type: 'transport.changed';
57
62
  mode: 'longpoll';
@@ -63,6 +68,7 @@ export type ConnectionRecoveredEvent = {
63
68
 
64
69
  export type StreamVideoEvent = (
65
70
  | WSEvent
71
+ | NetworkChangedEvent
66
72
  | ConnectionChangedEvent
67
73
  | TransportChangedEvent
68
74
  | ConnectionRecoveredEvent
@@ -29,7 +29,7 @@ export class BrowserPermission {
29
29
  this.setState('granted');
30
30
  };
31
31
 
32
- if (!canQueryPermissions()) {
32
+ if (!canQueryPermissions() && !isReactNative()) {
33
33
  return assumeGranted();
34
34
  }
35
35
 
@@ -61,7 +61,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
61
61
  this.logger('warn', 'could not apply target resolution', error);
62
62
  }
63
63
  }
64
- if (this.state.status === 'enabled') {
64
+ if (this.enabled) {
65
65
  const { width, height } = this.state
66
66
  .mediaStream!.getVideoTracks()[0]
67
67
  ?.getSettings();
@@ -63,6 +63,13 @@ export abstract class InputMediaDeviceManager<
63
63
  return this.getDevices();
64
64
  }
65
65
 
66
+ /**
67
+ * Returns `true` when this device is in enabled state.
68
+ */
69
+ get enabled() {
70
+ return this.state.status === 'enabled';
71
+ }
72
+
66
73
  /**
67
74
  * Starts stream.
68
75
  */
@@ -216,7 +223,7 @@ export abstract class InputMediaDeviceManager<
216
223
  };
217
224
 
218
225
  protected async applySettingsToStream() {
219
- if (this.state.status === 'enabled') {
226
+ if (this.enabled) {
220
227
  await this.muteStream();
221
228
  await this.unmuteStream();
222
229
  }
@@ -374,7 +381,7 @@ export abstract class InputMediaDeviceManager<
374
381
  .then(chainWith(parent), (error) => {
375
382
  this.logger(
376
383
  'warn',
377
- 'Fitler failed to start and will be ignored',
384
+ 'Filter failed to start and will be ignored',
378
385
  error,
379
386
  );
380
387
  return parent;
@@ -384,13 +391,15 @@ export abstract class InputMediaDeviceManager<
384
391
  }
385
392
  if (this.call.state.callingState === CallingState.JOINED) {
386
393
  await this.publishStream(stream);
394
+ } else {
395
+ this.logger('debug', 'Stream is not published as the call is not joined');
387
396
  }
388
397
  if (this.state.mediaStream !== stream) {
389
398
  this.state.setMediaStream(stream, await rootStream);
390
399
  this.getTracks().forEach((track) => {
391
400
  track.addEventListener('ended', async () => {
392
401
  await this.statusChangeSettled();
393
- if (this.state.status === 'enabled') {
402
+ if (this.enabled) {
394
403
  this.isTrackStoppedDueToTrackEnd = true;
395
404
  setTimeout(() => {
396
405
  this.isTrackStoppedDueToTrackEnd = false;
@@ -25,7 +25,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
25
25
  private noiseCancellation: INoiseCancellation | undefined;
26
26
  private noiseCancellationChangeUnsubscribe: (() => void) | undefined;
27
27
  private noiseCancellationRegistration?: Promise<void>;
28
- private uregisterNoiseCancellation?: () => Promise<void>;
28
+ private unregisterNoiseCancellation?: () => Promise<void>;
29
29
 
30
30
  constructor(
31
31
  call: Call,
@@ -144,7 +144,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
144
144
  noiseCancellation.toFilter(),
145
145
  );
146
146
  this.noiseCancellationRegistration = registrationResult.registered;
147
- this.uregisterNoiseCancellation = registrationResult.unregister;
147
+ this.unregisterNoiseCancellation = registrationResult.unregister;
148
148
  await this.noiseCancellationRegistration;
149
149
 
150
150
  // handles an edge case where a noise cancellation is enabled after
@@ -173,7 +173,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
173
173
  if (isReactNative()) {
174
174
  throw new Error('Noise cancellation is not supported in React Native');
175
175
  }
176
- await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
176
+ await (this.unregisterNoiseCancellation?.() ?? Promise.resolve())
177
177
  .then(() => this.noiseCancellation?.disable())
178
178
  .then(() => this.noiseCancellationChangeUnsubscribe?.())
179
179
  .catch((err) => {
@@ -16,7 +16,7 @@ import { lazy } from '../helpers/lazy';
16
16
  * Returns an Observable that emits the list of available devices
17
17
  * that meet the given constraints.
18
18
  *
19
- * @param constraints the constraints to use when requesting the devices.
19
+ * @param permission a BrowserPermission instance.
20
20
  * @param kind the kind of devices to enumerate.
21
21
  */
22
22
  const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {