f
hi
t
Thanks for reaching out, hardeep. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hi — welcome! How can I help you today? If you’re not sure where to start, a few common things I can help with: • Getting started with LiveKit (projects, rooms, SDKs) • Joining or creating a room and connection troubleshooting • Access token or authentication questions • SDK errors or sample code help (please share error text or code) • Billing or account questions Tell me what you’re working on or paste any error messages and I’ll help.
f
these are the disconnected reasons i receive on RoomDisconnectedEvent
Copy code
enum 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 wait
t
Thanks — you're asking which
DisconnectReason
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 Reason
f
the meeting connects successfully after receiving the 'clientInitiated' reason
t
Thanks — I’m responding to your note that the meeting connects successfully after receiving
clientInitiated
.
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).
f
im using flutter sdk and in my app user can switch the meeting
Copy code
SocketService().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;
  }
}
Copy code
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');
          }
        });
}
t
Thanks — I reviewed your code and question about seeing
clientInitiated
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 | DeepWiki
f
ok, as per ur suggestions i will wait for sometime in some specific reasons and on remaining reasons i will dispose the meeting immediately
t
That sounds like a good plan — thanks for confirming. If you'd like, I can: • Provide a small concrete
disposeRoom
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?
f
draft RoomDisconnectedEvent handler
t
Below is a concise draft
RoomDisconnectedEvent
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.
Copy code
// 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.
f
thanks