diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart index 90f9abf7a..e0137ee38 100644 --- a/example/lib/pages/room.dart +++ b/example/lib/pages/room.dart @@ -94,6 +94,11 @@ class _RoomPageState extends State { ..on((event) { context.showRecordingStatusChangedDialog(event.activeRecording); }) + ..on((event) { + print( + 'Attempting to reconnect ${event.attempt}/${event.maxAttemptsRetry}, ' + '(${event.nextRetryDelaysInMs}ms delay until next attempt)'); + }) ..on((_) => _sortParticipants()) ..on((_) => _sortParticipants()) ..on((_) => _sortParticipants()) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 5b881b6a8..6fea7c8f1 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -228,6 +228,7 @@ class Engine extends Disposable with EventsEmittable { await signalClient.cleanUp(); fullReconnectOnNext = false; + attemptingReconnect = false; clearPendingReconnect(); } @@ -625,8 +626,25 @@ class Engine extends Disposable with EventsEmittable { reconnectStart = DateTime.now(); } + if (reconnectAttempts! >= _reconnectCount) { + logger.fine('reconnectAttempts exceeded, disconnecting...'); + _isClosed = true; + await cleanUp(); + + events.emit(EngineDisconnectedEvent( + reason: DisconnectReason.reconnectAttemptsExceeded, + )); + return; + } + var delay = defaultRetryDelaysInMs[reconnectAttempts!]; + events.emit(EngineAttemptReconnectEvent( + attempt: reconnectAttempts! + 1, + maxAttempts: _reconnectCount, + nextRetryDelaysInMs: delay, + )); + clearReconnectTimeout(); logger.fine( 'WebSocket reconnecting in $delay ms, retry times $reconnectAttempts'); @@ -656,19 +674,10 @@ class Engine extends Disposable with EventsEmittable { fullReconnectOnNext = true; } - if (reconnectAttempts! >= _reconnectCount) { - logger.fine('reconnectAttempts exceeded, disconnecting...'); - events.emit(EngineDisconnectedEvent( - reason: DisconnectReason.connectionClosed, - )); - await cleanUp(); - return; - } - try { attemptingReconnect = true; - if (await signalClient.checkInternetConnection() == false) { + if (await signalClient.networkIsAvailable() == false) { logger.fine('no internet connection, waiting...'); await signalClient.events.waitFor( duration: connectOptions.timeouts.connection * 10, @@ -688,14 +697,12 @@ class Engine extends Disposable with EventsEmittable { } catch (e) { reconnectAttempts = reconnectAttempts! + 1; bool recoverable = true; - if (e is WebSocketException || - e is ConnectException || - e is MediaConnectException) { + if (e is WebSocketException || e is MediaConnectException) { // cannot resume connection, need to do full reconnect fullReconnectOnNext = true; - } else if (e is TimeoutException) { - fullReconnectOnNext = false; - } else { + } + + if (e is UnexpectedConnectionState) { recoverable = false; } @@ -704,7 +711,7 @@ class Engine extends Disposable with EventsEmittable { } else { logger.fine('attemptReconnect: disconnecting...'); events.emit(EngineDisconnectedEvent( - reason: DisconnectReason.connectionClosed, + reason: DisconnectReason.disconnected, )); await cleanUp(); } @@ -835,7 +842,7 @@ class Engine extends Disposable with EventsEmittable { } void _setUpEngineListeners() => - events.on((event) async { + events.on((event) async { // send queued requests if engine re-connected signalClient.sendQueuedRequests(); }); @@ -907,6 +914,7 @@ class Engine extends Disposable with EventsEmittable { }) ..on((event) async { logger.fine('Signal connecting'); + events.emit(const EngineConnectingEvent()); }) ..on((event) async { logger.fine('Signal reconnecting'); @@ -914,7 +922,7 @@ class Engine extends Disposable with EventsEmittable { }) ..on((event) async { logger.fine('Signal disconnected ${event.reason}'); - if (event.reason == DisconnectReason.connectionClosed && !_isClosed) { + if (event.reason == DisconnectReason.disconnected && !_isClosed) { await handleDisconnect(ClientDisconnectReason.signal); } else { events.emit(EngineDisconnectedEvent( diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index 3d635a970..2c18d5c2d 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -359,6 +359,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { events.emit(const RoomReconnectedEvent()); // re-send tracks permissions localParticipant?.sendTrackSubscriptionPermissions(); + notifyListeners(); }) ..on((event) async { events.emit(const RoomRestartingEvent()); @@ -379,6 +380,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { events.emit(ParticipantDisconnectedEvent(participant: participant)); await participant.dispose(); } + notifyListeners(); }) ..on((event) async { events.emit(const RoomRestartedEvent()); @@ -393,14 +395,32 @@ class Room extends DisposableChangeNotifier with EventsEmittable { } } } + notifyListeners(); }) ..on((event) async { events.emit(const RoomReconnectingEvent()); await _sendSyncState(); + notifyListeners(); + }) + ..on((event) async { + events.emit(RoomAttemptReconnectEvent( + attempt: event.attempt, + maxAttemptsRetry: event.maxAttempts, + nextRetryDelaysInMs: event.nextRetryDelaysInMs, + )); + notifyListeners(); }) ..on((event) async { - await _cleanUp(); - events.emit(RoomDisconnectedEvent(reason: event.reason)); + if (!engine.fullReconnectOnNext && + ![ + DisconnectReason.signalingConnectionFailure, + DisconnectReason.joinFailure, + DisconnectReason.noInternetConnection + ].contains(event.reason)) { + await _cleanUp(); + events.emit(RoomDisconnectedEvent(reason: event.reason)); + notifyListeners(); + } }) ..on( (event) => _onEngineActiveSpeakersUpdateEvent(event.speakers)) diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index a492141a7..7e989c459 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -61,8 +61,9 @@ class SignalClient extends Disposable with EventsEmittable { ConnectivityResult? _connectivityResult; StreamSubscription? connectivitySubscription; - Future checkInternetConnection() async { - if (!kIsWeb && !lkPlatformIsTest()) { + Future networkIsAvailable() async { + // Skip check for web or flutter test + if (kIsWeb || lkPlatformIsTest()) { return true; } _connectivityResult = await Connectivity().checkConnectivity(); @@ -98,15 +99,19 @@ class SignalClient extends Disposable with EventsEmittable { .onConnectivityChanged .listen((ConnectivityResult result) { if (_connectivityResult != result) { - _connectivityResult = result; if (result == ConnectivityResult.none) { - logger.warning('lost internet connection'); + logger.warning('lost connectivity'); } else { - logger.info('internet connection restored'); - events.emit(SignalConnectivityChangedEvent( - state: result, - )); + logger.info( + 'Connectivity changed, ${_connectivityResult!.name} => ${result.name}'); } + + events.emit(SignalConnectivityChangedEvent( + oldState: _connectivityResult!, + state: result, + )); + + _connectivityResult = result; } }); @@ -140,7 +145,7 @@ class SignalClient extends Disposable with EventsEmittable { // Clean up existing socket await cleanUp(); // Attempt to connect - _ws = await _wsConnector( + var future = _wsConnector( rtcUri, WebSocketEventHandlers( onData: _onSocketData, @@ -148,6 +153,8 @@ class SignalClient extends Disposable with EventsEmittable { onError: _onSocketError, ), ); + future = future.timeout(connectOptions.timeouts.connection); + _ws = await future; // Successful connection _connectionState = ConnectionState.connected; events.emit(const SignalConnectedEvent()); @@ -340,8 +347,7 @@ class SignalClient extends Disposable with EventsEmittable { return; } _connectionState = ConnectionState.disconnected; - events.emit( - SignalDisconnectedEvent(reason: DisconnectReason.connectionClosed)); + events.emit(SignalDisconnectedEvent(reason: DisconnectReason.disconnected)); } void _sendPing() { diff --git a/lib/src/events.dart b/lib/src/events.dart index e2b25d0b0..f18664143 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -66,6 +66,21 @@ class RoomReconnectingEvent with RoomEvent { String toString() => '${runtimeType}()'; } +/// report the number of attempts to reconnect to the room. +class RoomAttemptReconnectEvent with RoomEvent { + final int attempt; + final int maxAttemptsRetry; + final int nextRetryDelaysInMs; + const RoomAttemptReconnectEvent({ + required this.attempt, + required this.maxAttemptsRetry, + required this.nextRetryDelaysInMs, + }); + + @override + String toString() => '${runtimeType}()'; +} + /// Connection to room is re-established. All existing state is preserved. /// Emitted by [Room]. class RoomReconnectedEvent with RoomEvent { diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 46245bbf3..9c0fe796f 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -88,3 +88,8 @@ class LiveKitE2EEException extends LiveKitException { @override String toString() => 'E2EE Exception: [$runtimeType] $message'; } + +class UnexpectedConnectionState extends LiveKitException { + UnexpectedConnectionState([String msg = 'Unexpected connection state']) + : super._(msg); +} diff --git a/lib/src/internal/events.dart b/lib/src/internal/events.dart index 1bdbd62df..3a39f7e81 100644 --- a/lib/src/internal/events.dart +++ b/lib/src/internal/events.dart @@ -143,12 +143,19 @@ class SignalReconnectResponseEvent with SignalEvent, InternalEvent { @internal class SignalConnectivityChangedEvent with SignalEvent, InternalEvent { + final ConnectivityResult oldState; final ConnectivityResult state; const SignalConnectivityChangedEvent({ + required this.oldState, required this.state, }); } +@internal +class EngineConnectingEvent with InternalEvent, EngineEvent { + const EngineConnectingEvent(); +} + @internal class EngineConnectedEvent with InternalEvent, SignalEvent, EngineEvent { const EngineConnectedEvent(); @@ -167,6 +174,18 @@ class EngineFullRestartingEvent with InternalEvent, EngineEvent { const EngineFullRestartingEvent(); } +@internal +class EngineAttemptReconnectEvent with InternalEvent, EngineEvent { + int attempt; + int maxAttempts; + int nextRetryDelaysInMs; + EngineAttemptReconnectEvent({ + required this.attempt, + required this.maxAttempts, + required this.nextRetryDelaysInMs, + }); +} + @internal class EngineRestartedEvent with InternalEvent, EngineEvent { const EngineRestartedEvent(); diff --git a/lib/src/types/other.dart b/lib/src/types/other.dart index 615d331b5..28ea642da 100644 --- a/lib/src/types/other.dart +++ b/lib/src/types/other.dart @@ -84,9 +84,10 @@ enum DisconnectReason { roomDeleted, stateMismatch, joinFailure, - connectionClosed, + disconnected, signalingConnectionFailure, noInternetConnection, + reconnectAttemptsExceeded, } /// The reason why a track failed to publish.