@@ -106,9 +106,10 @@ public Proto.RoomOptions ToProto()
106106 }
107107 }
108108
109- public class Room
109+ public class Room : IDisposable
110110 {
111111 internal FfiHandle RoomHandle = null ;
112+ private bool _disposed = false ;
112113 private readonly Dictionary < string , RemoteParticipant > _participants = new ( ) ;
113114 private StreamHandlerRegistry _streamHandlers = new ( ) ;
114115
@@ -183,14 +184,46 @@ public ConnectInstruction Connect(string url, string token, RoomOptions options)
183184
184185 public void Disconnect ( )
185186 {
186- if ( this . RoomHandle == null )
187+ if ( _disposed || RoomHandle == null )
187188 return ;
188189 var ( response , _) = FFIBridge . Instance . SendDisconnectRequest ( this ) ;
189190 using ( response )
190191 {
191192 Utils . Debug ( $ "Disconnect.... { RoomHandle } ") ;
192193 Utils . Debug ( $ "Disconnect response.... { response } ") ;
193194 }
195+ // Release the Rust-side room synchronously. Without this the FfiRoom
196+ // (peer connection, signaling client, libwebrtc state) lingers in the
197+ // FFI handle table until the SafeHandle finalizer runs.
198+ Cleanup ( ) ;
199+ }
200+
201+ public void Dispose ( )
202+ {
203+ Disconnect ( ) ;
204+ GC . SuppressFinalize ( this ) ;
205+ }
206+
207+ private void Cleanup ( )
208+ {
209+ if ( _disposed )
210+ return ;
211+ _disposed = true ;
212+
213+ FfiClient . Instance . RoomEventReceived -= OnEventReceived ;
214+ FfiClient . Instance . RpcMethodInvocationReceived -= OnRpcMethodInvocationReceived ;
215+ FfiClient . Instance . DisconnectReceived -= OnDisconnectReceived ;
216+
217+ // Participant + track + publication FFI handles are independent entries in the
218+ // Rust handle table — dropping the room handle alone does not cascade to them, so
219+ // they would otherwise linger until C# GC finalizes each SafeHandle.
220+ LocalParticipant ? . DisposeHandles ( ) ;
221+ foreach ( var p in _participants . Values )
222+ p . DisposeHandles ( ) ;
223+ _participants . Clear ( ) ;
224+
225+ RoomHandle ? . Dispose ( ) ;
226+ RoomHandle = null ;
194227 }
195228
196229 /// <summary>
@@ -266,6 +299,10 @@ internal void OnRpcMethodInvocationReceived(RpcMethodInvocationEvent e)
266299
267300 internal void OnEventReceived ( RoomEvent e )
268301 {
302+ // After Cleanup() the handle is null but late events may still flow
303+ // through the FfiClient before the unsubscribe fully takes effect.
304+ if ( RoomHandle == null )
305+ return ;
269306 if ( e . RoomHandle != ( ulong ) RoomHandle . DangerousGetHandle ( ) )
270307 return ;
271308
@@ -564,8 +601,7 @@ private void OnDisconnectReceived(DisconnectCallback e)
564601
565602 private void OnDisconnect ( )
566603 {
567- FfiClient . Instance . RoomEventReceived -= OnEventReceived ;
568- FfiClient . Instance . RpcMethodInvocationReceived -= OnRpcMethodInvocationReceived ;
604+ Cleanup ( ) ;
569605 }
570606
571607 internal RemoteParticipant CreateRemoteParticipantWithTracks ( ConnectCallback . Types . ParticipantWithTracks item )
0 commit comments