How to leave a meeting with releasing all the reso...
# ask-ai
f
How to leave a meeting with releasing all the resources?
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.
f
Im using self hosted livekit
t
Thanks — you're asking how to leave a meeting and ensure all resources are released. Here are the concise steps you can take. 1. Disconnect the client • Call Room.disconnect() (or the SDK equivalent) to leave the room immediately. If you don’t call it, the participant will be disconnected automatically after ~15 seconds. 1. Stop and clean up local media • Stop any local tracks (audio/video) and release devices. • Dispose or null out any local objects/refs your app holds so they can be GC’d. 1. Stop egress (if active) • If you started recording/streaming, stop the egress job before or when leaving. Deleting the room will also stop egress automatically. 1. (When you want to end the session for everyone) Delete the room • Use the server-side API/CLI to delete the room. This forces all participants to disconnect and frees server-side resources (participants, egress, recordings). ◦ Node example:
await 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 docs
Thanks — since you’re self‑hosting LiveKit, here’s a concise checklist to leave a meeting and release resources both client‑ and server‑side. What I’m answering: how to properly leave a meeting and free all resources on a self‑hosted LiveKit deployment. Client-side (browser / mobile) 1. Stop local publish tracks: ◦ Stop and release any MediaStreamTracks or SDK Track objects (e.g.,
track.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.
f
im using flutter sdk, and want only client side cleanup
t
Thanks — since you’re using the Flutter SDK and want client‑side only, here’s the direct, minimal cleanup to leave a meeting and release resources. What I’m answering: how to leave a meeting and free client resources with LiveKit Flutter. Steps 1. Ensure you have the connected
Room
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:
Copy code
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 docs
f
Copy code
Future<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 fine
t
Thanks — I reviewed your
disposeRoom()
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:
Copy code
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?
f
sure. i want a more cleaner approach
t
Thanks — below is a cleaner, more robust version of your
disposeRoom()
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:
Copy code
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?
f
what do u suggest?
t
Nice — here’s a concise recommendation. What I suggest • Keep the 3s timeout for
disconnect()
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:
Copy code
// before disposing listener/room
_listener?.removeAllListeners(); // or detach specific callbacks
• Shorter timeout example (2s):
Copy code
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?
f
i think 3s would be fine. whats ur thought?
t
Thanks — 3s is a good balanced choice. Below is a final, compact version of your
disposeRoom()
that uses a 3‑second timeout, removes listeners before disposing, and wraps each step in try/catch so cleanup always proceeds.
Copy code
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.
f
thanks
can we check for room initialization as well?
t
Yes — add an initialization check and a simple "isInitialized" guard so you only run cleanup when the room was created/connected. Below is a compact final version that: • Verifies the room is non-null and connected/initialized • Stops tracks, unpublishes, disconnects with a 3s timeout + fallback cleanup • Removes listeners, disposes listener and room • Wraps each step in try/catch so cleanup proceeds even on errors
Copy code
import '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?