@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +528 -466
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +529 -465
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +528 -466
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +9 -8
  9. package/dist/src/coordinator/connection/types.d.ts +0 -4
  10. package/dist/src/devices/InputMediaDeviceManager.d.ts +2 -2
  11. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +2 -2
  12. package/dist/src/devices/MicrophoneManager.d.ts +3 -0
  13. package/dist/src/devices/MicrophoneManagerState.d.ts +18 -0
  14. package/dist/src/devices/SpeakerManager.d.ts +0 -1
  15. package/dist/src/devices/__tests__/mocks.d.ts +2 -4
  16. package/dist/src/devices/index.d.ts +2 -0
  17. package/dist/src/events/__tests__/mutes.test.d.ts +1 -0
  18. package/dist/src/events/index.d.ts +1 -0
  19. package/dist/src/events/mutes.d.ts +7 -0
  20. package/dist/src/rtc/Publisher.d.ts +2 -21
  21. package/dist/version.d.ts +1 -1
  22. package/package.json +5 -5
  23. package/src/Call.ts +17 -50
  24. package/src/coordinator/connection/types.ts +0 -4
  25. package/src/devices/CameraManager.ts +1 -1
  26. package/src/devices/InputMediaDeviceManager.ts +7 -5
  27. package/src/devices/InputMediaDeviceManagerState.ts +3 -3
  28. package/src/devices/MicrophoneManager.ts +56 -1
  29. package/src/devices/MicrophoneManagerState.ts +30 -0
  30. package/src/devices/SpeakerManager.ts +0 -2
  31. package/src/devices/__tests__/CameraManager.test.ts +3 -5
  32. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -7
  33. package/src/devices/__tests__/MicrophoneManager.test.ts +77 -6
  34. package/src/devices/__tests__/mocks.ts +14 -5
  35. package/src/devices/index.ts +2 -0
  36. package/src/events/__tests__/mutes.test.ts +133 -0
  37. package/src/events/callEventHandlers.ts +3 -0
  38. package/src/events/index.ts +1 -0
  39. package/src/events/mutes.ts +48 -0
  40. package/src/helpers/sound-detector.ts +7 -1
  41. package/src/rtc/Publisher.ts +2 -28
@@ -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/CameraManager';
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 microhpone
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
- readonly speaker: SpeakerManager;
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
- private muteStream;
62
+ protected muteStream(stopTracks?: boolean): Promise<void>;
63
63
  private muteTrack;
64
64
  private unmuteTrack;
65
65
  private stopTrack;
66
66
  private muteLocalStream;
67
- private unmuteStream;
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 stream
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,7 +1,6 @@
1
1
  import { SpeakerState } from './SpeakerState';
2
2
  export declare class SpeakerManager {
3
3
  readonly state: SpeakerState;
4
- constructor();
5
4
  /**
6
5
  * Lists the available audio output devices
7
6
  *
@@ -1,10 +1,8 @@
1
- import { CallingState } from '../../store';
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>;
@@ -5,3 +5,5 @@ export * from './CameraManager';
5
5
  export * from './CameraManagerState';
6
6
  export * from './MicrophoneManager';
7
7
  export * from './MicrophoneManagerState';
8
+ export * from './SpeakerManager';
9
+ export * from './SpeakerState';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  export * from './call';
2
2
  export * from './call-permissions';
3
3
  export * from './internal';
4
+ export * from './mutes';
4
5
  export * from './participant';
5
6
  export * from './speaker';
@@ -0,0 +1,7 @@
1
+ import { Call } from '../Call';
2
+ /**
3
+ * An event handler that handles soft mutes.
4
+ *
5
+ * @param call the call.
6
+ */
7
+ export declare const handleRemoteSoftMute: (call: Call) => () => void;
@@ -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 PublisherOpts = {
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, preferredVideoCodec, iceRestartDelay, }: PublisherOpts);
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.15";
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.15",
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.2",
55
+ "@vitest/coverage-v8": "^0.34.4",
56
56
  "dotenv": "^16.3.1",
57
- "happy-dom": "^10.11.0",
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.3",
64
- "vitest-mock-extended": "^1.2.0"
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 { CameraManager } from './devices/CameraManager';
120
- import { MicrophoneManager } from './devices/MicrophoneManager';
121
- import { CameraDirection } from './devices/CameraManagerState';
122
- import { SpeakerManager } from './devices/SpeakerManager';
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 microhpone
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
- * Device manager for the speaker
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 speicifies, and there is no local setting
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
- private async muteStream(stopTracks: boolean = true) {
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
- this.state.mediaStream.release();
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
- private async unmuteStream() {
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.setMediaStream(stream);
210
+ if (this.state.mediaStream !== stream) {
211
+ this.state.setMediaStream(stream);
212
+ }
211
213
  }
212
214
  }
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
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 stream
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 {
@@ -5,8 +5,6 @@ import { getAudioOutputDevices } from './devices';
5
5
  export class SpeakerManager {
6
6
  public readonly state = new SpeakerState();
7
7
 
8
- constructor() {}
9
-
10
8
  /**
11
9
  * Lists the available audio output devices
12
10
  *
@@ -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, vi, it, expect, Mock } from 'vitest';
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
- // @ts-expect-error
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
- // @ts-expect-error
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();