breezy-horse-36075
03/13/2026, 7:08 PMtall-airline-16756
03/13/2026, 7:09 PMtall-airline-16756
03/13/2026, 7:09 PMRoomDisconnectedEvent 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:
<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:
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 | DeepWikibreezy-horse-36075
03/13/2026, 7:10 PMtall-airline-16756
03/13/2026, 7:10 PMDisconnectReason.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.breezy-horse-36075
03/14/2026, 8:52 AMtall-airline-16756
03/14/2026, 8:52 AMDisconnectReason.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:
<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.breezy-horse-36075
03/14/2026, 8:57 AMclass AppManager {
AppManager._();
static final AppManager instance = AppManager._();
final ValueNotifier<RoomContext> roomContext = ValueNotifier(RoomContext());tall-airline-16756
03/14/2026, 8:57 AMroomContext 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:
<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.breezy-horse-36075
03/14/2026, 9:04 AMlkc.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,))
);tall-airline-16756
03/14/2026, 9:05 AMstateMismatch + 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:
<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