@stream-io/video-client 0.3.0 → 0.3.2
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 +19 -0
- package/dist/index.browser.es.js +863 -455
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +867 -453
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +863 -455
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +18 -1
- package/dist/src/devices/CameraManager.d.ts +31 -0
- package/dist/src/devices/CameraManagerState.d.ts +28 -0
- package/dist/src/devices/InputMediaDeviceManager.d.ts +47 -0
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +69 -0
- package/dist/src/devices/MicrophoneManager.d.ts +19 -0
- package/dist/src/devices/MicrophoneManagerState.d.ts +4 -0
- package/dist/src/devices/__tests__/CameraManager.test.d.ts +1 -0
- package/dist/src/devices/__tests__/InputMediaDeviceManager.test.d.ts +1 -0
- package/dist/src/devices/__tests__/MicrophoneManager.test.d.ts +1 -0
- package/dist/src/devices/__tests__/mocks.d.ts +13 -0
- package/dist/src/devices/index.d.ts +6 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +100 -3
- package/src/__tests__/StreamVideoClient.test.ts +3 -0
- package/src/devices/CameraManager.ts +73 -0
- package/src/devices/CameraManagerState.ts +61 -0
- package/src/devices/InputMediaDeviceManager.ts +121 -0
- package/src/devices/InputMediaDeviceManagerState.ts +111 -0
- package/src/devices/MicrophoneManager.ts +45 -0
- package/src/devices/MicrophoneManagerState.ts +9 -0
- package/src/devices/__tests__/CameraManager.test.ts +150 -0
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +159 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +103 -0
- package/src/devices/__tests__/mocks.ts +98 -0
- package/src/devices/index.ts +6 -0
- package/src/rtc/Publisher.ts +11 -19
- package/src/rtc/videoLayers.ts +7 -2
- package/src/types.ts +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { Call } from '../Call';
|
|
3
|
+
import { CallingState } from '../store';
|
|
4
|
+
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
5
|
+
import { disposeOfMediaStream } from './devices';
|
|
6
|
+
import { isReactNative } from '../helpers/platforms';
|
|
7
|
+
|
|
8
|
+
export abstract class InputMediaDeviceManager<
|
|
9
|
+
T extends InputMediaDeviceManagerState,
|
|
10
|
+
> {
|
|
11
|
+
constructor(protected readonly call: Call, public readonly state: T) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lists the available audio/video devices
|
|
15
|
+
*
|
|
16
|
+
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
17
|
+
*
|
|
18
|
+
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
19
|
+
*/
|
|
20
|
+
listDevices() {
|
|
21
|
+
return this.getDevices();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Starts camera/microphone
|
|
26
|
+
*/
|
|
27
|
+
async enable() {
|
|
28
|
+
if (this.state.status === 'enabled') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await this.startStream();
|
|
32
|
+
this.state.setStatus('enabled');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stops camera/microphone
|
|
37
|
+
* @returns
|
|
38
|
+
*/
|
|
39
|
+
async disable() {
|
|
40
|
+
if (this.state.status === 'disabled') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await this.stopStream();
|
|
44
|
+
this.state.setStatus('disabled');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* If current device statis is disabled, it will enable the device, else it will disable it.
|
|
49
|
+
* @returns
|
|
50
|
+
*/
|
|
51
|
+
async toggle() {
|
|
52
|
+
if (this.state.status === 'enabled') {
|
|
53
|
+
return this.disable();
|
|
54
|
+
} else {
|
|
55
|
+
return this.enable();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Select device
|
|
61
|
+
*
|
|
62
|
+
* Note: this method is not supported in React Native
|
|
63
|
+
*
|
|
64
|
+
* @param deviceId
|
|
65
|
+
*/
|
|
66
|
+
async select(deviceId: string | undefined) {
|
|
67
|
+
if (isReactNative()) {
|
|
68
|
+
throw new Error('This method is not supported in React Native');
|
|
69
|
+
}
|
|
70
|
+
if (deviceId === this.state.selectedDevice) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.state.setDevice(deviceId);
|
|
74
|
+
await this.applySettingsToStream();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected async applySettingsToStream() {
|
|
78
|
+
if (this.state.status === 'enabled') {
|
|
79
|
+
await this.stopStream();
|
|
80
|
+
await this.startStream();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
abstract pause(): void;
|
|
85
|
+
|
|
86
|
+
abstract resume(): void;
|
|
87
|
+
|
|
88
|
+
protected abstract getDevices(): Observable<MediaDeviceInfo[]>;
|
|
89
|
+
|
|
90
|
+
protected abstract getStream(
|
|
91
|
+
constraints: MediaTrackConstraints,
|
|
92
|
+
): Promise<MediaStream>;
|
|
93
|
+
|
|
94
|
+
protected abstract publishStream(stream: MediaStream): Promise<void>;
|
|
95
|
+
|
|
96
|
+
protected abstract stopPublishStream(): Promise<void>;
|
|
97
|
+
|
|
98
|
+
private async stopStream() {
|
|
99
|
+
if (!this.state.mediaStream) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.call.state.callingState === CallingState.JOINED) {
|
|
103
|
+
await this.stopPublishStream();
|
|
104
|
+
} else if (this.state.mediaStream) {
|
|
105
|
+
disposeOfMediaStream(this.state.mediaStream);
|
|
106
|
+
}
|
|
107
|
+
this.state.setMediaStream(undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async startStream() {
|
|
111
|
+
if (this.state.mediaStream) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const constraints = { deviceId: this.state.selectedDevice };
|
|
115
|
+
const stream = await this.getStream(constraints);
|
|
116
|
+
if (this.call.state.callingState === CallingState.JOINED) {
|
|
117
|
+
await this.publishStream(stream);
|
|
118
|
+
}
|
|
119
|
+
this.state.setMediaStream(stream);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
|
|
2
|
+
import { RxUtils } from '../store';
|
|
3
|
+
|
|
4
|
+
export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
|
|
5
|
+
|
|
6
|
+
export abstract class InputMediaDeviceManagerState {
|
|
7
|
+
protected statusSubject = new BehaviorSubject<InputDeviceStatus>(undefined);
|
|
8
|
+
protected mediaStreamSubject = new BehaviorSubject<MediaStream | undefined>(
|
|
9
|
+
undefined,
|
|
10
|
+
);
|
|
11
|
+
protected selectedDeviceSubject = new BehaviorSubject<string | undefined>(
|
|
12
|
+
undefined,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An Observable that emits the current media stream, or `undefined` if the device is currently disabled.
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
mediaStream$: Observable<MediaStream | undefined>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An Observable that emits the currently selected device
|
|
23
|
+
*/
|
|
24
|
+
selectedDevice$: Observable<string | undefined>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An Observable that emits the device status
|
|
28
|
+
*/
|
|
29
|
+
status$: Observable<InputDeviceStatus>;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.mediaStream$ = this.mediaStreamSubject.asObservable();
|
|
33
|
+
this.selectedDevice$ = this.selectedDeviceSubject
|
|
34
|
+
.asObservable()
|
|
35
|
+
.pipe(distinctUntilChanged());
|
|
36
|
+
this.status$ = this.statusSubject.asObservable();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The device status
|
|
41
|
+
*/
|
|
42
|
+
get status() {
|
|
43
|
+
return this.getCurrentValue(this.status$);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The currently selected device
|
|
48
|
+
*/
|
|
49
|
+
get selectedDevice() {
|
|
50
|
+
return this.getCurrentValue(this.selectedDevice$);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The current media stream, or `undefined` if the device is currently disabled.
|
|
55
|
+
*/
|
|
56
|
+
get mediaStream() {
|
|
57
|
+
return this.getCurrentValue(this.mediaStream$);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the current value of an observable, or undefined if the observable has
|
|
62
|
+
* not emitted a value yet.
|
|
63
|
+
*
|
|
64
|
+
* @param observable$ the observable to get the value from.
|
|
65
|
+
*/
|
|
66
|
+
getCurrentValue = RxUtils.getCurrentValue;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @internal
|
|
70
|
+
* @param status
|
|
71
|
+
*/
|
|
72
|
+
setStatus(status: InputDeviceStatus) {
|
|
73
|
+
this.setCurrentValue(this.statusSubject, status);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
* @param stream
|
|
79
|
+
*/
|
|
80
|
+
setMediaStream(stream: MediaStream | undefined) {
|
|
81
|
+
this.setCurrentValue(this.mediaStreamSubject, stream);
|
|
82
|
+
if (stream) {
|
|
83
|
+
this.setDevice(this.getDeviceIdFromStream(stream));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
* @param stream
|
|
90
|
+
*/
|
|
91
|
+
setDevice(deviceId: string | undefined) {
|
|
92
|
+
this.setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Updates the value of the provided Subject.
|
|
97
|
+
* An `update` can either be a new value or a function which takes
|
|
98
|
+
* the current value and returns a new value.
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*
|
|
102
|
+
* @param subject the subject to update.
|
|
103
|
+
* @param update the update to apply to the subject.
|
|
104
|
+
* @return the updated value.
|
|
105
|
+
*/
|
|
106
|
+
protected setCurrentValue = RxUtils.setCurrentValue;
|
|
107
|
+
|
|
108
|
+
protected abstract getDeviceIdFromStream(
|
|
109
|
+
stream: MediaStream,
|
|
110
|
+
): string | undefined;
|
|
111
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { Call } from '../Call';
|
|
3
|
+
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
4
|
+
import { MicrophoneManagerState } from './MicrophoneManagerState';
|
|
5
|
+
import { getAudioDevices, getAudioStream } from './devices';
|
|
6
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
|
+
|
|
8
|
+
export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
|
|
9
|
+
constructor(call: Call) {
|
|
10
|
+
super(call, new MicrophoneManagerState());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
14
|
+
return getAudioDevices();
|
|
15
|
+
}
|
|
16
|
+
protected getStream(
|
|
17
|
+
constraints: MediaTrackConstraints,
|
|
18
|
+
): Promise<MediaStream> {
|
|
19
|
+
return getAudioStream(constraints);
|
|
20
|
+
}
|
|
21
|
+
protected publishStream(stream: MediaStream): Promise<void> {
|
|
22
|
+
return this.call.publishAudioStream(stream);
|
|
23
|
+
}
|
|
24
|
+
protected stopPublishStream(): Promise<void> {
|
|
25
|
+
return this.call.stopPublish(TrackType.AUDIO);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Disables the audio tracks of the microphone
|
|
30
|
+
*/
|
|
31
|
+
pause() {
|
|
32
|
+
this.state.mediaStream?.getAudioTracks().forEach((track) => {
|
|
33
|
+
track.enabled = false;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* (Re)enables the audio tracks of the microphone
|
|
39
|
+
*/
|
|
40
|
+
resume() {
|
|
41
|
+
this.state.mediaStream?.getAudioTracks().forEach((track) => {
|
|
42
|
+
track.enabled = true;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
2
|
+
|
|
3
|
+
export class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
4
|
+
protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
|
|
5
|
+
return stream.getAudioTracks()[0]?.getSettings().deviceId as
|
|
6
|
+
| string
|
|
7
|
+
| undefined;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Call } from '../../Call';
|
|
2
|
+
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, vi, it, expect, Mock } from 'vitest';
|
|
6
|
+
import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
|
|
7
|
+
import { getVideoStream } from '../devices';
|
|
8
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
9
|
+
import { CameraManager } from '../CameraManager';
|
|
10
|
+
import { of } from 'rxjs';
|
|
11
|
+
import { CallSettingsResponse } from '../../gen/coordinator';
|
|
12
|
+
|
|
13
|
+
vi.mock('../devices.ts', () => {
|
|
14
|
+
console.log('MOCKING devices API');
|
|
15
|
+
return {
|
|
16
|
+
disposeOfMediaStream: vi.fn(),
|
|
17
|
+
getVideoDevices: vi.fn(() => {
|
|
18
|
+
return of(mockVideoDevices);
|
|
19
|
+
}),
|
|
20
|
+
getVideoStream: vi.fn(() => Promise.resolve(mockVideoStream())),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('../../Call.ts', () => {
|
|
25
|
+
console.log('MOCKING Call');
|
|
26
|
+
return {
|
|
27
|
+
Call: vi.fn(() => mockCall()),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('CameraManager', () => {
|
|
32
|
+
let manager: CameraManager;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
manager = new CameraManager(
|
|
36
|
+
new Call({
|
|
37
|
+
id: '',
|
|
38
|
+
type: '',
|
|
39
|
+
streamClient: new StreamClient('abc123'),
|
|
40
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('list devices', () => {
|
|
46
|
+
const spy = vi.fn();
|
|
47
|
+
manager.listDevices().subscribe(spy);
|
|
48
|
+
|
|
49
|
+
expect(spy).toHaveBeenCalledWith(mockVideoDevices);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('get stream', async () => {
|
|
53
|
+
await manager.enable();
|
|
54
|
+
|
|
55
|
+
expect(getVideoStream).toHaveBeenCalledWith({
|
|
56
|
+
deviceId: undefined,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should get device id from stream', async () => {
|
|
61
|
+
expect(manager.state.selectedDevice).toBeUndefined();
|
|
62
|
+
|
|
63
|
+
await manager.enable();
|
|
64
|
+
|
|
65
|
+
expect(manager.state.selectedDevice).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('publish stream', async () => {
|
|
69
|
+
// @ts-expect-error
|
|
70
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
71
|
+
|
|
72
|
+
await manager.enable();
|
|
73
|
+
|
|
74
|
+
expect(manager['call'].publishVideoStream).toHaveBeenCalledWith(
|
|
75
|
+
manager.state.mediaStream,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('stop publish stream', async () => {
|
|
80
|
+
// @ts-expect-error
|
|
81
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
82
|
+
await manager.enable();
|
|
83
|
+
|
|
84
|
+
await manager.disable();
|
|
85
|
+
|
|
86
|
+
expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.VIDEO);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('flip', async () => {
|
|
90
|
+
await manager.selectDirection('front');
|
|
91
|
+
|
|
92
|
+
await manager.flip();
|
|
93
|
+
|
|
94
|
+
expect(manager.state.direction).toBe('back');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('select camera direction', async () => {
|
|
98
|
+
expect(manager.state.direction).toBe(undefined);
|
|
99
|
+
|
|
100
|
+
await manager.enable();
|
|
101
|
+
|
|
102
|
+
expect(getVideoStream).toHaveBeenCalledWith({ deviceId: undefined });
|
|
103
|
+
|
|
104
|
+
await manager.selectDirection('front');
|
|
105
|
+
|
|
106
|
+
expect(getVideoStream).toHaveBeenCalledWith({
|
|
107
|
+
deviceId: undefined,
|
|
108
|
+
facingMode: 'user',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await manager.selectDirection('back');
|
|
112
|
+
|
|
113
|
+
expect(getVideoStream).toHaveBeenCalledWith({
|
|
114
|
+
deviceId: undefined,
|
|
115
|
+
facingMode: 'environment',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it(`shouldn't set deviceId and facingMode at the same time`, async () => {
|
|
120
|
+
await manager.enable();
|
|
121
|
+
|
|
122
|
+
await manager.flip();
|
|
123
|
+
|
|
124
|
+
expect(getVideoStream).toHaveBeenCalledWith({ facingMode: 'environment' });
|
|
125
|
+
|
|
126
|
+
const deviceId = mockVideoDevices[1].deviceId;
|
|
127
|
+
await manager.select(deviceId);
|
|
128
|
+
|
|
129
|
+
expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
|
|
130
|
+
deviceId,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should pause and resume tracks', async () => {
|
|
135
|
+
await manager.enable();
|
|
136
|
+
|
|
137
|
+
manager.pause();
|
|
138
|
+
|
|
139
|
+
expect(manager.state.mediaStream?.getVideoTracks()[0].enabled).toBe(false);
|
|
140
|
+
|
|
141
|
+
manager.resume();
|
|
142
|
+
|
|
143
|
+
expect(manager.state.mediaStream?.getVideoTracks()[0].enabled).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
vi.clearAllMocks();
|
|
148
|
+
vi.resetModules();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Call } from '../../Call';
|
|
2
|
+
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
|
|
6
|
+
import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
|
|
7
|
+
import { InputMediaDeviceManager } from '../InputMediaDeviceManager';
|
|
8
|
+
import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
|
|
9
|
+
import { of } from 'rxjs';
|
|
10
|
+
|
|
11
|
+
vi.mock('../../Call.ts', () => {
|
|
12
|
+
console.log('MOCKING Call');
|
|
13
|
+
return {
|
|
14
|
+
Call: vi.fn(() => mockCall()),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
class TestInputMediaDeviceManagerState extends InputMediaDeviceManagerState {
|
|
19
|
+
public getDeviceIdFromStream = vi.fn(() => 'mock-device-id');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMediaDeviceManagerState> {
|
|
23
|
+
public getDevices = vi.fn(() => of(mockVideoDevices));
|
|
24
|
+
public getStream = vi.fn(() => Promise.resolve(mockVideoStream()));
|
|
25
|
+
public publishStream = vi.fn();
|
|
26
|
+
public stopPublishStream = vi.fn();
|
|
27
|
+
public pause = vi.fn();
|
|
28
|
+
public resume = vi.fn();
|
|
29
|
+
|
|
30
|
+
constructor(call: Call) {
|
|
31
|
+
super(call, new TestInputMediaDeviceManagerState());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('InputMediaDeviceManager.test', () => {
|
|
36
|
+
let manager: TestInputMediaDeviceManager;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
manager = new TestInputMediaDeviceManager(
|
|
40
|
+
new Call({
|
|
41
|
+
id: '',
|
|
42
|
+
type: '',
|
|
43
|
+
streamClient: new StreamClient('abc123'),
|
|
44
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('list devices', () => {
|
|
50
|
+
const spy = vi.fn();
|
|
51
|
+
manager.listDevices().subscribe(spy);
|
|
52
|
+
|
|
53
|
+
expect(spy).toHaveBeenCalledWith(mockVideoDevices);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('enable device - before joined to call', async () => {
|
|
57
|
+
vi.spyOn(manager, 'getStream');
|
|
58
|
+
|
|
59
|
+
await manager.enable();
|
|
60
|
+
|
|
61
|
+
expect(manager.getStream).toHaveBeenCalledWith({
|
|
62
|
+
deviceId: undefined,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(manager.state.mediaStream).toBeDefined();
|
|
66
|
+
expect(manager.state.status).toBe('enabled');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('enable device - after joined to call', async () => {
|
|
70
|
+
// @ts-expect-error
|
|
71
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
72
|
+
|
|
73
|
+
await manager.enable();
|
|
74
|
+
|
|
75
|
+
expect(manager.publishStream).toHaveBeenCalledWith(
|
|
76
|
+
manager.state.mediaStream,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('enable device should set device id', async () => {
|
|
81
|
+
expect(manager.state.selectedDevice).toBeUndefined();
|
|
82
|
+
|
|
83
|
+
await manager.enable();
|
|
84
|
+
|
|
85
|
+
expect(manager.state.selectedDevice).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('disable device - before joined to call', async () => {
|
|
89
|
+
await manager.disable();
|
|
90
|
+
|
|
91
|
+
expect(manager.state.mediaStream).toBeUndefined();
|
|
92
|
+
expect(manager.state.status).toBe('disabled');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('disable camera - after joined to call', async () => {
|
|
96
|
+
// @ts-expect-error
|
|
97
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
98
|
+
await manager.enable();
|
|
99
|
+
|
|
100
|
+
await manager.disable();
|
|
101
|
+
|
|
102
|
+
expect(manager.stopPublishStream).toHaveBeenCalledWith();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('toggle device', async () => {
|
|
106
|
+
vi.spyOn(manager, 'disable');
|
|
107
|
+
vi.spyOn(manager, 'enable');
|
|
108
|
+
|
|
109
|
+
await manager.toggle();
|
|
110
|
+
|
|
111
|
+
expect(manager.enable).toHaveBeenCalled();
|
|
112
|
+
|
|
113
|
+
await manager.toggle();
|
|
114
|
+
|
|
115
|
+
expect(manager.disable).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('select device when status is disabled', async () => {
|
|
119
|
+
const deviceId = mockVideoDevices[0].deviceId;
|
|
120
|
+
await manager.select(deviceId);
|
|
121
|
+
|
|
122
|
+
expect(manager.state.selectedDevice).toBe(deviceId);
|
|
123
|
+
expect(manager.getStream).not.toHaveBeenCalledWith();
|
|
124
|
+
expect(manager.publishStream).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('select device when status is enabled and in call', async () => {
|
|
128
|
+
// @ts-expect-error
|
|
129
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
130
|
+
await manager.enable();
|
|
131
|
+
|
|
132
|
+
const deviceId = mockVideoDevices[1].deviceId;
|
|
133
|
+
await manager.select(deviceId);
|
|
134
|
+
|
|
135
|
+
expect(manager.stopPublishStream).toHaveBeenCalledWith();
|
|
136
|
+
expect(manager.getStream).toHaveBeenCalledWith({
|
|
137
|
+
deviceId,
|
|
138
|
+
});
|
|
139
|
+
expect(manager.publishStream).toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it(`changing media stream constraints shouldn't toggle status`, async () => {
|
|
143
|
+
await manager.enable();
|
|
144
|
+
const spy = vi.fn();
|
|
145
|
+
manager.state.status$.subscribe(spy);
|
|
146
|
+
|
|
147
|
+
expect(spy.mock.calls.length).toBe(1);
|
|
148
|
+
|
|
149
|
+
const deviceId = mockVideoDevices[1].deviceId;
|
|
150
|
+
await manager.select(deviceId);
|
|
151
|
+
|
|
152
|
+
expect(spy.mock.calls.length).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.clearAllMocks();
|
|
157
|
+
vi.resetModules();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Call } from '../../Call';
|
|
2
|
+
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
|
|
6
|
+
import { mockCall, mockAudioDevices, mockAudioStream } from './mocks';
|
|
7
|
+
import { getAudioStream } from '../devices';
|
|
8
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
9
|
+
import { MicrophoneManager } from '../MicrophoneManager';
|
|
10
|
+
import { of } from 'rxjs';
|
|
11
|
+
|
|
12
|
+
vi.mock('../devices.ts', () => {
|
|
13
|
+
console.log('MOCKING devices API');
|
|
14
|
+
return {
|
|
15
|
+
disposeOfMediaStream: vi.fn(),
|
|
16
|
+
getAudioDevices: vi.fn(() => {
|
|
17
|
+
return of(mockAudioDevices);
|
|
18
|
+
}),
|
|
19
|
+
getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('../../Call.ts', () => {
|
|
24
|
+
console.log('MOCKING Call');
|
|
25
|
+
return {
|
|
26
|
+
Call: vi.fn(() => mockCall()),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('MicrophoneManager', () => {
|
|
31
|
+
let manager: MicrophoneManager;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
manager = new MicrophoneManager(
|
|
35
|
+
new Call({
|
|
36
|
+
id: '',
|
|
37
|
+
type: '',
|
|
38
|
+
streamClient: new StreamClient('abc123'),
|
|
39
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
it('list devices', () => {
|
|
44
|
+
const spy = vi.fn();
|
|
45
|
+
manager.listDevices().subscribe(spy);
|
|
46
|
+
|
|
47
|
+
expect(spy).toHaveBeenCalledWith(mockAudioDevices);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('get stream', async () => {
|
|
51
|
+
await manager.enable();
|
|
52
|
+
|
|
53
|
+
expect(getAudioStream).toHaveBeenCalledWith({
|
|
54
|
+
deviceId: undefined,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should get device id from stream', async () => {
|
|
59
|
+
expect(manager.state.selectedDevice).toBeUndefined();
|
|
60
|
+
|
|
61
|
+
await manager.enable();
|
|
62
|
+
|
|
63
|
+
expect(manager.state.selectedDevice).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('publish stream', async () => {
|
|
67
|
+
// @ts-expect-error
|
|
68
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
69
|
+
|
|
70
|
+
await manager.enable();
|
|
71
|
+
|
|
72
|
+
expect(manager['call'].publishAudioStream).toHaveBeenCalledWith(
|
|
73
|
+
manager.state.mediaStream,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('stop publish stream', async () => {
|
|
78
|
+
// @ts-expect-error
|
|
79
|
+
manager['call'].state.callingState = CallingState.JOINED;
|
|
80
|
+
await manager.enable();
|
|
81
|
+
|
|
82
|
+
await manager.disable();
|
|
83
|
+
|
|
84
|
+
expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.AUDIO);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should pause and resume tracks', async () => {
|
|
88
|
+
await manager.enable();
|
|
89
|
+
|
|
90
|
+
manager.pause();
|
|
91
|
+
|
|
92
|
+
expect(manager.state.mediaStream?.getAudioTracks()[0].enabled).toBe(false);
|
|
93
|
+
|
|
94
|
+
manager.resume();
|
|
95
|
+
|
|
96
|
+
expect(manager.state.mediaStream?.getAudioTracks()[0].enabled).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
vi.clearAllMocks();
|
|
101
|
+
vi.resetModules();
|
|
102
|
+
});
|
|
103
|
+
});
|