@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.1
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 +18 -0
- package/dist/index.browser.es.js +1126 -791
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1126 -791
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1126 -791
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +37 -11
- package/dist/src/StreamVideoClient.d.ts +15 -21
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/events/call.d.ts +0 -11
- package/dist/src/gen/coordinator/index.d.ts +73 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
- package/dist/src/ringing/RingStatePoller.d.ts +46 -0
- package/dist/src/ringing/RingTimeout.d.ts +27 -0
- package/dist/src/ringing/index.d.ts +4 -0
- package/dist/src/ringing/reconcileRingState.d.ts +18 -0
- package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
- package/dist/src/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +118 -267
- package/dist/src/store/ClientState.d.ts +62 -0
- package/dist/src/store/index.d.ts +1 -1
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/types.d.ts +22 -0
- package/dist/src/types.d.ts +33 -4
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/Call.ts +285 -125
- package/src/StreamSfuClient.ts +6 -2
- package/src/StreamVideoClient.ts +40 -47
- package/src/__tests__/Call.autodrop.test.ts +118 -66
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +499 -6
- package/src/__tests__/Call.publishing.test.ts +2 -2
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/__tests__/StreamVideoClient.disconnect.test.ts +62 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/devices/__tests__/CameraManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManager.test.ts +2 -2
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
- package/src/devices/__tests__/ScreenShareManager.test.ts +3 -3
- package/src/devices/__tests__/SpeakerManager.test.ts +4 -4
- package/src/events/__tests__/call.test.ts +5 -220
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +2 -2
- package/src/helpers/__tests__/DynascaleManager.test.ts +2 -2
- package/src/helpers/__tests__/ViewportTracker.test.ts +2 -2
- package/src/reporting/ClientEventReporter.ts +30 -4
- package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
- package/src/ringing/RingStatePoller.ts +177 -0
- package/src/ringing/RingTimeout.ts +90 -0
- package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
- package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
- package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
- package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
- package/src/ringing/index.ts +4 -0
- package/src/ringing/reconcileRingState.ts +108 -0
- package/src/ringing/resolveOwnRingOutcome.ts +58 -0
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Call.reconnect.test.ts +5 -5
- package/src/rtc/__tests__/Publisher.test.ts +209 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +305 -550
- package/src/store/ClientState.ts +101 -0
- package/src/store/__tests__/CallState.test.ts +60 -1
- package/src/store/__tests__/ClientState.test.ts +35 -0
- package/src/store/__tests__/rxUtils.test.ts +67 -1
- package/src/store/index.ts +1 -1
- package/src/store/rxUtils.ts +8 -4
- package/src/store/subjects.ts +30 -0
- package/src/store/types.ts +45 -0
- package/src/types.ts +40 -4
- package/dist/src/store/stateStore.d.ts +0 -84
- package/src/store/stateStore.ts +0 -156
package/src/Call.ts
CHANGED
|
@@ -22,11 +22,7 @@ import {
|
|
|
22
22
|
registerEventHandlers,
|
|
23
23
|
registerRingingCallEventHandlers,
|
|
24
24
|
} from './events/callEventHandlers';
|
|
25
|
-
import {
|
|
26
|
-
CallingState,
|
|
27
|
-
CallState,
|
|
28
|
-
StreamVideoWriteableStateStore,
|
|
29
|
-
} from './store';
|
|
25
|
+
import { CallingState, CallState, ClientState } from './store';
|
|
30
26
|
import {
|
|
31
27
|
createSafeAsyncSubscription,
|
|
32
28
|
createSubscription,
|
|
@@ -50,6 +46,7 @@ import {
|
|
|
50
46
|
EndCallResponse,
|
|
51
47
|
GetCallReportResponse,
|
|
52
48
|
GetCallResponse,
|
|
49
|
+
GetCallRingStateResponse,
|
|
53
50
|
GetCallSessionParticipantStatsDetailsResponse,
|
|
54
51
|
GetOrCreateCallRequest,
|
|
55
52
|
GetOrCreateCallResponse,
|
|
@@ -152,7 +149,7 @@ import {
|
|
|
152
149
|
StatsReporter,
|
|
153
150
|
Tracer,
|
|
154
151
|
} from './stats';
|
|
155
|
-
import type { ClientEventReporter, JoinReason } from './reporting';
|
|
152
|
+
import type { ClientEventReporter, JoinReason, JoinSource } from './reporting';
|
|
156
153
|
import { AudioBindingsWatchdog } from './helpers/AudioBindingsWatchdog';
|
|
157
154
|
import { BlockedAudioTracker } from './helpers/BlockedAudioTracker';
|
|
158
155
|
import { TrackSubscriptionManager } from './helpers/TrackSubscriptionManager';
|
|
@@ -160,6 +157,7 @@ import { DynascaleManager } from './helpers/DynascaleManager';
|
|
|
160
157
|
import { createFirstVideoFrameDetector } from './helpers/firstVideoFrame';
|
|
161
158
|
import { ViewportTracker } from './helpers/ViewportTracker';
|
|
162
159
|
import { PermissionsContext } from './permissions';
|
|
160
|
+
import { RingStatePoller, RingTimeout, resolveOwnRingOutcome } from './ringing';
|
|
163
161
|
import { CallTypes } from './CallType';
|
|
164
162
|
import { StreamClient } from './coordinator/connection/client';
|
|
165
163
|
import { retryInterval, sleep } from './coordinator/connection/utils';
|
|
@@ -302,9 +300,10 @@ export class Call {
|
|
|
302
300
|
private statsReporter?: StatsReporter;
|
|
303
301
|
private sfuStatsReporter?: SfuStatsReporter;
|
|
304
302
|
private lastStatsOptions?: StatsOptions;
|
|
305
|
-
private
|
|
303
|
+
private ringTimeout: RingTimeout | undefined;
|
|
304
|
+
private ringStatePoller: RingStatePoller | undefined;
|
|
306
305
|
|
|
307
|
-
private readonly
|
|
306
|
+
private readonly clientState: ClientState;
|
|
308
307
|
public readonly streamClient: StreamClient;
|
|
309
308
|
public readonly clientEventReporter: ClientEventReporter;
|
|
310
309
|
private sfuClient?: StreamSfuClient;
|
|
@@ -387,7 +386,7 @@ export class Call {
|
|
|
387
386
|
members,
|
|
388
387
|
ownCapabilities,
|
|
389
388
|
sortParticipantsBy,
|
|
390
|
-
|
|
389
|
+
clientState,
|
|
391
390
|
ringing = false,
|
|
392
391
|
watching = false,
|
|
393
392
|
}: CallConstructor) {
|
|
@@ -398,7 +397,7 @@ export class Call {
|
|
|
398
397
|
this.watching = watching;
|
|
399
398
|
this.streamClient = streamClient;
|
|
400
399
|
this.clientEventReporter = clientEventReporter;
|
|
401
|
-
this.
|
|
400
|
+
this.clientState = clientState;
|
|
402
401
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
403
402
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
404
403
|
|
|
@@ -537,40 +536,27 @@ export class Call {
|
|
|
537
536
|
createSubscription(this.state.session$, (session) => {
|
|
538
537
|
if (!this.ringing) return;
|
|
539
538
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
this.cancelAutoDrop();
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const isAcceptedElsewhere =
|
|
551
|
-
isAcceptedByMe && this.state.callingState === CallingState.RINGING;
|
|
539
|
+
const { settledByMe, leaveReason } = resolveOwnRingOutcome({
|
|
540
|
+
session,
|
|
541
|
+
currentUserId: this.currentUserId,
|
|
542
|
+
callingState: this.state.callingState,
|
|
543
|
+
});
|
|
544
|
+
if (settledByMe) this.cancelAutoDrop();
|
|
545
|
+
if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag)) return;
|
|
552
546
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(
|
|
558
|
-
this,
|
|
559
|
-
isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected',
|
|
547
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
|
|
548
|
+
this.leave().catch(() => {
|
|
549
|
+
this.logger.error(
|
|
550
|
+
'Could not leave a call that was accepted or rejected elsewhere',
|
|
560
551
|
);
|
|
561
|
-
|
|
562
|
-
this.logger.error(
|
|
563
|
-
'Could not leave a call that was accepted or rejected elsewhere',
|
|
564
|
-
);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
552
|
+
});
|
|
567
553
|
}),
|
|
568
554
|
);
|
|
569
555
|
};
|
|
570
556
|
|
|
571
557
|
private handleRingingCall = () => {
|
|
572
558
|
const callSession = this.state.session;
|
|
573
|
-
const receiver_id = this.
|
|
559
|
+
const receiver_id = this.clientState.connectedUser?.id;
|
|
574
560
|
const ended_at = callSession?.ended_at;
|
|
575
561
|
const created_by_id = this.state.createdBy?.id;
|
|
576
562
|
|
|
@@ -608,6 +594,7 @@ export class Call {
|
|
|
608
594
|
this.state.setCallingState(CallingState.RINGING);
|
|
609
595
|
}
|
|
610
596
|
this.scheduleAutoDrop();
|
|
597
|
+
this.scheduleRingStatePolling();
|
|
611
598
|
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
612
599
|
}
|
|
613
600
|
};
|
|
@@ -713,6 +700,13 @@ export class Call {
|
|
|
713
700
|
throw new Error('Cannot leave call that has already been left.');
|
|
714
701
|
}
|
|
715
702
|
|
|
703
|
+
// before the first await: the calling state stays RINGING well into the
|
|
704
|
+
// teardown, so pause both watchdogs before they can race this leave. They
|
|
705
|
+
// keep their deadlines, so a failed leave resumes them without handing the
|
|
706
|
+
// ring another window.
|
|
707
|
+
this.ringTimeout?.pause();
|
|
708
|
+
this.ringStatePoller?.pause();
|
|
709
|
+
|
|
716
710
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
717
711
|
const callingState = this.state.callingState;
|
|
718
712
|
|
|
@@ -796,6 +790,11 @@ export class Call {
|
|
|
796
790
|
await this.dynascaleManager?.dispose();
|
|
797
791
|
|
|
798
792
|
this.state.setCallingState(CallingState.LEFT);
|
|
793
|
+
// `ringingSubject` is cleared further down, so this still reads true for
|
|
794
|
+
// a call that was ringing.
|
|
795
|
+
if (this.ringing) {
|
|
796
|
+
globalThis.streamRNVideoSDK?.ringingCallLifecycle?.onLeave(this);
|
|
797
|
+
}
|
|
799
798
|
this.state.setParticipants([]);
|
|
800
799
|
this.state.dispose();
|
|
801
800
|
|
|
@@ -821,7 +820,8 @@ export class Call {
|
|
|
821
820
|
this.unifiedSessionId = undefined;
|
|
822
821
|
this.ringingSubject.next(false);
|
|
823
822
|
this.cancelAutoDrop();
|
|
824
|
-
this.
|
|
823
|
+
this.cancelRingStatePolling();
|
|
824
|
+
this.clientState.unregisterCall(this);
|
|
825
825
|
|
|
826
826
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
827
827
|
isRingingTypeCall: this.ringing,
|
|
@@ -863,6 +863,17 @@ export class Call {
|
|
|
863
863
|
this.logger.warn('Failed to dispose media engine', err);
|
|
864
864
|
});
|
|
865
865
|
}
|
|
866
|
+
}).catch((err) => {
|
|
867
|
+
if (
|
|
868
|
+
!hasPending(this.joinLeaveConcurrencyTag) &&
|
|
869
|
+
this.state.callingState === CallingState.RINGING
|
|
870
|
+
) {
|
|
871
|
+
// resume, never re-arm: a fresh watchdog would restart the ring
|
|
872
|
+
// window this leave was already most of the way through
|
|
873
|
+
this.ringTimeout?.start();
|
|
874
|
+
this.ringStatePoller?.resume();
|
|
875
|
+
}
|
|
876
|
+
throw err;
|
|
866
877
|
});
|
|
867
878
|
};
|
|
868
879
|
|
|
@@ -877,7 +888,7 @@ export class Call {
|
|
|
877
888
|
* Retrieves the current user ID.
|
|
878
889
|
*/
|
|
879
890
|
get currentUserId() {
|
|
880
|
-
return this.
|
|
891
|
+
return this.clientState.connectedUser?.id;
|
|
881
892
|
}
|
|
882
893
|
|
|
883
894
|
/**
|
|
@@ -942,8 +953,8 @@ export class Call {
|
|
|
942
953
|
this.ringingSubject.next(true);
|
|
943
954
|
// we remove the instance from the calls list to enable the following filter in useCalls hook
|
|
944
955
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
945
|
-
const calls = this.
|
|
946
|
-
this.
|
|
956
|
+
const calls = this.clientState.calls.filter((c) => c.cid !== this.cid);
|
|
957
|
+
this.clientState.setCalls([this, ...calls]);
|
|
947
958
|
await this.applyDeviceConfig(settings, { publish: false });
|
|
948
959
|
};
|
|
949
960
|
|
|
@@ -973,12 +984,14 @@ export class Call {
|
|
|
973
984
|
members_limit?: number;
|
|
974
985
|
video?: boolean;
|
|
975
986
|
}): Promise<GetCallResponse> => {
|
|
987
|
+
const getLeaveGeneration = this.leaveGeneration;
|
|
976
988
|
await this.setup();
|
|
977
989
|
|
|
978
990
|
const response = await this.streamClient.get<GetCallResponse>(
|
|
979
991
|
this.streamClientBasePath,
|
|
980
992
|
params,
|
|
981
993
|
);
|
|
994
|
+
if (this.leaveGeneration !== getLeaveGeneration) return response;
|
|
982
995
|
|
|
983
996
|
this.updateFromCallStateResponse(response);
|
|
984
997
|
|
|
@@ -988,7 +1001,7 @@ export class Call {
|
|
|
988
1001
|
|
|
989
1002
|
if (this.streamClient._hasConnectionID()) {
|
|
990
1003
|
this.watching = true;
|
|
991
|
-
this.
|
|
1004
|
+
this.clientState.registerOrUpdateCall(this);
|
|
992
1005
|
}
|
|
993
1006
|
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
994
1007
|
|
|
@@ -1016,7 +1029,7 @@ export class Call {
|
|
|
1016
1029
|
|
|
1017
1030
|
if (this.streamClient._hasConnectionID()) {
|
|
1018
1031
|
this.watching = true;
|
|
1019
|
-
this.
|
|
1032
|
+
this.clientState.registerOrUpdateCall(this);
|
|
1020
1033
|
}
|
|
1021
1034
|
|
|
1022
1035
|
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
@@ -1056,6 +1069,27 @@ export class Call {
|
|
|
1056
1069
|
);
|
|
1057
1070
|
};
|
|
1058
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* Returns who accepted, rejected or missed the ring for a call session.
|
|
1074
|
+
* Safe to poll: it performs no writes and emits no events.
|
|
1075
|
+
*
|
|
1076
|
+
* @param callSessionId the call session to read. Defaults to the current one.
|
|
1077
|
+
* Pass it explicitly to read a session that has already ended, as ending a
|
|
1078
|
+
* call clears its current session.
|
|
1079
|
+
*/
|
|
1080
|
+
getRingState = async (
|
|
1081
|
+
callSessionId?: string,
|
|
1082
|
+
): Promise<GetCallRingStateResponse> => {
|
|
1083
|
+
const sessionId = callSessionId ?? this.state.session?.id;
|
|
1084
|
+
if (!sessionId) {
|
|
1085
|
+
throw new Error('Cannot read the ring state: the call has no session');
|
|
1086
|
+
}
|
|
1087
|
+
return this.streamClient.get<GetCallRingStateResponse>(
|
|
1088
|
+
`${this.streamClientBasePath}/ring_state`,
|
|
1089
|
+
{ call_session_id: sessionId },
|
|
1090
|
+
);
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1059
1093
|
/**
|
|
1060
1094
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
1061
1095
|
* Will send a `call.notification` event to the call members.
|
|
@@ -1104,6 +1138,10 @@ export class Call {
|
|
|
1104
1138
|
/**
|
|
1105
1139
|
* Will start to watch for call related WebSocket events and initiate a call session with the server.
|
|
1106
1140
|
*
|
|
1141
|
+
* One instance is one call flow: discard it after leaving, cancelling, or a
|
|
1142
|
+
* failed join, and create a fresh one for a later flow. Reconnection inside a
|
|
1143
|
+
* live call is handled here and needs no new instance.
|
|
1144
|
+
*
|
|
1107
1145
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
1108
1146
|
*/
|
|
1109
1147
|
join = singleFlight(
|
|
@@ -1112,19 +1150,30 @@ export class Call {
|
|
|
1112
1150
|
joinResponseTimeout,
|
|
1113
1151
|
rpcRequestTimeout,
|
|
1114
1152
|
allowOwnTracksLoopback = false,
|
|
1153
|
+
joinSource,
|
|
1115
1154
|
...data
|
|
1116
1155
|
}: JoinCallData & {
|
|
1117
1156
|
maxJoinRetries?: number;
|
|
1118
1157
|
joinResponseTimeout?: number;
|
|
1119
1158
|
rpcRequestTimeout?: number;
|
|
1120
1159
|
allowOwnTracksLoopback?: boolean;
|
|
1160
|
+
joinSource?: JoinSource;
|
|
1121
1161
|
} = {}): Promise<void> => {
|
|
1122
1162
|
const callingState = this.state.callingState;
|
|
1123
1163
|
|
|
1164
|
+
// Ahead of the failure boundary below on purpose: a duplicate join on a
|
|
1165
|
+
// live call is refused without tearing that call down.
|
|
1124
1166
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
1125
1167
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
1126
1168
|
}
|
|
1127
1169
|
|
|
1170
|
+
// snapshot before the first await: a leave() landing at any point from
|
|
1171
|
+
// here on supersedes this join, and the retry loop must bail out instead
|
|
1172
|
+
// of resurrecting a call that leave() already tore down.
|
|
1173
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
1174
|
+
const supersededByLeave = () =>
|
|
1175
|
+
this.leaveGeneration !== joinLeaveGeneration;
|
|
1176
|
+
|
|
1128
1177
|
// we need this to be set before the callingx.joinCall() is
|
|
1129
1178
|
// called to avoid registering the test call in the CallKit/Telecom
|
|
1130
1179
|
this.allowOwnTracksLoopback = allowOwnTracksLoopback;
|
|
@@ -1133,35 +1182,67 @@ export class Call {
|
|
|
1133
1182
|
this.ringingSubject.next(true);
|
|
1134
1183
|
}
|
|
1135
1184
|
|
|
1185
|
+
// A ringing call is joined by the SDK rather than by app code, so React
|
|
1186
|
+
// Native prepares it from in here - there is no earlier point at which the
|
|
1187
|
+
// app holds the call. Read after `data.ring` above, which is what makes an
|
|
1188
|
+
// outgoing call ringing in the first place.
|
|
1189
|
+
const ringingLifecycle = this.ringing
|
|
1190
|
+
? globalThis.streamRNVideoSDK?.ringingCallLifecycle
|
|
1191
|
+
: undefined;
|
|
1136
1192
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1193
|
+
try {
|
|
1194
|
+
if (ringingLifecycle) {
|
|
1195
|
+
await ringingLifecycle.beforeJoin(this);
|
|
1196
|
+
if (supersededByLeave()) {
|
|
1197
|
+
this.logger.debug('Join superseded by leave; not joining');
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1141
1201
|
|
|
1142
|
-
|
|
1202
|
+
if (callingX) {
|
|
1203
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
1204
|
+
await callingX.joinCall(
|
|
1205
|
+
this,
|
|
1206
|
+
this.clientState.calls,
|
|
1207
|
+
supersededByLeave,
|
|
1208
|
+
);
|
|
1209
|
+
if (supersededByLeave()) {
|
|
1210
|
+
this.logger.debug('Join superseded by leave; not setting up');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1143
1214
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
});
|
|
1215
|
+
await this.setup();
|
|
1216
|
+
if (supersededByLeave()) {
|
|
1217
|
+
this.logger.debug('Join superseded by leave; not registering');
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1150
1220
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1221
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
1222
|
+
callType: this.type,
|
|
1223
|
+
callId: this.id,
|
|
1224
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
1225
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
this.joinResponseTimeout = joinResponseTimeout;
|
|
1229
|
+
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
1230
|
+
// we will count the number of join failures per SFU.
|
|
1231
|
+
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
1232
|
+
// field to force the coordinator to provide us another SFU
|
|
1233
|
+
const sfuJoinFailures = new Map<string, number>();
|
|
1234
|
+
const joinData: JoinCallData = data;
|
|
1235
|
+
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
1160
1236
|
await this.clientEventReporter.withJoinLifecycle(
|
|
1161
1237
|
this.cid,
|
|
1162
|
-
'first-attempt',
|
|
1238
|
+
{ joinReason: 'first-attempt', joinSource },
|
|
1163
1239
|
async () => {
|
|
1164
1240
|
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
1241
|
+
// Also covers a leave that lands during the backoff below.
|
|
1242
|
+
if (supersededByLeave()) {
|
|
1243
|
+
this.logger.debug('Join superseded by leave; not attempting');
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1165
1246
|
try {
|
|
1166
1247
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
1167
1248
|
await this.doJoin(data);
|
|
@@ -1169,6 +1250,10 @@ export class Call {
|
|
|
1169
1250
|
delete joinData.migrating_from_list;
|
|
1170
1251
|
return;
|
|
1171
1252
|
} catch (err) {
|
|
1253
|
+
if (supersededByLeave()) {
|
|
1254
|
+
this.logger.debug('Join superseded by leave; not retrying');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1172
1257
|
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
1173
1258
|
if (
|
|
1174
1259
|
(err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
@@ -1209,6 +1294,9 @@ export class Call {
|
|
|
1209
1294
|
);
|
|
1210
1295
|
} catch (error) {
|
|
1211
1296
|
callingX?.endCall(this, 'error');
|
|
1297
|
+
// Ends the failed ringing flow and releases what its setup installed.
|
|
1298
|
+
// Never rejects, so `error` is what the caller sees.
|
|
1299
|
+
await ringingLifecycle?.onJoinFailed(this);
|
|
1212
1300
|
throw error;
|
|
1213
1301
|
}
|
|
1214
1302
|
},
|
|
@@ -1237,6 +1325,10 @@ export class Call {
|
|
|
1237
1325
|
// globals resolve to the call's factory. Idempotent across
|
|
1238
1326
|
// reconnect/migration attempts.
|
|
1239
1327
|
await this.ensureMediaFactory();
|
|
1328
|
+
if (supersededByLeave()) {
|
|
1329
|
+
this.logger.debug('Join superseded by leave; not wiring media');
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1240
1332
|
|
|
1241
1333
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
1242
1334
|
if (callingX) {
|
|
@@ -1264,10 +1356,17 @@ export class Call {
|
|
|
1264
1356
|
'CoordinatorJoin',
|
|
1265
1357
|
() => this.doJoinRequest(data),
|
|
1266
1358
|
);
|
|
1359
|
+
if (!joinResponse || supersededByLeave()) {
|
|
1360
|
+
this.logger.debug(
|
|
1361
|
+
'Join superseded by leave; not creating SFU client',
|
|
1362
|
+
);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1267
1365
|
this.credentials = joinResponse.credentials;
|
|
1268
1366
|
statsOptions = joinResponse.stats_options;
|
|
1269
1367
|
this.lastStatsOptions = statsOptions;
|
|
1270
1368
|
} catch (error) {
|
|
1369
|
+
if (supersededByLeave()) return;
|
|
1271
1370
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
1272
1371
|
const avoidRestoreState =
|
|
1273
1372
|
this.state.callingState === CallingState.OFFLINE;
|
|
@@ -1322,6 +1421,10 @@ export class Call {
|
|
|
1322
1421
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
1323
1422
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
1324
1423
|
]);
|
|
1424
|
+
if (supersededByLeave()) {
|
|
1425
|
+
this.logger.debug('Join superseded by leave; not joining SFU');
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1325
1428
|
const isReconnecting =
|
|
1326
1429
|
this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
1327
1430
|
const reconnectDetails = isReconnecting
|
|
@@ -1355,6 +1458,12 @@ export class Call {
|
|
|
1355
1458
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
1356
1459
|
}),
|
|
1357
1460
|
);
|
|
1461
|
+
if (supersededByLeave()) {
|
|
1462
|
+
this.logger.debug(
|
|
1463
|
+
'Join superseded by leave; not applying SFU response',
|
|
1464
|
+
);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1358
1467
|
|
|
1359
1468
|
this.currentPublishOptions = publishOptions;
|
|
1360
1469
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
@@ -1366,6 +1475,10 @@ export class Call {
|
|
|
1366
1475
|
);
|
|
1367
1476
|
}
|
|
1368
1477
|
} catch (error) {
|
|
1478
|
+
if (supersededByLeave()) {
|
|
1479
|
+
this.logger.debug('Join superseded by leave; ignoring SFU failure');
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1369
1482
|
this.logger.warn('Join SFU request failed', error);
|
|
1370
1483
|
sfuClient.close(
|
|
1371
1484
|
StreamSfuClient.JOIN_FAILED,
|
|
@@ -1377,8 +1490,8 @@ export class Call {
|
|
|
1377
1490
|
}
|
|
1378
1491
|
}
|
|
1379
1492
|
|
|
1380
|
-
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
1381
|
-
// peer-connection setup below
|
|
1493
|
+
// If the user left while this join was in flight, bail before re-setting JOINED
|
|
1494
|
+
// or starting peer-connection setup below.
|
|
1382
1495
|
if (supersededByLeave()) {
|
|
1383
1496
|
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
1384
1497
|
return;
|
|
@@ -1408,6 +1521,10 @@ export class Call {
|
|
|
1408
1521
|
unifiedSessionId: this.unifiedSessionId,
|
|
1409
1522
|
});
|
|
1410
1523
|
}
|
|
1524
|
+
if (supersededByLeave()) {
|
|
1525
|
+
this.logger.debug('Join superseded by leave; not completing join flow');
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1411
1528
|
|
|
1412
1529
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
1413
1530
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
@@ -1593,6 +1710,7 @@ export class Call {
|
|
|
1593
1710
|
closePreviousInstances: boolean;
|
|
1594
1711
|
unifiedSessionId: string;
|
|
1595
1712
|
}) => {
|
|
1713
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
1596
1714
|
const {
|
|
1597
1715
|
sfuClient,
|
|
1598
1716
|
connectionConfig,
|
|
@@ -1609,10 +1727,12 @@ export class Call {
|
|
|
1609
1727
|
// Flush the previous reporter's final sample while its peer connections are
|
|
1610
1728
|
// still alive, before we dispose them below. Awaits only the sampling step.
|
|
1611
1729
|
await this.sfuStatsReporter?.flush();
|
|
1730
|
+
if (this.leaveGeneration !== joinLeaveGeneration) return;
|
|
1612
1731
|
this.sfuStatsReporter?.stop();
|
|
1613
1732
|
this.sfuStatsReporter = undefined;
|
|
1614
1733
|
if (closePreviousInstances && this.subscriber) {
|
|
1615
1734
|
await this.subscriber.dispose();
|
|
1735
|
+
if (this.leaveGeneration !== joinLeaveGeneration) return;
|
|
1616
1736
|
this.state.removeAllOrphanedTracks();
|
|
1617
1737
|
}
|
|
1618
1738
|
const basePeerConnectionOptions: BasePeerConnectionOpts = {
|
|
@@ -1659,6 +1779,7 @@ export class Call {
|
|
|
1659
1779
|
if (!isAnonymous) {
|
|
1660
1780
|
if (closePreviousInstances && this.publisher) {
|
|
1661
1781
|
await this.publisher.dispose();
|
|
1782
|
+
if (this.leaveGeneration !== joinLeaveGeneration) return;
|
|
1662
1783
|
}
|
|
1663
1784
|
this.publisher = new Publisher(
|
|
1664
1785
|
basePeerConnectionOptions,
|
|
@@ -1702,9 +1823,19 @@ export class Call {
|
|
|
1702
1823
|
*
|
|
1703
1824
|
* @internal
|
|
1704
1825
|
* @param data the join call data.
|
|
1826
|
+
* @returns The coordinator response, or undefined if leave superseded the request before it was sent.
|
|
1705
1827
|
*/
|
|
1706
|
-
doJoinRequest = async (
|
|
1828
|
+
doJoinRequest = async (
|
|
1829
|
+
data?: JoinCallData,
|
|
1830
|
+
): Promise<JoinCallResponse | undefined> => {
|
|
1831
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
1707
1832
|
const location = await this.streamClient.getLocationHint();
|
|
1833
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
1834
|
+
this.logger.debug(
|
|
1835
|
+
'Join superseded by leave; not sending coordinator request',
|
|
1836
|
+
);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1708
1839
|
const e2ee = !!this.e2eeManager;
|
|
1709
1840
|
const request: JoinCallRequest = { ...data, location, e2ee };
|
|
1710
1841
|
const joinResponse = await this.streamClient.post<
|
|
@@ -1712,6 +1843,13 @@ export class Call {
|
|
|
1712
1843
|
JoinCallRequest
|
|
1713
1844
|
>(`${this.streamClientBasePath}/join`, request);
|
|
1714
1845
|
|
|
1846
|
+
if (this.leaveGeneration !== joinLeaveGeneration) {
|
|
1847
|
+
this.logger.debug(
|
|
1848
|
+
'Join superseded by leave; not applying coordinator response',
|
|
1849
|
+
);
|
|
1850
|
+
return joinResponse;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1715
1853
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
1716
1854
|
this.state.setMembers(joinResponse.members);
|
|
1717
1855
|
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
@@ -1726,11 +1864,12 @@ export class Call {
|
|
|
1726
1864
|
if (!isReconnecting && this.ringing && !this.isCreatedByMe) {
|
|
1727
1865
|
// signals other users that I have accepted the incoming call.
|
|
1728
1866
|
await this.accept();
|
|
1867
|
+
if (this.leaveGeneration !== joinLeaveGeneration) return joinResponse;
|
|
1729
1868
|
}
|
|
1730
1869
|
|
|
1731
1870
|
if (this.streamClient._hasConnectionID()) {
|
|
1732
1871
|
this.watching = true;
|
|
1733
|
-
this.
|
|
1872
|
+
this.clientState.registerOrUpdateCall(this);
|
|
1734
1873
|
}
|
|
1735
1874
|
|
|
1736
1875
|
return joinResponse;
|
|
@@ -1830,6 +1969,7 @@ export class Call {
|
|
|
1830
1969
|
callingState === CallingState.JOINING ||
|
|
1831
1970
|
callingState === CallingState.RECONNECTING ||
|
|
1832
1971
|
callingState === CallingState.MIGRATING ||
|
|
1972
|
+
callingState === CallingState.LEFT ||
|
|
1833
1973
|
callingState === CallingState.RECONNECTING_FAILED
|
|
1834
1974
|
)
|
|
1835
1975
|
return;
|
|
@@ -1840,19 +1980,26 @@ export class Call {
|
|
|
1840
1980
|
// `POST /join` per entry) once the call is already healthy again.
|
|
1841
1981
|
if (hasPending(this.reconnectConcurrencyTag)) return;
|
|
1842
1982
|
|
|
1983
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
1984
|
+
const supersededByLeave = () =>
|
|
1985
|
+
this.leaveGeneration !== reconnectLeaveGeneration;
|
|
1843
1986
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
1987
|
+
if (supersededByLeave()) return;
|
|
1844
1988
|
const reconnectStartTime = Date.now();
|
|
1845
1989
|
this.reconnectStrategy = strategy;
|
|
1846
1990
|
this.reconnectReason = reason;
|
|
1847
1991
|
const sfuRejoinFailures = new Map<string, number>();
|
|
1848
1992
|
|
|
1849
1993
|
const markAsReconnectingFailed = async () => {
|
|
1994
|
+
if (supersededByLeave()) return;
|
|
1850
1995
|
try {
|
|
1851
1996
|
// attempt to fetch the call data from the server, as the call
|
|
1852
1997
|
// state might have changed while we were reconnecting or were offline
|
|
1853
1998
|
await this.get();
|
|
1854
1999
|
} finally {
|
|
1855
|
-
|
|
2000
|
+
if (!supersededByLeave()) {
|
|
2001
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
2002
|
+
}
|
|
1856
2003
|
}
|
|
1857
2004
|
};
|
|
1858
2005
|
|
|
@@ -1922,6 +2069,7 @@ export class Call {
|
|
|
1922
2069
|
try {
|
|
1923
2070
|
// wait until the network is available
|
|
1924
2071
|
await this.networkAvailableTask?.promise;
|
|
2072
|
+
if (supersededByLeave()) return;
|
|
1925
2073
|
|
|
1926
2074
|
this.logger.info(
|
|
1927
2075
|
`[Reconnect] Reconnecting with strategy ${
|
|
@@ -1972,6 +2120,7 @@ export class Call {
|
|
|
1972
2120
|
this.consecutiveNegotiationFailures = 0;
|
|
1973
2121
|
break; // do-while loop, reconnection worked, exit the loop
|
|
1974
2122
|
} catch (error) {
|
|
2123
|
+
if (supersededByLeave()) return;
|
|
1975
2124
|
if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
|
|
1976
2125
|
const failedSfu = this.credentials?.server.edge_name;
|
|
1977
2126
|
if (failedSfu) {
|
|
@@ -2015,6 +2164,7 @@ export class Call {
|
|
|
2015
2164
|
|
|
2016
2165
|
// exponential backoff with jitter, capped at 5 s
|
|
2017
2166
|
await sleep(retryInterval(attempt));
|
|
2167
|
+
if (supersededByLeave()) return;
|
|
2018
2168
|
|
|
2019
2169
|
const wasMigrating =
|
|
2020
2170
|
this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
@@ -2045,6 +2195,7 @@ export class Call {
|
|
|
2045
2195
|
);
|
|
2046
2196
|
}
|
|
2047
2197
|
} while (
|
|
2198
|
+
!supersededByLeave() &&
|
|
2048
2199
|
this.state.callingState !== CallingState.JOINED &&
|
|
2049
2200
|
this.state.callingState !== CallingState.RECONNECTING_FAILED &&
|
|
2050
2201
|
this.state.callingState !== CallingState.LEFT
|
|
@@ -2058,10 +2209,12 @@ export class Call {
|
|
|
2058
2209
|
* @internal
|
|
2059
2210
|
*/
|
|
2060
2211
|
private reconnectFast = async () => {
|
|
2212
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
2061
2213
|
const reconnectStartTime = Date.now();
|
|
2062
2214
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
2063
2215
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
2064
2216
|
await this.doJoin(this.joinCallData);
|
|
2217
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2065
2218
|
await this.get(); // fetch the latest call state, as it might have changed
|
|
2066
2219
|
this.sfuStatsReporter?.sendReconnectionTime(
|
|
2067
2220
|
WebsocketReconnectStrategy.FAST,
|
|
@@ -2074,6 +2227,7 @@ export class Call {
|
|
|
2074
2227
|
* @internal
|
|
2075
2228
|
*/
|
|
2076
2229
|
private reconnectRejoin = async () => {
|
|
2230
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
2077
2231
|
const reconnectStartTime = Date.now();
|
|
2078
2232
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
2079
2233
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
@@ -2081,10 +2235,14 @@ export class Call {
|
|
|
2081
2235
|
this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
2082
2236
|
? 'network-available'
|
|
2083
2237
|
: 'full-rejoin';
|
|
2084
|
-
await this.clientEventReporter.withJoinLifecycle(
|
|
2085
|
-
this.
|
|
2238
|
+
await this.clientEventReporter.withJoinLifecycle(
|
|
2239
|
+
this.cid,
|
|
2240
|
+
{ joinReason },
|
|
2241
|
+
() => this.doJoin(this.joinCallData),
|
|
2086
2242
|
);
|
|
2243
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2087
2244
|
await this.restorePublishedTracks();
|
|
2245
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2088
2246
|
this.restoreSubscribedTracks();
|
|
2089
2247
|
this.sfuStatsReporter?.sendReconnectionTime(
|
|
2090
2248
|
WebsocketReconnectStrategy.REJOIN,
|
|
@@ -2097,6 +2255,7 @@ export class Call {
|
|
|
2097
2255
|
* @internal
|
|
2098
2256
|
*/
|
|
2099
2257
|
private reconnectMigrate = async () => {
|
|
2258
|
+
const reconnectLeaveGeneration = this.leaveGeneration;
|
|
2100
2259
|
const reconnectStartTime = Date.now();
|
|
2101
2260
|
const currentSfuClient = this.sfuClient;
|
|
2102
2261
|
if (!currentSfuClient) {
|
|
@@ -2114,32 +2273,35 @@ export class Call {
|
|
|
2114
2273
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
2115
2274
|
|
|
2116
2275
|
try {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
this.
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2276
|
+
try {
|
|
2277
|
+
const currentSfu = currentSfuClient.edgeName;
|
|
2278
|
+
await this.clientEventReporter.withJoinLifecycle(
|
|
2279
|
+
this.cid,
|
|
2280
|
+
{ joinReason: 'migration' },
|
|
2281
|
+
() =>
|
|
2282
|
+
this.doJoin({
|
|
2283
|
+
...this.joinCallData,
|
|
2284
|
+
migrating_from: currentSfu,
|
|
2285
|
+
migrating_from_list: [currentSfu],
|
|
2286
|
+
}),
|
|
2287
|
+
);
|
|
2288
|
+
} finally {
|
|
2289
|
+
// cleanup the migration_from field after the migration is complete or failed
|
|
2290
|
+
// as we don't want to keep dirty data in the join call data
|
|
2291
|
+
delete this.joinCallData?.migrating_from;
|
|
2292
|
+
delete this.joinCallData?.migrating_from_list;
|
|
2293
|
+
}
|
|
2134
2294
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2295
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2296
|
+
await this.restorePublishedTracks();
|
|
2297
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2298
|
+
this.restoreSubscribedTracks();
|
|
2137
2299
|
|
|
2138
|
-
try {
|
|
2139
2300
|
// Wait for the migration to complete, then close the previous SFU client
|
|
2140
2301
|
// and the peer connection instances. In case of failure, the migration
|
|
2141
2302
|
// task would throw an error and REJOIN would be attempted.
|
|
2142
2303
|
await migrationTask();
|
|
2304
|
+
if (this.leaveGeneration !== reconnectLeaveGeneration) return;
|
|
2143
2305
|
|
|
2144
2306
|
// in MIGRATE, we can consider the call as joined only after
|
|
2145
2307
|
// `participantMigrationComplete` event is received, signaled by
|
|
@@ -2473,10 +2635,11 @@ export class Call {
|
|
|
2473
2635
|
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
2474
2636
|
* configured for E2EE.
|
|
2475
2637
|
*
|
|
2476
|
-
* The
|
|
2477
|
-
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2638
|
+
* The application owns the manager's lifetime - the SDK never disposes it, and
|
|
2639
|
+
* closing the peer connections does not release it. Discard the manager
|
|
2640
|
+
* together with this call instance once its flow has ended, and create a fresh
|
|
2641
|
+
* pair for a later flow: a disposed manager throws from `encrypt`/`decrypt`
|
|
2642
|
+
* rather than silently publishing nothing.
|
|
2480
2643
|
*
|
|
2481
2644
|
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
2482
2645
|
* built-in AES-GCM scheme, or pass your own implementation.
|
|
@@ -2529,7 +2692,7 @@ export class Call {
|
|
|
2529
2692
|
};
|
|
2530
2693
|
|
|
2531
2694
|
/**
|
|
2532
|
-
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state
|
|
2695
|
+
* Will enhance the reported stats with additional participant-specific information (the `callStatsReport$` state variable).
|
|
2533
2696
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
2534
2697
|
*
|
|
2535
2698
|
* @param sessionId the sessionId to start reporting for.
|
|
@@ -3114,42 +3277,39 @@ export class Call {
|
|
|
3114
3277
|
*/
|
|
3115
3278
|
private scheduleAutoDrop = () => {
|
|
3116
3279
|
this.cancelAutoDrop();
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
if (!settings) return;
|
|
3120
|
-
// ignore if the call is not ringing
|
|
3121
|
-
if (this.state.callingState !== CallingState.RINGING) return;
|
|
3122
|
-
|
|
3123
|
-
const timeoutInMs = this.isCreatedByMe
|
|
3124
|
-
? settings.ring.auto_cancel_timeout_ms
|
|
3125
|
-
: settings.ring.incoming_call_timeout_ms;
|
|
3126
|
-
|
|
3127
|
-
// 0 means no auto-drop
|
|
3128
|
-
if (timeoutInMs <= 0) return;
|
|
3129
|
-
this.dropTimeout = setTimeout(() => {
|
|
3130
|
-
// the call might have stopped ringing by this point,
|
|
3131
|
-
// e.g. it was already accepted and joined
|
|
3132
|
-
if (this.state.callingState !== CallingState.RINGING) return;
|
|
3133
|
-
this.leave({
|
|
3134
|
-
reject: true,
|
|
3135
|
-
reason: 'timeout',
|
|
3136
|
-
message: `ringing timeout - ${
|
|
3137
|
-
this.isCreatedByMe
|
|
3138
|
-
? 'no one accepted'
|
|
3139
|
-
: `user didn't interact with incoming call screen`
|
|
3140
|
-
}`,
|
|
3141
|
-
}).catch((err) => {
|
|
3142
|
-
this.logger.error('Failed to drop call', err);
|
|
3143
|
-
});
|
|
3144
|
-
}, timeoutInMs);
|
|
3280
|
+
this.ringTimeout = new RingTimeout(this);
|
|
3281
|
+
this.ringTimeout.start();
|
|
3145
3282
|
};
|
|
3146
3283
|
|
|
3147
3284
|
/**
|
|
3148
3285
|
* Cancels a scheduled auto-drop timeout.
|
|
3149
3286
|
*/
|
|
3150
3287
|
private cancelAutoDrop = () => {
|
|
3151
|
-
|
|
3152
|
-
this.
|
|
3288
|
+
this.ringTimeout?.stop();
|
|
3289
|
+
this.ringTimeout = undefined;
|
|
3290
|
+
};
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* Starts polling for the ring outcome. Applicable only to ringing calls the
|
|
3294
|
+
* current user created.
|
|
3295
|
+
*/
|
|
3296
|
+
private scheduleRingStatePolling = () => {
|
|
3297
|
+
this.cancelRingStatePolling();
|
|
3298
|
+
|
|
3299
|
+
if (!this.isCreatedByMe) return;
|
|
3300
|
+
const options = this.streamClient.options.ringStatePolling;
|
|
3301
|
+
if (options === false) return;
|
|
3302
|
+
|
|
3303
|
+
this.ringStatePoller = new RingStatePoller(this, options);
|
|
3304
|
+
this.ringStatePoller.start();
|
|
3305
|
+
};
|
|
3306
|
+
|
|
3307
|
+
/**
|
|
3308
|
+
* Cancels the ring state polling.
|
|
3309
|
+
*/
|
|
3310
|
+
private cancelRingStatePolling = () => {
|
|
3311
|
+
this.ringStatePoller?.stop();
|
|
3312
|
+
this.ringStatePoller = undefined;
|
|
3153
3313
|
};
|
|
3154
3314
|
|
|
3155
3315
|
/**
|