@stream-io/video-client 0.3.12 → 0.3.14
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 +614 -391
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +613 -390
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +614 -391
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +6 -1
- package/dist/src/devices/CameraManager.d.ts +1 -2
- package/dist/src/devices/InputMediaDeviceManager.d.ts +10 -3
- package/dist/src/devices/MicrophoneManager.d.ts +1 -2
- package/dist/src/devices/SpeakerManager.d.ts +28 -0
- package/dist/src/devices/SpeakerState.d.ts +64 -0
- package/dist/src/devices/__tests__/SpeakerManager.test.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +6 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +91 -6
- package/src/StreamSfuClient.ts +4 -0
- package/src/coordinator/connection/location.ts +2 -2
- package/src/devices/CameraManager.ts +7 -8
- package/src/devices/InputMediaDeviceManager.ts +58 -12
- package/src/devices/MicrophoneManager.ts +3 -8
- package/src/devices/SpeakerManager.ts +50 -0
- package/src/devices/SpeakerState.ts +90 -0
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +5 -12
- package/src/devices/__tests__/SpeakerManager.test.ts +66 -0
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/helpers/DynascaleManager.ts +25 -7
- package/src/helpers/__tests__/DynascaleManager.test.ts +33 -2
- package/src/rtc/Publisher.ts +35 -7
- package/src/rtc/__tests__/Publisher.test.ts +1 -0
- package/src/rtc/codecs.ts +0 -2
- package/src/types.ts +2 -0
package/dist/src/Call.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { DynascaleManager } from './helpers/DynascaleManager';
|
|
|
7
7
|
import { PermissionsContext } from './permissions';
|
|
8
8
|
import { StreamClient } from './coordinator/connection/client';
|
|
9
9
|
import { CallEventHandler, CallEventTypes, EventTypes, Logger } from './coordinator/connection/types';
|
|
10
|
-
import { CameraManager
|
|
10
|
+
import { CameraManager } from './devices/CameraManager';
|
|
11
|
+
import { MicrophoneManager } from './devices/MicrophoneManager';
|
|
12
|
+
import { SpeakerManager } from './devices/SpeakerManager';
|
|
11
13
|
/**
|
|
12
14
|
* An object representation of a `Call`.
|
|
13
15
|
*/
|
|
@@ -45,6 +47,7 @@ export declare class Call {
|
|
|
45
47
|
* The DynascaleManager instance.
|
|
46
48
|
*/
|
|
47
49
|
readonly dynascaleManager: DynascaleManager;
|
|
50
|
+
readonly speaker: SpeakerManager;
|
|
48
51
|
/**
|
|
49
52
|
* Flag telling whether this call is a "ringing" call.
|
|
50
53
|
*/
|
|
@@ -251,6 +254,8 @@ export declare class Call {
|
|
|
251
254
|
*
|
|
252
255
|
*
|
|
253
256
|
* @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
|
|
257
|
+
*
|
|
258
|
+
* @deprecated use `call.speaker` instead
|
|
254
259
|
*/
|
|
255
260
|
setAudioOutputDevice: (deviceId?: string) => void;
|
|
256
261
|
/**
|
|
@@ -28,6 +28,5 @@ export declare class CameraManager extends InputMediaDeviceManager<CameraManager
|
|
|
28
28
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
29
29
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
30
30
|
protected stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
31
|
-
protected
|
|
32
|
-
protected unmuteTracks(): void;
|
|
31
|
+
protected getTrack(): MediaStreamTrack | undefined;
|
|
33
32
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
4
|
+
import { Logger } from '../coordinator/connection/types';
|
|
5
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
4
6
|
export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState> {
|
|
5
7
|
protected readonly call: Call;
|
|
6
8
|
readonly state: T;
|
|
9
|
+
protected readonly trackType: TrackType;
|
|
7
10
|
/**
|
|
8
11
|
* @internal
|
|
9
12
|
*/
|
|
@@ -12,7 +15,8 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
12
15
|
* @internal
|
|
13
16
|
*/
|
|
14
17
|
disablePromise?: Promise<void>;
|
|
15
|
-
|
|
18
|
+
logger: Logger;
|
|
19
|
+
constructor(call: Call, state: T, trackType: TrackType);
|
|
16
20
|
/**
|
|
17
21
|
* Lists the available audio/video devices
|
|
18
22
|
*
|
|
@@ -54,8 +58,11 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
54
58
|
protected abstract getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
55
59
|
protected abstract publishStream(stream: MediaStream): Promise<void>;
|
|
56
60
|
protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
57
|
-
protected abstract
|
|
58
|
-
protected abstract unmuteTracks(): void;
|
|
61
|
+
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
59
62
|
private muteStream;
|
|
63
|
+
private muteTrack;
|
|
64
|
+
private unmuteTrack;
|
|
65
|
+
private stopTrack;
|
|
66
|
+
private muteLocalStream;
|
|
60
67
|
private unmuteStream;
|
|
61
68
|
}
|
|
@@ -8,6 +8,5 @@ export declare class MicrophoneManager extends InputMediaDeviceManager<Microphon
|
|
|
8
8
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
9
9
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
10
10
|
protected stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
11
|
-
protected
|
|
12
|
-
protected unmuteTracks(): void;
|
|
11
|
+
protected getTrack(): MediaStreamTrack | undefined;
|
|
13
12
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SpeakerState } from './SpeakerState';
|
|
2
|
+
export declare class SpeakerManager {
|
|
3
|
+
readonly state: SpeakerState;
|
|
4
|
+
constructor();
|
|
5
|
+
/**
|
|
6
|
+
* Lists the available audio output devices
|
|
7
|
+
*
|
|
8
|
+
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
9
|
+
*
|
|
10
|
+
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
11
|
+
*/
|
|
12
|
+
listDevices(): import("rxjs").Observable<MediaDeviceInfo[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Select device
|
|
15
|
+
*
|
|
16
|
+
* Note: this method is not supported in React Native
|
|
17
|
+
*
|
|
18
|
+
* @param deviceId empty string means the system default
|
|
19
|
+
*/
|
|
20
|
+
select(deviceId: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Set the volume of the audio elements
|
|
23
|
+
* @param volume a number between 0 and 1
|
|
24
|
+
*
|
|
25
|
+
* Note: this method is not supported in React Native
|
|
26
|
+
*/
|
|
27
|
+
setVolume(volume: number): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
2
|
+
import { RxUtils } from '../store';
|
|
3
|
+
export declare class SpeakerState {
|
|
4
|
+
protected selectedDeviceSubject: BehaviorSubject<string>;
|
|
5
|
+
protected volumeSubject: BehaviorSubject<number>;
|
|
6
|
+
/**
|
|
7
|
+
* [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
|
|
8
|
+
*/
|
|
9
|
+
readonly isDeviceSelectionSupported: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* An Observable that emits the currently selected device
|
|
12
|
+
*
|
|
13
|
+
* Note: this feature is not supported in React Native
|
|
14
|
+
*/
|
|
15
|
+
selectedDevice$: Observable<string>;
|
|
16
|
+
/**
|
|
17
|
+
* An Observable that emits the currently selected volume
|
|
18
|
+
*
|
|
19
|
+
* Note: this feature is not supported in React Native
|
|
20
|
+
*/
|
|
21
|
+
volume$: Observable<number>;
|
|
22
|
+
constructor();
|
|
23
|
+
/**
|
|
24
|
+
* The currently selected device
|
|
25
|
+
*
|
|
26
|
+
* Note: this feature is not supported in React Native
|
|
27
|
+
*/
|
|
28
|
+
get selectedDevice(): string;
|
|
29
|
+
/**
|
|
30
|
+
* The currently selected volume
|
|
31
|
+
*
|
|
32
|
+
* Note: this feature is not supported in React Native
|
|
33
|
+
*/
|
|
34
|
+
get volume(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Gets the current value of an observable, or undefined if the observable has
|
|
37
|
+
* not emitted a value yet.
|
|
38
|
+
*
|
|
39
|
+
* @param observable$ the observable to get the value from.
|
|
40
|
+
*/
|
|
41
|
+
getCurrentValue: <T>(observable$: Observable<T>) => T;
|
|
42
|
+
/**
|
|
43
|
+
* @internal
|
|
44
|
+
* @param deviceId
|
|
45
|
+
*/
|
|
46
|
+
setDevice(deviceId: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* @internal
|
|
49
|
+
* @param volume
|
|
50
|
+
*/
|
|
51
|
+
setVolume(volume: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* Updates the value of the provided Subject.
|
|
54
|
+
* An `update` can either be a new value or a function which takes
|
|
55
|
+
* the current value and returns a new value.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*
|
|
59
|
+
* @param subject the subject to update.
|
|
60
|
+
* @param update the update to apply to the subject.
|
|
61
|
+
* @return the updated value.
|
|
62
|
+
*/
|
|
63
|
+
protected setCurrentValue: <T>(subject: import("rxjs").Subject<T>, update: RxUtils.Patch<T>) => T;
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -85,6 +85,12 @@ export declare class Publisher {
|
|
|
85
85
|
* @param trackType the track type to check.
|
|
86
86
|
*/
|
|
87
87
|
isPublishing: (trackType: TrackType) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if the given track type is currently live
|
|
90
|
+
*
|
|
91
|
+
* @param trackType the track type to check.
|
|
92
|
+
*/
|
|
93
|
+
isLive: (trackType: TrackType) => boolean;
|
|
88
94
|
private notifyTrackMuteStateChanged;
|
|
89
95
|
/**
|
|
90
96
|
* Stops publishing all tracks and stop all tracks.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
|
|
|
77
77
|
* The device ID of the currently selected audio output device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
|
|
78
78
|
*
|
|
79
79
|
* If the value is not defined, the user hasn't selected any device (in these cases the default system audio output could be used)
|
|
80
|
+
*
|
|
81
|
+
* @deprecated use call.speaker.state.selectedDevice
|
|
80
82
|
*/
|
|
81
83
|
audioOutputDeviceId?: string;
|
|
82
84
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.3.
|
|
1
|
+
export declare const version = "0.3.14";
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -116,7 +116,10 @@ import {
|
|
|
116
116
|
} from './coordinator/connection/types';
|
|
117
117
|
import { getClientDetails, getSdkInfo } from './client-details';
|
|
118
118
|
import { getLogger } from './logger';
|
|
119
|
-
import {
|
|
119
|
+
import { CameraManager } from './devices/CameraManager';
|
|
120
|
+
import { MicrophoneManager } from './devices/MicrophoneManager';
|
|
121
|
+
import { CameraDirection } from './devices/CameraManagerState';
|
|
122
|
+
import { SpeakerManager } from './devices/SpeakerManager';
|
|
120
123
|
|
|
121
124
|
/**
|
|
122
125
|
* An object representation of a `Call`.
|
|
@@ -163,6 +166,11 @@ export class Call {
|
|
|
163
166
|
*/
|
|
164
167
|
readonly dynascaleManager = new DynascaleManager(this);
|
|
165
168
|
|
|
169
|
+
/*
|
|
170
|
+
* Device manager for the speaker
|
|
171
|
+
*/
|
|
172
|
+
readonly speaker: SpeakerManager;
|
|
173
|
+
|
|
166
174
|
/**
|
|
167
175
|
* Flag telling whether this call is a "ringing" call.
|
|
168
176
|
*/
|
|
@@ -269,6 +277,38 @@ export class Call {
|
|
|
269
277
|
|
|
270
278
|
this.camera = new CameraManager(this);
|
|
271
279
|
this.microphone = new MicrophoneManager(this);
|
|
280
|
+
|
|
281
|
+
this.state.localParticipant$.subscribe(async (p) => {
|
|
282
|
+
// Mute via device manager
|
|
283
|
+
// If integrator doesn't use device manager, we mute using stopPublish
|
|
284
|
+
if (
|
|
285
|
+
!p?.publishedTracks.includes(TrackType.VIDEO) &&
|
|
286
|
+
this.publisher?.isPublishing(TrackType.VIDEO)
|
|
287
|
+
) {
|
|
288
|
+
this.logger(
|
|
289
|
+
'info',
|
|
290
|
+
`Local participant's video track is muted remotely`,
|
|
291
|
+
);
|
|
292
|
+
await this.camera.disable();
|
|
293
|
+
if (this.publisher.isPublishing(TrackType.VIDEO)) {
|
|
294
|
+
this.stopPublish(TrackType.VIDEO);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!p?.publishedTracks.includes(TrackType.AUDIO) &&
|
|
299
|
+
this.publisher?.isPublishing(TrackType.AUDIO)
|
|
300
|
+
) {
|
|
301
|
+
this.logger(
|
|
302
|
+
'info',
|
|
303
|
+
`Local participant's audio track is muted remotely`,
|
|
304
|
+
);
|
|
305
|
+
await this.microphone.disable();
|
|
306
|
+
if (this.publisher.isPublishing(TrackType.AUDIO)) {
|
|
307
|
+
this.stopPublish(TrackType.AUDIO);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
this.speaker = new SpeakerManager();
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
private registerEffects() {
|
|
@@ -300,10 +340,50 @@ export class Call {
|
|
|
300
340
|
const hasPermission = this.permissionsContext.hasPermission(
|
|
301
341
|
permission as OwnCapability,
|
|
302
342
|
);
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
343
|
+
if (
|
|
344
|
+
!hasPermission &&
|
|
345
|
+
(this.publisher.isPublishing(trackType) ||
|
|
346
|
+
this.publisher.isLive(trackType))
|
|
347
|
+
) {
|
|
348
|
+
// Stop tracks, then notify device manager
|
|
349
|
+
this.stopPublish(trackType)
|
|
350
|
+
.catch((err) => {
|
|
351
|
+
this.logger(
|
|
352
|
+
'error',
|
|
353
|
+
`Error stopping publish ${trackType}`,
|
|
354
|
+
err,
|
|
355
|
+
);
|
|
356
|
+
})
|
|
357
|
+
.then(() => {
|
|
358
|
+
if (
|
|
359
|
+
trackType === TrackType.VIDEO &&
|
|
360
|
+
this.camera.state.status === 'enabled'
|
|
361
|
+
) {
|
|
362
|
+
this.camera
|
|
363
|
+
.disable()
|
|
364
|
+
.catch((err) =>
|
|
365
|
+
this.logger(
|
|
366
|
+
'error',
|
|
367
|
+
`Error disabling camera after pemission revoked`,
|
|
368
|
+
err,
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
trackType === TrackType.AUDIO &&
|
|
374
|
+
this.microphone.state.status === 'enabled'
|
|
375
|
+
) {
|
|
376
|
+
this.microphone
|
|
377
|
+
.disable()
|
|
378
|
+
.catch((err) =>
|
|
379
|
+
this.logger(
|
|
380
|
+
'error',
|
|
381
|
+
`Error disabling microphone after pemission revoked`,
|
|
382
|
+
err,
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
307
387
|
}
|
|
308
388
|
}
|
|
309
389
|
}),
|
|
@@ -1103,7 +1183,10 @@ export class Call {
|
|
|
1103
1183
|
* @param stopTrack if `true` the track will be stopped, else it will be just disabled
|
|
1104
1184
|
*/
|
|
1105
1185
|
stopPublish = async (trackType: TrackType, stopTrack: boolean = true) => {
|
|
1106
|
-
this.logger(
|
|
1186
|
+
this.logger(
|
|
1187
|
+
'info',
|
|
1188
|
+
`stopPublish ${TrackType[trackType]}, stop tracks: ${stopTrack}`,
|
|
1189
|
+
);
|
|
1107
1190
|
await this.publisher?.unpublishStream(trackType, stopTrack);
|
|
1108
1191
|
};
|
|
1109
1192
|
|
|
@@ -1230,6 +1313,8 @@ export class Call {
|
|
|
1230
1313
|
*
|
|
1231
1314
|
*
|
|
1232
1315
|
* @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
|
|
1316
|
+
*
|
|
1317
|
+
* @deprecated use `call.speaker` instead
|
|
1233
1318
|
*/
|
|
1234
1319
|
setAudioOutputDevice = (deviceId?: string) => {
|
|
1235
1320
|
if (!this.sfuClient) return;
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -5,7 +5,7 @@ const HINT_URL = `https://hint.stream-io-video.com/`;
|
|
|
5
5
|
|
|
6
6
|
export const getLocationHint = async (
|
|
7
7
|
hintUrl: string = HINT_URL,
|
|
8
|
-
timeout: number =
|
|
8
|
+
timeout: number = 2000,
|
|
9
9
|
) => {
|
|
10
10
|
const abortController = new AbortController();
|
|
11
11
|
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
@@ -18,7 +18,7 @@ export const getLocationHint = async (
|
|
|
18
18
|
logger('debug', `Location header: ${awsPop}`);
|
|
19
19
|
return awsPop.substring(0, 3); // AMS1-P2 -> AMS
|
|
20
20
|
} catch (e) {
|
|
21
|
-
logger('
|
|
21
|
+
logger('warn', `Failed to get location hint from ${HINT_URL}`, e);
|
|
22
22
|
return 'ERR';
|
|
23
23
|
} finally {
|
|
24
24
|
clearTimeout(timeoutId);
|
|
@@ -12,7 +12,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
constructor(call: Call) {
|
|
15
|
-
super(call, new CameraManagerState());
|
|
15
|
+
super(call, new CameraManagerState(), TrackType.VIDEO);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -59,6 +59,10 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
59
59
|
height !== this.targetResolution.height
|
|
60
60
|
)
|
|
61
61
|
await this.applySettingsToStream();
|
|
62
|
+
this.logger(
|
|
63
|
+
'debug',
|
|
64
|
+
`${width}x${height} target resolution applied to media stream`,
|
|
65
|
+
);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -85,12 +89,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
85
89
|
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
protected
|
|
89
|
-
this.state.mediaStream
|
|
90
|
-
?.getVideoTracks()
|
|
91
|
-
.forEach((t) => (t.enabled = false));
|
|
92
|
-
}
|
|
93
|
-
protected unmuteTracks(): void {
|
|
94
|
-
this.state.mediaStream?.getVideoTracks().forEach((t) => (t.enabled = true));
|
|
92
|
+
protected getTrack() {
|
|
93
|
+
return this.state.mediaStream?.getVideoTracks()[0];
|
|
95
94
|
}
|
|
96
95
|
}
|
|
@@ -2,8 +2,10 @@ import { Observable } from 'rxjs';
|
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { CallingState } from '../store';
|
|
4
4
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
5
|
-
import { disposeOfMediaStream } from './devices';
|
|
6
5
|
import { isReactNative } from '../helpers/platforms';
|
|
6
|
+
import { Logger } from '../coordinator/connection/types';
|
|
7
|
+
import { getLogger } from '../logger';
|
|
8
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
9
|
|
|
8
10
|
export abstract class InputMediaDeviceManager<
|
|
9
11
|
T extends InputMediaDeviceManagerState,
|
|
@@ -16,7 +18,15 @@ export abstract class InputMediaDeviceManager<
|
|
|
16
18
|
* @internal
|
|
17
19
|
*/
|
|
18
20
|
disablePromise?: Promise<void>;
|
|
19
|
-
|
|
21
|
+
logger: Logger;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
protected readonly call: Call,
|
|
25
|
+
public readonly state: T,
|
|
26
|
+
protected readonly trackType: TrackType,
|
|
27
|
+
) {
|
|
28
|
+
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Lists the available audio/video devices
|
|
@@ -129,32 +139,68 @@ export abstract class InputMediaDeviceManager<
|
|
|
129
139
|
|
|
130
140
|
protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
131
141
|
|
|
132
|
-
protected abstract
|
|
133
|
-
|
|
134
|
-
protected abstract unmuteTracks(): void;
|
|
142
|
+
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
135
143
|
|
|
136
144
|
private async muteStream(stopTracks: boolean = true) {
|
|
137
145
|
if (!this.state.mediaStream) {
|
|
138
146
|
return;
|
|
139
147
|
}
|
|
148
|
+
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
140
149
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
141
150
|
await this.stopPublishStream(stopTracks);
|
|
142
|
-
} else if (this.state.mediaStream) {
|
|
143
|
-
stopTracks
|
|
144
|
-
? disposeOfMediaStream(this.state.mediaStream)
|
|
145
|
-
: this.muteTracks();
|
|
146
151
|
}
|
|
147
|
-
|
|
152
|
+
this.muteLocalStream(stopTracks);
|
|
153
|
+
if (this.getTrack()?.readyState === 'ended') {
|
|
154
|
+
// @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
|
|
155
|
+
if (typeof this.state.mediaStream.release === 'function') {
|
|
156
|
+
// @ts-expect-error
|
|
157
|
+
this.state.mediaStream.release();
|
|
158
|
+
}
|
|
148
159
|
this.state.setMediaStream(undefined);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
private muteTrack() {
|
|
164
|
+
const track = this.getTrack();
|
|
165
|
+
if (!track || !track.enabled) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
track.enabled = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private unmuteTrack() {
|
|
172
|
+
const track = this.getTrack();
|
|
173
|
+
if (!track || track.enabled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
track.enabled = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private stopTrack() {
|
|
180
|
+
const track = this.getTrack();
|
|
181
|
+
if (!track || track.readyState === 'ended') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
track.stop();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private muteLocalStream(stopTracks: boolean) {
|
|
188
|
+
if (!this.state.mediaStream) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
stopTracks ? this.stopTrack() : this.muteTrack();
|
|
192
|
+
}
|
|
193
|
+
|
|
152
194
|
private async unmuteStream() {
|
|
195
|
+
this.logger('debug', 'Starting stream');
|
|
153
196
|
let stream: MediaStream;
|
|
154
|
-
if (this.state.mediaStream) {
|
|
197
|
+
if (this.state.mediaStream && this.getTrack()?.readyState === 'live') {
|
|
155
198
|
stream = this.state.mediaStream;
|
|
156
|
-
this.
|
|
199
|
+
this.unmuteTrack();
|
|
157
200
|
} else {
|
|
201
|
+
if (this.state.mediaStream) {
|
|
202
|
+
this.stopTrack();
|
|
203
|
+
}
|
|
158
204
|
const constraints = { deviceId: this.state.selectedDevice };
|
|
159
205
|
stream = await this.getStream(constraints);
|
|
160
206
|
}
|
|
@@ -7,7 +7,7 @@ import { TrackType } from '../gen/video/sfu/models/models';
|
|
|
7
7
|
|
|
8
8
|
export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
|
|
9
9
|
constructor(call: Call) {
|
|
10
|
-
super(call, new MicrophoneManagerState());
|
|
10
|
+
super(call, new MicrophoneManagerState(), TrackType.AUDIO);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
@@ -25,12 +25,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
25
25
|
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
protected
|
|
29
|
-
this.state.mediaStream
|
|
30
|
-
?.getAudioTracks()
|
|
31
|
-
.forEach((t) => (t.enabled = false));
|
|
32
|
-
}
|
|
33
|
-
protected unmuteTracks(): void {
|
|
34
|
-
this.state.mediaStream?.getAudioTracks().forEach((t) => (t.enabled = true));
|
|
28
|
+
protected getTrack() {
|
|
29
|
+
return this.state.mediaStream?.getAudioTracks()[0];
|
|
35
30
|
}
|
|
36
31
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isReactNative } from '../helpers/platforms';
|
|
2
|
+
import { SpeakerState } from './SpeakerState';
|
|
3
|
+
import { getAudioOutputDevices } from './devices';
|
|
4
|
+
|
|
5
|
+
export class SpeakerManager {
|
|
6
|
+
public readonly state = new SpeakerState();
|
|
7
|
+
|
|
8
|
+
constructor() {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lists the available audio output devices
|
|
12
|
+
*
|
|
13
|
+
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
14
|
+
*
|
|
15
|
+
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
16
|
+
*/
|
|
17
|
+
listDevices() {
|
|
18
|
+
return getAudioOutputDevices();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Select device
|
|
23
|
+
*
|
|
24
|
+
* Note: this method is not supported in React Native
|
|
25
|
+
*
|
|
26
|
+
* @param deviceId empty string means the system default
|
|
27
|
+
*/
|
|
28
|
+
select(deviceId: string) {
|
|
29
|
+
if (isReactNative()) {
|
|
30
|
+
throw new Error('This feature is not supported in React Native');
|
|
31
|
+
}
|
|
32
|
+
this.state.setDevice(deviceId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set the volume of the audio elements
|
|
37
|
+
* @param volume a number between 0 and 1
|
|
38
|
+
*
|
|
39
|
+
* Note: this method is not supported in React Native
|
|
40
|
+
*/
|
|
41
|
+
setVolume(volume: number) {
|
|
42
|
+
if (isReactNative()) {
|
|
43
|
+
throw new Error('This feature is not supported in React Native');
|
|
44
|
+
}
|
|
45
|
+
if (volume && (volume < 0 || volume > 1)) {
|
|
46
|
+
throw new Error('Volume must be between 0 and 1');
|
|
47
|
+
}
|
|
48
|
+
this.state.setVolume(volume);
|
|
49
|
+
}
|
|
50
|
+
}
|