From aabbe819df5003d68e141b22f25196457e6e7747 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:28:19 +0200 Subject: [PATCH 1/2] Expose DisconnectReason on Room and surface it in the Meet sample The Disconnected and ParticipantDisconnected FFI events already carry a DisconnectReason, but it was dropped before reaching the public API. - Add Room.DisconnectReason, set from the Disconnected event. - Add Room.DisconnectedWithReason (Room, DisconnectReason) alongside the existing parameterless Disconnected event. - Add Room.ParticipantDisconnectedWithReason (Participant, DisconnectReason) alongside the existing ParticipantDisconnected event. - Wire both into the Meet sample to log the reason. Existing event signatures are unchanged, so subscribers keep working. Co-Authored-By: Claude Opus 4.8 (1M context) --- Runtime/Scripts/Room.cs | 8 ++++++++ Samples~/Meet/Assets/Runtime/MeetManager.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Runtime/Scripts/Room.cs b/Runtime/Scripts/Room.cs index 8302f9c9..c70f279e 100644 --- a/Runtime/Scripts/Room.cs +++ b/Runtime/Scripts/Room.cs @@ -126,6 +126,8 @@ public class Room : IDisposable public delegate void SipDtmfDelegate(Participant participant, UInt32 code, string digit); public delegate void ConnectionStateChangeDelegate(ConnectionState connectionState); public delegate void ConnectionDelegate(Room room); + public delegate void DisconnectDelegate(Room room, DisconnectReason reason); + public delegate void ParticipantDisconnectDelegate(Participant participant, DisconnectReason reason); public delegate void E2EeStateChangedDelegate(Participant participant, EncryptionState state); public delegate void DataTrackPublishedDelegate(RemoteDataTrack track); public delegate void DataTrackUnpublishedDelegate(string sid); @@ -137,11 +139,13 @@ public class Room : IDisposable public LocalParticipant LocalParticipant { private set; get; } public ConnectionState ConnectionState { private set; get; } public bool IsConnected => RoomHandle != null && ConnectionState != ConnectionState.ConnDisconnected; + public DisconnectReason DisconnectReason { private set; get; } public E2EEManager E2EEManager { internal set; get; } public IReadOnlyDictionary RemoteParticipants => _participants; public event ParticipantDelegate ParticipantConnected; public event ParticipantDelegate ParticipantDisconnected; + public event ParticipantDisconnectDelegate ParticipantDisconnectedWithReason; public event LocalPublishDelegate LocalTrackPublished; public event LocalPublishDelegate LocalTrackUnpublished; public event PublishDelegate TrackPublished; @@ -157,6 +161,7 @@ public class Room : IDisposable public event ConnectionStateChangeDelegate ConnectionStateChanged; public event ConnectionDelegate Connected; public event ConnectionDelegate Disconnected; + public event DisconnectDelegate DisconnectedWithReason; public event ConnectionDelegate Reconnecting; public event ConnectionDelegate Reconnected; public event E2EeStateChangedDelegate E2EeStateChanged; @@ -366,6 +371,7 @@ internal void OnEventReceived(RoomEvent e) var participant = RemoteParticipants[sid]; _participants.Remove(sid); ParticipantDisconnected?.Invoke(participant); + ParticipantDisconnectedWithReason?.Invoke(participant, e.ParticipantDisconnected.DisconnectReason); } break; case RoomEvent.MessageOneofCase.TrackPublished: @@ -536,7 +542,9 @@ internal void OnEventReceived(RoomEvent e) ConnectionStateChanged?.Invoke(e.ConnectionStateChanged.State); break; case RoomEvent.MessageOneofCase.Disconnected: + DisconnectReason = e.Disconnected.Reason; Disconnected?.Invoke(this); + DisconnectedWithReason?.Invoke(this, DisconnectReason); OnDisconnect(); break; case RoomEvent.MessageOneofCase.Reconnecting: diff --git a/Samples~/Meet/Assets/Runtime/MeetManager.cs b/Samples~/Meet/Assets/Runtime/MeetManager.cs index 7cd2707f..0226b949 100644 --- a/Samples~/Meet/Assets/Runtime/MeetManager.cs +++ b/Samples~/Meet/Assets/Runtime/MeetManager.cs @@ -229,7 +229,8 @@ private IEnumerator ConnectToRoom() _room.TrackMuted += OnTrackMuted; _room.TrackUnmuted += OnTrackUnmuted; _room.ParticipantConnected += OnParticipantConnected; - _room.ParticipantDisconnected += OnParticipantDisconnected; + _room.ParticipantDisconnectedWithReason += OnParticipantDisconnected; + _room.Disconnected += OnDisconnected; _room.DataReceived += OnDataReceived; var connect = _room.Connect(details.ServerUrl, details.ParticipantToken, new RoomOptions()); @@ -400,8 +401,10 @@ private void OnDataReceived(byte[] data, Participant participant, DataPacketKind private void OnParticipantConnected(Participant participant) => EnsureParticipantTile(participant.Identity); - private void OnParticipantDisconnected(Participant participant) + private void OnParticipantDisconnected(Participant participant, DisconnectReason reason) { + Debug.Log($"Participant {participant.Identity} disconnected: {reason}"); + var owned = new List(); foreach (var kv in _extraVideoOwners) if (kv.Value == participant.Identity) owned.Add(kv.Key); @@ -410,6 +413,9 @@ private void OnParticipantDisconnected(Participant participant) DestroyParticipantTile(participant.Identity); } + private void OnDisconnected(Room room) + => Debug.Log($"Disconnected from room: {room.DisconnectReason}"); + private void OnTrackMuted(TrackPublication publication, Participant participant) { if (publication.Kind == TrackKind.KindAudio From e257a5f5d45d0259a4eac41c327adaa8802cedea Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:05:15 +0200 Subject: [PATCH 2/2] Add EditMode tests for DisconnectReason surfacing Drive Room.OnEventReceived with synthetic Disconnected and ParticipantDisconnected FFI events to verify the reason reaches Room.DisconnectReason, DisconnectedWithReason, and ParticipantDisconnectedWithReason. Also covers the default (UnknownReason) value. No server required. Co-Authored-By: Claude Opus 4.8 (1M context) --- Tests/EditMode/RoomDisconnectReasonTests.cs | 87 +++++++++++++++++++ .../RoomDisconnectReasonTests.cs.meta | 11 +++ 2 files changed, 98 insertions(+) create mode 100644 Tests/EditMode/RoomDisconnectReasonTests.cs create mode 100644 Tests/EditMode/RoomDisconnectReasonTests.cs.meta diff --git a/Tests/EditMode/RoomDisconnectReasonTests.cs b/Tests/EditMode/RoomDisconnectReasonTests.cs new file mode 100644 index 00000000..5124d719 --- /dev/null +++ b/Tests/EditMode/RoomDisconnectReasonTests.cs @@ -0,0 +1,87 @@ +using System; +using LiveKit.Internal; +using LiveKit.Proto; +using NUnit.Framework; + +namespace LiveKit.EditModeTests +{ + // Drives Room.OnEventReceived with synthetic FFI events to verify that the + // DisconnectReason carried by the native layer is surfaced through the public + // API. A zero FfiHandle is treated as invalid by the SafeHandle, so disposal + // is a no-op and no native FFI drop is attempted; matching the event's + // RoomHandle to it (also 0) lets OnEventReceived process the event. + public class RoomDisconnectReasonTests + { + [Test] + public void DisconnectReason_DefaultsToUnknown() + { + var room = new Room(); + Assert.AreEqual(DisconnectReason.UnknownReason, room.DisconnectReason); + } + + [Test] + public void Disconnected_SurfacesReasonOnPropertyAndEvent() + { + var room = new Room(); + room.RoomHandle = new FfiHandle(IntPtr.Zero); + + DisconnectReason? eventReason = null; + Room eventRoom = null; + room.DisconnectedWithReason += (r, reason) => + { + eventRoom = r; + eventReason = reason; + }; + + room.OnEventReceived(new RoomEvent + { + RoomHandle = 0, + Disconnected = new Disconnected { Reason = DisconnectReason.ServerShutdown } + }); + + Assert.AreEqual(DisconnectReason.ServerShutdown, room.DisconnectReason, + "Room.DisconnectReason should reflect the reason from the FFI event."); + Assert.AreEqual(DisconnectReason.ServerShutdown, eventReason, + "DisconnectedWithReason should fire carrying the reason."); + Assert.AreSame(room, eventRoom); + } + + [Test] + public void ParticipantDisconnected_SurfacesReasonOnEvent() + { + var room = new Room(); + room.RoomHandle = new FfiHandle(IntPtr.Zero); + + const string identity = "remote-participant"; + // Id 0 -> invalid FfiHandle, so the participant carries no live native handle. + room.CreateRemoteParticipant(new OwnedParticipant + { + Handle = new FfiOwnedHandle { Id = 0 }, + Info = new ParticipantInfo { Identity = identity } + }); + + DisconnectReason? eventReason = null; + Participant eventParticipant = null; + room.ParticipantDisconnectedWithReason += (participant, reason) => + { + eventParticipant = participant; + eventReason = reason; + }; + + room.OnEventReceived(new RoomEvent + { + RoomHandle = 0, + ParticipantDisconnected = new ParticipantDisconnected + { + ParticipantIdentity = identity, + DisconnectReason = DisconnectReason.ParticipantRemoved + } + }); + + Assert.AreEqual(DisconnectReason.ParticipantRemoved, eventReason, + "ParticipantDisconnectedWithReason should fire carrying the reason."); + Assert.IsNotNull(eventParticipant); + Assert.AreEqual(identity, eventParticipant.Identity); + } + } +} diff --git a/Tests/EditMode/RoomDisconnectReasonTests.cs.meta b/Tests/EditMode/RoomDisconnectReasonTests.cs.meta new file mode 100644 index 00000000..498958d2 --- /dev/null +++ b/Tests/EditMode/RoomDisconnectReasonTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57b33d3610cd4a83992cb05cb2428e65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: