Skip to content

Commit 973a6db

Browse files
authored
chore(llc): cancel call operations on leave (#1022)
* cancel call operations on leave * racing sfuError vs joinResponse * try catch network requests during clean
1 parent 650befb commit 973a6db

File tree

6 files changed

+230
-101
lines changed

6 files changed

+230
-101
lines changed

packages/stream_video/lib/src/call/call.dart

Lines changed: 157 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ class Call {
286286
bool _initialized = false;
287287
bool _leaveCallTriggered = false;
288288

289+
// Completer that will be completed when call lifecycle ends (call leave is called)
290+
final Completer<void> _callLifecycleCompleter = Completer<void>();
291+
289292
final Map<String, Timer> _reactionTimers = {};
290293
final Map<String, Timer> _captionsTimers = {};
291294
final List<CancelableOperation<void>> _sfuStatsTimers = [];
@@ -727,6 +730,11 @@ class Call {
727730
return result;
728731
}
729732

733+
if (_callLifecycleCompleter.isCompleted) {
734+
_logger.w(() => '[join] rejected (call was left)');
735+
return Result.error('call was left');
736+
}
737+
730738
_stateManager.lifecycleCallConnecting(
731739
attempt: _reconnectAttempts,
732740
strategy: _reconnectStrategy,
@@ -748,6 +756,11 @@ class Call {
748756
_credentials = joinedResult.data;
749757
_previousSession = _session;
750758

759+
if (_callLifecycleCompleter.isCompleted) {
760+
_logger.w(() => '[join] rejected (call was left during joining)');
761+
return Result.error('call was left');
762+
}
763+
751764
final reconnectDetails =
752765
_reconnectStrategy == SfuReconnectionStrategy.unspecified
753766
? null
@@ -786,6 +799,13 @@ class Call {
786799
sessionId: _session!.sessionId,
787800
);
788801

802+
if (_callLifecycleCompleter.isCompleted) {
803+
_logger.w(
804+
() => '[join] rejected (call was left during session creation)',
805+
);
806+
return Result.error('call was left');
807+
}
808+
789809
_logger.d(() => '[join] starting sfu session');
790810

791811
final sessionResult = await _startSession(
@@ -918,6 +938,11 @@ class Call {
918938
}) async {
919939
_logger.d(() => '[joinCall] cid: $callCid, migratingFrom: $migratingFrom');
920940

941+
if (_callLifecycleCompleter.isCompleted) {
942+
_logger.w(() => '[joinCall] rejected (call was left)');
943+
return Result.error('call was left');
944+
}
945+
921946
final joinResult = await _coordinatorClient.joinCall(
922947
callCid: callCid,
923948
create: create,
@@ -1074,6 +1099,11 @@ class Call {
10741099
localStats: localStats,
10751100
);
10761101

1102+
if (_callLifecycleCompleter.isCompleted) {
1103+
_logger.w(() => '[startSession] rejected (call was left)');
1104+
return Result.error('call was left');
1105+
}
1106+
10771107
final result = await session.start(
10781108
reconnectDetails: reconnectDetails,
10791109
onRtcManagerCreatedCallback: (_) async {
@@ -1222,6 +1252,11 @@ class Call {
12221252

12231253
var attempt = 0;
12241254
do {
1255+
if (_callLifecycleCompleter.isCompleted) {
1256+
_logger.w(() => '[reconnect] rejected (call was left)');
1257+
return;
1258+
}
1259+
12251260
_stateManager.lifecycleCallConnecting(
12261261
attempt: _reconnectAttempts,
12271262
strategy: strategy,
@@ -1241,6 +1276,13 @@ class Call {
12411276
return;
12421277
}
12431278

1279+
if (_callLifecycleCompleter.isCompleted) {
1280+
_logger.w(
1281+
() => '[reconnect] rejected (call was left during network wait)',
1282+
);
1283+
return;
1284+
}
1285+
12441286
await _sfuStatsReporter?.sendSfuStats();
12451287

12461288
switch (_reconnectStrategy) {
@@ -1370,16 +1412,28 @@ class Call {
13701412
final previousCheckInterval = networkMonitor.checkInterval;
13711413
networkMonitor.setIntervalAndResetTimer(const Duration(seconds: 1));
13721414

1373-
final connectionStatus = await networkMonitor.onStatusChange
1415+
final networkFuture = networkMonitor.onStatusChange
13741416
.startWithFuture(networkMonitor.internetStatus)
13751417
.firstWhere((status) => status == InternetStatus.connected)
13761418
.timeout(
1377-
_retryPolicy.config.callRejoinTimeout,
1378-
onTimeout: () {
1379-
_logger.w(() => '[awaitNetworkAwailable] timeout');
1380-
return InternetStatus.disconnected;
1381-
},
1382-
)
1419+
_retryPolicy.config.callRejoinTimeout,
1420+
onTimeout: () {
1421+
_logger.w(() => '[awaitNetworkAwailable] timeout');
1422+
return InternetStatus.disconnected;
1423+
},
1424+
);
1425+
1426+
final lifecycleFuture = _callLifecycleCompleter.future.then((_) {
1427+
_logger.w(() => '[awaitNetworkAwailable] call was left');
1428+
return InternetStatus.disconnected;
1429+
});
1430+
1431+
// Race the network future against the call lifecycle cancellable
1432+
// to ensure we don't wait for the network if the call was left
1433+
final connectionStatus = await Future.any([
1434+
networkFuture,
1435+
lifecycleFuture,
1436+
])
13831437
.asCancelable()
13841438
.storeIn(_idFastReconnectTimeout, _cancelables)
13851439
.valueOrDefault(InternetStatus.disconnected);
@@ -1412,7 +1466,19 @@ class Call {
14121466

14131467
if (futureResult != null) {
14141468
_logger.v(() => '[awaitIfNeeded] return cancelable');
1415-
return futureResult.asCancelable().storeIn(_idAwait, _cancelables).value;
1469+
1470+
final lifecycleFuture =
1471+
_callLifecycleCompleter.future.then<Result<None>>((_) {
1472+
_logger.w(() => '[awaitIfNeeded] call was left');
1473+
return Result.error('call was left');
1474+
});
1475+
1476+
// Race the await future against the call lifecycle cancellable
1477+
// to ensure we don't wait for the call status change if it was left
1478+
return Future.any([futureResult, lifecycleFuture])
1479+
.asCancelable()
1480+
.storeIn(_idAwait, _cancelables)
1481+
.value;
14161482
}
14171483

14181484
return const Result.success(none);
@@ -1424,16 +1490,22 @@ class Call {
14241490
Future<Result<None>> leave({DisconnectReason? reason}) async {
14251491
try {
14261492
if (_leaveCallTriggered) {
1427-
_logger.w(() => '[leave] rejected (already leaving call)');
1493+
_logger.i(() => '[leave] rejected (already leaving call)');
14281494
return const Result.success(none);
14291495
}
1496+
14301497
_leaveCallTriggered = true;
14311498

1499+
// Complete the leave completer to cancel ongoing operations
1500+
if (!_callLifecycleCompleter.isCompleted) {
1501+
_callLifecycleCompleter.complete();
1502+
}
1503+
14321504
final state = this.state.value;
14331505
_logger.i(() => '[leave] state: $state');
14341506

14351507
if (state.status.isDisconnected) {
1436-
_logger.w(() => '[leave] rejected (state.status is disconnected)');
1508+
_logger.d(() => '[leave] rejected (state.status is disconnected)');
14371509
return const Result.success(none);
14381510
}
14391511

@@ -1458,7 +1530,11 @@ class Call {
14581530

14591531
if (state.value.settings.audio.noiseCancellation?.mode ==
14601532
NoiceCancellationSettingsMode.autoOn) {
1461-
await stopAudioProcessing();
1533+
try {
1534+
await stopAudioProcessing();
1535+
} catch (e) {
1536+
_logger.w(() => '[clear] stopAudioProcessing failed: $e');
1537+
}
14621538
}
14631539

14641540
for (final timer in [
@@ -1473,14 +1549,18 @@ class Call {
14731549
}
14741550

14751551
_sfuStatsReporter?.stop();
1476-
14771552
_subscriptions.cancelAll();
14781553
_cancelables.cancelAll();
1479-
await _session?.dispose();
1554+
1555+
try {
1556+
await _session?.dispose();
1557+
} catch (e) {
1558+
_logger.w(() => '[clear] stop dispose failed: $e');
1559+
}
1560+
14801561
await dynascaleManager.dispose();
14811562

14821563
await _streamVideo.state.removeActiveCall(this);
1483-
14841564
if (_streamVideo.state.outgoingCall.valueOrNull?.callCid == callCid) {
14851565
await _streamVideo.state.setOutgoingCall(null);
14861566
}
@@ -1853,6 +1933,35 @@ class Call {
18531933
);
18541934
}
18551935

1936+
Future<Result<T>> _performGetOperation<T>({
1937+
required bool watch,
1938+
required Future<Result<T>> Function() coordinatorCall,
1939+
required CallMetadata Function(T data) onSuccess,
1940+
}) async {
1941+
if (watch) {
1942+
_observeEvents();
1943+
}
1944+
1945+
final response = await coordinatorCall();
1946+
1947+
return response.fold(
1948+
success: (success) async {
1949+
_logger.v(() => '[performGetOperation] success: $success');
1950+
1951+
final callMetadata = onSuccess(success.data);
1952+
await _applyCallSettingsToConnectOptions(
1953+
callMetadata.settings,
1954+
);
1955+
1956+
return success;
1957+
},
1958+
failure: (error) {
1959+
_logger.e(() => '[performGetOperation] failed: $error');
1960+
return error;
1961+
},
1962+
);
1963+
}
1964+
18561965
/// Loads the information about the call.
18571966
///
18581967
/// - [ringing]: If `true`, sends a VoIP notification, triggering the native call screen on iOS and Android.
@@ -1868,36 +1977,27 @@ class Call {
18681977
bool watch = true,
18691978
}) async {
18701979
_logger.d(
1871-
() => '[get] cid: $callCid, membersLimit: $membersLimit'
1872-
', ringing: $ringing, notify: $notify, video: $video',
1980+
() => '[get] callCid: $callCid, membersLimit: $membersLimit, '
1981+
'ringing: $ringing, notify: $notify, video: $video, watch: $watch',
18731982
);
18741983

1875-
if (watch) {
1876-
_observeEvents();
1877-
}
1878-
1879-
final response = await _coordinatorClient.getCall(
1880-
callCid: callCid,
1881-
membersLimit: membersLimit,
1882-
ringing: ringing,
1883-
notify: notify,
1884-
video: video,
1885-
);
1886-
1887-
return response.fold(
1888-
success: (it) {
1984+
return _performGetOperation<CallReceivedData>(
1985+
watch: watch,
1986+
coordinatorCall: () => _coordinatorClient.getCall(
1987+
callCid: callCid,
1988+
membersLimit: membersLimit,
1989+
ringing: ringing,
1990+
notify: notify,
1991+
video: video,
1992+
),
1993+
onSuccess: (data) {
18891994
_stateManager.updateFromCallReceivedData(
1890-
it.data,
1995+
data,
18911996
ringing: ringing,
18921997
notify: notify,
18931998
);
18941999

1895-
_logger.v(() => '[get] completed: ${it.data}');
1896-
return it;
1897-
},
1898-
failure: (it) {
1899-
_logger.e(() => '[get] failed: ${it.error}');
1900-
return it;
2000+
return data.metadata;
19012001
},
19022002
);
19032003
}
@@ -1947,19 +2047,6 @@ class Call {
19472047
StreamFrameRecordingSettings? frameRecording,
19482048
Map<String, Object> custom = const {},
19492049
}) async {
1950-
_logger.d(
1951-
() => '[getOrCreate] cid: $callCid, ringing: $ringing, '
1952-
'memberIds: $memberIds',
1953-
);
1954-
1955-
if (watch) {
1956-
_observeEvents();
1957-
}
1958-
1959-
if (ringing) {
1960-
await _streamVideo.state.setOutgoingCall(this);
1961-
}
1962-
19632050
final settingsOverride = CallSettingsRequest(
19642051
audio: audio?.toOpenDto(),
19652052
video: videoSettings?.toOpenDto(),
@@ -1984,37 +2071,32 @@ class Call {
19842071
...members,
19852072
];
19862073

1987-
final response = await _coordinatorClient.getOrCreateCall(
1988-
callCid: callCid,
1989-
ringing: ringing,
1990-
members: aggregatedMembers,
1991-
team: team,
1992-
notify: notify,
1993-
video: video,
1994-
startsAt: startsAt,
1995-
membersLimit: membersLimit,
1996-
settingsOverride: settingsOverride,
1997-
custom: custom,
1998-
);
1999-
2000-
return response.fold(
2001-
success: (it) async {
2002-
await _applyCallSettingsToConnectOptions(
2003-
it.data.data.metadata.settings,
2004-
);
2074+
if (ringing) {
2075+
await _streamVideo.state.setOutgoingCall(this);
2076+
}
20052077

2078+
return _performGetOperation<CallReceivedOrCreatedData>(
2079+
watch: watch,
2080+
coordinatorCall: () => _coordinatorClient.getOrCreateCall(
2081+
callCid: callCid,
2082+
ringing: ringing,
2083+
members: aggregatedMembers,
2084+
team: team,
2085+
notify: notify,
2086+
video: video,
2087+
startsAt: startsAt,
2088+
membersLimit: membersLimit,
2089+
settingsOverride: settingsOverride,
2090+
custom: custom,
2091+
),
2092+
onSuccess: (data) {
20062093
_stateManager.updateFromCallCreatedData(
2007-
it.data.data,
2094+
data.data,
20082095
ringing: ringing,
20092096
callConnectOptions: connectOptions,
20102097
);
20112098

2012-
_logger.v(() => '[getOrCreate] completed: ${it.data}');
2013-
return it;
2014-
},
2015-
failure: (it) {
2016-
_logger.e(() => '[getOrCreate] failed: ${it.error}');
2017-
return it;
2099+
return data.data.metadata;
20182100
},
20192101
);
20202102
}

0 commit comments

Comments
 (0)