@stream-io/video-client 0.4.4 → 0.4.6
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 +14 -0
- package/dist/index.browser.es.js +138 -136
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +137 -140
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +138 -136
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/CameraManager.d.ts +18 -2
- package/dist/src/devices/InputMediaDeviceManager.d.ts +6 -0
- package/dist/src/devices/SpeakerManager.d.ts +3 -0
- package/dist/src/devices/devices.d.ts +2 -41
- package/package.json +2 -2
- package/src/Call.ts +19 -12
- package/src/devices/CameraManager.ts +32 -8
- package/src/devices/InputMediaDeviceManager.ts +98 -4
- package/src/devices/SpeakerManager.ts +27 -1
- package/src/devices/__tests__/CameraManager.test.ts +24 -1
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +105 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +7 -1
- package/src/devices/__tests__/ScreenShareManager.test.ts +2 -1
- package/src/devices/__tests__/SpeakerManager.test.ts +12 -1
- package/src/devices/__tests__/mocks.ts +56 -7
- package/src/devices/devices.ts +11 -123
- package/src/rtc/flows/join.ts +0 -33
|
@@ -3,7 +3,12 @@ import { StreamClient } from '../../coordinator/connection/client';
|
|
|
3
3
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
mockAudioDevices,
|
|
8
|
+
mockAudioStream,
|
|
9
|
+
mockCall,
|
|
10
|
+
mockDeviceIds$,
|
|
11
|
+
} from './mocks';
|
|
7
12
|
import { getAudioStream } from '../devices';
|
|
8
13
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
9
14
|
import { MicrophoneManager } from '../MicrophoneManager';
|
|
@@ -22,6 +27,7 @@ vi.mock('../devices.ts', () => {
|
|
|
22
27
|
return of(mockAudioDevices);
|
|
23
28
|
}),
|
|
24
29
|
getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
|
|
30
|
+
deviceIds$: mockDeviceIds$(),
|
|
25
31
|
};
|
|
26
32
|
});
|
|
27
33
|
|
|
@@ -4,7 +4,7 @@ import { Call } from '../../Call';
|
|
|
4
4
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
5
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
6
6
|
import * as RxUtils from '../../store/rxUtils';
|
|
7
|
-
import { mockCall, mockScreenShareStream } from './mocks';
|
|
7
|
+
import { mockCall, mockDeviceIds$, mockScreenShareStream } from './mocks';
|
|
8
8
|
import { getScreenShareStream } from '../devices';
|
|
9
9
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
10
10
|
|
|
@@ -14,6 +14,7 @@ vi.mock('../devices.ts', () => {
|
|
|
14
14
|
disposeOfMediaStream: vi.fn(),
|
|
15
15
|
getScreenShareStream: vi.fn(() => Promise.resolve(mockScreenShareStream())),
|
|
16
16
|
checkIfAudioOutputChangeSupported: vi.fn(() => Promise.resolve(true)),
|
|
17
|
+
deviceIds$: () => mockDeviceIds$(),
|
|
17
18
|
};
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
|
|
2
|
-
import { mockAudioDevices } from './mocks';
|
|
2
|
+
import { emitDeviceIds, mockAudioDevices, mockDeviceIds$ } from './mocks';
|
|
3
3
|
import { of } from 'rxjs';
|
|
4
4
|
import { SpeakerManager } from '../SpeakerManager';
|
|
5
5
|
import { checkIfAudioOutputChangeSupported } from '../devices';
|
|
@@ -9,6 +9,7 @@ vi.mock('../devices.ts', () => {
|
|
|
9
9
|
return {
|
|
10
10
|
getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
|
|
11
11
|
checkIfAudioOutputChangeSupported: vi.fn(() => true),
|
|
12
|
+
deviceIds$: mockDeviceIds$(),
|
|
12
13
|
};
|
|
13
14
|
});
|
|
14
15
|
|
|
@@ -59,6 +60,16 @@ describe('SpeakerManager.test', () => {
|
|
|
59
60
|
expect(manager.state.volume).toBe(0.5);
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
it('should disable device if selected device is disconnected', () => {
|
|
64
|
+
emitDeviceIds(mockAudioDevices);
|
|
65
|
+
const deviceId = mockAudioDevices[1].deviceId;
|
|
66
|
+
manager.select(deviceId);
|
|
67
|
+
|
|
68
|
+
emitDeviceIds(mockAudioDevices.slice(2));
|
|
69
|
+
|
|
70
|
+
expect(manager.state.selectedDevice).toBe('');
|
|
71
|
+
});
|
|
72
|
+
|
|
62
73
|
afterEach(() => {
|
|
63
74
|
vi.clearAllMocks();
|
|
64
75
|
vi.resetModules();
|
|
@@ -2,6 +2,7 @@ import { vi } from 'vitest';
|
|
|
2
2
|
import { CallingState, CallState } from '../../store';
|
|
3
3
|
import { OwnCapability } from '../../gen/coordinator';
|
|
4
4
|
import { Call } from '../../Call';
|
|
5
|
+
import { Subject } from 'rxjs';
|
|
5
6
|
|
|
6
7
|
export const mockVideoDevices = [
|
|
7
8
|
{
|
|
@@ -80,8 +81,14 @@ export const mockCall = (): Partial<Call> => {
|
|
|
80
81
|
};
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
export type MockTrack = Partial<MediaStreamTrack> & {
|
|
85
|
+
eventHandlers: { [key: string]: EventListenerOrEventListenerObject };
|
|
86
|
+
readyState: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
83
89
|
export const mockAudioStream = () => {
|
|
84
|
-
const track = {
|
|
90
|
+
const track: MockTrack = {
|
|
91
|
+
eventHandlers: {},
|
|
85
92
|
getSettings: () => ({
|
|
86
93
|
deviceId: mockAudioDevices[0].deviceId,
|
|
87
94
|
}),
|
|
@@ -90,15 +97,22 @@ export const mockAudioStream = () => {
|
|
|
90
97
|
stop: () => {
|
|
91
98
|
track.readyState = 'ended';
|
|
92
99
|
},
|
|
100
|
+
addEventListener: (
|
|
101
|
+
event: string,
|
|
102
|
+
handler: EventListenerOrEventListenerObject,
|
|
103
|
+
) => {
|
|
104
|
+
track.eventHandlers[event] = handler;
|
|
105
|
+
},
|
|
93
106
|
};
|
|
94
107
|
return {
|
|
95
108
|
getTracks: () => [track],
|
|
96
109
|
getAudioTracks: () => [track],
|
|
97
|
-
} as MediaStream;
|
|
110
|
+
} as any as MediaStream;
|
|
98
111
|
};
|
|
99
112
|
|
|
100
113
|
export const mockVideoStream = () => {
|
|
101
|
-
const track = {
|
|
114
|
+
const track: MockTrack = {
|
|
115
|
+
eventHandlers: {},
|
|
102
116
|
getSettings: () => ({
|
|
103
117
|
deviceId: mockVideoDevices[0].deviceId,
|
|
104
118
|
width: 1280,
|
|
@@ -109,15 +123,22 @@ export const mockVideoStream = () => {
|
|
|
109
123
|
stop: () => {
|
|
110
124
|
track.readyState = 'ended';
|
|
111
125
|
},
|
|
126
|
+
addEventListener: (
|
|
127
|
+
event: string,
|
|
128
|
+
handler: EventListenerOrEventListenerObject,
|
|
129
|
+
) => {
|
|
130
|
+
track.eventHandlers[event] = handler;
|
|
131
|
+
},
|
|
112
132
|
};
|
|
113
133
|
return {
|
|
114
134
|
getTracks: () => [track],
|
|
115
135
|
getVideoTracks: () => [track],
|
|
116
|
-
} as MediaStream;
|
|
136
|
+
} as any as MediaStream;
|
|
117
137
|
};
|
|
118
138
|
|
|
119
139
|
export const mockScreenShareStream = (includeAudio: boolean = true) => {
|
|
120
140
|
const track = {
|
|
141
|
+
eventHandlers: {},
|
|
121
142
|
getSettings: () => ({
|
|
122
143
|
deviceId: 'screen',
|
|
123
144
|
}),
|
|
@@ -126,11 +147,18 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
|
|
|
126
147
|
stop: () => {
|
|
127
148
|
track.readyState = 'ended';
|
|
128
149
|
},
|
|
150
|
+
addEventListener: (
|
|
151
|
+
event: string,
|
|
152
|
+
handler: EventListenerOrEventListenerObject,
|
|
153
|
+
) => {
|
|
154
|
+
track.eventHandlers[event] = handler;
|
|
155
|
+
},
|
|
129
156
|
};
|
|
130
157
|
|
|
131
158
|
const tracks = [track];
|
|
132
159
|
if (includeAudio) {
|
|
133
|
-
|
|
160
|
+
const audioTrack = {
|
|
161
|
+
eventHandlers: {},
|
|
134
162
|
getSettings: () => ({
|
|
135
163
|
deviceId: 'screen-audio',
|
|
136
164
|
}),
|
|
@@ -139,12 +167,33 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
|
|
|
139
167
|
stop: () => {
|
|
140
168
|
track.readyState = 'ended';
|
|
141
169
|
},
|
|
142
|
-
|
|
170
|
+
addEventListener: (
|
|
171
|
+
event: string,
|
|
172
|
+
handler: EventListenerOrEventListenerObject,
|
|
173
|
+
) => {
|
|
174
|
+
audioTrack.eventHandlers[event] = handler;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
tracks.push(audioTrack);
|
|
143
178
|
}
|
|
144
179
|
|
|
145
180
|
return {
|
|
146
181
|
getTracks: () => tracks,
|
|
147
182
|
getVideoTracks: () => tracks,
|
|
148
183
|
getAudioTracks: () => tracks,
|
|
149
|
-
} as MediaStream;
|
|
184
|
+
} as any as MediaStream;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let deviceIds: Subject<MediaDeviceInfo[]>;
|
|
188
|
+
export const mockDeviceIds$ = () => {
|
|
189
|
+
global.navigator = {
|
|
190
|
+
//@ts-expect-error
|
|
191
|
+
mediaDevices: {},
|
|
192
|
+
};
|
|
193
|
+
deviceIds = new Subject();
|
|
194
|
+
return deviceIds;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const emitDeviceIds = (values: MediaDeviceInfo[]) => {
|
|
198
|
+
deviceIds.next(values);
|
|
150
199
|
};
|
package/src/devices/devices.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
combineLatest,
|
|
3
2
|
concatMap,
|
|
4
3
|
debounceTime,
|
|
5
|
-
filter,
|
|
6
4
|
from,
|
|
7
5
|
map,
|
|
8
6
|
merge,
|
|
9
7
|
Observable,
|
|
10
|
-
pairwise,
|
|
11
8
|
shareReplay,
|
|
12
9
|
} from 'rxjs';
|
|
13
10
|
import { getLogger } from '../logger';
|
|
@@ -61,8 +58,7 @@ const getDevices = (
|
|
|
61
58
|
/**
|
|
62
59
|
* [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
|
|
63
60
|
*
|
|
64
|
-
*
|
|
65
|
-
*/
|
|
61
|
+
* */
|
|
66
62
|
export const checkIfAudioOutputChangeSupported = () => {
|
|
67
63
|
if (typeof document === 'undefined') return false;
|
|
68
64
|
const element = document.createElement('audio');
|
|
@@ -258,124 +254,16 @@ export const getScreenShareStream = async (
|
|
|
258
254
|
}
|
|
259
255
|
};
|
|
260
256
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
devices$ = getVideoDevices();
|
|
272
|
-
break;
|
|
273
|
-
case 'audiooutput':
|
|
274
|
-
devices$ = getAudioOutputDevices();
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
return combineLatest([devices$, deviceId$]).pipe(
|
|
278
|
-
filter(
|
|
279
|
-
([devices, deviceId]) =>
|
|
280
|
-
!!deviceId && !devices.find((d) => d.deviceId === deviceId),
|
|
281
|
-
),
|
|
282
|
-
map(() => true),
|
|
283
|
-
);
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Notifies the subscriber if a given 'audioinput' device is disconnected
|
|
288
|
-
*
|
|
289
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
290
|
-
* @param deviceId$ an Observable that specifies which device to watch for
|
|
291
|
-
* @returns
|
|
292
|
-
*/
|
|
293
|
-
export const watchForDisconnectedAudioDevice = (
|
|
294
|
-
deviceId$: Observable<string | undefined>,
|
|
295
|
-
) => {
|
|
296
|
-
return watchForDisconnectedDevice('audioinput', deviceId$);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Notifies the subscriber if a given 'videoinput' device is disconnected
|
|
301
|
-
*
|
|
302
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
303
|
-
* @param deviceId$ an Observable that specifies which device to watch for
|
|
304
|
-
* @returns
|
|
305
|
-
*/
|
|
306
|
-
export const watchForDisconnectedVideoDevice = (
|
|
307
|
-
deviceId$: Observable<string | undefined>,
|
|
308
|
-
) => {
|
|
309
|
-
return watchForDisconnectedDevice('videoinput', deviceId$);
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Notifies the subscriber if a given 'audiooutput' device is disconnected
|
|
314
|
-
*
|
|
315
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
316
|
-
* @param deviceId$ an Observable that specifies which device to watch for
|
|
317
|
-
* @returns
|
|
318
|
-
*/
|
|
319
|
-
export const watchForDisconnectedAudioOutputDevice = (
|
|
320
|
-
deviceId$: Observable<string | undefined>,
|
|
321
|
-
) => {
|
|
322
|
-
return watchForDisconnectedDevice('audiooutput', deviceId$);
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const watchForAddedDefaultDevice = (kind: MediaDeviceKind) => {
|
|
326
|
-
let devices$;
|
|
327
|
-
switch (kind) {
|
|
328
|
-
case 'audioinput':
|
|
329
|
-
devices$ = getAudioDevices();
|
|
330
|
-
break;
|
|
331
|
-
case 'videoinput':
|
|
332
|
-
devices$ = getVideoDevices();
|
|
333
|
-
break;
|
|
334
|
-
case 'audiooutput':
|
|
335
|
-
devices$ = getAudioOutputDevices();
|
|
336
|
-
break;
|
|
337
|
-
default:
|
|
338
|
-
throw new Error('Unknown MediaDeviceKind', kind);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return devices$.pipe(
|
|
342
|
-
pairwise(),
|
|
343
|
-
filter(([prev, current]) => {
|
|
344
|
-
const prevDefault = prev.find((device) => device.deviceId === 'default');
|
|
345
|
-
const currentDefault = current.find(
|
|
346
|
-
(device) => device.deviceId === 'default',
|
|
347
|
-
);
|
|
348
|
-
return !!(
|
|
349
|
-
current.length > prev.length &&
|
|
350
|
-
prevDefault &&
|
|
351
|
-
currentDefault &&
|
|
352
|
-
prevDefault.groupId !== currentDefault.groupId
|
|
353
|
-
);
|
|
354
|
-
}),
|
|
355
|
-
map(() => true),
|
|
356
|
-
);
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Notifies the subscriber about newly added default audio input device.
|
|
361
|
-
* @returns Observable<boolean>
|
|
362
|
-
*/
|
|
363
|
-
export const watchForAddedDefaultAudioDevice = () =>
|
|
364
|
-
watchForAddedDefaultDevice('audioinput');
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Notifies the subscriber about newly added default audio output device.
|
|
368
|
-
* @returns Observable<boolean>
|
|
369
|
-
*/
|
|
370
|
-
export const watchForAddedDefaultAudioOutputDevice = () =>
|
|
371
|
-
watchForAddedDefaultDevice('audiooutput');
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Notifies the subscriber about newly added default video input device.
|
|
375
|
-
* @returns Observable<boolean>
|
|
376
|
-
*/
|
|
377
|
-
export const watchForAddedDefaultVideoDevice = () =>
|
|
378
|
-
watchForAddedDefaultDevice('videoinput');
|
|
257
|
+
export const deviceIds$ =
|
|
258
|
+
typeof navigator !== 'undefined' &&
|
|
259
|
+
typeof navigator.mediaDevices !== 'undefined'
|
|
260
|
+
? memoizedObservable(() =>
|
|
261
|
+
merge(
|
|
262
|
+
from(navigator.mediaDevices.enumerateDevices()),
|
|
263
|
+
getDeviceChangeObserver(),
|
|
264
|
+
).pipe(shareReplay(1)),
|
|
265
|
+
)()
|
|
266
|
+
: undefined;
|
|
379
267
|
|
|
380
268
|
/**
|
|
381
269
|
* Deactivates MediaStream (stops and removes tracks) to be later garbage collected
|
package/src/rtc/flows/join.ts
CHANGED
|
@@ -44,25 +44,6 @@ const doJoin = async (
|
|
|
44
44
|
...data,
|
|
45
45
|
location,
|
|
46
46
|
};
|
|
47
|
-
|
|
48
|
-
// FIXME OL: remove this once cascading is enabled by default
|
|
49
|
-
const cascadingModeParams = getCascadingModeParams();
|
|
50
|
-
if (cascadingModeParams) {
|
|
51
|
-
// FIXME OL: remove after SFU migration is done
|
|
52
|
-
if (data?.migrating_from && cascadingModeParams['next_sfu_id']) {
|
|
53
|
-
cascadingModeParams['sfu_id'] = cascadingModeParams['next_sfu_id'];
|
|
54
|
-
}
|
|
55
|
-
return httpClient.doAxiosRequest<JoinCallResponse, JoinCallRequest>(
|
|
56
|
-
'post',
|
|
57
|
-
`/call/${type}/${id}/join`,
|
|
58
|
-
request,
|
|
59
|
-
{
|
|
60
|
-
params: {
|
|
61
|
-
...cascadingModeParams,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
47
|
return httpClient.post<JoinCallResponse, JoinCallRequest>(
|
|
67
48
|
`/call/${type}/${id}/join`,
|
|
68
49
|
request,
|
|
@@ -81,20 +62,6 @@ const toRtcConfiguration = (config?: ICEServer[]) => {
|
|
|
81
62
|
return rtcConfig;
|
|
82
63
|
};
|
|
83
64
|
|
|
84
|
-
const getCascadingModeParams = () => {
|
|
85
|
-
if (typeof window === 'undefined') return null;
|
|
86
|
-
const params = new URLSearchParams(window.location?.search);
|
|
87
|
-
const cascadingEnabled = params.get('cascading') !== null;
|
|
88
|
-
if (cascadingEnabled) {
|
|
89
|
-
const rawParams: Record<string, string> = {};
|
|
90
|
-
params.forEach((value, key) => {
|
|
91
|
-
rawParams[key] = value;
|
|
92
|
-
});
|
|
93
|
-
return rawParams;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
65
|
/**
|
|
99
66
|
* Reconciles the local state of the source participant into the target participant.
|
|
100
67
|
*
|