@@ -102,6 +102,7 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO
102102 if (connection_state_ != ConnectionState::Disconnected) {
103103 throw std::runtime_error (" already connected" );
104104 }
105+ teardown_started_ = false ;
105106 connection_state_ = ConnectionState::Reconnecting;
106107 }
107108
@@ -212,7 +213,10 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO
212213
213214bool Room::disconnect (DisconnectReason reason) {
214215 TRACE_EVENT0 (" livekit" , " Room::disconnect" );
216+ return teardown (true , reason, true );
217+ }
215218
219+ bool Room::teardown (bool disconnect_ffi, DisconnectReason reason, bool notify_delegate) {
216220 std::shared_ptr<FfiHandle> handle;
217221 RoomDelegate* delegate_snapshot = nullptr ;
218222 std::shared_ptr<LocalParticipant> local_participant_to_cleanup;
@@ -224,58 +228,81 @@ bool Room::disconnect(DisconnectReason reason) {
224228
225229 {
226230 const std::scoped_lock<std::mutex> g (lock_);
227- if (connection_state_ == ConnectionState::Disconnected) {
228- // Already torn down (or never connected). Nothing to do.
231+ const bool has_room_state = connection_state_ != ConnectionState::Disconnected || listener_id_ != 0 ||
232+ room_handle_ || local_participant_ || !remote_participants_.empty ();
233+ if (teardown_started_ || !has_room_state) {
229234 return false ;
230235 }
231- handle = room_handle_;
236+ teardown_started_ = true ;
237+ handle = std::move (room_handle_);
232238 delegate_snapshot = delegate_;
233- // Take ownership of everything under the lock so the kEos handler (which
234- // also tries to move it out) loses any race here — only one teardown
235- // path operates on this state.
236239 local_participant_to_cleanup = std::move (local_participant_);
237240 remote_participants_to_clear = std::move (remote_participants_);
238241 e2ee_manager_to_clear = std::move (e2ee_manager_);
239242 text_stream_readers_to_clear = std::move (text_stream_readers_);
240243 byte_stream_readers_to_clear = std::move (byte_stream_readers_);
241244 listener_to_remove = listener_id_;
242245 listener_id_ = 0 ;
243- room_handle_.reset ();
244- // Flip state immediately so the in-flight Disconnected room-event we'll
245- // get back doesn't double-fire onDisconnected. Mirrors Python's
246- // Room.disconnect()
247246 connection_state_ = ConnectionState::Disconnected;
248247 }
249248
250- // Drain in-flight RPC handlers BEFORE telling Rust to tear down the room.
251- // Mirrors client-sdk-python's Room.disconnect() ordering
249+ bool teardown_ok = true ;
252250 if (local_participant_to_cleanup) {
253- local_participant_to_cleanup->shutdown ();
251+ try {
252+ local_participant_to_cleanup->shutdown ();
253+ } catch (const std::exception& e) {
254+ LK_LOG_ERROR (" Room teardown: local participant shutdown failed: {}" , e.what ());
255+ teardown_ok = false ;
256+ } catch (...) {
257+ LK_LOG_ERROR (" Room teardown: local participant shutdown failed: unknown exception" );
258+ teardown_ok = false ;
259+ }
254260 }
255261
256- // Tell the FFI to close the room and wait for the callback. If this fails
257- // we still complete local-side teardown below
258- bool ffi_ok = true ;
259- if (handle) {
262+ if (disconnect_ffi && handle && handle->valid ()) {
260263 try {
261264 FfiClient::instance ().disconnectAsync (handle->get (), reason).get ();
262265 } catch (const std::exception& e) {
263- LK_LOG_ERROR (" Room::disconnect: FFI disconnect failed (continuing local teardown): {}" , e.what ());
264- ffi_ok = false ;
266+ LK_LOG_ERROR (" Room teardown: FFI disconnect failed (continuing local teardown): {}" , e.what ());
267+ teardown_ok = false ;
268+ } catch (...) {
269+ LK_LOG_ERROR (" Room teardown: FFI disconnect failed (continuing local teardown): unknown exception" );
270+ teardown_ok = false ;
265271 }
266272 }
267273
268- // Stop dispatcher so no track callbacks fire mid-teardown.
269274 if (subscription_thread_dispatcher_) {
270- subscription_thread_dispatcher_->stopAll ();
275+ try {
276+ subscription_thread_dispatcher_->stopAll ();
277+ } catch (const std::exception& e) {
278+ LK_LOG_ERROR (" Room teardown: subscription shutdown failed: {}" , e.what ());
279+ teardown_ok = false ;
280+ } catch (...) {
281+ LK_LOG_ERROR (" Room teardown: subscription shutdown failed: unknown exception" );
282+ teardown_ok = false ;
283+ }
271284 }
272285
273286 if (listener_to_remove != 0 ) {
274- FfiClient::instance ().removeListener (listener_to_remove);
287+ try {
288+ FfiClient::instance ().removeListener (listener_to_remove);
289+ } catch (const std::exception& e) {
290+ LK_LOG_ERROR (" Room teardown: listener removal failed: {}" , e.what ());
291+ teardown_ok = false ;
292+ } catch (...) {
293+ LK_LOG_ERROR (" Room teardown: listener removal failed: unknown exception" );
294+ teardown_ok = false ;
295+ }
275296 }
276297
277- // Fire onDisconnected exactly once, with the reason the caller passed.
278- if (delegate_snapshot) {
298+ local_participant_to_cleanup.reset ();
299+ remote_participants_to_clear.clear ();
300+ e2ee_manager_to_clear.reset ();
301+ text_stream_readers_to_clear.clear ();
302+ byte_stream_readers_to_clear.clear ();
303+ handle.reset ();
304+
305+ if (notify_delegate && delegate_snapshot) {
279306 DisconnectedEvent ev;
280307 ev.reason = reason;
281308 try {
@@ -287,9 +314,7 @@ bool Room::disconnect(DisconnectReason reason) {
287314 }
288315 }
289316
290- // Moved-out state (local participant, remote participants, e2ee manager,
291- // stream readers) destructs here, releasing FFI handles.
292- return ffi_ok;
317+ return teardown_ok;
293318}
294319
295320RoomInfoData Room::roomInfo () const {
@@ -1168,22 +1193,8 @@ void Room::onEvent(const FfiEvent& event) {
11681193 break ;
11691194 }
11701195 case proto::RoomEvent::kDisconnected : {
1171- // If disconnect() was driven from our side, it already flipped state
1172- // to Disconnected and fired the delegate; skip the duplicate here.
1173- bool already_disconnected = false ;
1174- {
1175- const std::scoped_lock<std::mutex> guard (lock_);
1176- already_disconnected = (connection_state_ == ConnectionState::Disconnected);
1177- connection_state_ = ConnectionState::Disconnected;
1178- }
1179- if (already_disconnected) {
1180- break ;
1181- }
1182- DisconnectedEvent ev;
1183- ev.reason = toDisconnectReason (re.disconnected ().reason ());
1184- if (delegate_snapshot) {
1185- delegate_snapshot->onDisconnected (*this , ev);
1186- }
1196+ const auto reason = toDisconnectReason (re.disconnected ().reason ());
1197+ (void )teardown (false , reason, true );
11871198 break ;
11881199 }
11891200 case proto::RoomEvent::kReconnecting : {
@@ -1208,56 +1219,7 @@ void Room::onEvent(const FfiEvent& event) {
12081219 break ;
12091220 }
12101221 case proto::RoomEvent::kEos : {
1211- if (subscription_thread_dispatcher_) {
1212- subscription_thread_dispatcher_->stopAll ();
1213- }
1214-
1215- int listener_to_remove = 0 ;
1216-
1217- // Move state out of lock scope before destroying to avoid holding lock
1218- // during potentially long destructors
1219- std::shared_ptr<LocalParticipant> old_local_participant;
1220- std::unordered_map<std::string, std::shared_ptr<RemoteParticipant>> old_remote_participants;
1221- std::shared_ptr<FfiHandle> old_room_handle;
1222- std::shared_ptr<E2EEManager> old_e2ee_manager;
1223- std::unordered_map<std::string, std::shared_ptr<TextStreamReader>> old_text_readers;
1224- std::unordered_map<std::string, std::shared_ptr<ByteStreamReader>> old_byte_readers;
1225-
1226- {
1227- const std::scoped_lock<std::mutex> guard (lock_);
1228- listener_to_remove = listener_id_;
1229- listener_id_ = 0 ;
1230-
1231- // Reset connection state
1232- connection_state_ = ConnectionState::Disconnected;
1233-
1234- // Move state out for cleanup outside lock
1235- old_local_participant = std::move (local_participant_);
1236- old_remote_participants = std::move (remote_participants_);
1237- old_room_handle = std::move (room_handle_);
1238- old_e2ee_manager = std::move (e2ee_manager_);
1239- old_text_readers = std::move (text_stream_readers_);
1240- old_byte_readers = std::move (byte_stream_readers_);
1241- }
1242-
1243- // Drain in-flight RPC invocations before destroying the local
1244- // participant's FFI handle. Mirrors the ordering in disconnect();
1245- // without this, a listener-thread RPC handler can race with handle
1246- // disposal and send to a dead handle → INVALID_HANDLE → terminate.
1247- if (old_local_participant) {
1248- old_local_participant->shutdown ();
1249- }
1250-
1251- // Remove listener outside lock
1252- if (listener_to_remove != 0 ) {
1253- FfiClient::instance ().removeListener (listener_to_remove);
1254- }
1255-
1256- if (old_local_participant) {
1257- old_local_participant->shutdown ();
1258- }
1259-
1260- // old_* state is destroyed here when going out of scope
1222+ (void )teardown (false , DisconnectReason::Unknown, false );
12611223
12621224 const RoomEosEvent ev;
12631225 if (delegate_snapshot) {
0 commit comments