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;