flaky-jelly-17724
12/23/2025, 5:01 AMtall-airline-16756
12/23/2025, 5:01 AMtall-airline-16756
12/23/2025, 5:01 AMflaky-jelly-17724
12/23/2025, 5:02 AMenum DisconnectReason {
unknown,
clientInitiated,
duplicateIdentity,
serverShutdown,
participantRemoved,
roomDeleted,
stateMismatch,
joinFailure,
disconnected,
signalingConnectionFailure,
reconnectAttemptsExceeded,
}
on which reasons, i should dispose the room and on which i should waittall-airline-16756
12/23/2025, 5:03 AMDisconnectReason values mean you should dispose the room vs which you can wait/retry. I’ll map each reason and give a short recommendation.
Dispose the room (clean up resources; do not auto-retry)
• clientInitiated — client intentionally disconnected. Dispose.
• duplicateIdentity — another participant used the same identity; reconnecting will fail until identity conflict is resolved. Dispose.
• participantRemoved — server explicitly removed the participant. Dispose.
• roomDeleted — room no longer exists. Dispose.
• serverShutdown — server is shutting down; session cannot be recovered. Dispose.
Wait / allow retry (transient or recoverable; consider limited retries, token refresh)
• signalingConnectionFailure — signaling/WebSocket failure; retry after network check.
• disconnected — generic disconnect (network/server); try short automatic retries.
• joinFailure — failed to join (token expired or transient); refresh token and retry.
• stateMismatch — client/server out of sync after abrupt exit; create a fresh connection attempt.
• reconnectAttemptsExceeded — SDK exhausted automatic retries; you may choose to retry manually after a delay.
Notes and suggestions
• Limit retries and backoff to avoid loops.
• Refresh access tokens before retrying when relevant.
• For duplicateIdentity and participantRemoved, surface a user-facing error explaining why reconnect won't succeed.
• For transient reasons, offer a manual “Reconnect” option if automatic retries fail.
If you want, I can provide a small pseudocode/retry strategy for your SDK (how many retries, backoff, token refresh) — which platform/SDK are you using?
Sources: Connecting to LiveKit | LiveKit docs | Server-side | livekit/livekit_composite | DeepWiki | Diagnosing Connection Errors with Connection Test Utility | Understanding the STATE_MISMATCH Disconnect Reasonflaky-jelly-17724
12/23/2025, 5:06 AMtall-airline-16756
12/23/2025, 5:06 AMclientInitiated.
clientInitiated means the SDK or your app intentionally called disconnect (or the SDK treated the session as intentionally closed). That event indicates the previous Room session should be considered ended and its resources disposed. The fact a new meeting connects successfully after that is expected: you can create a new Room/connection after disposing the old one.
To help pinpoint why you received clientInitiated in your case, please tell me:
• Which LiveKit SDK/platform and version are you using (Web, iOS, Android, Flutter, or server SDK)?
• The sequence of actions or logs around the disconnect (did your code call disconnect() or similar, or was there an implicit call?)
• Any relevant code snippet around connection/disconnection or reconnection logic.
With that I can suggest specific fixes (prevent accidental disconnect calls, defer disposal until you want a fresh session, or adjust reconnect behavior).flaky-jelly-17724
12/23/2025, 5:08 AMSocketService().onShiftRoom.listen((event) async {
Logger.log(
'Received room shift request: ${event.displayName}, url length: ${event.socketURL?.length}, signature length is ${event.signature?.length}',
);
if (_isShiftingRoom) return;
_isShiftingRoom = true;
_meetingState = MeetingState.reconnecting;
_notifyImmediate();
try {
Logger.log('Shifting room');
showToast('You are being moved to another room');
await disposeRoom(isShiftingRoom: true);
Logger.log('previous room disposed');
_meetingState = MeetingState.connecting;
_notifyImmediate();
Logger.log('initializing new room socket');
await initializeRoom(event.socketURL ?? '', event.signature ?? '');
Logger.log(
'new room initialized and clearing participant selection if any',
);
Helper.clearParticipantSelection();
showToast('You have been moved to another room');
} catch (e, st) {
await Logger.logException('Exception on room shift $e', st);
fw.debugPrint('Shift room failed: $e\n$st');
await disposeRoom();
// showToast("Unable to rejoin meeting");
} finally {
Logger.log('Shift room finally');
_isShiftingRoom = false;
_notifyImmediate();
}
});
}
showToast(String message) {
UserFeedback.showToast(Helper.context, message, duration: 5);
}
bool _roomOperationInProgress = false;
/// Initialize and connect to the room
Future<void> initializeRoom(String url, String token) async {
if (_meetingState == MeetingState.connected) {
fw.debugPrint("initializeRoom ignored: already connecting/connected");
Logger.log("initializeRoom ignored: already connecting/connected");
return;
}
if (_roomOperationInProgress) {
Logger.log("Room operation already in progress");
fw.debugPrint("Room operation already in progress");
return;
}
_roomOperationInProgress = true;
Logger.log("Initializing room and setting meeting state as connecting");
_meetingState = MeetingState.connecting;
_notify();
_room = Room(
roomOptions: RoomOptions(
adaptiveStream: true,
dynacast: true,
defaultVideoPublishOptions: VideoPublishOptions(simulcast: true),
),
);
Logger.log("Room initialized successfully");
// Setup listeners
_setupRoomListeners();
Logger.log("set up room listeners done");
_setupEventListeners();
Logger.log("set up event listeners done");
try {
await _room!.connect(
url,
token,
connectOptions: const ConnectOptions(
timeouts: Timeouts(
connection: Duration(seconds: 30),
peerConnection: Duration(seconds: 20),
publish: Duration(seconds: 20),
debounce: Duration(seconds: 5),
iceRestart: Duration(seconds: 20),
),
),
);
Logger.log("Room connected successfully");
// Publish camera & mic
await _publishInitialTracks();
await Future.delayed(const Duration(seconds: 1));
// Apply selected audio output if one was chosen before connection
if (_selectedAudioOutputDevice != null) {
await updateAudioOutput(_selectedAudioOutputDevice!);
}
Logger.log("Published initial tracks and now resetting the ctrls");
//reset before sync event
await resetCtrl();
Logger.log('calling fireSyncEvent Api');
await MeetingHelper.fireSyncEvent('', MeetingEventType.initialSync);
Logger.log('fireSyncEvent Api done');
_meetingState = MeetingState.connected;
_notifyImmediate();
Logger.log("Room connected successfully and meeting state is connected");
} catch (e, s) {
_meetingState = MeetingState.error;
_notifyImmediate();
await Logger.logException('Exception in room initialization $e', s);
await Future.delayed(const Duration(milliseconds: 500));
await disposeRoom();
SocketService().leaveMeetingChannels();
await Future.delayed(const Duration(seconds: 1));
Helper.navigateTo(RouteNames.main, popAll: true);
fw.debugPrint(
'exception in join meeting ${e.toString()}, ${s.toString()}',
);
String msg =
e.toString().contains("expired")
? "Meeting link has expired. Please request a new one."
: "Unable to join meeting. Please try again.";
Logger.log('exception message shown to user: $msg');
showToast(msg);
Logger.log('room disposed');
} finally {
Logger.log('room initialization finally called');
_roomOperationInProgress = false;
}
}
void _setupEventListeners() {
_listener =
_room!.createListener()
..on<TrackMutedEvent>((event) {
MeetingHelper.handleMuteEvent(event, room);
_notify();
Logger.log('track mute event ${event.toString()}');
})
..on<TrackUnmutedEvent>((event) {
MeetingHelper.handleUnmuteEvent(event, room);
_notify();
Logger.log('track unmute event ${event.toString()}');
})
..on<ParticipantConnectedEvent>((event) {
//update the participant object in participant wrapper to make sure that participant never gets null
Helper.context
.read<ParticipantWrapperCtrl>()
.updateParticipantObject(event.participant);
Logger.log('participant connected event ${event.toString()}');
})
..on<ParticipantDisconnectedEvent>((event) {
MeetingHelper.fireSyncEvent(
event.participant.sid,
MeetingEventType.participantLeft,
);
Logger.log('participant disconnected event ${event.toString()}');
})
..on<ActiveSpeakersChangedEvent>((event) {
final lp = room?.localParticipant;
final nowSpeaking = event.speakers.any((s) => s.sid == lp?.sid);
if (_isSpeaking != nowSpeaking) {
_isSpeaking = nowSpeaking;
_notify();
}
// Logger.log('active speaker changed event ${event.toString()}');
})
..on<RoomConnectedEvent>((event) {
Logger.log('room connected event ${event.room.connectionState}');
_meetingState = MeetingState.connected;
_notify();
})
..on<RoomReconnectingEvent>((event) {
Logger.log('room reconnecting event ${event.toString()}');
_meetingState = MeetingState.reconnecting;
_notify();
})
..on<RoomReconnectedEvent>((event) {
Logger.log('room reconnected event ${event.toString()}');
_meetingState = MeetingState.connected;
_notify();
})
..on<RoomDisconnectedEvent>((event) {
Logger.log('room disconnected event ${event.reason?.name}');
fw.debugPrint('disconnected event ${event.reason?.name}');
_meetingState = MeetingHelper().getMeetingState(event.reason);
_notify();
})
..on<DataReceivedEvent>((event) {
Logger.log('room data received ${event.toString()}');
try {
final Map<String, dynamic> json =
jsonDecode(utf8.decode(event.data)) as Map<String, dynamic>;
final target = json['target'] as String?;
final actionStr = json['action'] as String?;
final payload = json['payload'] as Map<String, dynamic>?;
fw.debugPrint(
'Received action request: $actionStr, payload: $payload',
);
Logger.log(
'Received action request: $actionStr, payload: $payload',
);
if (target != _room?.localParticipant?.sid) return;
// Find enum safely (nullable)
ActionRequestEnum? action = ActionRequestEnum.values
.firstWhereOrNull((a) => a.name == actionStr);
if (action == null) {
Logger.log('Received unknown action request: $actionStr');
fw.debugPrint('Unknown action received: $actionStr');
return;
}
MeetingHelper.handleActionRequest(action, payload: payload);
} catch (e, st) {
Logger.logException('Failed to parse action request: $e', st);
fw.debugPrint('Failed to parse action request: $e\n$st');
}
});
}tall-airline-16756
12/23/2025, 5:09 AMclientInitiated during a room shift.
Short answer: this is expected. clientInitiated means the SDK observed a local, intentional disconnect (your disposeRoom() / room.disconnect()), so disposing the old Room and creating a new one is correct and why the new meeting connects successfully.
A few targeted suggestions to make the shift flow robust:
• Ensure disposeRoom does a full, graceful cleanup:
◦ remove/ dispose the _listener
◦ await _room?.disconnect() (catch/log errors)
◦ cancel any subscriptions tied to the old room
◦ set _room = null after disconnect
• In your shift flow keep these guards (you already have many):
◦ _isShiftingRoom and _roomOperationInProgress to prevent concurrent ops
◦ check and set meeting state consistently (connecting → connected)
• Distinguish expected vs unexpected disconnects in your listener:
◦ If event.reason == DisconnectReason.clientInitiated, treat it as part of the shift and do not show an error to the user.
◦ For transient reasons (signalingConnectionFailure, disconnected, joinFailure, stateMismatch, reconnectAttemptsExceeded), show a reconnect message and consider limited retries/backoff or token refresh before disposing.
• Token refresh: if joinFailure or auth-related errors occur during initialize, refresh/renew the token before retrying.
If you want, I can:
• Suggest a concrete disposeRoom implementation for your code.
• Draft the RoomDisconnectedEvent handler logic that branches on event.reason.
Which would you prefer?
Sources: Connecting to LiveKit | LiveKit docs | Understanding the STATE_MISMATCH Disconnect Reason | Server-side | livekit/livekit_composite | DeepWikiflaky-jelly-17724
12/23/2025, 5:16 AMtall-airline-16756
12/23/2025, 5:16 AMdisposeRoom implementation for your Flutter code, or
• Draft a RoomDisconnectedEvent handler that branches on event.reason with retry/backoff logic and token refresh.
Which one should I prepare?flaky-jelly-17724
12/23/2025, 5:16 AMtall-airline-16756
12/23/2025, 5:17 AMRoomDisconnectedEvent handler for Flutter that:
• treats permanent reasons as immediate dispose,
• retries transient reasons with exponential backoff and token refresh,
• keeps your existing guards (_roomOperationInProgress / _isShiftingRoom) in mind.
Adjust retry limits, delays, and token-refresh logic to fit your app.
// Place inside _setupEventListeners() where you register RoomDisconnectedEvent.
..on<RoomDisconnectedEvent>((event) async {
final reason = event.reason;
Logger.log('room disconnected event ${reason?.name}');
fw.debugPrint('disconnected event ${reason?.name}');
// Early guards: if we're actively shifting rooms, ignore handling here
if (_isShiftingRoom) {
Logger.log('Ignoring disconnected event during room shift');
return;
}
// Permanent reasons -> immediate dispose
final permanent = {
DisconnectReason.clientInitiated,
DisconnectReason.duplicateIdentity,
DisconnectReason.participantRemoved,
DisconnectReason.roomDeleted,
DisconnectReason.serverShutdown,
};
if (reason != null && permanent.contains(reason)) {
Logger.log('Permanent disconnect ($reason). Disposing room now.');
_meetingState = MeetingState.disconnected;
_notify();
await disposeRoom(); // ensure disposeRoom does full cleanup
return;
}
// Transient reasons -> attempt retries with backoff & token refresh
final transient = {
DisconnectReason.signalingConnectionFailure,
DisconnectReason.disconnected,
DisconnectReason.joinFailure,
DisconnectReason.stateMismatch,
DisconnectReason.reconnectAttemptsExceeded,
};
if (reason == null || transient.contains(reason)) {
Logger.log('Transient disconnect ($reason). Starting retry flow.');
_meetingState = MeetingState.reconnecting;
_notify();
// Retry policy
const int maxAttempts = 4;
int attempt = 0;
int baseDelayMs = 1000; // 1s initial
bool reconnected = false;
while (attempt < maxAttempts && !_isShiftingRoom) {
attempt++;
final delayMs = baseDelayMs * (1 << (attempt - 1)); // exponential backoff
Logger.log('Reconnect attempt $attempt/$maxAttempts after ${delayMs}ms');
await Future.delayed(Duration(milliseconds: delayMs));
// Optionally refresh the token on certain reasons (e.g., joinFailure)
if (reason == DisconnectReason.joinFailure || attempt == 2) {
try {
Logger.log('Refreshing token before retry');
final newToken = await MeetingHelper.refreshToken(); // implement this
if (newToken != null) {
// update token variable used by initializeRoom
_lastKnownToken = newToken;
}
} catch (e, s) {
Logger.logException('Token refresh failed: $e', s);
}
}
if (_roomOperationInProgress) {
Logger.log('Room op in progress; aborting reconnect attempts');
break;
}
try {
_roomOperationInProgress = true;
// If old room exists: ensure it's disposed first
if (_room != null) {
await disposeRoom(isShiftingRoom: false);
}
_meetingState = MeetingState.connecting;
_notify();
// initializeRoom should use the latest token/url stored in your service
await initializeRoom(_lastKnownUrl ?? '', _lastKnownToken ?? '');
// If initializeRoom finishes without throwing, we're connected
reconnected = true;
break;
} catch (e, s) {
Logger.logException('Reconnect attempt $attempt failed: $e', s);
// continue loop to next attempt
} finally {
_roomOperationInProgress = false;
}
}
if (!reconnected) {
Logger.log('All reconnect attempts failed. Disposing room.');
_meetingState = MeetingState.error;
_notify();
await disposeRoom();
// Optionally navigate or show final error to user
showToast('Connection lost. Please try rejoining the meeting.');
} else {
Logger.log('Reconnected successfully after transient disconnect');
}
return;
}
// Fallback: unknown reason -> dispose to be safe
Logger.log('Unknown disconnect reason; disposing room as fallback');
await disposeRoom();
});
Notes:
• Implement MeetingHelper.refreshToken() to obtain a fresh token/URL from your backend.
• Ensure disposeRoom() fully removes listeners, awaits _room?.disconnect() and sets _room = null.
• Keep _roomOperationInProgress and _isShiftingRoom accurate to avoid races.
• Tune maxAttempts and backoff to your UX needs.
Tell me if you want this adapted into your exact disposeRoom implementation or wired to your MeetingHelper.refreshToken() function.flaky-jelly-17724
12/23/2025, 5:18 AM