diff --git a/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs b/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs new file mode 100644 index 00000000..ed5b8a8a --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using StreamVideo.Core.StatefulModels; + +namespace Core +{ + /// + /// User for sorting of + /// + internal class CallParticipantComparer : IComparer + { + public bool OrderChanged { get; private set; } + + public int Compare(IStreamVideoCallParticipant x, IStreamVideoCallParticipant y) + { + var result = InternalCompare(x, y); + if (result != 0) + { + OrderChanged = true; + } + + return result; + } + + public void Reset() => OrderChanged = false; + + private int InternalCompare(IStreamVideoCallParticipant x, IStreamVideoCallParticipant y) + { + if (x == null && y == null) return 0; + if (x == null) return -1; + if (y == null) return 1; + + if (x.IsPinned && !y.IsPinned) return -1; + if (!x.IsPinned && y.IsPinned) return 1; + + if (x.IsScreenSharing && !y.IsScreenSharing) return -1; + if (!x.IsScreenSharing && y.IsScreenSharing) return 1; + + if (x.IsDominantSpeaker && !y.IsDominantSpeaker) return -1; + if (!x.IsDominantSpeaker && y.IsDominantSpeaker) return 1; + + if (x.IsVideoEnabled && !y.IsVideoEnabled) return -1; + if (!x.IsVideoEnabled && y.IsVideoEnabled) return 1; + + if (x.IsAudioEnabled && !y.IsAudioEnabled) return -1; + if (!x.IsAudioEnabled && y.IsAudioEnabled) return 1; + + return x.JoinedAt.CompareTo(y.JoinedAt); // Earlier joiners first + } + } +} \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs.meta b/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs.meta new file mode 100644 index 00000000..8372ff93 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/CallParticipantComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44d109ef7707478f9064e87fced251c8 +timeCreated: 1704721516 \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index 36d5ed50..6f2fa5ea 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -523,13 +523,13 @@ private void OnSfuTrackUnpublished(TrackUnpublished trackUnpublished) var cause = trackUnpublished.Cause; // Optionally available. Read TrackUnpublished.participant comment in events.proto - var participant = trackUnpublished.Participant; + var participantSfuDto = trackUnpublished.Participant; - UpdateParticipantTracksState(userId, sessionId, type, isEnabled: false, out var streamParticipant); + UpdateParticipantTracksState(userId, sessionId, type, isEnabled: false, out var participant); - if (participant != null && streamParticipant != null) + if (participantSfuDto != null && participant != null) { - streamParticipant.UpdateFromSfu(participant); + participant.UpdateFromSfu(participantSfuDto); } //StreamTodo: raise an event so user can react to track unpublished? Otherwise the video will just freeze @@ -542,13 +542,13 @@ private void OnSfuTrackPublished(TrackPublished trackPublished) var type = trackPublished.Type.ToPublicEnum(); // Optionally available. Read TrackUnpublished.participant comment in events.proto - var participant = trackPublished.Participant; + var participantSfuDto = trackPublished.Participant; - UpdateParticipantTracksState(userId, sessionId, type, isEnabled: true, out var streamParticipant); + UpdateParticipantTracksState(userId, sessionId, type, isEnabled: true, out var participant); - if (participant != null && streamParticipant != null) + if (participantSfuDto != null && participant != null) { - streamParticipant.UpdateFromSfu(participant); + participant.UpdateFromSfu(participantSfuDto); } //StreamTodo: fixes the case when joining a call where other participant starts with no video and activates video track after we've joined - @@ -573,6 +573,8 @@ private void UpdateParticipantTracksState(string userId, string sessionId, Track } participant.SetTrackEnabled(trackType, isEnabled); + + ActiveCall.NotifyTrackStateChanged(participant, trackType, isEnabled); } private void OnSfuParticipantJoined(ParticipantJoined participantJoined) diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs index 546843ca..1d0e9e06 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs @@ -40,7 +40,7 @@ public interface IStreamCall : IStreamStatefulModel /// /// Notifies that the collection was updated /// - //event Action SortedParticipantsUpdated; + event Action SortedParticipantsUpdated; Credentials Credentials { get; } @@ -73,11 +73,11 @@ public interface IStreamCall : IStreamStatefulModel /// - anyone who is pinned (locally pinned first, then remotely pinned) /// - anyone who is screen-sharing /// - dominant speaker - /// - all other video participants by when they joined - /// - audio only participants by when they joined + /// - all other video participants + /// - audio only participants /// Any update to this collection will trigger the event. /// - //IEnumerable SortedParticipants { get; } + IEnumerable SortedParticipants { get; } IReadOnlyList OwnCapabilities { get; } diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs index 252ea418..0f641254 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs @@ -13,11 +13,44 @@ namespace StreamVideo.Core.StatefulModels public interface IStreamVideoCallParticipant : IStreamStatefulModel { event ParticipantTrackChangedHandler TrackAdded; - + + /// + /// Is this participant "pinned" in the call meaning it will have precedence in list + /// + bool IsPinned { get; } + + /// + /// Is this participant currently streaming a screen share track + /// + bool IsScreenSharing { get; } + + /// + /// Is this participant currently streaming a video track + /// + bool IsVideoEnabled { get; } + + /// + /// Is this participant currently streaming an audio track + /// + bool IsAudioEnabled { get; } + + /// + /// Is this participant currently the most actively speaking participant. + /// + bool IsDominantSpeaker { get; } string UserId { get; } + + /// + /// Session ID is a unique identifier for a in a . + /// A single user can join a call through multiple devices therefore a single call can have multiple participants with the same . + /// string SessionId { get; } string TrackLookupPrefix { get; } string Name { get; } + + /// + /// Is this the participant from this device + /// bool IsLocalParticipant { get; } IStreamVideoUser User { get; set; } IStreamTrack VideoTrack { get; } @@ -27,7 +60,6 @@ public interface IStreamVideoCallParticipant : IStreamStatefulModel float AudioLevel { get; } bool IsSpeaking { get; } ConnectionQuality ConnectionQuality { get; } - bool IsDominantSpeaker { get; } IEnumerable GetTracks(); } diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs index c23dcefb..ba3abded 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Core; using Core.Utils; using Stream.Video.v1.Sfu.Events; using Stream.Video.v1.Sfu.Models; @@ -18,6 +19,7 @@ using StreamVideo.Core.StatefulModels; using StreamVideo.Core.StatefulModels.Tracks; using StreamVideo.Core.Utils; +using TrackType = StreamVideo.Core.Models.Sfu.TrackType; namespace StreamVideo.Core { @@ -46,7 +48,7 @@ internal sealed class StreamCall : StreamStatefulModelBase, public event Action PinnedParticipantsUpdated; - //public event Action SortedParticipantsUpdated; //StreamTodo: implement + public event Action SortedParticipantsUpdated; public event CallReactionAddedHandler ReactionAdded; @@ -74,6 +76,7 @@ private set if (prev != value) { + UpdateSortedParticipants(); PreviousDominantSpeaker = prev; DominantSpeakerChanged?.Invoke(value, prev); } @@ -83,7 +86,7 @@ private set public IStreamVideoCallParticipant PreviousDominantSpeaker { get; private set; } public IReadOnlyList PinnedParticipants => _pinnedParticipants; - //public IEnumerable SortedParticipants => _sortedParticipants; + public IEnumerable SortedParticipants => _sortedParticipants; #region State @@ -365,16 +368,14 @@ public void PinLocally(IStreamVideoCallParticipant participant) _localPinsSessionIds.Remove(participant.SessionId); _localPinsSessionIds.AddFirst(participant.SessionId); - UpdatePinnedParticipants(); - UpdateSortedParticipants(); + UpdatePinnedParticipants(out _); } public void UnpinLocally(IStreamVideoCallParticipant participant) { _localPinsSessionIds.Remove(participant.SessionId); - UpdatePinnedParticipants(); - UpdateSortedParticipants(); + UpdatePinnedParticipants(out _); } public bool IsPinnedLocally(IStreamVideoCallParticipant participant) @@ -488,6 +489,7 @@ internal void UpdateFromSfu(JoinResponse joinResponse) internal void UpdateFromSfu(ParticipantJoined participantJoined, ICache cache) { var participant = Session.UpdateFromSfu(participantJoined, cache); + UpdateSortedParticipants(); ParticipantJoined?.Invoke(participant); } @@ -497,8 +499,12 @@ internal void UpdateFromSfu(ParticipantLeft participantLeft, ICache cache) _localPinsSessionIds.RemoveAll(participant.sessionId); _serverPinsSessionIds.RemoveAll(pin => pin == participant.sessionId); - UpdatePinnedParticipants(); - UpdateSortedParticipants(); + + UpdatePinnedParticipants(out var updatedSortedParticipants); + if (!updatedSortedParticipants) + { + UpdateSortedParticipants(); + } cache.CallParticipants.TryRemove(participant.sessionId); @@ -508,16 +514,22 @@ internal void UpdateFromSfu(ParticipantLeft participantLeft, ICache cache) internal void UpdateFromSfu(DominantSpeakerChanged dominantSpeakerChanged, ICache cache) { + var prev = DominantSpeaker; DominantSpeaker = Participants.FirstOrDefault(p => p.SessionId == dominantSpeakerChanged.SessionId); + + if (prev != DominantSpeaker) + { + UpdateSortedParticipants(); + } } internal void UpdateFromSfu(PinsChanged pinsChanged, ICache cache) { UpdateServerPins(pinsChanged.Pins); - UpdatePinnedParticipants(); - UpdateSortedParticipants(); + UpdatePinnedParticipants(out _); } + //StreamTodo: missing TrackRemoved or perhaps we should not care whether a track was added/removed but only published/unpublished -> enabled/disabled internal void NotifyTrackAdded(IStreamVideoCallParticipant participant, IStreamTrack track) => TrackAdded?.Invoke(participant, track); @@ -601,10 +613,14 @@ internal void InternalHandleCallRecordingStartedEvent( CallRecordingStartedEventInternalDTO callRecordingStartedEvent) => RecordingStarted?.Invoke(); - public void InternalHandleCallRecordingStoppedEvent( + internal void InternalHandleCallRecordingStoppedEvent( CallRecordingStoppedEventInternalDTO callRecordingStoppedEvent) => RecordingStopped?.Invoke(); + internal void NotifyTrackStateChanged(StreamVideoCallParticipant participant, TrackType trackType, + bool isEnabled) + => UpdateSortedParticipants(); + protected override string InternalUniqueId { get => Cid; @@ -632,6 +648,11 @@ protected override string InternalUniqueId private readonly List _pinnedParticipants = new List(); + private readonly List + _sortedParticipants = new List(); + + private readonly CallParticipantComparer _participantComparer = new CallParticipantComparer(); + private readonly Dictionary> _capabilitiesByRole = new Dictionary>(); #endregion @@ -652,7 +673,7 @@ private void UpdateServerPins(IEnumerable pins) } } - private void UpdatePinnedParticipants() + private void UpdatePinnedParticipants(out bool sortedParticipants) { _pinnedParticipants.Clear(); @@ -673,12 +694,40 @@ private void UpdatePinnedParticipants() } } + //StreamTodo: optimize + var anyChanged = false; + foreach (var participant in Participants) + { + var prevIsPinned = participant.IsPinned; + var isPinned = IsPinned(participant); + + if (prevIsPinned != isPinned) + { + ((StreamVideoCallParticipant)participant).SetIsPinned(isPinned); + anyChanged = true; + } + } + + if (!anyChanged) + { + sortedParticipants = false; + return; + } + + sortedParticipants = true; + UpdateSortedParticipants(); PinnedParticipantsUpdated?.Invoke(); } private void UpdateSortedParticipants() { - //SortedParticipantsUpdated?.Invoke(); + _participantComparer.Reset(); + _sortedParticipants.Sort(_participantComparer); + + if (_participantComparer.OrderChanged) + { + SortedParticipantsUpdated?.Invoke(); + } } private void UpdateMembersFromDto(IEnumerable membersDtos) diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs index fa941ed3..16ef1734 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs @@ -20,6 +20,13 @@ internal sealed class StreamVideoCallParticipant : StreamStatefulModelBase UserSessionId == Client.InternalLowLevelClient.RtcSession.SessionId; + + public bool IsPinned { get; private set; } + + public bool IsScreenSharing => ScreenShareTrack?.Enabled ?? false; + + public bool IsVideoEnabled => VideoTrack?.Enabled ?? false; + public bool IsAudioEnabled => AudioTrack?.Enabled ?? false; #region Tracks @@ -47,6 +54,7 @@ internal sealed class StreamVideoCallParticipant : StreamStatefulModelBase PublishedTracks => _publishedTracks; + public string TrackLookupPrefix { get; private set; } public ConnectionQuality ConnectionQuality { get; private set; } public bool IsSpeaking { get; private set; } @@ -168,6 +176,8 @@ internal void SetTrackEnabled(TrackType type, bool enabled) //StreamTodo: we should trigger some event that track status changed } + internal void SetIsPinned(bool isPinned) => IsPinned = isPinned; + protected override string InternalUniqueId { get => UserSessionId;