flaky-jelly-17724
12/19/2025, 5:08 AMtall-airline-16756
12/19/2025, 5:08 AMflaky-jelly-17724
12/19/2025, 5:08 AMtall-airline-16756
12/19/2025, 5:08 AMawait roomServiceClient.deleteRoom(roomName);
◦ Python example: await api_client.room.delete_room(api.DeleteRoomRequest(room=room_name))
◦ CLI: lk room delete myroom
1. Agent-specific cleanup (if applicable)
• If you run AI agents or other server processes tied to the room, run your shutdown/cleanup hooks (e.g., save state, release models) so those resources are freed.
When to use each action
• End just your client: call Room.disconnect() and stop local tracks.
• End the entire meeting and free server resources: delete the room (this also stops egress and disconnects participants).
If you tell me which SDK you're using (JavaScript, Android, iOS, or server) and whether you have egress/recording or agents running, I’ll give exact code snippets for your case.
Sources: Connecting to LiveKit | LiveKit docs | Job lifecycle | LiveKit docs | Managing rooms | LiveKit docstall-airline-16756
12/19/2025, 5:08 AMtrack.stop() / SDK stop APIs).
1. Unpublish tracks (optional but recommended):
◦ Call SDK unpublish methods if you need the server to remove published tracks before disconnect.
1. Disconnect from the room:
◦ Call room.disconnect() (or the platform equivalent). This triggers the SDK to close transports and emit disconnected events.
1. Dispose any SDK objects:
◦ Free any UI references, event listeners, and SDK objects to avoid memory leaks.
Server-side (self‑hosted LiveKit)
1. If the session should end for everyone, delete the room:
◦ Use the Room API (delete_room) or CLI (lk room delete <room>) to forcefully remove the room and free server resources. This will disconnect all participants and stop egress/recording.
◦ Example (Node): await roomServiceClient.deleteRoom(roomName);
◦ Example (CLI): lk room delete myroom
1. Stop egress if running separately:
◦ Deleting the room will stop egress automatically; otherwise explicitly stop active egress jobs via the Egress API.
1. Verify server cleanup:
◦ Check LiveKit server logs and admin UI (if any) to confirm participants, tracks, and egress are cleared.
Additional notes for self-hosted:
• If you rely on custom shutdown hooks (agents, background tasks), ensure those complete before killing processes.
• If clients may drop unexpectedly, call room.disconnect() from the client where possible; otherwise deleting the room ensures immediate cleanup.
• If you need scripted/automated cleanup, use the Room API or LiveKit CLI from your management tooling.
If you want, tell me:
• which SDK you’re using (JS/Android/iOS), and
• whether you want client-only cleanup or server-side deletion — I’ll give the exact code/snippets.flaky-jelly-17724
12/19/2025, 5:14 AMtall-airline-16756
12/19/2025, 5:15 AMRoom instance.
2. Stop/unpublish any local tracks if you explicitly want them removed on the server (optional):
◦ Unpublish any published tracks before disconnecting if you need them removed immediately.
1. Call disconnect:
await room.disconnect();
What this does
• Closes WebSocket & WebRTC connections, stops local audio/video tracks, removes event listeners, and emits the Disconnected event.
• Does not delete the room server‑side or stop egress — it only cleans up the local client. The room remains until other participants leave.
Quick tip
• After disconnect() you should not reuse the Room instance; create a new Room if reconnecting. Sources: Connecting to LiveKit | LiveKit docsflaky-jelly-17724
12/19/2025, 5:16 AMFuture<void> disposeRoom() async {
final room = _room;
if (room == null) return;
try {
Logger.log("Disposing room...");
fw.debugPrint("Disposing room...");
// Immediately stop sending media (even before unpublish)
room.localParticipant?.videoTrackPublications.forEach((p) {
p.track?.stop();
});
room.localParticipant?.audioTrackPublications.forEach((p) {
p.track?.stop();
});
// Try unpublishing
await room.localParticipant?.unpublishAllTracks();
// Force end peer connection + socket manually if stuck
final disconnectFuture = room.disconnect();
//If it doesn't complete within 3 seconds → cancel it, and run your fallback (cleanUp())
await disconnectFuture.timeout(
const Duration(seconds: 3),
onTimeout: () async {
Logger.log("Room disconnect timed out — forcing cleanup.");
fw.debugPrint("Room disconnect timed out — forcing cleanup.");
try {
await room.engine.cleanUp();
} catch (_) {}
},
);
await _listener?.dispose();
// Fully dispose internal state
await room.dispose();
_meetingState = MeetingState.disconnected;
fw.debugPrint("Room disposed successfully.");
Logger.log("Room disposed successfully.");
} catch (e, st) {
await Logger.logException(e, st);
fw.debugPrint("Error disposing room: $e\n$st");
} finally {
_room = null;
}
}
check this method if everything is finetall-airline-16756
12/19/2025, 5:16 AMdisposeRoom() method. Overall it’s solid and follows the right sequence (stop tracks, unpublish, disconnect, fallback cleanup, dispose). A few concise improvements and notes to make it more robust:
What’s good
• You stop local tracks before unpublishing so media stops immediately.
• You attempt to unpublish tracks, then call room.disconnect().
• You add a timeout fallback that calls room.engine.cleanUp() if disconnect hangs.
• You dispose listeners and then call room.dispose() and clear _room.
Minor improvements / suggestions
• Stop tracks defensively: check and stop the underlying MediaStreamTrack too (you already call p.track?.stop() — good). Consider null-checking track.mediaStreamTrack if you access it elsewhere.
• Await unpublishAllTracks explicitly: currently you await room.localParticipant?.unpublishAllTracks(); — ensure you handle the case this returns null (no-op is fine).
• Prefer cancelling the disconnect rather than leaving it unresolved: when you call .timeout(..., onTimeout: () async { ... }) the original disconnectFuture may still be running. Consider:
a. Start disconnect() and await with timeout.
b. If timeout fires, call room.engine.cleanUp() and ignore the original future result. That’s acceptable, but be aware the underlying disconnect may still complete later.
• Ensure room.dispose() is safe after `cleanUp()`: calling both is fine, but wrap room.dispose() in try/catch to avoid exceptions blocking finalization.
• Listener disposal: call `_listener?.removeAllListeners()`/appropriate remove methods before disposing if applicable.
• Order: calling room.disconnect() before unpublishAllTracks() can sometimes be fine because disconnect stops tracks; but your order (unpublish → disconnect) is clearer. Keep it.
• Shorter timeout option: 3s is reasonable, but ensure it matches your app’s UX expectations.
Small code tweaks (concise)
• Make timeout handler non-async to avoid returning a future into onTimeout, or explicitly handle it:
final disconnectFuture = room.disconnect();
try {
await disconnectFuture.timeout(const Duration(seconds: 3));
} on TimeoutException {
Logger.log("Room disconnect timed out — forcing cleanup.");
try {
await room.engine.cleanUp();
} catch (_) {}
}
• Wrap disposals in their own try/catch blocks so one failing step doesn’t prevent subsequent cleanup.
Final note
• This is client-side cleanup only: the room and any server-side egress/recording remain until managed server-side. Your method correctly avoids server-side deletion.
If you want, I can return a revised version of your function with the try/catch protections and the timeout pattern applied. Which would you prefer?flaky-jelly-17724
12/19/2025, 5:19 AMtall-airline-16756
12/19/2025, 5:20 AMdisposeRoom() that keeps the same flow but tightens error handling, uses a clear timeout pattern, and ensures each cleanup step runs even if a prior step fails.
Key changes:
• Use try/catch around each major step so one failure won’t skip later cleanup.
• Use timeout with onTimeout handled by catching TimeoutException (avoids returning an async closure).
• Null-safe handling and small defensive checks.
• Log and ignore non-fatal errors but continue cleanup.
• Dispose listener before disposing the room.
Revised implementation:
import 'dart:async';
Future<void> disposeRoom() async {
final room = _room;
if (room == null) return;
Logger.log("Disposing room...");
fw.debugPrint("Disposing room...");
// 1) Stop local media immediately (defensive)
try {
final local = room.localParticipant;
if (local != null) {
for (final p in local.videoTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping video track: $e");
}
}
for (final p in local.audioTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping audio track: $e");
}
}
}
} catch (e, st) {
await Logger.logException(e, st);
}
// 2) Unpublish all tracks (best-effort)
try {
await room.localParticipant?.unpublishAllTracks();
} catch (e, st) {
Logger.log("unpublishAllTracks failed: $e");
await Logger.logException(e, st);
}
// 3) Disconnect with a timeout and fallback cleanup
try {
final disconnectFuture = room.disconnect();
try {
await disconnectFuture.timeout(const Duration(seconds: 3));
} on TimeoutException {
Logger.log("Room.disconnect() timed out — forcing engine cleanup.");
try {
await room.engine.cleanUp();
} catch (e) {
Logger.log("engine.cleanUp() failed: $e");
}
} catch (e, st) {
// Any other disconnect error
Logger.log("room.disconnect() error: $e");
await Logger.logException(e, st);
}
} catch (e, st) {
Logger.log("Initiating disconnect failed: $e");
await Logger.logException(e, st);
}
// 4) Dispose listeners (safe)
try {
await _listener?.dispose();
} catch (e, st) {
Logger.log("Listener dispose failed: $e");
await Logger.logException(e, st);
}
// 5) Dispose room instance (safe)
try {
await room.dispose();
} catch (e, st) {
Logger.log("Room dispose failed: $e");
await Logger.logException(e, st);
} finally {
_room = null;
}
_meetingState = MeetingState.disconnected;
fw.debugPrint("Room disposed successfully.");
Logger.log("Room disposed successfully.");
}
Would you like this adapted to a shorter timeout, or to explicitly remove event handlers before disposing the listener?flaky-jelly-17724
12/19/2025, 5:21 AMtall-airline-16756
12/19/2025, 5:22 AMdisconnect() as a sensible default (short UX delay + quick fallback). Use 1–5s depending on how responsive you want the UI:
◦ 1–2s: very aggressive (fast UI, higher chance of using fallback)
◦ 3s: balanced (recommended)
◦ 4–5s: conservative (wait longer for graceful disconnect)
• Always remove event listeners / callbacks before disposing the room to avoid stray callbacks.
• Continue using per-step try/catch so one failure doesn’t block later cleanup.
• Call room.engine.cleanUp() only as a fallback after a timed-out disconnect() — this forces transports closed.
• After successful cleanup, set _room = null and update state.
Small code additions to the version you accepted
• Explicitly remove handlers:
// before disposing listener/room
_listener?.removeAllListeners(); // or detach specific callbacks
• Shorter timeout example (2s):
await disconnectFuture.timeout(const Duration(seconds: 2));
If you want, I can apply the 3s timeout plus explicit listener-removal and return the final compact function. Which timeout would you prefer: 2s, 3s (recommended), or 5s?flaky-jelly-17724
12/19/2025, 5:23 AMtall-airline-16756
12/19/2025, 5:23 AMdisposeRoom() that uses a 3‑second timeout, removes listeners before disposing, and wraps each step in try/catch so cleanup always proceeds.
import 'dart:async';
Future<void> disposeRoom() async {
final room = _room;
if (room == null) return;
Logger.log("Disposing room...");
fw.debugPrint("Disposing room...");
// 1) Stop local media immediately (defensive)
try {
final local = room.localParticipant;
if (local != null) {
for (final p in local.videoTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping video track: $e");
}
}
for (final p in local.audioTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping audio track: $e");
}
}
}
} catch (e, st) {
await Logger.logException(e, st);
}
// 2) Unpublish all tracks (best-effort)
try {
await room.localParticipant?.unpublishAllTracks();
} catch (e, st) {
Logger.log("unpublishAllTracks failed: $e");
await Logger.logException(e, st);
}
// 3) Disconnect with 3s timeout and fallback engine cleanup
try {
final disconnectFuture = room.disconnect();
try {
await disconnectFuture.timeout(const Duration(seconds: 3));
} on TimeoutException {
Logger.log("room.disconnect() timed out — forcing engine.cleanup()");
try {
await room.engine.cleanUp();
} catch (e) {
Logger.log("engine.cleanUp() failed: $e");
}
} catch (e, st) {
Logger.log("room.disconnect() error: $e");
await Logger.logException(e, st);
}
} catch (e, st) {
Logger.log("Initiating disconnect failed: $e");
await Logger.logException(e, st);
}
// 4) Remove event listeners and dispose listener
try {
try {
_listener?.removeAllListeners(); // replace with your detach method if different
} catch (_) {}
await _listener?.dispose();
} catch (e, st) {
Logger.log("Listener dispose failed: $e");
await Logger.logException(e, st);
}
// 5) Dispose room instance (safe)
try {
await room.dispose();
} catch (e, st) {
Logger.log("Room dispose failed: $e");
await Logger.logException(e, st);
} finally {
_room = null;
}
_meetingState = MeetingState.disconnected;
fw.debugPrint("Room disposed successfully.");
Logger.log("Room disposed successfully.");
}
If you want, I can:
• Replace removeAllListeners() with the exact detach calls you use, or
• Shorten or extend the timeout if you later decide to tweak UX.flaky-jelly-17724
12/19/2025, 5:25 AMflaky-jelly-17724
12/19/2025, 5:26 AMtall-airline-16756
12/19/2025, 5:26 AMimport 'dart:async';
Future<void> disposeRoom() async {
final room = _room;
if (room == null) return;
// Optional: guard to ensure room was connected/initialized
final isInitialized = room.state == RoomState.connected || room.state == RoomState.connecting;
if (!isInitialized) {
// If it's not initialized, still attempt a safe dispose sequence
try {
await room.dispose();
} catch (_) {}
_room = null;
_meetingState = MeetingState.disconnected;
return;
}
Logger.log("Disposing room...");
fw.debugPrint("Disposing room...");
// 1) Stop local media immediately (defensive)
try {
final local = room.localParticipant;
if (local != null) {
for (final p in local.videoTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping video track: $e");
}
}
for (final p in local.audioTrackPublications) {
try {
p.track?.stop();
} catch (e) {
Logger.log("Error stopping audio track: $e");
}
}
}
} catch (e, st) {
await Logger.logException(e, st);
}
// 2) Unpublish all tracks (best-effort)
try {
await room.localParticipant?.unpublishAllTracks();
} catch (e, st) {
Logger.log("unpublishAllTracks failed: $e");
await Logger.logException(e, st);
}
// 3) Disconnect with 3s timeout and fallback engine cleanup
try {
final disconnectFuture = room.disconnect();
try {
await disconnectFuture.timeout(const Duration(seconds: 3));
} on TimeoutException {
Logger.log("room.disconnect() timed out — forcing engine.cleanup()");
try {
await room.engine.cleanUp();
} catch (e) {
Logger.log("engine.cleanUp() failed: $e");
}
} catch (e, st) {
Logger.log("room.disconnect() error: $e");
await Logger.logException(e, st);
}
} catch (e, st) {
Logger.log("Initiating disconnect failed: $e");
await Logger.logException(e, st);
}
// 4) Remove event listeners and dispose listener
try {
try {
_listener?.removeAllListeners(); // replace with your detach method if different
} catch (_) {}
await _listener?.dispose();
} catch (e, st) {
Logger.log("Listener dispose failed: $e");
await Logger.logException(e, st);
}
// 5) Dispose room instance (safe)
try {
await room.dispose();
} catch (e, st) {
Logger.log("Room dispose failed: $e");
await Logger.logException(e, st);
} finally {
_room = null;
}
_meetingState = MeetingState.disconnected;
fw.debugPrint("Room disposed successfully.");
Logger.log("Room disposed successfully.");
}
If you want, I can:
• Replace room.state checks with whatever initialization flag you use, or
• Add an explicit bool _roomInitialized you set when connect succeeds, and check that instead. Which do you prefer?