@stream-io/video-client 0.3.5 → 0.3.7
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 +122 -61
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +122 -61
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +122 -61
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/CameraManager.d.ts +8 -0
- package/dist/src/devices/InputMediaDeviceManager.d.ts +8 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +51 -30
- package/src/devices/CameraManager.ts +32 -0
- package/src/devices/InputMediaDeviceManager.ts +28 -5
- package/src/devices/__tests__/CameraManager.test.ts +53 -2
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +17 -0
- package/src/devices/__tests__/mocks.ts +2 -0
|
@@ -3,6 +3,7 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
4
|
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
5
5
|
export declare class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
6
|
+
private targetResolution;
|
|
6
7
|
constructor(call: Call);
|
|
7
8
|
/**
|
|
8
9
|
* Select the camera direaction
|
|
@@ -16,6 +17,13 @@ export declare class CameraManager extends InputMediaDeviceManager<CameraManager
|
|
|
16
17
|
* @returns
|
|
17
18
|
*/
|
|
18
19
|
flip(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
selectTargetResolution(resolution: {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}): Promise<void>;
|
|
19
27
|
protected getDevices(): Observable<MediaDeviceInfo[]>;
|
|
20
28
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
21
29
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
@@ -4,6 +4,14 @@ import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
|
4
4
|
export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState> {
|
|
5
5
|
protected readonly call: Call;
|
|
6
6
|
readonly state: T;
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
enablePromise?: Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
disablePromise?: Promise<void>;
|
|
7
15
|
constructor(call: Call, state: T);
|
|
8
16
|
/**
|
|
9
17
|
* Lists the available audio/video devices
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.3.
|
|
1
|
+
export declare const version = "0.3.7";
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -926,8 +926,12 @@ export class Call {
|
|
|
926
926
|
|
|
927
927
|
// React uses a different device management for now
|
|
928
928
|
if (getSdkInfo()?.type !== SdkType.REACT) {
|
|
929
|
-
|
|
930
|
-
|
|
929
|
+
try {
|
|
930
|
+
await this.initCamera();
|
|
931
|
+
await this.initMic();
|
|
932
|
+
} catch (error) {
|
|
933
|
+
this.logger('warn', 'Camera and/or mic init failed during join call');
|
|
934
|
+
}
|
|
931
935
|
}
|
|
932
936
|
|
|
933
937
|
// 3. once we have the "joinResponse", and possibly reconciled the local state
|
|
@@ -1713,7 +1717,15 @@ export class Call {
|
|
|
1713
1717
|
);
|
|
1714
1718
|
};
|
|
1715
1719
|
|
|
1716
|
-
private initCamera() {
|
|
1720
|
+
private async initCamera() {
|
|
1721
|
+
// Wait for any in progress camera operation
|
|
1722
|
+
if (this.camera.enablePromise) {
|
|
1723
|
+
await this.camera.enablePromise;
|
|
1724
|
+
}
|
|
1725
|
+
if (this.camera.disablePromise) {
|
|
1726
|
+
await this.camera.disablePromise;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1717
1729
|
if (
|
|
1718
1730
|
this.state.localParticipant?.videoStream ||
|
|
1719
1731
|
!this.permissionsContext.hasPermission('send-video')
|
|
@@ -1722,36 +1734,48 @@ export class Call {
|
|
|
1722
1734
|
}
|
|
1723
1735
|
|
|
1724
1736
|
// Set camera direction if it's not yet set
|
|
1725
|
-
// This will also start publishing if camera is enabled
|
|
1726
1737
|
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
1727
1738
|
let defaultDirection: CameraDirection = 'front';
|
|
1728
1739
|
const backendSetting = this.state.settings?.video.camera_facing;
|
|
1729
1740
|
if (backendSetting) {
|
|
1730
1741
|
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
1731
1742
|
}
|
|
1732
|
-
this.camera.
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
if (!this.state.localParticipant?.videoStream) {
|
|
1740
|
-
this.publishVideoStream(stream!);
|
|
1741
|
-
}
|
|
1742
|
-
});
|
|
1743
|
+
this.camera.state.setDirection(defaultDirection);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Set target resolution
|
|
1747
|
+
const targetResolution = this.state.settings?.video.target_resolution;
|
|
1748
|
+
if (targetResolution) {
|
|
1749
|
+
await this.camera.selectTargetResolution(targetResolution);
|
|
1743
1750
|
}
|
|
1744
1751
|
|
|
1745
|
-
//
|
|
1752
|
+
// Publish already that was set before we joined
|
|
1753
|
+
if (
|
|
1754
|
+
this.camera.state.status === 'enabled' &&
|
|
1755
|
+
this.camera.state.mediaStream &&
|
|
1756
|
+
!this.publisher?.isPublishing(TrackType.VIDEO)
|
|
1757
|
+
) {
|
|
1758
|
+
await this.publishVideoStream(this.camera.state.mediaStream);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Start camera if backend config speicifies, and there is no local setting
|
|
1746
1762
|
if (
|
|
1747
1763
|
this.camera.state.status === undefined &&
|
|
1748
1764
|
this.state.settings?.video.camera_default_on
|
|
1749
1765
|
) {
|
|
1750
|
-
|
|
1766
|
+
await this.camera.enable();
|
|
1751
1767
|
}
|
|
1752
1768
|
}
|
|
1753
1769
|
|
|
1754
|
-
private initMic() {
|
|
1770
|
+
private async initMic() {
|
|
1771
|
+
// Wait for any in progress mic operation
|
|
1772
|
+
if (this.microphone.enablePromise) {
|
|
1773
|
+
await this.microphone.enablePromise;
|
|
1774
|
+
}
|
|
1775
|
+
if (this.microphone.disablePromise) {
|
|
1776
|
+
await this.microphone.disablePromise;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1755
1779
|
if (
|
|
1756
1780
|
this.state.localParticipant?.audioStream ||
|
|
1757
1781
|
!this.permissionsContext.hasPermission('send-audio')
|
|
@@ -1759,24 +1783,21 @@ export class Call {
|
|
|
1759
1783
|
return;
|
|
1760
1784
|
}
|
|
1761
1785
|
|
|
1762
|
-
// Publish
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
this.microphone.state.mediaStream
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
this.publishAudioStream(stream!);
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1786
|
+
// Publish media stream that was set before we joined
|
|
1787
|
+
if (
|
|
1788
|
+
this.microphone.state.status === 'enabled' &&
|
|
1789
|
+
this.microphone.state.mediaStream &&
|
|
1790
|
+
!this.publisher?.isPublishing(TrackType.AUDIO)
|
|
1791
|
+
) {
|
|
1792
|
+
this.publishAudioStream(this.microphone.state.mediaStream);
|
|
1772
1793
|
}
|
|
1773
1794
|
|
|
1774
|
-
//
|
|
1795
|
+
// Start mic if backend config speicifies, and there is no local setting
|
|
1775
1796
|
if (
|
|
1776
1797
|
this.microphone.state.status === undefined &&
|
|
1777
1798
|
this.state.settings?.audio.mic_default_on
|
|
1778
1799
|
) {
|
|
1779
|
-
|
|
1800
|
+
await this.microphone.enable();
|
|
1780
1801
|
}
|
|
1781
1802
|
}
|
|
1782
1803
|
}
|
|
@@ -6,6 +6,11 @@ import { getVideoDevices, getVideoStream } from './devices';
|
|
|
6
6
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
7
|
|
|
8
8
|
export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
9
|
+
private targetResolution = {
|
|
10
|
+
width: 1280,
|
|
11
|
+
height: 720,
|
|
12
|
+
};
|
|
13
|
+
|
|
9
14
|
constructor(call: Call) {
|
|
10
15
|
super(call, new CameraManagerState());
|
|
11
16
|
}
|
|
@@ -32,12 +37,39 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
32
37
|
this.selectDirection(newDirection);
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
async selectTargetResolution(resolution: { width: number; height: number }) {
|
|
44
|
+
this.targetResolution.height = resolution.height;
|
|
45
|
+
this.targetResolution.width = resolution.width;
|
|
46
|
+
if (this.enablePromise) {
|
|
47
|
+
try {
|
|
48
|
+
await this.enablePromise;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// couldn't enable device, target resolution will be applied the next time user attempts to start the device
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (this.state.status === 'enabled') {
|
|
54
|
+
const { width, height } = this.state
|
|
55
|
+
.mediaStream!.getVideoTracks()[0]
|
|
56
|
+
?.getSettings();
|
|
57
|
+
if (
|
|
58
|
+
width !== this.targetResolution.width ||
|
|
59
|
+
height !== this.targetResolution.height
|
|
60
|
+
)
|
|
61
|
+
await this.applySettingsToStream();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
35
65
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
36
66
|
return getVideoDevices();
|
|
37
67
|
}
|
|
38
68
|
protected getStream(
|
|
39
69
|
constraints: MediaTrackConstraints,
|
|
40
70
|
): Promise<MediaStream> {
|
|
71
|
+
constraints.width = this.targetResolution.width;
|
|
72
|
+
constraints.height = this.targetResolution.height;
|
|
41
73
|
// We can't set both device id and facing mode
|
|
42
74
|
// Device id has higher priority
|
|
43
75
|
if (!constraints.deviceId && this.state.direction) {
|
|
@@ -8,6 +8,14 @@ import { isReactNative } from '../helpers/platforms';
|
|
|
8
8
|
export abstract class InputMediaDeviceManager<
|
|
9
9
|
T extends InputMediaDeviceManagerState,
|
|
10
10
|
> {
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
enablePromise?: Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
disablePromise?: Promise<void>;
|
|
11
19
|
constructor(protected readonly call: Call, public readonly state: T) {}
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -28,8 +36,14 @@ export abstract class InputMediaDeviceManager<
|
|
|
28
36
|
if (this.state.status === 'enabled') {
|
|
29
37
|
return;
|
|
30
38
|
}
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
this.enablePromise = this.unmuteStream();
|
|
40
|
+
try {
|
|
41
|
+
await this.enablePromise;
|
|
42
|
+
this.state.setStatus('enabled');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.enablePromise = undefined;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
/**
|
|
@@ -38,12 +52,21 @@ export abstract class InputMediaDeviceManager<
|
|
|
38
52
|
* @returns
|
|
39
53
|
*/
|
|
40
54
|
async disable() {
|
|
55
|
+
this.state.prevStatus = this.state.status;
|
|
41
56
|
if (this.state.status === 'disabled') {
|
|
42
57
|
return;
|
|
43
58
|
}
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
this.disablePromise = this.muteStream(
|
|
60
|
+
this.state.disableMode === 'stop-tracks',
|
|
61
|
+
);
|
|
62
|
+
try {
|
|
63
|
+
await this.disablePromise;
|
|
64
|
+
this.state.setStatus('disabled');
|
|
65
|
+
this.disablePromise = undefined;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.disablePromise = undefined;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
/**
|
|
@@ -53,6 +53,8 @@ describe('CameraManager', () => {
|
|
|
53
53
|
|
|
54
54
|
expect(getVideoStream).toHaveBeenCalledWith({
|
|
55
55
|
deviceId: undefined,
|
|
56
|
+
width: 1280,
|
|
57
|
+
height: 720,
|
|
56
58
|
});
|
|
57
59
|
});
|
|
58
60
|
|
|
@@ -101,12 +103,18 @@ describe('CameraManager', () => {
|
|
|
101
103
|
|
|
102
104
|
await manager.enable();
|
|
103
105
|
|
|
104
|
-
expect(getVideoStream).toHaveBeenCalledWith({
|
|
106
|
+
expect(getVideoStream).toHaveBeenCalledWith({
|
|
107
|
+
deviceId: undefined,
|
|
108
|
+
width: 1280,
|
|
109
|
+
height: 720,
|
|
110
|
+
});
|
|
105
111
|
|
|
106
112
|
await manager.selectDirection('front');
|
|
107
113
|
|
|
108
114
|
expect(getVideoStream).toHaveBeenCalledWith({
|
|
109
115
|
deviceId: undefined,
|
|
116
|
+
width: 1280,
|
|
117
|
+
height: 720,
|
|
110
118
|
facingMode: 'user',
|
|
111
119
|
});
|
|
112
120
|
|
|
@@ -115,6 +123,8 @@ describe('CameraManager', () => {
|
|
|
115
123
|
expect(getVideoStream).toHaveBeenCalledWith({
|
|
116
124
|
deviceId: undefined,
|
|
117
125
|
facingMode: 'environment',
|
|
126
|
+
width: 1280,
|
|
127
|
+
height: 720,
|
|
118
128
|
});
|
|
119
129
|
});
|
|
120
130
|
|
|
@@ -123,16 +133,57 @@ describe('CameraManager', () => {
|
|
|
123
133
|
|
|
124
134
|
await manager.flip();
|
|
125
135
|
|
|
126
|
-
expect(getVideoStream).toHaveBeenCalledWith({
|
|
136
|
+
expect(getVideoStream).toHaveBeenCalledWith({
|
|
137
|
+
facingMode: 'environment',
|
|
138
|
+
width: 1280,
|
|
139
|
+
height: 720,
|
|
140
|
+
});
|
|
127
141
|
|
|
128
142
|
const deviceId = mockVideoDevices[1].deviceId;
|
|
129
143
|
await manager.select(deviceId);
|
|
130
144
|
|
|
131
145
|
expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
|
|
132
146
|
deviceId,
|
|
147
|
+
width: 1280,
|
|
148
|
+
height: 720,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it(`should set target resolution, but shouldn't change device status`, async () => {
|
|
153
|
+
manager['targetResolution'] = { width: 640, height: 480 };
|
|
154
|
+
|
|
155
|
+
expect(manager.state.status).toBe(undefined);
|
|
156
|
+
|
|
157
|
+
await manager.selectTargetResolution({ width: 1280, height: 720 });
|
|
158
|
+
|
|
159
|
+
const targetResolution = manager['targetResolution'];
|
|
160
|
+
|
|
161
|
+
expect(targetResolution.width).toBe(1280);
|
|
162
|
+
expect(targetResolution.height).toBe(720);
|
|
163
|
+
expect(manager.state.status).toBe(undefined);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should apply target resolution to existing media stream track', async () => {
|
|
167
|
+
await manager.enable();
|
|
168
|
+
await manager.selectTargetResolution({ width: 640, height: 480 });
|
|
169
|
+
|
|
170
|
+
expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
|
|
171
|
+
deviceId: mockVideoDevices[0].deviceId,
|
|
172
|
+
width: 640,
|
|
173
|
+
height: 480,
|
|
133
174
|
});
|
|
134
175
|
});
|
|
135
176
|
|
|
177
|
+
it(`should do nothing if existing track has the correct resolution`, async () => {
|
|
178
|
+
await manager.enable();
|
|
179
|
+
|
|
180
|
+
expect(getVideoStream).toHaveBeenCalledOnce();
|
|
181
|
+
|
|
182
|
+
await manager.selectTargetResolution({ width: 1280, height: 720 });
|
|
183
|
+
|
|
184
|
+
expect(getVideoStream).toHaveBeenCalledOnce();
|
|
185
|
+
});
|
|
186
|
+
|
|
136
187
|
afterEach(() => {
|
|
137
188
|
vi.clearAllMocks();
|
|
138
189
|
vi.resetModules();
|
|
@@ -195,6 +195,23 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
195
195
|
expect(manager.enable).not.toHaveBeenCalled();
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it(`shouldn't resume if it were disabled while in pause`, async () => {
|
|
199
|
+
vi.spyOn(manager, 'enable');
|
|
200
|
+
|
|
201
|
+
await manager.enable();
|
|
202
|
+
|
|
203
|
+
expect(manager.enable).toHaveBeenCalledOnce();
|
|
204
|
+
|
|
205
|
+
// first call is pause
|
|
206
|
+
await manager.disable();
|
|
207
|
+
// second call is for example mute from call admin
|
|
208
|
+
await manager.disable();
|
|
209
|
+
|
|
210
|
+
await manager.resume();
|
|
211
|
+
|
|
212
|
+
expect(manager.enable).toHaveBeenCalledOnce();
|
|
213
|
+
});
|
|
214
|
+
|
|
198
215
|
afterEach(() => {
|
|
199
216
|
vi.clearAllMocks();
|
|
200
217
|
vi.resetModules();
|