@@ -76,32 +76,14 @@ void readyForRoomEvent(std::uint64_t room_handle) {
7676Room::Room () : subscription_thread_dispatcher_(std::make_unique<SubscriptionThreadDispatcher>()) {}
7777
7878Room::~Room () {
79- if (subscription_thread_dispatcher_) {
80- subscription_thread_dispatcher_->stopAll ();
81- }
82-
83- int listener_to_remove = 0 ;
84- std::unique_ptr<LocalParticipant> local_participant_to_cleanup;
85- {
86- const std::scoped_lock<std::mutex> g (lock_);
87- listener_to_remove = listener_id_;
88- listener_id_ = 0 ;
89- // Move local participant out for cleanup outside the lock
90- local_participant_to_cleanup = std::move (local_participant_);
91- }
92-
93- // Shutdown local participant (unregisters RPC handlers, etc.) before
94- // removing the listener. This prevents in-flight RPC responses from
95- // trying to use destroyed handles.
96- if (local_participant_to_cleanup) {
97- local_participant_to_cleanup->shutdown ();
98- }
99-
100- if (listener_to_remove != 0 ) {
101- FfiClient::instance ().removeListener (listener_to_remove);
79+ // disconnect() handles all destruction/graceful teardown functionality, simply call it here
80+ try {
81+ (void )disconnect (); // Don't need return value
82+ } catch (const std::exception& e) {
83+ LK_LOG_ERROR (" Room::~Room: graceful disconnect failed: {}" , e.what ());
84+ } catch (...) {
85+ LK_LOG_ERROR (" Room::~Room: graceful disconnect failed: unknown exception" );
10286 }
103-
104- // local_participant_to_cleanup is destroyed here after listener is removed
10587}
10688
10789void Room::setDelegate (RoomDelegate* delegate) {
@@ -234,6 +216,88 @@ bool Room::Connect(const std::string& url, const std::string& token, const RoomO
234216 return connect (url, token, options);
235217}
236218
219+ bool Room::disconnect (DisconnectReason reason) {
220+ TRACE_EVENT0 (" livekit" , " Room::disconnect" );
221+
222+ std::shared_ptr<FfiHandle> handle;
223+ RoomDelegate* delegate_snapshot = nullptr ;
224+ std::unique_ptr<LocalParticipant> local_participant_to_cleanup;
225+ std::unordered_map<std::string, std::shared_ptr<RemoteParticipant>> remote_participants_to_clear;
226+ std::unique_ptr<E2EEManager> e2ee_manager_to_clear;
227+ std::unordered_map<std::string, std::shared_ptr<TextStreamReader>> text_stream_readers_to_clear;
228+ std::unordered_map<std::string, std::shared_ptr<ByteStreamReader>> byte_stream_readers_to_clear;
229+ int listener_to_remove = 0 ;
230+
231+ {
232+ const std::scoped_lock<std::mutex> g (lock_);
233+ if (connection_state_ == ConnectionState::Disconnected) {
234+ // Already torn down (or never connected). Nothing to do.
235+ return false ;
236+ }
237+ handle = room_handle_;
238+ delegate_snapshot = delegate_;
239+ // Take ownership of everything under the lock so the kEos handler (which
240+ // also tries to move it out) loses any race here — only one teardown
241+ // path operates on this state.
242+ local_participant_to_cleanup = std::move (local_participant_);
243+ remote_participants_to_clear = std::move (remote_participants_);
244+ e2ee_manager_to_clear = std::move (e2ee_manager_);
245+ text_stream_readers_to_clear = std::move (text_stream_readers_);
246+ byte_stream_readers_to_clear = std::move (byte_stream_readers_);
247+ listener_to_remove = listener_id_;
248+ listener_id_ = 0 ;
249+ room_handle_.reset ();
250+ // Flip state immediately so the in-flight Disconnected room-event we'll
251+ // get back doesn't double-fire onDisconnected. Mirrors Python's
252+ // Room.disconnect()
253+ connection_state_ = ConnectionState::Disconnected;
254+ }
255+
256+ // Drain in-flight RPC handlers BEFORE telling Rust to tear down the room.
257+ // Mirrors client-sdk-python's Room.disconnect() ordering
258+ if (local_participant_to_cleanup) {
259+ local_participant_to_cleanup->shutdown ();
260+ }
261+
262+ // Tell the FFI to close the room and wait for the callback. If this fails
263+ // we still complete local-side teardown below
264+ bool ffi_ok = true ;
265+ if (handle) {
266+ try {
267+ FfiClient::instance ().disconnectAsync (handle->get (), reason).get ();
268+ } catch (const std::exception& e) {
269+ LK_LOG_ERROR (" Room::disconnect: FFI disconnect failed (continuing local teardown): {}" , e.what ());
270+ ffi_ok = false ;
271+ }
272+ }
273+
274+ // Stop dispatcher so no track callbacks fire mid-teardown.
275+ if (subscription_thread_dispatcher_) {
276+ subscription_thread_dispatcher_->stopAll ();
277+ }
278+
279+ if (listener_to_remove != 0 ) {
280+ FfiClient::instance ().removeListener (listener_to_remove);
281+ }
282+
283+ // Fire onDisconnected exactly once, with the reason the caller passed.
284+ if (delegate_snapshot) {
285+ DisconnectedEvent ev;
286+ ev.reason = reason;
287+ try {
288+ delegate_snapshot->onDisconnected (*this , ev);
289+ } catch (const std::exception& e) {
290+ LK_LOG_ERROR (" Room::disconnect: onDisconnected threw: {}" , e.what ());
291+ } catch (...) {
292+ LK_LOG_ERROR (" Room::disconnect: onDisconnected threw: unknown exception" );
293+ }
294+ }
295+
296+ // Moved-out state (local participant, remote participants, e2ee manager,
297+ // stream readers) destructs here, releasing FFI handles.
298+ return ffi_ok;
299+ }
300+
237301RoomInfoData Room::roomInfo () const {
238302 const std::scoped_lock<std::mutex> g (lock_);
239303 return room_info_;
@@ -1139,6 +1203,17 @@ void Room::onEvent(const FfiEvent& event) {
11391203 break ;
11401204 }
11411205 case proto::RoomEvent::kDisconnected : {
1206+ // If disconnect() was driven from our side, it already flipped state
1207+ // to Disconnected and fired the delegate; skip the duplicate here.
1208+ bool already_disconnected = false ;
1209+ {
1210+ const std::scoped_lock<std::mutex> guard (lock_);
1211+ already_disconnected = (connection_state_ == ConnectionState::Disconnected);
1212+ connection_state_ = ConnectionState::Disconnected;
1213+ }
1214+ if (already_disconnected) {
1215+ break ;
1216+ }
11421217 DisconnectedEvent ev;
11431218 ev.reason = toDisconnectReason (re.disconnected ().reason ());
11441219 if (delegate_snapshot) {
@@ -1193,6 +1268,14 @@ void Room::onEvent(const FfiEvent& event) {
11931268 old_byte_readers = std::move (byte_stream_readers_);
11941269 }
11951270
1271+ // Drain in-flight RPC invocations before destroying the local
1272+ // participant's FFI handle. Mirrors the ordering in disconnect();
1273+ // without this, a listener-thread RPC handler can race with handle
1274+ // disposal and send to a dead handle → INVALID_HANDLE → terminate.
1275+ if (old_local_participant) {
1276+ old_local_participant->shutdown ();
1277+ }
1278+
11961279 // Remove listener outside lock
11971280 if (listener_to_remove != 0 ) {
11981281 FfiClient::instance ().removeListener (listener_to_remove);
0 commit comments