powerful-magician-63563
12/30/2025, 11:15 AMtall-airline-16756
12/30/2025, 11:15 AMtall-airline-16756
12/30/2025, 11:15 AMpowerful-magician-63563
12/30/2025, 11:16 AMtall-airline-16756
12/30/2025, 11:16 AMSession and LocalMedia out of the View init and into a controlled start flow (e.g., start() called from onAppear) so objects are only created once per appearance.
2. Ensure orderly async teardown — on exit call your disconnect/cleanup and await its completion before releasing instances:
◦ call await session.disconnect() (or equivalent cleanup API),
◦ then nil out `localMedia`/`session`.
1. Guard against double starts — add an `isInitialized`/`isStarting` flag or manager-level guard so init is only performed once while animations or view re-inits occur.
2. Use a manager/service for lifecycle — encapsulate session creation/teardown in a single LiveKitConversationManager (ObservableObject) rather than creating Session/LocalMedia directly in the View init. This centralizes guards and cleanup.
3. Reset state on complete teardown — after disconnect completes, set the flag so a subsequent conversation can start cleanly.
Example pattern (concept)
@StateObject private var manager = LiveKitConversationManager()
@State private var isInitialized = false
.onAppear {
if !isInitialized {
isInitialized = true
manager.start()
}
}
.onDisappear {
Task {
await manager.stop() // await full disconnect and cleanup
isInitialized = false
}
}
Additional safeguards if crashes persist
• Ensure you wait for the SDK disconnect callback to finish before allowing a new start.
• Add logging around cleanup start/completion to confirm no overlapping lifecycles.
• If you keep seeing double cleanup() calls, trace what triggers cleanup (view lifecycle vs manager) and ensure only one owner calls teardown.
If you’d like, paste the key parts of your cleanup/disconnect code (or the manager implementation) and I’ll point out where to add guards/waits or suggest precise code changes.
Sources: client-sdk-flutter | livekit/livekit_composite | DeepWiki | client-sdk-swift | livekit/livekit_composite | DeepWikitall-airline-16756
12/30/2025, 11:28 AMawait session.disconnect()
// ensure cleanup finished
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms
// now allow new session creation
1. Make teardown idempotent and single-owner:
◦ Ensure only one component owns cleanup (preferably the manager). Have onDisappear call manager.stop() but avoid duplicate stop calls from deinit or elsewhere.
◦ Add a guard flag inside stop(): if isStopping { return } and set/clear it appropriately.
1. Add start guard to prevent double starts:
◦ If isStarting or isInitialized, ignore additional start attempts until start completes (or fails).
◦ Use Task cancellation or a mutex to serialize start/stop.
1. Await disconnect completion before navigating:
◦ If your UI navigates away as soon as stop() is called, ensure you await stop completion or wait for manager state transition before creating/presenting the next conversation.
1. Add verbose lifecycle logs around start/stop:
◦ Log timestamps for start requested, start completed, stop requested, stop completed, and manager deinit so you can confirm there’s no overlap.
Additional checks
• Paste the actual Xcode crash report (stack trace) if you have one — that will show whether the crash is in WebRTC native code, LiveKit callbacks, or your app code.
• Confirm you moved Session/LocalMedia creation into start() and not the View init. Also confirm stop() calls await session.disconnect() and then nils the references.
• If you use AVAudioSession modifications, ensure you restore/teardown audio session states in a consistent place (manager.stop()).
If you want, paste your manager’s start/stop implementations (the functions that create Session/LocalMedia and call disconnect/cleanup). I’ll point out exactly where to add the guards, delay, and idempotency checks.powerful-magician-63563
12/30/2025, 12:02 PMtall-airline-16756
12/30/2025, 12:02 PM@StateObject, singleton).
2. Clear the store on end or before start:
◦ On disconnect/cleanup: messages.removeAll()
◦ Or just before calling manager.start() for the new conversation.
1. Tie clearing to lifecycle events:
◦ After await session.disconnect() or RoomEvent.disconnected, call messages.removeAll().
◦ Or in your manager’s stop() / start() methods: clear on stop; initialize empty on start.
1. If messages are shared globally, scope them per session ID:
◦ Use a dictionary [sessionId: [Message]] and switch to an empty array for a new session.
1. Update the UI reactively so the view reflects the cleared array immediately.
If you paste where you currently hold messages (ViewModel/manager code or @StateObject), I’ll show the exact lines to add to safely clear state during your start/stop flow.
Sources: Text and transcriptions | LiveKit docs