Skip to content

Commit c9ef0ae

Browse files
MaxHeimbrockclaude
andcommitted
Release Room FFI handles on disconnect
Room never disposed its RoomHandle on Disconnect(), and Participant / TrackPublication / Track never disposed theirs at all. Each handle is an independent entry in the Rust FFI handle table, so dropping one does not cascade. The handles only got freed when GC eventually finalized each SafeHandle, which in practice meant the entire Rust-side room (peer connection, signaling client, libwebrtc state) plus every wrapped participant, publication, and track lingered for an unpredictable amount of time after the user-visible session had ended. Make Room implement IDisposable. Disconnect() now sends the FFI request and then runs Cleanup(), which unsubscribes from FfiClient events, walks LocalParticipant + RemoteParticipants disposing each participant + its publications + the publications' tracks, and finally disposes RoomHandle itself. The same Cleanup() runs from the Disconnected room event so server-initiated drops behave the same as client-initiated ones. OnEventReceived guards against late events arriving after Cleanup, so the FfiClient unsubscribe race is harmless. DisposeHandles is added as an internal cascade: Track disposes its own handle, TrackPublication forwards to its Track, RemoteTrackPublication also disposes its publication handle, and Participant walks _tracks before disposing its own handle. The Meet sample's OnDestroy also calls Disconnect now so a scene change or quit while still connected releases the handles instead of leaking them until process exit. Verified with the FFI handle table diagnostic: a connect / hold / disconnect cycle now drops rooms, participants, tracks, and remote publications back to zero. Local publications and audio/video source handles still leak on this path; those are addressed separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 59a98af commit c9ef0ae

5 files changed

Lines changed: 65 additions & 4 deletions

File tree

Runtime/Scripts/Participant.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ internal void OnTrackUnpublished(RemoteTrackPublication publication)
5555
{
5656
TrackUnpublished?.Invoke(publication);
5757
}
58+
59+
internal void DisposeHandles()
60+
{
61+
foreach (var pub in _tracks.Values)
62+
pub.DisposeHandles();
63+
_tracks.Clear();
64+
Handle?.Dispose();
65+
}
5866
}
5967

6068
public sealed class LocalParticipant : Participant

Runtime/Scripts/Room.cs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

Runtime/Scripts/Track.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ internal void UpdateMuted(bool muted)
108108
{
109109
_info.Muted = muted;
110110
}
111+
112+
internal void DisposeHandles()
113+
{
114+
Handle?.Dispose();
115+
}
111116
}
112117

113118
public sealed class LocalAudioTrack : Track, ILocalTrack, IAudioTrack

Runtime/Scripts/TrackPublication.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ internal void UpdateMuted(bool muted)
4040
_info.Muted = muted;
4141
Track?.UpdateMuted(muted);
4242
}
43+
44+
internal virtual void DisposeHandles()
45+
{
46+
Track?.DisposeHandles();
47+
}
4348
}
4449

4550
public sealed class RemoteTrackPublication : TrackPublication
@@ -54,6 +59,12 @@ internal RemoteTrackPublication(TrackPublicationInfo info, FfiHandle handle) : b
5459
Handle = handle;
5560
}
5661

62+
internal override void DisposeHandles()
63+
{
64+
base.DisposeHandles();
65+
Handle?.Dispose();
66+
}
67+
5768
public void SetSubscribed(bool subscribed)
5869
{
5970
Subscribed = subscribed;

Samples~/Meet/Assets/Runtime/MeetManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private void Update()
8282
private void OnDestroy()
8383
{
8484
_webCamTexture?.Stop();
85+
_room?.Disconnect();
8586
}
8687

8788
#endregion

0 commit comments

Comments
 (0)