@stream-io/video-client 0.3.15 → 0.3.17
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 +528 -466
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +529 -465
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +528 -466
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -8
- package/dist/src/coordinator/connection/types.d.ts +0 -4
- package/dist/src/devices/InputMediaDeviceManager.d.ts +2 -2
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +2 -2
- package/dist/src/devices/MicrophoneManager.d.ts +3 -0
- package/dist/src/devices/MicrophoneManagerState.d.ts +18 -0
- package/dist/src/devices/SpeakerManager.d.ts +0 -1
- package/dist/src/devices/__tests__/mocks.d.ts +2 -4
- package/dist/src/devices/index.d.ts +2 -0
- package/dist/src/events/__tests__/mutes.test.d.ts +1 -0
- package/dist/src/events/index.d.ts +1 -0
- package/dist/src/events/mutes.d.ts +7 -0
- package/dist/src/rtc/Publisher.d.ts +2 -21
- package/dist/version.d.ts +1 -1
- package/package.json +5 -5
- package/src/Call.ts +17 -50
- package/src/coordinator/connection/types.ts +0 -4
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +7 -5
- package/src/devices/InputMediaDeviceManagerState.ts +3 -3
- package/src/devices/MicrophoneManager.ts +56 -1
- package/src/devices/MicrophoneManagerState.ts +30 -0
- package/src/devices/SpeakerManager.ts +0 -2
- package/src/devices/__tests__/CameraManager.test.ts +3 -5
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -7
- package/src/devices/__tests__/MicrophoneManager.test.ts +77 -6
- package/src/devices/__tests__/mocks.ts +14 -5
- package/src/devices/index.ts +2 -0
- package/src/events/__tests__/mutes.test.ts +133 -0
- package/src/events/callEventHandlers.ts +3 -0
- package/src/events/index.ts +1 -0
- package/src/events/mutes.ts +48 -0
- package/src/helpers/sound-detector.ts +7 -1
- package/src/rtc/Publisher.ts +2 -28
package/dist/src/Call.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SfuEventKinds, SfuEventListener } from './rtc';
|
|
1
|
+
import { Publisher, SfuEventKinds, SfuEventListener, Subscriber } from './rtc';
|
|
2
2
|
import { TrackType } from './gen/video/sfu/models/models';
|
|
3
3
|
import { CallState } from './store';
|
|
4
4
|
import { AcceptCallResponse, BlockUserResponse, EndCallResponse, GetCallResponse, GetOrCreateCallRequest, GetOrCreateCallResponse, GoLiveRequest, GoLiveResponse, ListRecordingsResponse, MuteUsersResponse, PinRequest, PinResponse, QueryMembersRequest, QueryMembersResponse, RejectCallResponse, RequestPermissionRequest, RequestPermissionResponse, SendEventResponse, SendReactionRequest, SendReactionResponse, StartBroadcastingResponse, StartRecordingResponse, StopBroadcastingResponse, StopLiveResponse, StopRecordingResponse, UnblockUserResponse, UnpinRequest, UnpinResponse, UpdateCallMembersRequest, UpdateCallMembersResponse, UpdateCallRequest, UpdateCallResponse, UpdateUserPermissionsRequest, UpdateUserPermissionsResponse } from './gen/coordinator';
|
|
@@ -7,9 +7,7 @@ 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 } from './devices
|
|
11
|
-
import { MicrophoneManager } from './devices/MicrophoneManager';
|
|
12
|
-
import { SpeakerManager } from './devices/SpeakerManager';
|
|
10
|
+
import { CameraManager, MicrophoneManager, SpeakerManager } from './devices';
|
|
13
11
|
/**
|
|
14
12
|
* An object representation of a `Call`.
|
|
15
13
|
*/
|
|
@@ -40,14 +38,19 @@ export declare class Call {
|
|
|
40
38
|
*/
|
|
41
39
|
readonly camera: CameraManager;
|
|
42
40
|
/**
|
|
43
|
-
* Device manager for the
|
|
41
|
+
* Device manager for the microphone.
|
|
44
42
|
*/
|
|
45
43
|
readonly microphone: MicrophoneManager;
|
|
44
|
+
/**
|
|
45
|
+
* Device manager for the speaker.
|
|
46
|
+
*/
|
|
47
|
+
readonly speaker: SpeakerManager;
|
|
46
48
|
/**
|
|
47
49
|
* The DynascaleManager instance.
|
|
48
50
|
*/
|
|
49
51
|
readonly dynascaleManager: DynascaleManager;
|
|
50
|
-
|
|
52
|
+
subscriber?: Subscriber;
|
|
53
|
+
publisher?: Publisher;
|
|
51
54
|
/**
|
|
52
55
|
* Flag telling whether this call is a "ringing" call.
|
|
53
56
|
*/
|
|
@@ -62,8 +65,6 @@ export declare class Call {
|
|
|
62
65
|
* @private
|
|
63
66
|
*/
|
|
64
67
|
private readonly dispatcher;
|
|
65
|
-
private subscriber?;
|
|
66
|
-
private publisher?;
|
|
67
68
|
private trackSubscriptionsSubject;
|
|
68
69
|
private statsReporter?;
|
|
69
70
|
private dropTimeout;
|
|
@@ -98,10 +98,6 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
98
98
|
secret?: string;
|
|
99
99
|
warmUp?: boolean;
|
|
100
100
|
wsConnection?: StableWSConnection;
|
|
101
|
-
/**
|
|
102
|
-
* The preferred video codec to use.
|
|
103
|
-
*/
|
|
104
|
-
preferredVideoCodec?: string;
|
|
105
101
|
};
|
|
106
102
|
export type TokenProvider = () => Promise<string>;
|
|
107
103
|
export type TokenOrProvider = null | string | TokenProvider | undefined;
|
|
@@ -59,10 +59,10 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
59
59
|
protected abstract publishStream(stream: MediaStream): Promise<void>;
|
|
60
60
|
protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
61
61
|
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
62
|
-
|
|
62
|
+
protected muteStream(stopTracks?: boolean): Promise<void>;
|
|
63
63
|
private muteTrack;
|
|
64
64
|
private unmuteTrack;
|
|
65
65
|
private stopTrack;
|
|
66
66
|
private muteLocalStream;
|
|
67
|
-
|
|
67
|
+
protected unmuteStream(): Promise<void>;
|
|
68
68
|
}
|
|
@@ -50,12 +50,12 @@ export declare abstract class InputMediaDeviceManagerState {
|
|
|
50
50
|
setStatus(status: InputDeviceStatus): void;
|
|
51
51
|
/**
|
|
52
52
|
* @internal
|
|
53
|
-
* @param stream
|
|
53
|
+
* @param stream the stream to set.
|
|
54
54
|
*/
|
|
55
55
|
setMediaStream(stream: MediaStream | undefined): void;
|
|
56
56
|
/**
|
|
57
57
|
* @internal
|
|
58
|
-
* @param
|
|
58
|
+
* @param deviceId the device id to set.
|
|
59
59
|
*/
|
|
60
60
|
setDevice(deviceId: string | undefined): void;
|
|
61
61
|
/**
|
|
@@ -3,10 +3,13 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
4
4
|
import { MicrophoneManagerState } from './MicrophoneManagerState';
|
|
5
5
|
export declare class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
|
|
6
|
+
private soundDetectorCleanup?;
|
|
6
7
|
constructor(call: Call);
|
|
7
8
|
protected getDevices(): Observable<MediaDeviceInfo[]>;
|
|
8
9
|
protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
|
|
9
10
|
protected publishStream(stream: MediaStream): Promise<void>;
|
|
10
11
|
protected stopPublishStream(stopTracks: boolean): Promise<void>;
|
|
11
12
|
protected getTrack(): MediaStreamTrack | undefined;
|
|
13
|
+
private startSpeakingWhileMutedDetection;
|
|
14
|
+
private stopSpeakingWhileMutedDetection;
|
|
12
15
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
1
2
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
2
3
|
export declare class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
4
|
+
private speakingWhileMutedSubject;
|
|
5
|
+
/**
|
|
6
|
+
* An Observable that emits `true` if the user's microphone is muted but they'are speaking.
|
|
7
|
+
*
|
|
8
|
+
* This feature is not available in the React Native SDK.
|
|
9
|
+
*/
|
|
10
|
+
speakingWhileMuted$: Observable<boolean>;
|
|
3
11
|
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* `true` if the user's microphone is muted but they'are speaking.
|
|
14
|
+
*
|
|
15
|
+
* This feature is not available in the React Native SDK.
|
|
16
|
+
*/
|
|
17
|
+
get speakingWhileMuted(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
setSpeakingWhileMuted(isSpeaking: boolean): void;
|
|
4
22
|
protected getDeviceIdFromStream(stream: MediaStream): string | undefined;
|
|
5
23
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CallState } from '../../store';
|
|
2
2
|
export declare const mockVideoDevices: MediaDeviceInfo[];
|
|
3
3
|
export declare const mockAudioDevices: MediaDeviceInfo[];
|
|
4
4
|
export declare const mockCall: () => {
|
|
5
|
-
state:
|
|
6
|
-
callingState: CallingState;
|
|
7
|
-
};
|
|
5
|
+
state: CallState;
|
|
8
6
|
publishVideoStream: import("@vitest/spy").Mock<any, any>;
|
|
9
7
|
publishAudioStream: import("@vitest/spy").Mock<any, any>;
|
|
10
8
|
stopPublish: import("@vitest/spy").Mock<any, any>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,14 +3,13 @@ import { TrackInfo, TrackType } from '../gen/video/sfu/models/models';
|
|
|
3
3
|
import { CallState } from '../store';
|
|
4
4
|
import { PublishOptions } from '../types';
|
|
5
5
|
import { Dispatcher } from './Dispatcher';
|
|
6
|
-
export type
|
|
6
|
+
export type PublisherConstructorOpts = {
|
|
7
7
|
sfuClient: StreamSfuClient;
|
|
8
8
|
state: CallState;
|
|
9
9
|
dispatcher: Dispatcher;
|
|
10
10
|
connectionConfig?: RTCConfiguration;
|
|
11
11
|
isDtxEnabled: boolean;
|
|
12
12
|
isRedEnabled: boolean;
|
|
13
|
-
preferredVideoCodec?: string;
|
|
14
13
|
iceRestartDelay?: number;
|
|
15
14
|
};
|
|
16
15
|
/**
|
|
@@ -31,17 +30,8 @@ export declare class Publisher {
|
|
|
31
30
|
private transceiverInitOrder;
|
|
32
31
|
private readonly trackKindMapping;
|
|
33
32
|
private readonly trackLayersCache;
|
|
34
|
-
/**
|
|
35
|
-
* A map keeping track of track types that were published to the SFU.
|
|
36
|
-
* This map shouldn't be cleared when unpublishing a track, as it is used
|
|
37
|
-
* to determine whether a track was published before.
|
|
38
|
-
*
|
|
39
|
-
* @private
|
|
40
|
-
*/
|
|
41
|
-
private readonly trackTypePublishHistory;
|
|
42
33
|
private readonly isDtxEnabled;
|
|
43
34
|
private readonly isRedEnabled;
|
|
44
|
-
private readonly preferredVideoCodec?;
|
|
45
35
|
private readonly unsubscribeOnIceRestart;
|
|
46
36
|
private readonly iceRestartDelay;
|
|
47
37
|
private isIceRestarting;
|
|
@@ -58,10 +48,9 @@ export declare class Publisher {
|
|
|
58
48
|
* @param dispatcher the dispatcher to use.
|
|
59
49
|
* @param isDtxEnabled whether DTX is enabled.
|
|
60
50
|
* @param isRedEnabled whether RED is enabled.
|
|
61
|
-
* @param preferredVideoCodec the preferred video codec.
|
|
62
51
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
|
|
63
52
|
*/
|
|
64
|
-
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled,
|
|
53
|
+
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay, }: PublisherConstructorOpts);
|
|
65
54
|
private createPeerConnection;
|
|
66
55
|
/**
|
|
67
56
|
* Closes the publisher PeerConnection and cleans up the resources.
|
|
@@ -93,14 +82,6 @@ export declare class Publisher {
|
|
|
93
82
|
* @param trackType the track type to check.
|
|
94
83
|
*/
|
|
95
84
|
isPublishing: (trackType: TrackType) => boolean;
|
|
96
|
-
/**
|
|
97
|
-
* Returns true if the given track type was ever published to the SFU.
|
|
98
|
-
* Contrary to `isPublishing`, this method returns true if a certain
|
|
99
|
-
* track type was published before, even if it is currently unpublished.
|
|
100
|
-
*
|
|
101
|
-
* @param trackType the track type to check.
|
|
102
|
-
*/
|
|
103
|
-
hasEverPublished: (trackType: TrackType) => boolean;
|
|
104
85
|
/**
|
|
105
86
|
* Returns true if the given track type is currently live
|
|
106
87
|
*
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.3.
|
|
1
|
+
export declare const version = "0.3.17";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"packageManager": "yarn@3.2.4",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.es.js",
|
|
@@ -52,15 +52,15 @@
|
|
|
52
52
|
"@types/sdp-transform": "^2.4.6",
|
|
53
53
|
"@types/ua-parser-js": "^0.7.36",
|
|
54
54
|
"@types/ws": "^8.5.4",
|
|
55
|
-
"@vitest/coverage-v8": "^0.34.
|
|
55
|
+
"@vitest/coverage-v8": "^0.34.4",
|
|
56
56
|
"dotenv": "^16.3.1",
|
|
57
|
-
"happy-dom": "^
|
|
57
|
+
"happy-dom": "^11.0.2",
|
|
58
58
|
"prettier": "^2.8.4",
|
|
59
59
|
"rimraf": "^3.0.2",
|
|
60
60
|
"rollup": "^3.28.1",
|
|
61
61
|
"typescript": "^4.9.5",
|
|
62
62
|
"vite": "^4.4.9",
|
|
63
|
-
"vitest": "^0.34.
|
|
64
|
-
"vitest-mock-extended": "^1.2.
|
|
63
|
+
"vitest": "^0.34.4",
|
|
64
|
+
"vitest-mock-extended": "^1.2.1"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/Call.ts
CHANGED
|
@@ -116,10 +116,12 @@ import {
|
|
|
116
116
|
} from './coordinator/connection/types';
|
|
117
117
|
import { getClientDetails, getSdkInfo } from './client-details';
|
|
118
118
|
import { getLogger } from './logger';
|
|
119
|
-
import {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
import {
|
|
120
|
+
CameraDirection,
|
|
121
|
+
CameraManager,
|
|
122
|
+
MicrophoneManager,
|
|
123
|
+
SpeakerManager,
|
|
124
|
+
} from './devices';
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
127
|
* An object representation of a `Call`.
|
|
@@ -157,19 +159,22 @@ export class Call {
|
|
|
157
159
|
readonly camera: CameraManager;
|
|
158
160
|
|
|
159
161
|
/**
|
|
160
|
-
* Device manager for the
|
|
162
|
+
* Device manager for the microphone.
|
|
161
163
|
*/
|
|
162
164
|
readonly microphone: MicrophoneManager;
|
|
163
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Device manager for the speaker.
|
|
168
|
+
*/
|
|
169
|
+
readonly speaker: SpeakerManager;
|
|
170
|
+
|
|
164
171
|
/**
|
|
165
172
|
* The DynascaleManager instance.
|
|
166
173
|
*/
|
|
167
174
|
readonly dynascaleManager = new DynascaleManager(this);
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
*/
|
|
172
|
-
readonly speaker: SpeakerManager;
|
|
176
|
+
subscriber?: Subscriber;
|
|
177
|
+
publisher?: Publisher;
|
|
173
178
|
|
|
174
179
|
/**
|
|
175
180
|
* Flag telling whether this call is a "ringing" call.
|
|
@@ -188,8 +193,6 @@ export class Call {
|
|
|
188
193
|
*/
|
|
189
194
|
private readonly dispatcher = new Dispatcher();
|
|
190
195
|
|
|
191
|
-
private subscriber?: Subscriber;
|
|
192
|
-
private publisher?: Publisher;
|
|
193
196
|
private trackSubscriptionsSubject = new BehaviorSubject<{
|
|
194
197
|
type: DebounceType;
|
|
195
198
|
data: TrackSubscriptionDetails[];
|
|
@@ -277,41 +280,6 @@ export class Call {
|
|
|
277
280
|
|
|
278
281
|
this.camera = new CameraManager(this);
|
|
279
282
|
this.microphone = new MicrophoneManager(this);
|
|
280
|
-
|
|
281
|
-
// FIXME OL: disable soft-mutes as they are not working properly
|
|
282
|
-
// this.state.localParticipant$.subscribe(async (p) => {
|
|
283
|
-
// if (!this.publisher) return;
|
|
284
|
-
// // Mute via device manager
|
|
285
|
-
// // If integrator doesn't use device manager, we mute using stopPublish
|
|
286
|
-
// if (
|
|
287
|
-
// this.publisher.hasEverPublished(TrackType.VIDEO) &&
|
|
288
|
-
// this.publisher.isPublishing(TrackType.VIDEO) &&
|
|
289
|
-
// !p?.publishedTracks.includes(TrackType.VIDEO)
|
|
290
|
-
// ) {
|
|
291
|
-
// this.logger(
|
|
292
|
-
// 'info',
|
|
293
|
-
// `Local participant's video track is muted remotely`,
|
|
294
|
-
// );
|
|
295
|
-
// await this.camera.disable();
|
|
296
|
-
// if (this.publisher.isPublishing(TrackType.VIDEO)) {
|
|
297
|
-
// await this.stopPublish(TrackType.VIDEO);
|
|
298
|
-
// }
|
|
299
|
-
// }
|
|
300
|
-
// if (
|
|
301
|
-
// this.publisher.hasEverPublished(TrackType.AUDIO) &&
|
|
302
|
-
// this.publisher.isPublishing(TrackType.AUDIO) &&
|
|
303
|
-
// !p?.publishedTracks.includes(TrackType.AUDIO)
|
|
304
|
-
// ) {
|
|
305
|
-
// this.logger(
|
|
306
|
-
// 'info',
|
|
307
|
-
// `Local participant's audio track is muted remotely`,
|
|
308
|
-
// );
|
|
309
|
-
// await this.microphone.disable();
|
|
310
|
-
// if (this.publisher.isPublishing(TrackType.AUDIO)) {
|
|
311
|
-
// await this.stopPublish(TrackType.AUDIO);
|
|
312
|
-
// }
|
|
313
|
-
// }
|
|
314
|
-
// });
|
|
315
283
|
this.speaker = new SpeakerManager();
|
|
316
284
|
}
|
|
317
285
|
|
|
@@ -396,7 +364,7 @@ export class Call {
|
|
|
396
364
|
this.leaveCallHooks.add(
|
|
397
365
|
// handles the case when the user is blocked by the call owner.
|
|
398
366
|
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
399
|
-
if (!blockedUserIds) return;
|
|
367
|
+
if (!blockedUserIds || blockedUserIds.length === 0) return;
|
|
400
368
|
const currentUserId = this.currentUserId;
|
|
401
369
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
402
370
|
this.logger('info', 'Leaving call because of being blocked');
|
|
@@ -932,7 +900,6 @@ export class Call {
|
|
|
932
900
|
connectionConfig,
|
|
933
901
|
isDtxEnabled,
|
|
934
902
|
isRedEnabled,
|
|
935
|
-
preferredVideoCodec: this.streamClient.options.preferredVideoCodec,
|
|
936
903
|
});
|
|
937
904
|
}
|
|
938
905
|
|
|
@@ -1902,10 +1869,10 @@ export class Call {
|
|
|
1902
1869
|
this.microphone.state.mediaStream &&
|
|
1903
1870
|
!this.publisher?.isPublishing(TrackType.AUDIO)
|
|
1904
1871
|
) {
|
|
1905
|
-
this.publishAudioStream(this.microphone.state.mediaStream);
|
|
1872
|
+
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
1906
1873
|
}
|
|
1907
1874
|
|
|
1908
|
-
// Start mic if backend config
|
|
1875
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
1909
1876
|
if (
|
|
1910
1877
|
this.microphone.state.status === undefined &&
|
|
1911
1878
|
this.state.settings?.audio.mic_default_on
|
|
@@ -127,10 +127,6 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
127
127
|
// Set the instance of StableWSConnection on chat client. Its purely for testing purpose and should
|
|
128
128
|
// not be used in production apps.
|
|
129
129
|
wsConnection?: StableWSConnection;
|
|
130
|
-
/**
|
|
131
|
-
* The preferred video codec to use.
|
|
132
|
-
*/
|
|
133
|
-
preferredVideoCodec?: string;
|
|
134
130
|
};
|
|
135
131
|
|
|
136
132
|
export type TokenProvider = () => Promise<string>;
|
|
@@ -34,7 +34,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
34
34
|
*/
|
|
35
35
|
async flip() {
|
|
36
36
|
const newDirection = this.state.direction === 'front' ? 'back' : 'front';
|
|
37
|
-
this.selectDirection(newDirection);
|
|
37
|
+
await this.selectDirection(newDirection);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -87,7 +87,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
87
87
|
this.state.prevStatus === 'enabled' &&
|
|
88
88
|
this.state.status === 'disabled'
|
|
89
89
|
) {
|
|
90
|
-
this.enable();
|
|
90
|
+
await this.enable();
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -141,7 +141,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
141
141
|
|
|
142
142
|
protected abstract getTrack(): undefined | MediaStreamTrack;
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
protected async muteStream(stopTracks: boolean = true) {
|
|
145
145
|
if (!this.state.mediaStream) {
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
@@ -154,7 +154,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
154
154
|
// @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
|
|
155
155
|
if (typeof this.state.mediaStream.release === 'function') {
|
|
156
156
|
// @ts-expect-error
|
|
157
|
-
|
|
157
|
+
stream.release();
|
|
158
158
|
}
|
|
159
159
|
this.state.setMediaStream(undefined);
|
|
160
160
|
}
|
|
@@ -191,7 +191,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
191
191
|
stopTracks ? this.stopTrack() : this.muteTrack();
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
protected async unmuteStream() {
|
|
195
195
|
this.logger('debug', 'Starting stream');
|
|
196
196
|
let stream: MediaStream;
|
|
197
197
|
if (this.state.mediaStream && this.getTrack()?.readyState === 'live') {
|
|
@@ -207,6 +207,8 @@ export abstract class InputMediaDeviceManager<
|
|
|
207
207
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
208
208
|
await this.publishStream(stream);
|
|
209
209
|
}
|
|
210
|
-
this.state.
|
|
210
|
+
if (this.state.mediaStream !== stream) {
|
|
211
|
+
this.state.setMediaStream(stream);
|
|
212
|
+
}
|
|
211
213
|
}
|
|
212
214
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BehaviorSubject,
|
|
1
|
+
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
|
|
2
2
|
import { RxUtils } from '../store';
|
|
3
3
|
|
|
4
4
|
export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
|
|
@@ -85,7 +85,7 @@ export abstract class InputMediaDeviceManagerState {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* @internal
|
|
88
|
-
* @param stream
|
|
88
|
+
* @param stream the stream to set.
|
|
89
89
|
*/
|
|
90
90
|
setMediaStream(stream: MediaStream | undefined) {
|
|
91
91
|
this.setCurrentValue(this.mediaStreamSubject, stream);
|
|
@@ -96,7 +96,7 @@ export abstract class InputMediaDeviceManagerState {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* @internal
|
|
99
|
-
* @param
|
|
99
|
+
* @param deviceId the device id to set.
|
|
100
100
|
*/
|
|
101
101
|
setDevice(deviceId: string | undefined) {
|
|
102
102
|
this.setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
@@ -1,13 +1,42 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
1
|
+
import { combineLatest, Observable } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
4
4
|
import { MicrophoneManagerState } from './MicrophoneManagerState';
|
|
5
5
|
import { getAudioDevices, getAudioStream } from './devices';
|
|
6
6
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
|
+
import { createSoundDetector } from '../helpers/sound-detector';
|
|
8
|
+
import { isReactNative } from '../helpers/platforms';
|
|
9
|
+
import { OwnCapability } from '../gen/coordinator';
|
|
10
|
+
import { CallingState } from '../store';
|
|
7
11
|
|
|
8
12
|
export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
|
|
13
|
+
private soundDetectorCleanup?: Function;
|
|
14
|
+
|
|
9
15
|
constructor(call: Call) {
|
|
10
16
|
super(call, new MicrophoneManagerState(), TrackType.AUDIO);
|
|
17
|
+
|
|
18
|
+
combineLatest([
|
|
19
|
+
this.call.state.callingState$,
|
|
20
|
+
this.call.state.ownCapabilities$,
|
|
21
|
+
this.state.selectedDevice$,
|
|
22
|
+
this.state.status$,
|
|
23
|
+
]).subscribe(async ([callingState, ownCapabilities, deviceId, status]) => {
|
|
24
|
+
if (callingState !== CallingState.JOINED) {
|
|
25
|
+
if (callingState === CallingState.LEFT) {
|
|
26
|
+
await this.stopSpeakingWhileMutedDetection();
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
|
|
31
|
+
if (status === 'disabled') {
|
|
32
|
+
await this.startSpeakingWhileMutedDetection(deviceId);
|
|
33
|
+
} else {
|
|
34
|
+
await this.stopSpeakingWhileMutedDetection();
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
await this.stopSpeakingWhileMutedDetection();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
11
40
|
}
|
|
12
41
|
|
|
13
42
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
@@ -28,4 +57,30 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
28
57
|
protected getTrack() {
|
|
29
58
|
return this.state.mediaStream?.getAudioTracks()[0];
|
|
30
59
|
}
|
|
60
|
+
|
|
61
|
+
private async startSpeakingWhileMutedDetection(deviceId?: string) {
|
|
62
|
+
if (isReactNative()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await this.stopSpeakingWhileMutedDetection();
|
|
66
|
+
// Need to start a new stream that's not connected to publisher
|
|
67
|
+
const stream = await this.getStream({
|
|
68
|
+
deviceId,
|
|
69
|
+
});
|
|
70
|
+
this.soundDetectorCleanup = createSoundDetector(stream, (event) => {
|
|
71
|
+
this.state.setSpeakingWhileMuted(event.isSoundDetected);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async stopSpeakingWhileMutedDetection() {
|
|
76
|
+
if (isReactNative() || !this.soundDetectorCleanup) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.state.setSpeakingWhileMuted(false);
|
|
80
|
+
try {
|
|
81
|
+
await this.soundDetectorCleanup();
|
|
82
|
+
} finally {
|
|
83
|
+
this.soundDetectorCleanup = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
31
86
|
}
|
|
@@ -1,8 +1,38 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
|
|
1
2
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
2
3
|
|
|
3
4
|
export class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
5
|
+
private speakingWhileMutedSubject = new BehaviorSubject<boolean>(false);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An Observable that emits `true` if the user's microphone is muted but they'are speaking.
|
|
9
|
+
*
|
|
10
|
+
* This feature is not available in the React Native SDK.
|
|
11
|
+
*/
|
|
12
|
+
speakingWhileMuted$: Observable<boolean>;
|
|
13
|
+
|
|
4
14
|
constructor() {
|
|
5
15
|
super('disable-tracks');
|
|
16
|
+
|
|
17
|
+
this.speakingWhileMuted$ = this.speakingWhileMutedSubject
|
|
18
|
+
.asObservable()
|
|
19
|
+
.pipe(distinctUntilChanged());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `true` if the user's microphone is muted but they'are speaking.
|
|
24
|
+
*
|
|
25
|
+
* This feature is not available in the React Native SDK.
|
|
26
|
+
*/
|
|
27
|
+
get speakingWhileMuted() {
|
|
28
|
+
return this.getCurrentValue(this.speakingWhileMuted$);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
setSpeakingWhileMuted(isSpeaking: boolean) {
|
|
35
|
+
this.setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
|
|
6
36
|
}
|
|
7
37
|
|
|
8
38
|
protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
|
|
@@ -2,7 +2,7 @@ import { Call } from '../../Call';
|
|
|
2
2
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
3
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
4
|
|
|
5
|
-
import { afterEach, beforeEach, describe,
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
|
6
6
|
import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
|
|
7
7
|
import { getVideoStream } from '../devices';
|
|
8
8
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
@@ -67,8 +67,7 @@ describe('CameraManager', () => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('publish stream', async () => {
|
|
70
|
-
|
|
71
|
-
manager['call'].state.callingState = CallingState.JOINED;
|
|
70
|
+
manager['call'].state.setCallingState(CallingState.JOINED);
|
|
72
71
|
|
|
73
72
|
await manager.enable();
|
|
74
73
|
|
|
@@ -78,8 +77,7 @@ describe('CameraManager', () => {
|
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
it('stop publish stream', async () => {
|
|
81
|
-
|
|
82
|
-
manager['call'].state.callingState = CallingState.JOINED;
|
|
80
|
+
manager['call'].state.setCallingState(CallingState.JOINED);
|
|
83
81
|
await manager.enable();
|
|
84
82
|
|
|
85
83
|
await manager.disable();
|