@stream-io/video-client 1.44.6-beta.0 → 1.45.0
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 +10 -0
- package/dist/index.browser.es.js +58 -79
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +58 -79
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +58 -79
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +1 -1
- package/dist/src/coordinator/connection/types.d.ts +22 -1
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/types.d.ts +5 -37
- package/package.json +1 -1
- package/src/Call.ts +40 -85
- package/src/coordinator/connection/types.ts +23 -0
- package/src/devices/DeviceManager.ts +20 -1
- package/src/devices/SpeakerManager.ts +0 -1
- package/src/devices/__tests__/DeviceManager.test.ts +8 -0
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/events/call.ts +0 -3
- package/src/helpers/AudioBindingsWatchdog.ts +14 -2
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +27 -1
- package/src/store/stateStore.ts +1 -1
- package/src/types.ts +5 -48
package/dist/src/Call.d.ts
CHANGED
|
@@ -162,7 +162,7 @@ export declare class Call {
|
|
|
162
162
|
/**
|
|
163
163
|
* A flag indicating whether the call was created by the current user.
|
|
164
164
|
*/
|
|
165
|
-
get isCreatedByMe(): boolean
|
|
165
|
+
get isCreatedByMe(): boolean;
|
|
166
166
|
/**
|
|
167
167
|
* Update from the call response from the "call.ring" event
|
|
168
168
|
* @internal
|
|
@@ -3,6 +3,7 @@ import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
5
|
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
6
|
+
import { InputDeviceStatus } from '../../devices';
|
|
6
7
|
export type UR = Record<string, unknown>;
|
|
7
8
|
export type User = (Omit<UserRequest, 'role'> & {
|
|
8
9
|
type?: 'authenticated';
|
|
@@ -79,7 +80,27 @@ export type MicCaptureReportEvent = {
|
|
|
79
80
|
*/
|
|
80
81
|
label?: string;
|
|
81
82
|
};
|
|
82
|
-
export type
|
|
83
|
+
export type DeviceDisconnectedEvent = {
|
|
84
|
+
type: 'device.disconnected';
|
|
85
|
+
call_cid: string;
|
|
86
|
+
/**
|
|
87
|
+
* The device status at the time it was disconnected.
|
|
88
|
+
*/
|
|
89
|
+
status: InputDeviceStatus;
|
|
90
|
+
/**
|
|
91
|
+
* The disconnected device ID.
|
|
92
|
+
*/
|
|
93
|
+
deviceId: string;
|
|
94
|
+
/**
|
|
95
|
+
* The human-readable label of the disconnected device.
|
|
96
|
+
*/
|
|
97
|
+
label?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The disconnected device kind.
|
|
100
|
+
*/
|
|
101
|
+
kind: MediaDeviceKind;
|
|
102
|
+
};
|
|
103
|
+
export type StreamVideoEvent = (VideoEvent | NetworkChangedEvent | ConnectionChangedEvent | TransportChangedEvent | ConnectionRecoveredEvent | MicCaptureReportEvent | DeviceDisconnectedEvent) & {
|
|
83
104
|
received_at?: string | Date;
|
|
84
105
|
};
|
|
85
106
|
export type StreamCallEvent = Extract<StreamVideoEvent, {
|
|
@@ -104,6 +104,7 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
|
|
|
104
104
|
private get mediaDeviceKind();
|
|
105
105
|
private handleDisconnectedOrReplacedDevices;
|
|
106
106
|
protected findDevice(devices: MediaDeviceInfo[], deviceId: string): MediaDeviceInfo | undefined;
|
|
107
|
+
private dispatchDeviceDisconnectedEvent;
|
|
107
108
|
private persistPreference;
|
|
108
109
|
protected applyPersistedPreferences(enabledInCallType: boolean): Promise<boolean>;
|
|
109
110
|
private applyMutedState;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { RejectReason, StreamClientOptions, TokenProvider, User } from './c
|
|
|
5
5
|
import type { Comparator } from './sorting';
|
|
6
6
|
import type { StreamVideoWriteableStateStore } from './store';
|
|
7
7
|
import { AxiosError } from 'axios';
|
|
8
|
-
import type { Call } from './Call';
|
|
9
8
|
export type StreamReaction = Pick<ReactionResponse, 'type' | 'emoji_code' | 'custom'>;
|
|
10
9
|
export declare enum VisibilityState {
|
|
11
10
|
UNKNOWN = "UNKNOWN",
|
|
@@ -317,53 +316,22 @@ export type StartCallRecordingFnType = {
|
|
|
317
316
|
(request: StartRecordingRequest): Promise<StartRecordingResponse>;
|
|
318
317
|
(request: StartRecordingRequest, type: CallRecordingType): Promise<StartRecordingResponse>;
|
|
319
318
|
};
|
|
320
|
-
type StreamRNVideoSDKCallManagerRingingParams = {
|
|
321
|
-
isRingingTypeCall: boolean;
|
|
322
|
-
};
|
|
323
|
-
type StreamRNVideoSDKCallManagerSetupParams = StreamRNVideoSDKCallManagerRingingParams & {
|
|
324
|
-
defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
|
|
325
|
-
};
|
|
326
|
-
type StreamRNVideoSDKEndCallReason =
|
|
327
|
-
/** Call ended by the local user (e.g., hanging up). */
|
|
328
|
-
'local'
|
|
329
|
-
/** Call ended by the remote party, or outgoing call was not answered. */
|
|
330
|
-
| 'remote'
|
|
331
|
-
/** Call was rejected/declined by the user. */
|
|
332
|
-
| 'rejected'
|
|
333
|
-
/** Remote party was busy. */
|
|
334
|
-
| 'busy'
|
|
335
|
-
/** Call was answered on another device. */
|
|
336
|
-
| 'answeredElsewhere'
|
|
337
|
-
/** No response to an incoming call. */
|
|
338
|
-
| 'missed'
|
|
339
|
-
/** Call failed due to an error (e.g., network issue). */
|
|
340
|
-
| 'error'
|
|
341
|
-
/** Call was canceled before the remote party could answer. */
|
|
342
|
-
| 'canceled'
|
|
343
|
-
/** Call restricted (e.g., airplane mode, dialing restrictions). */
|
|
344
|
-
| 'restricted'
|
|
345
|
-
/** Unknown or unspecified disconnect reason. */
|
|
346
|
-
| 'unknown';
|
|
347
|
-
type StreamRNVideoSDKCallingX = {
|
|
348
|
-
joinCall: (call: Call, activeCalls: Call[]) => Promise<void>;
|
|
349
|
-
endCall: (call: Call, reason?: StreamRNVideoSDKEndCallReason) => Promise<void>;
|
|
350
|
-
registerOutgoingCall: (call: Call) => Promise<void>;
|
|
351
|
-
};
|
|
352
319
|
export type StreamRNVideoSDKGlobals = {
|
|
353
|
-
callingX: StreamRNVideoSDKCallingX;
|
|
354
320
|
callManager: {
|
|
355
321
|
/**
|
|
356
322
|
* Sets up the in call manager.
|
|
357
323
|
*/
|
|
358
|
-
setup({ defaultDevice,
|
|
324
|
+
setup({ defaultDevice, }: {
|
|
325
|
+
defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
|
|
326
|
+
}): void;
|
|
359
327
|
/**
|
|
360
328
|
* Starts the in call manager.
|
|
361
329
|
*/
|
|
362
|
-
start(
|
|
330
|
+
start(): void;
|
|
363
331
|
/**
|
|
364
332
|
* Stops the in call manager.
|
|
365
333
|
*/
|
|
366
|
-
stop(
|
|
334
|
+
stop(): void;
|
|
367
335
|
};
|
|
368
336
|
permissions: {
|
|
369
337
|
/**
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -421,7 +421,6 @@ export class Call {
|
|
|
421
421
|
const currentUserId = this.currentUserId;
|
|
422
422
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
423
423
|
this.logger.info('Leaving call because of being blocked');
|
|
424
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
|
|
425
424
|
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
426
425
|
this.logger.error('Error leaving call after being blocked', err);
|
|
427
426
|
});
|
|
@@ -466,10 +465,6 @@ export class Call {
|
|
|
466
465
|
(isAcceptedElsewhere || isRejectedByMe) &&
|
|
467
466
|
!hasPending(this.joinLeaveConcurrencyTag)
|
|
468
467
|
) {
|
|
469
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(
|
|
470
|
-
this,
|
|
471
|
-
isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected',
|
|
472
|
-
);
|
|
473
468
|
this.leave().catch(() => {
|
|
474
469
|
this.logger.error(
|
|
475
470
|
'Could not leave a call that was accepted or rejected elsewhere',
|
|
@@ -485,10 +480,6 @@ export class Call {
|
|
|
485
480
|
const receiver_id = this.clientStore.connectedUser?.id;
|
|
486
481
|
const ended_at = callSession?.ended_at;
|
|
487
482
|
const created_by_id = this.state.createdBy?.id;
|
|
488
|
-
|
|
489
|
-
if (this.currentUserId && created_by_id === this.currentUserId) {
|
|
490
|
-
globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
|
|
491
|
-
}
|
|
492
483
|
const rejected_by = callSession?.rejected_by;
|
|
493
484
|
const accepted_by = callSession?.accepted_by;
|
|
494
485
|
let leaveCallIdle = false;
|
|
@@ -645,30 +636,16 @@ export class Call {
|
|
|
645
636
|
|
|
646
637
|
if (callingState === CallingState.RINGING && reject !== false) {
|
|
647
638
|
if (reject) {
|
|
648
|
-
|
|
649
|
-
timeout: 'missed',
|
|
650
|
-
cancel: 'canceled',
|
|
651
|
-
busy: 'busy',
|
|
652
|
-
decline: 'rejected',
|
|
653
|
-
} as const;
|
|
654
|
-
const rejectReason = reason ?? 'decline';
|
|
655
|
-
const endCallReason =
|
|
656
|
-
reasonToEndCallReason[
|
|
657
|
-
rejectReason as keyof typeof reasonToEndCallReason
|
|
658
|
-
] ?? 'rejected';
|
|
659
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
660
|
-
await this.reject(rejectReason);
|
|
639
|
+
await this.reject(reason ?? 'decline');
|
|
661
640
|
} else {
|
|
662
641
|
// if reject was undefined, we still have to cancel the call automatically
|
|
663
642
|
// when I am the creator and everyone else left the call
|
|
664
643
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
665
644
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
666
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
667
645
|
await this.reject('cancel');
|
|
668
646
|
}
|
|
669
647
|
}
|
|
670
648
|
}
|
|
671
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
672
649
|
|
|
673
650
|
this.statsReporter?.stop();
|
|
674
651
|
this.statsReporter = undefined;
|
|
@@ -703,9 +680,7 @@ export class Call {
|
|
|
703
680
|
this.cancelAutoDrop();
|
|
704
681
|
this.clientStore.unregisterCall(this);
|
|
705
682
|
|
|
706
|
-
globalThis.streamRNVideoSDK?.callManager.stop(
|
|
707
|
-
isRingingTypeCall: this.ringing,
|
|
708
|
-
});
|
|
683
|
+
globalThis.streamRNVideoSDK?.callManager.stop();
|
|
709
684
|
|
|
710
685
|
this.camera.dispose();
|
|
711
686
|
this.microphone.dispose();
|
|
@@ -745,9 +720,7 @@ export class Call {
|
|
|
745
720
|
* A flag indicating whether the call was created by the current user.
|
|
746
721
|
*/
|
|
747
722
|
get isCreatedByMe() {
|
|
748
|
-
return
|
|
749
|
-
this.currentUserId && this.state.createdBy?.id === this.currentUserId
|
|
750
|
-
);
|
|
723
|
+
return this.state.createdBy?.id === this.currentUserId;
|
|
751
724
|
}
|
|
752
725
|
|
|
753
726
|
/**
|
|
@@ -793,7 +766,6 @@ export class Call {
|
|
|
793
766
|
video?: boolean;
|
|
794
767
|
}): Promise<GetCallResponse> => {
|
|
795
768
|
await this.setup();
|
|
796
|
-
|
|
797
769
|
const response = await this.streamClient.get<GetCallResponse>(
|
|
798
770
|
this.streamClientBasePath,
|
|
799
771
|
params,
|
|
@@ -833,7 +805,6 @@ export class Call {
|
|
|
833
805
|
*/
|
|
834
806
|
getOrCreate = async (data?: GetOrCreateCallRequest) => {
|
|
835
807
|
await this.setup();
|
|
836
|
-
|
|
837
808
|
const response = await this.streamClient.post<
|
|
838
809
|
GetOrCreateCallResponse,
|
|
839
810
|
GetOrCreateCallRequest
|
|
@@ -959,73 +930,60 @@ export class Call {
|
|
|
959
930
|
joinResponseTimeout?: number;
|
|
960
931
|
rpcRequestTimeout?: number;
|
|
961
932
|
} = {}): Promise<void> => {
|
|
933
|
+
await this.setup();
|
|
962
934
|
const callingState = this.state.callingState;
|
|
963
935
|
|
|
964
936
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
965
937
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
966
938
|
}
|
|
967
939
|
|
|
968
|
-
if (data?.ring) {
|
|
969
|
-
this.ringingSubject.next(true);
|
|
970
|
-
}
|
|
971
|
-
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
972
|
-
if (callingX) {
|
|
973
|
-
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
974
|
-
await callingX.joinCall(this, this.clientStore.calls);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
await this.setup();
|
|
978
|
-
|
|
979
940
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
980
941
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
942
|
+
|
|
981
943
|
// we will count the number of join failures per SFU.
|
|
982
944
|
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
983
945
|
// field to force the coordinator to provide us another SFU
|
|
984
946
|
const sfuJoinFailures = new Map<string, number>();
|
|
985
947
|
const joinData: JoinCallData = data;
|
|
986
948
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
949
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
950
|
+
try {
|
|
951
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
952
|
+
await this.doJoin(data);
|
|
953
|
+
delete joinData.migrating_from;
|
|
954
|
+
delete joinData.migrating_from_list;
|
|
955
|
+
break;
|
|
956
|
+
} catch (err) {
|
|
957
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
958
|
+
if (
|
|
959
|
+
(err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
960
|
+
(err instanceof SfuJoinError && err.unrecoverable)
|
|
961
|
+
) {
|
|
962
|
+
// if the error is unrecoverable, we should not retry as that signals
|
|
963
|
+
// that connectivity is good, but the coordinator doesn't allow the user
|
|
964
|
+
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
965
|
+
throw err;
|
|
966
|
+
}
|
|
1006
967
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
968
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
969
|
+
const switchSfu =
|
|
970
|
+
err instanceof SfuJoinError &&
|
|
971
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
972
|
+
|
|
973
|
+
const sfuId = this.credentials?.server.edge_name || '';
|
|
974
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
975
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
976
|
+
if (switchSfu || failures >= 2) {
|
|
977
|
+
joinData.migrating_from = sfuId;
|
|
978
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
979
|
+
}
|
|
1019
980
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
981
|
+
if (attempt === maxJoinRetries - 1) {
|
|
982
|
+
throw err;
|
|
1023
983
|
}
|
|
1024
|
-
await sleep(retryInterval(attempt));
|
|
1025
984
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
throw error;
|
|
985
|
+
|
|
986
|
+
await sleep(retryInterval(attempt));
|
|
1029
987
|
}
|
|
1030
988
|
};
|
|
1031
989
|
|
|
@@ -1208,9 +1166,7 @@ export class Call {
|
|
|
1208
1166
|
// re-apply them on later reconnections or server-side data fetches
|
|
1209
1167
|
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
1210
1168
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
1211
|
-
globalThis.streamRNVideoSDK?.callManager.start(
|
|
1212
|
-
isRingingTypeCall: this.ringing,
|
|
1213
|
-
});
|
|
1169
|
+
globalThis.streamRNVideoSDK?.callManager.start();
|
|
1214
1170
|
this.deviceSettingsAppliedOnce = true;
|
|
1215
1171
|
}
|
|
1216
1172
|
|
|
@@ -1755,7 +1711,6 @@ export class Call {
|
|
|
1755
1711
|
if (SfuJoinError.isJoinErrorCode(e)) return;
|
|
1756
1712
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED) return;
|
|
1757
1713
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
1758
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
|
|
1759
1714
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
1760
1715
|
this.logger.warn(`Can't leave call after disconnect request`, err);
|
|
1761
1716
|
});
|
|
@@ -3,6 +3,7 @@ import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
5
|
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
6
|
+
import { InputDeviceStatus } from '../../devices';
|
|
6
7
|
|
|
7
8
|
export type UR = Record<string, unknown>;
|
|
8
9
|
|
|
@@ -126,6 +127,27 @@ export type MicCaptureReportEvent = {
|
|
|
126
127
|
label?: string;
|
|
127
128
|
};
|
|
128
129
|
|
|
130
|
+
export type DeviceDisconnectedEvent = {
|
|
131
|
+
type: 'device.disconnected';
|
|
132
|
+
call_cid: string;
|
|
133
|
+
/**
|
|
134
|
+
* The device status at the time it was disconnected.
|
|
135
|
+
*/
|
|
136
|
+
status: InputDeviceStatus;
|
|
137
|
+
/**
|
|
138
|
+
* The disconnected device ID.
|
|
139
|
+
*/
|
|
140
|
+
deviceId: string;
|
|
141
|
+
/**
|
|
142
|
+
* The human-readable label of the disconnected device.
|
|
143
|
+
*/
|
|
144
|
+
label?: string;
|
|
145
|
+
/**
|
|
146
|
+
* The disconnected device kind.
|
|
147
|
+
*/
|
|
148
|
+
kind: MediaDeviceKind;
|
|
149
|
+
};
|
|
150
|
+
|
|
129
151
|
export type StreamVideoEvent = (
|
|
130
152
|
| VideoEvent
|
|
131
153
|
| NetworkChangedEvent
|
|
@@ -133,6 +155,7 @@ export type StreamVideoEvent = (
|
|
|
133
155
|
| TransportChangedEvent
|
|
134
156
|
| ConnectionRecoveredEvent
|
|
135
157
|
| MicCaptureReportEvent
|
|
158
|
+
| DeviceDisconnectedEvent
|
|
136
159
|
) & { received_at?: string | Date };
|
|
137
160
|
|
|
138
161
|
// TODO: we should use WSCallEvent here but that needs fixing
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { combineLatest, firstValueFrom, Observable, pairwise } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
|
+
import type { DeviceDisconnectedEvent } from '../coordinator/connection/types';
|
|
3
4
|
import { TrackPublishOptions } from '../rtc';
|
|
4
5
|
import { CallingState } from '../store';
|
|
5
6
|
import { createSubscription, getCurrentValue } from '../store/rxUtils';
|
|
@@ -13,6 +14,7 @@ import { ScopedLogger, videoLoggerSystem } from '../logger';
|
|
|
13
14
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
14
15
|
import { deviceIds$ } from './devices';
|
|
15
16
|
import {
|
|
17
|
+
hasPending,
|
|
16
18
|
settled,
|
|
17
19
|
withCancellation,
|
|
18
20
|
withoutConcurrency,
|
|
@@ -543,6 +545,7 @@ export abstract class DeviceManager<
|
|
|
543
545
|
}
|
|
544
546
|
|
|
545
547
|
if (isDeviceDisconnected) {
|
|
548
|
+
this.dispatchDeviceDisconnectedEvent(prevDevice!);
|
|
546
549
|
await this.disable();
|
|
547
550
|
await this.select(undefined);
|
|
548
551
|
}
|
|
@@ -553,7 +556,7 @@ export abstract class DeviceManager<
|
|
|
553
556
|
) {
|
|
554
557
|
await this.enable();
|
|
555
558
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
556
|
-
} else {
|
|
559
|
+
} else if (!hasPending(this.statusChangeConcurrencyTag)) {
|
|
557
560
|
await this.applySettingsToStream();
|
|
558
561
|
}
|
|
559
562
|
}
|
|
@@ -573,6 +576,22 @@ export abstract class DeviceManager<
|
|
|
573
576
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
574
577
|
}
|
|
575
578
|
|
|
579
|
+
private dispatchDeviceDisconnectedEvent(device: MediaDeviceInfo) {
|
|
580
|
+
const event: DeviceDisconnectedEvent = {
|
|
581
|
+
type: 'device.disconnected',
|
|
582
|
+
call_cid: this.call.cid,
|
|
583
|
+
status: this.isTrackStoppedDueToTrackEnd
|
|
584
|
+
? this.state.prevStatus
|
|
585
|
+
: this.state.status,
|
|
586
|
+
deviceId: device.deviceId,
|
|
587
|
+
label: device.label,
|
|
588
|
+
kind: device.kind,
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
this.call.tracer.trace('device.disconnected', event);
|
|
592
|
+
this.call.streamClient.dispatchEvent(event);
|
|
593
|
+
}
|
|
594
|
+
|
|
576
595
|
private persistPreference(
|
|
577
596
|
selectedDevice: string | undefined,
|
|
578
597
|
status: InputDeviceStatus,
|
|
@@ -380,6 +380,14 @@ describe('Device Manager', () => {
|
|
|
380
380
|
|
|
381
381
|
expect(manager.state.selectedDevice).toBe(undefined);
|
|
382
382
|
expect(manager.state.status).toBe('disabled');
|
|
383
|
+
expect(manager['call'].streamClient.dispatchEvent).toHaveBeenCalledWith({
|
|
384
|
+
type: 'device.disconnected',
|
|
385
|
+
call_cid: manager['call'].cid,
|
|
386
|
+
status: 'enabled',
|
|
387
|
+
deviceId: device.deviceId,
|
|
388
|
+
label: device.label,
|
|
389
|
+
kind: device.kind,
|
|
390
|
+
});
|
|
383
391
|
|
|
384
392
|
vi.useRealTimers();
|
|
385
393
|
});
|
|
@@ -95,9 +95,13 @@ export const mockCall = (): Partial<Call> => {
|
|
|
95
95
|
}),
|
|
96
96
|
);
|
|
97
97
|
return {
|
|
98
|
+
cid: 'default:test-call',
|
|
98
99
|
state: callState,
|
|
99
100
|
publish: vi.fn(),
|
|
100
101
|
stopPublish: vi.fn(),
|
|
102
|
+
streamClient: fromPartial({
|
|
103
|
+
dispatchEvent: vi.fn(),
|
|
104
|
+
}),
|
|
101
105
|
notifyNoiseCancellationStarting: vi.fn().mockResolvedValue(undefined),
|
|
102
106
|
notifyNoiseCancellationStopped: vi.fn().mockResolvedValue(undefined),
|
|
103
107
|
tracer: new Tracer('tests'),
|
package/src/events/call.ts
CHANGED
|
@@ -69,7 +69,6 @@ export const watchCallRejected = (call: Call) => {
|
|
|
69
69
|
} else {
|
|
70
70
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
71
71
|
call.logger.info('call creator rejected, leaving call');
|
|
72
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
73
72
|
await call.leave({ message: 'ring: creator rejected' });
|
|
74
73
|
}
|
|
75
74
|
}
|
|
@@ -81,7 +80,6 @@ export const watchCallRejected = (call: Call) => {
|
|
|
81
80
|
*/
|
|
82
81
|
export const watchCallEnded = (call: Call) => {
|
|
83
82
|
return function onCallEnded() {
|
|
84
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
85
83
|
const { callingState } = call.state;
|
|
86
84
|
if (
|
|
87
85
|
callingState !== CallingState.IDLE &&
|
|
@@ -115,7 +113,6 @@ export const watchSfuCallEnded = (call: Call) => {
|
|
|
115
113
|
// update the call state to reflect the call has ended.
|
|
116
114
|
call.state.setEndedAt(new Date());
|
|
117
115
|
const reason = CallEndedReason[e.reason];
|
|
118
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
119
116
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
120
117
|
} catch (err) {
|
|
121
118
|
call.logger.error(
|
|
@@ -3,6 +3,7 @@ import { CallingState, CallState } from '../store';
|
|
|
3
3
|
import { createSubscription } from '../store/rxUtils';
|
|
4
4
|
import { videoLoggerSystem } from '../logger';
|
|
5
5
|
import { Tracer } from '../stats';
|
|
6
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
6
7
|
|
|
7
8
|
const toBindingKey = (
|
|
8
9
|
sessionId: string,
|
|
@@ -91,12 +92,23 @@ export class AudioBindingsWatchdog {
|
|
|
91
92
|
const danglingUserIds: string[] = [];
|
|
92
93
|
for (const p of this.state.participants) {
|
|
93
94
|
if (p.isLocalParticipant) continue;
|
|
94
|
-
const {
|
|
95
|
-
|
|
95
|
+
const {
|
|
96
|
+
audioStream,
|
|
97
|
+
screenShareAudioStream,
|
|
98
|
+
sessionId,
|
|
99
|
+
userId,
|
|
100
|
+
publishedTracks,
|
|
101
|
+
} = p;
|
|
102
|
+
if (
|
|
103
|
+
audioStream &&
|
|
104
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
105
|
+
!this.bindings.has(toBindingKey(sessionId))
|
|
106
|
+
) {
|
|
96
107
|
danglingUserIds.push(userId);
|
|
97
108
|
}
|
|
98
109
|
if (
|
|
99
110
|
screenShareAudioStream &&
|
|
111
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
100
112
|
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))
|
|
101
113
|
) {
|
|
102
114
|
danglingUserIds.push(userId);
|
|
@@ -11,6 +11,7 @@ import { StreamClient } from '../../coordinator/connection/client';
|
|
|
11
11
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
12
12
|
import { noopComparator } from '../../sorting';
|
|
13
13
|
import { fromPartial } from '@total-typescript/shoehorn';
|
|
14
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
14
15
|
|
|
15
16
|
describe('AudioBindingsWatchdog', () => {
|
|
16
17
|
let watchdog: AudioBindingsWatchdog;
|
|
@@ -44,12 +45,17 @@ describe('AudioBindingsWatchdog', () => {
|
|
|
44
45
|
screenShareAudioStream?: MediaStream;
|
|
45
46
|
},
|
|
46
47
|
) => {
|
|
48
|
+
const publishedTracks = [];
|
|
49
|
+
if (streams?.audioStream) publishedTracks.push(TrackType.AUDIO);
|
|
50
|
+
if (streams?.screenShareAudioStream) {
|
|
51
|
+
publishedTracks.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
52
|
+
}
|
|
47
53
|
call.state.updateOrAddParticipant(
|
|
48
54
|
sessionId,
|
|
49
55
|
fromPartial({
|
|
50
56
|
userId,
|
|
51
57
|
sessionId,
|
|
52
|
-
publishedTracks
|
|
58
|
+
publishedTracks,
|
|
53
59
|
...streams,
|
|
54
60
|
}),
|
|
55
61
|
);
|
|
@@ -233,6 +239,26 @@ describe('AudioBindingsWatchdog', () => {
|
|
|
233
239
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('user-1'));
|
|
234
240
|
});
|
|
235
241
|
|
|
242
|
+
it('should not warn when audioStream exists but audio is not published', () => {
|
|
243
|
+
// @ts-expect-error private property
|
|
244
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
245
|
+
|
|
246
|
+
call.state.updateOrAddParticipant(
|
|
247
|
+
'session-1',
|
|
248
|
+
fromPartial({
|
|
249
|
+
userId: 'user-1',
|
|
250
|
+
sessionId: 'session-1',
|
|
251
|
+
publishedTracks: [],
|
|
252
|
+
audioStream: new MediaStream(),
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
257
|
+
vi.advanceTimersByTime(3000);
|
|
258
|
+
|
|
259
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
236
262
|
it('should not warn when screenShareAudio element is bound', () => {
|
|
237
263
|
// @ts-expect-error private property
|
|
238
264
|
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
package/src/store/stateStore.ts
CHANGED
|
@@ -42,7 +42,7 @@ export class StreamVideoWriteableStateStore {
|
|
|
42
42
|
* The currently connected user.
|
|
43
43
|
*/
|
|
44
44
|
get connectedUser(): OwnUserResponse | undefined {
|
|
45
|
-
return this.connectedUserSubject
|
|
45
|
+
return RxUtils.getCurrentValue(this.connectedUserSubject);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|