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 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: