@stream-io/video-client 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.browser.es.js +982 -675
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +984 -673
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +982 -675
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +21 -9
- package/dist/src/StreamVideoClient.d.ts +3 -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__/mocks.d.ts +13 -0
- package/dist/src/devices/index.d.ts +4 -0
- package/dist/src/events/call-permissions.d.ts +0 -5
- package/dist/src/events/call.d.ts +0 -6
- package/dist/src/events/index.d.ts +0 -6
- package/dist/src/rtc/Dispatcher.d.ts +2 -2
- package/dist/src/rtc/Publisher.d.ts +0 -1
- package/dist/src/store/CallState.d.ts +164 -89
- package/dist/src/types.d.ts +5 -7
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +130 -44
- package/src/StreamVideoClient.ts +14 -17
- 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 +4 -0
- package/src/events/__tests__/call-permissions.test.ts +1 -61
- package/src/events/__tests__/call.test.ts +5 -50
- package/src/events/call-permissions.ts +0 -14
- package/src/events/call.ts +5 -16
- package/src/events/callEventHandlers.ts +2 -57
- package/src/events/index.ts +0 -6
- package/src/rtc/Dispatcher.ts +2 -2
- package/src/rtc/Publisher.ts +4 -6
- package/src/store/CallState.ts +475 -119
- package/src/store/__tests__/CallState.test.ts +447 -1
- package/src/types.ts +4 -8
- package/dist/src/events/__tests__/sessions.test.d.ts +0 -1
- package/dist/src/events/backstage.d.ts +0 -6
- package/dist/src/events/members.d.ts +0 -18
- package/dist/src/events/moderation.d.ts +0 -14
- package/dist/src/events/reactions.d.ts +0 -8
- package/dist/src/events/recording.d.ts +0 -18
- package/dist/src/events/sessions.d.ts +0 -26
- package/src/events/__tests__/backstage.test.ts +0 -15
- package/src/events/__tests__/members.test.ts +0 -135
- package/src/events/__tests__/recording.test.ts +0 -65
- package/src/events/__tests__/sessions.test.ts +0 -135
- package/src/events/backstage.ts +0 -15
- package/src/events/members.ts +0 -62
- package/src/events/moderation.ts +0 -35
- package/src/events/reactions.ts +0 -30
- package/src/events/recording.ts +0 -64
- package/src/events/sessions.ts +0 -102
- /package/dist/src/{events/__tests__/backstage.test.d.ts → devices/__tests__/CameraManager.test.d.ts} +0 -0
- /package/dist/src/{events/__tests__/members.test.d.ts → devices/__tests__/InputMediaDeviceManager.test.d.ts} +0 -0
- /package/dist/src/{events/__tests__/recording.test.d.ts → devices/__tests__/MicrophoneManager.test.d.ts} +0 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { CallingState } from '../../store';
|
|
3
|
+
|
|
4
|
+
export const mockVideoDevices = [
|
|
5
|
+
{
|
|
6
|
+
deviceId:
|
|
7
|
+
'9d7b77d613cc935c023c08779a714db96e9f6d5589aae20869b5ab6e42f5b1fe',
|
|
8
|
+
kind: 'videoinput',
|
|
9
|
+
label: 'Logi Capture',
|
|
10
|
+
groupId: '81b3a3c5fec920079136100dc4b185209859077ce622a24fec747406d50cd694',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
deviceId:
|
|
14
|
+
'af5cbd8fa1ff57b16f5f3c2c03dd9502f552cbdada3992a5c10b8d240debf1b5',
|
|
15
|
+
kind: 'videoinput',
|
|
16
|
+
label: 'HD Pro Webcam C920 (046d:08e5)',
|
|
17
|
+
groupId: 'bb0a866e4d66d493a104d7bd5a5dc2fee3d5947fab34aa0a99282386556a8f17',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
deviceId:
|
|
21
|
+
'f66cf2801fa16223e46a65be7d8d98d23670dd16fdd8961dd5c0dbb0cad372f3',
|
|
22
|
+
kind: 'videoinput',
|
|
23
|
+
label: 'FaceTime HD Camera (Built-in) (05ac:8514)',
|
|
24
|
+
groupId: '0387adca5bc7ab2850a9b9594a5622d838de4f78cbdf4d31c3506b701c58d94d',
|
|
25
|
+
},
|
|
26
|
+
] as MediaDeviceInfo[];
|
|
27
|
+
|
|
28
|
+
export const mockAudioDevices = [
|
|
29
|
+
{
|
|
30
|
+
deviceId: 'default',
|
|
31
|
+
kind: 'audioinput',
|
|
32
|
+
label: 'Default - MacBook Pro Microphone (Built-in)',
|
|
33
|
+
groupId: 'a8708545cb3969c0b7e3712d2ffdbc381992aeb5577b06cd71ba7a94ee20cf3e',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
deviceId:
|
|
37
|
+
'6ce9fde261809389d6476791d329695ea4c98bd432ac5f6a60e7017c96943d32',
|
|
38
|
+
kind: 'audioinput',
|
|
39
|
+
label: 'iPhone Microphone',
|
|
40
|
+
groupId: '65a8661fe6575d0eb180e7ab3f1ec887a285965293b9e75ceed938766f6d32bd',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
deviceId:
|
|
44
|
+
'bb784adda6f0199966c97f8a2ca0fb93169e55cae050e8e8dd4ef76ffd1e638d',
|
|
45
|
+
kind: 'audioinput',
|
|
46
|
+
label: 'HD Pro Webcam C920 (046d:08e5)',
|
|
47
|
+
groupId: 'bb0a866e4d66d493a104d7bd5a5dc2fee3d5947fab34aa0a99282386556a8f17',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
deviceId:
|
|
51
|
+
'45b2467601574a522fe96048a5b2122ea6be0a478e04b990eaa4d93b49e2428a',
|
|
52
|
+
kind: 'audioinput',
|
|
53
|
+
label: 'MacBook Pro Microphone (Built-in)',
|
|
54
|
+
groupId: 'a8708545cb3969c0b7e3712d2ffdbc381992aeb5577b06cd71ba7a94ee20cf3e',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
deviceId:
|
|
58
|
+
'b936c0572d3536857d646af780b8704c368a872e7ba8138ef83d72d726710975',
|
|
59
|
+
kind: 'audioinput',
|
|
60
|
+
label: 'ZoomAudioDevice (Virtual)',
|
|
61
|
+
groupId: '423bb83eab8607fe47313f2e0307600c40e03c37ad63a3b8412d3d5eb0671a55',
|
|
62
|
+
},
|
|
63
|
+
] as MediaDeviceInfo[];
|
|
64
|
+
|
|
65
|
+
export const mockCall = () => {
|
|
66
|
+
return {
|
|
67
|
+
state: {
|
|
68
|
+
callingState: CallingState.IDLE,
|
|
69
|
+
},
|
|
70
|
+
publishVideoStream: vi.fn(),
|
|
71
|
+
publishAudioStream: vi.fn(),
|
|
72
|
+
stopPublish: vi.fn(),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const mockAudioStream = () => {
|
|
77
|
+
const track = {
|
|
78
|
+
getSettings: () => ({
|
|
79
|
+
deviceId: mockAudioDevices[0].deviceId,
|
|
80
|
+
}),
|
|
81
|
+
enabled: true,
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
getAudioTracks: () => [track],
|
|
85
|
+
} as MediaStream;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const mockVideoStream = () => {
|
|
89
|
+
const track = {
|
|
90
|
+
getSettings: () => ({
|
|
91
|
+
deviceId: mockVideoDevices[0].deviceId,
|
|
92
|
+
}),
|
|
93
|
+
enabled: true,
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
getVideoTracks: () => [track],
|
|
97
|
+
} as MediaStream;
|
|
98
|
+
};
|