room gets disconnected in ios in background flutt...
# ask-ai
b
room gets disconnected in ios in background flutter: 230724: Event => RoomContext: RoomDisconnectedEvent 1oIEsF
t
Thanks for reaching out, Ahmed. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I see you're seeing a
RoomDisconnectedEvent
on iOS when the app goes into the background. I can help troubleshoot this. Below are the most likely causes and practical steps you can try, plus a couple of targeted questions so I can narrow down the root cause. Why this happens • iOS suspends apps in the background and restricts network activity. That can terminate the WebRTC peer connection or signaling channel and produce a
RoomDisconnectedEvent
. • If media (audio/video) stops flowing or the app is suspended, the server may time out the connection. Immediate things to try 1. Enable iOS Background Modes ◦ In your app’s Info.plist, enable background modes for audio and voip:
Copy code
<key>UIBackgroundModes</key>
     <array>
       <string>audio</string>
       <string>voip</string>
     </array>
• This signals iOS that your app needs to keep audio/network active. 1. Keep an active audio session (Flutter/iOS) ◦ If your app uses audio, make sure an active AVAudioSession is maintained (category like
playAndRecord
). ◦ In Flutter, a common pattern is using a background audio task or playing a silent audio track to keep the process alive when the app is backgrounded. 1. Avoid unintentionally disconnecting the Room ◦ Confirm you’re not calling
room.disconnect()
or disposing the
Room
instance during lifecycle callbacks (e.g., onPause/onStop). ◦ Ensure the Room object lifecycle aligns with app lifecycle. 1. Enable/expect reconnection handling ◦ The Flutter SDK emits
disconnected
,
reconnecting
, and
reconnected
events. Handle these to restore UI state and notify users. ◦ When the app comes back to foreground, allow the SDK to re-establish the connection and re-subscribe to tracks. 1. Reduce audio processing interruptions ◦ If applicable, initialize LiveKit with voice processing bypass to reduce audio-side interruptions:
Copy code
await LiveKitClient.initialize(bypassVoiceProcessing: true);
Questions to narrow the cause • Which Flutter LiveKit SDK version are you using? • Does this happen every time the app backgrounds, or only sometimes (e.g., after N seconds)? • Are you using microphone/audio in the room, or is it only data/video? • Do you have background modes already enabled in Info.plist? If yes, which ones? • Are you seeing any additional logs around the same time (e.g., network loss, ICE timeouts, or lifecycle callbacks)? What I can help with next • If you share SDK version and whether background modes are enabled, I’ll give step-by-step code/lifecycle checks for Flutter. • I can also suggest a minimal reproducible test flow you can run to verify whether background audio or reconnection handling fixes it. If you prefer, share a short snippet (how you connect to the room and any lifecycle handling) and I’ll point out specific changes to keep the room connected. Sources: How we built a globally distributed mesh network to scale WebRTC | LiveKit | client-sdk-flutter | livekit/livekit_composite | DeepWiki
b
more logs [log] [WebRTC] room disconnected DisconnectReason.stateMismatch flutter: 230724: Event => RoomContext: RoomDisconnectedEvent 1oIEsF [log] [WebRTC] room disconnected DisconnectReason.clientInitiated
t
Thanks — that extra log is helpful. I’m responding to the two disconnect lines you shared:
DisconnectReason.stateMismatch
and
DisconnectReason.clientInitiated
. Below I’ll explain what each usually means and give focused troubleshooting steps you can try in your Flutter/iOS app. What the logs indicate • DisconnectReason.stateMismatch ◦ This typically means the SDK detected an unexpected internal state when handling the connection (for example, a signaling state or session state that didn’t line up with what the SDK expected). On iOS backgrounding this can happen when lifecycle changes cause the Room or PeerConnection objects to be manipulated while the SDK expects them to remain active. • DisconnectReason.clientInitiated ◦ This indicates the client (your app) initiated a disconnect, either directly (calling
room.disconnect()
), indirectly (disposing the Room or destroying objects tied to the connection), or via lifecycle code that tears down resources when the app backgrounds. Practical checks and fixes to try 1. Verify you are not explicitly disconnecting ◦ Search your code for any calls to
room.disconnect()
or for disposal of the Room when the app lifecycle changes (app pause/stop). ◦ In Flutter, check
dispose()
,
didChangeAppLifecycleState
, and any platform-channel handlers that might call native teardown. 1. Keep the Room object alive across background/foreground transitions ◦ Ensure the Room instance isn’t garbage-collected or recreated on backgrounding. Keep it in a long-lived service/provider rather than a widget that gets disposed. ◦ If you use state management (Provider, Riverpod, Bloc), keep the Room in a top-level provider that survives route/widget teardown. 1. Review lifecycle callbacks for accidental teardown ◦ If you detect `AppLifecycleState.paused`/`inactive`, avoid immediately disconnecting. Instead, rely on reconnection behavior when the app returns to foreground. ◦ If you must release resources on background, do it conditionally (only after a deliberate user action). 1. Enable/confirm background modes for audio/voip (if you need persistent audio) ◦ Add
audio
and
voip
to
UIBackgroundModes
in Info.plist to reduce risk of suspension when a media session is active. 1. Maintain an active audio session if appropriate ◦ If your app uses audio, ensure
AVAudioSession
remains active (category
playAndRecord
or appropriate for your use). Playing or holding an audio session prevents the OS from treating the app as idle. 1. Handle reconnection events and recover gracefully ◦ Listen for
reconnecting
and
reconnected
(or equivalent Flutter Room events). When you see
clientInitiated
, check whether the client triggered a disconnect path and ensure your app can re-join automatically if needed. 1. Check SDK version and known issues ◦ Share the Flutter LiveKit SDK version you’re using. State mismatches and lifecycle edge-cases are sometimes addressed in SDK updates; upgrading may fix race conditions. Targeted diagnostic steps I recommend now 1. Share a small snippet showing: ◦ How you create/connect to the Room (connection code). ◦ Any lifecycle handling (didChangeAppLifecycleState or dispose). ◦ Where the Room instance is stored (widget state vs top-level provider). 1. Run a quick test: ◦ Keep the app connected, put it to background for 30–60s, then bring it foreground. Note whether logs show
clientInitiated
immediately on background or later. ◦ Temporarily comment out any disconnect/dispose calls in lifecycle handlers to see if the stateMismatch stops. If you share the connection code and lifecycle handlers, I’ll point out exact lines to change and give a minimal pattern that keeps the Room alive across background/foreground transitions.
b
client version is 2.4.9 and components version is 1.2.2+hotfix.1
t
Thanks — that version info is helpful. I’m responding to your Flutter client v2.4.9 + components v1.2.2+hotfix.1 and the disconnect logs (
DisconnectReason.stateMismatch
and
DisconnectReason.clientInitiated
). Summary of likely causes •
stateMismatch
usually means the SDK hit an unexpected internal state (race between lifecycle changes and connection handling). •
clientInitiated
means something in your app code triggered a disconnect (explicit
room.disconnect()
or implicit teardown of the Room/PeerConnection when app lifecycle changes). • On iOS backgrounding, both can appear when the Room or underlying RTC objects are disposed or the app is suspended (no network/media), causing the SDK to transition unexpectedly. Concrete next steps to diagnose and fix 1. Confirm you are not explicitly disconnecting on lifecycle events ◦ Search for any
room.disconnect()
,
room.close()
, or disposal of the Room in: ▪︎
dispose()
of widgets ▪︎
didChangeAppLifecycleState
handling for `paused`/`inactive` ▪︎ any platform channel/native callbacks ◦ If found, comment out or guard that call and re-test. 1. Keep the Room instance long-lived ◦ Store the
Room
in a top-level service/provider (Provider, Riverpod, Bloc) that survives widget teardown. ◦ Avoid constructing the Room inside short-lived widgets which get disposed when app backgrounds. 1. Avoid tearing down PeerConnection resources on background ◦ Do not stop tracks, release local tracks, or deinitialize WebRTC when receiving
AppLifecycleState.paused
. ◦ If you need to release, do it only after explicit user action (end call). 1. Enable and validate iOS background modes (if you need persistent audio) ◦ Add
audio
and
voip
to
UIBackgroundModes
in Info.plist:
Copy code
<key>UIBackgroundModes</key>
     <array>
       <string>audio</string>
       <string>voip</string>
     </array>
• Ensure you maintain an active AVAudioSession (category
playAndRecord
) if audio should continue in background. 1. Test whether disconnect is immediate or delayed ◦ Keep logs open, background the app and watch timestamps: ▪︎ If
clientInitiated
appears immediately when app backgrounds, a lifecycle handler is likely disconnecting. ▪︎ If it appears after a timeout, iOS suspension or network inactivity timed out the connection. 1. Capture and share minimal connection + lifecycle snippet ◦ Share the code you use to: i. Create/connect to the Room ii. Where you hold the Room instance (widget state vs provider) iii. Any
didChangeAppLifecycleState
and
dispose()
handlers ◦ With that I can point to exact lines to change and show a minimal pattern to keep the Room alive across backgrounding. 1. Consider SDK upgrade ◦ Some lifecycle edge-cases and reconnection fixes are addressed in later releases. If you can, test with the latest Flutter LiveKit client and components on a branch and see if behavior changes. Quick debugging checklist to run now 1. Temporarily remove any disconnect/dispose code in lifecycle callbacks. 2. Keep the Room in a top-level provider so it's not GC'd. 3. Background the app for 30–60s and bring it back; watch logs for whether disconnect occurs and which reason. 4. Share the observed timing and the small code snippet if disconnect still happens. If you share the connection and lifecycle code (the three items above), I’ll give exact changes and a minimal Flutter pattern that prevents accidental client-initiated disconnects and reduces state mismatch races.
b
I put roomContext in a manager
Copy code
class AppManager {
  AppManager._();
  static final AppManager instance = AppManager._();

  final ValueNotifier<RoomContext> roomContext = ValueNotifier(RoomContext());
t
Thanks — that helps. I’m responding to your AppManager snippet where you keep
roomContext
in a singleton ValueNotifier. Storing the RoomContext there is the right direction: keeping the Room in a long-lived manager usually prevents widget disposal from causing client-initiated disconnects. A few focused checks and concrete changes to ensure the Room truly survives backgrounding and avoids stateMismatch/clientInitiated disconnects 1) Confirm where the actual Room connection is created • Does
RoomContext()
create/connect the LiveKit
Room
inside its constructor or lazily when you call
connect()
? • Share the code that performs the connect (the call that uses
roomContext.value.connect(...)
or similar). I’ll point out exact edits. 2) Ensure you never call disconnect from lifecycle handlers • Check for any
room.disconnect()
,
room.close()
, or
roomContext.value = RoomContext()
calls inside: ◦
dispose()
of any widget that uses the Room ◦
didChangeAppLifecycleState
for `paused`/`inactive` ◦ platform-channel or native callbacks • If you find such calls, guard them so disconnect only happens after an explicit user action (end call). 3) Recommended pattern for a long-lived Room in Flutter • Keep your Room/RoomContext in the singleton (as you have). Ensure widgets only subscribe to the ValueNotifier and never own the Room lifecycle. • Only create/connect the Room once and store it on the manager. Example pattern (pseudocode): 1. On app start or when user chooses to join: ◦ await AppManager.instance.roomContext.value.ensureConnected(...) 1. In widgets: ◦ Listen to AppManager.instance.roomContext (ValueListenableBuilder) for state updates. ◦ Do not call
dispose()
on the Room in widget lifecycle. 1. On user-initiated hangup: ◦ Call a manager method that explicitly disconnects: ▪︎ AppManager.instance.leaveRoom() { await room.disconnect(); roomContext.value = RoomContext(); } 4) Avoid GC or reassigning the RoomContext • Do not reassign
roomContext.value
to a fresh RoomContext except when you intentionally want to destroy/cleanup after user leaves. • If you must recreate, do it after confirming
disconnect()
completed. 4) App lifecycle handling • In your
WidgetsBindingObserver.didChangeAppLifecycleState
, do not disconnect on `paused`/`inactive`. Instead: ◦ Option A: Do nothing and rely on background modes + audio session to keep the connection. ◦ Option B: If you must release resources, wait a user-confirmed timeout, and present a reconnection strategy on foreground. 5) iOS background considerations • If you want the connection kept alive in background for calls, ensure Info.plist includes:
Copy code
<key>UIBackgroundModes</key>
  <array>
    <string>audio</string>
    <string>voip</string>
  </array>
• Maintain an active AVAudioSession (category
playAndRecord
) or a background audio task to reduce suspension risk. 6) Debugging to pinpoint the cause • Add logging around these spots and re-run: ◦ When you call connect() ◦ Any
didChangeAppLifecycleState
events ◦ Any
room.disconnect()
or
roomContext.value = ...
calls ◦ When the RoomContext is GC'd (if you have finalizers or override dispose) • Observe whether
DisconnectReason.clientInitiated
is logged immediately after a lifecycle event — that proves your code initiated the disconnect. If it’s delayed, iOS suspension/timeout is likely. If you can share: • The connect() code inside your RoomContext • Any lifecycle handling (didChangeAppLifecycleState and dispose methods) I’ll give precise edits you can apply to the manager and lifecycle handlers to stop accidental disconnects and avoid the state mismatch race.
b
Copy code
lkc.RoomContext roomContext = lkc.RoomContext(
    url: BlocProvider.of<PartyBLoc>(context)
        .state
        .party
        ?.roomMetadata
        ?.wsUrl ??
        '',
    token: BlocProvider.of<PartyBLoc>(context)
        .state
        .party
        ?.roomMetadata
        ?.accessToken ??
        '',
    enableAudioVisulizer: true,
    connectOptions: ConnectOptions(timeouts: Timeouts(    connection: Timeouts.defaultTimeouts.connection,
      debounce: Timeouts.defaultTimeouts.debounce,
      publish: Timeouts.defaultTimeouts.publish,
      peerConnection: Timeouts.defaultTimeouts.peerConnection,
      iceRestart: Timeouts.defaultTimeouts.iceRestart,))
);
t
Thanks — this is helpful. I’m responding to the RoomContext snippet you shared and the disconnect logs (
stateMismatch
+
clientInitiated
) while backgrounding on iOS. Short summary • Keeping the RoomContext in a long-lived manager is correct and helps prevent widget disposal from causing client-initiated disconnects. • The logs still suggest either your app (or a lifecycle handler) is initiating a disconnect, or iOS background suspension is stopping WebRTC keep-alives and the server reports a state mismatch. Concrete next steps to check and small changes to make now 1) Where and how you call connect() • Confirm whether you call
roomContext.connect(...)
(or equivalent) from a short-lived widget or from the manager. • Action: Move the connect call into your AppManager (or another long-lived service) so the Room is created/connected there, not inside a widget lifecycle. 2) Don’t disconnect on lifecycle pause • Action: Ensure you are NOT calling
room.disconnect()
or reassigning
roomContext.value
in: ◦
dispose()
◦
didChangeAppLifecycleState
when state == `paused`/`inactive` • Instead, only disconnect on explicit user action (hang up). 3) Keep the RoomContext instance stable • Action: Do not replace
roomContext.value = RoomContext()
except after a deliberate leave flow. Reassigning implicitly will trigger client-initiated disconnect. 4) Add brief lifecycle logging to find the initiator • Add logs for: ◦ When you call connect() ◦ Every
didChangeAppLifecycleState
event ◦ Any call to
room.disconnect()
or reassignment of
roomContext
• This will tell you whether
clientInitiated
appears immediately after a lifecycle callback. 5) iOS background settings (if you want persistent connection in background) • Ensure Info.plist includes:
Copy code
<key>UIBackgroundModes</key>
  <array>
    <string>audio</string>
    <string>voip</string>
  </array>
• Maintain an active AVAudioSession (e.g.,
playAndRecord
) or use a background audio task if your app needs to stay fully connected while backgrounded. 6) Test to determine timing • Test steps: a. Connect to a room. b. Open logs, background the app and wait 10–30s. c. Bring app to foreground and observe logs. • If
clientInitiated
logs immediately on backgrounding → some code triggered disconnect. • If
stateMismatch
appears after ~20s → iOS suspension / missing ICE keep-alives likely. If you want, paste these three small snippets and I’ll point to exact changes: • The code that calls connect() on your RoomContext • Your AppManager (complete) showing how you create/store the RoomContext • Any App lifecycle handling (
didChangeAppLifecycleState
/
dispose
) I’ll then give exact edits to keep RoomContext alive across backgrounding and avoid the state mismatch / client-initiated disconnect. Sources: Understanding the STATE_MISMATCH Disconnect Reason | client-sdk-flutter | livekit/livekit_composite | DeepWiki | client-sdk-android | livekit/livekit_composite | DeepWiki | Agent speech and audio | LiveKit Documentation | client-sdk-js | livekit/livekit_composite | DeepWiki