@stream-io/video-client 0.3.6 → 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 +7 -0
- package/dist/index.browser.es.js +88 -36
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +88 -36
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +88 -36
- 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 +41 -24
- package/src/devices/CameraManager.ts +32 -0
- package/src/devices/InputMediaDeviceManager.ts +27 -4
- package/src/devices/__tests__/CameraManager.test.ts +53 -2
- 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
|
@@ -1718,6 +1718,14 @@ export class Call {
|
|
|
1718
1718
|
};
|
|
1719
1719
|
|
|
1720
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
|
+
|
|
1721
1729
|
if (
|
|
1722
1730
|
this.state.localParticipant?.videoStream ||
|
|
1723
1731
|
!this.permissionsContext.hasPermission('send-video')
|
|
@@ -1726,27 +1734,31 @@ export class Call {
|
|
|
1726
1734
|
}
|
|
1727
1735
|
|
|
1728
1736
|
// Set camera direction if it's not yet set
|
|
1729
|
-
// This will also start publishing if camera is enabled
|
|
1730
1737
|
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
1731
1738
|
let defaultDirection: CameraDirection = 'front';
|
|
1732
1739
|
const backendSetting = this.state.settings?.video.camera_facing;
|
|
1733
1740
|
if (backendSetting) {
|
|
1734
1741
|
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
1735
1742
|
}
|
|
1736
|
-
this.camera.
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
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);
|
|
1747
1759
|
}
|
|
1748
1760
|
|
|
1749
|
-
//
|
|
1761
|
+
// Start camera if backend config speicifies, and there is no local setting
|
|
1750
1762
|
if (
|
|
1751
1763
|
this.camera.state.status === undefined &&
|
|
1752
1764
|
this.state.settings?.video.camera_default_on
|
|
@@ -1756,6 +1768,14 @@ export class Call {
|
|
|
1756
1768
|
}
|
|
1757
1769
|
|
|
1758
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
|
+
|
|
1759
1779
|
if (
|
|
1760
1780
|
this.state.localParticipant?.audioStream ||
|
|
1761
1781
|
!this.permissionsContext.hasPermission('send-audio')
|
|
@@ -1763,19 +1783,16 @@ export class Call {
|
|
|
1763
1783
|
return;
|
|
1764
1784
|
}
|
|
1765
1785
|
|
|
1766
|
-
// Publish
|
|
1767
|
-
if (
|
|
1768
|
-
|
|
1769
|
-
this.microphone.state.mediaStream
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
this.publishAudioStream(stream!);
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
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);
|
|
1776
1793
|
}
|
|
1777
1794
|
|
|
1778
|
-
//
|
|
1795
|
+
// Start mic if backend config speicifies, and there is no local setting
|
|
1779
1796
|
if (
|
|
1780
1797
|
this.microphone.state.status === undefined &&
|
|
1781
1798
|
this.state.settings?.audio.mic_default_on
|
|
@@ -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
|
/**
|
|
@@ -42,8 +56,17 @@ export abstract class InputMediaDeviceManager<
|
|
|
42
56
|
if (this.state.status === 'disabled') {
|
|
43
57
|
return;
|
|
44
58
|
}
|
|
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();
|