@@ -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