@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.
- package/CHANGELOG.md +231 -0
- package/dist/index.browser.es.js +1976 -1476
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1974 -1473
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1976 -1476
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +93 -9
- package/dist/src/StreamSfuClient.d.ts +72 -56
- package/dist/src/StreamVideoClient.d.ts +2 -2
- package/dist/src/coordinator/connection/client.d.ts +3 -4
- package/dist/src/coordinator/connection/types.d.ts +5 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +1 -3
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
- package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
- package/dist/src/helpers/ensureExhausted.d.ts +1 -0
- package/dist/src/helpers/withResolvers.d.ts +14 -0
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/rpc/createClient.d.ts +2 -0
- package/dist/src/rpc/index.d.ts +1 -0
- package/dist/src/rpc/retryable.d.ts +23 -0
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
- package/dist/src/rtc/Publisher.d.ts +24 -25
- package/dist/src/rtc/Subscriber.d.ts +12 -11
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +46 -2
- package/package.json +5 -5
- package/src/Call.ts +618 -563
- package/src/StreamSfuClient.ts +277 -246
- package/src/StreamVideoClient.ts +15 -16
- package/src/__tests__/Call.test.ts +145 -2
- package/src/__tests__/StreamVideoClient.api.test.ts +168 -0
- package/src/coordinator/connection/client.ts +25 -8
- package/src/coordinator/connection/connection.ts +2 -1
- package/src/coordinator/connection/types.ts +6 -0
- package/src/devices/BrowserPermission.ts +1 -1
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +12 -3
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +10 -13
- package/src/events/__tests__/participant.test.ts +75 -0
- package/src/events/callEventHandlers.ts +4 -7
- package/src/events/internal.ts +20 -3
- package/src/events/mutes.ts +5 -3
- package/src/events/participant.ts +48 -15
- package/src/gen/video/sfu/event/events.ts +451 -8
- package/src/gen/video/sfu/models/models.ts +211 -204
- package/src/helpers/ensureExhausted.ts +5 -0
- package/src/helpers/withResolvers.ts +43 -0
- package/src/logger.ts +3 -1
- package/src/rpc/__tests__/retryable.test.ts +72 -0
- package/src/rpc/createClient.ts +21 -0
- package/src/rpc/index.ts +1 -0
- package/src/rpc/retryable.ts +57 -0
- package/src/rtc/Dispatcher.ts +6 -2
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +127 -163
- package/src/rtc/Subscriber.ts +94 -155
- package/src/rtc/__tests__/Publisher.test.ts +18 -95
- package/src/rtc/__tests__/Subscriber.test.ts +63 -99
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/helpers/rtcConfiguration.ts +11 -0
- package/src/rtc/helpers/tracks.ts +27 -7
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +1 -10
- package/src/stats/SfuStatsReporter.ts +1 -0
- package/src/store/CallState.ts +109 -2
- package/src/store/__tests__/CallState.test.ts +48 -37
- package/dist/src/rtc/flows/join.d.ts +0 -20
- package/src/rtc/flows/join.ts +0 -65
package/src/StreamVideoClient.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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
|
-
'
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
176
|
+
await (this.unregisterNoiseCancellation?.() ?? Promise.resolve())
|
|
177
177
|
.then(() => this.noiseCancellation?.disable())
|
|
178
178
|
.then(() => this.noiseCancellationChangeUnsubscribe?.())
|
|
179
179
|
.catch((err) => {
|
package/src/devices/devices.ts
CHANGED
|
@@ -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
|
|
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) => {
|