diff --git a/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs b/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs index a1d5f182..af8f0b3a 100644 --- a/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs +++ b/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs @@ -38,6 +38,7 @@ public interface IStreamVideoClient : IStreamVideoClientEventsListener, IDisposa /// /// Currently ongoing call session. This will be NULL if there's no call active. /// You can subscribe to and events to get notified when a call is started/ended. + /// The client can only be in a single call at a time. /// IStreamCall ActiveCall { get; } @@ -76,7 +77,7 @@ Task JoinCallAsync(StreamCallType callType, string callId, bool cre bool notify); /// - /// Will return null if the call doesn't exist + /// Gets call information without joining it. Will return null if the call doesn't exist /// Task GetCallAsync(StreamCallType callType, string callId); diff --git a/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs b/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs index d8c18439..8ed29a77 100644 --- a/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs +++ b/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs @@ -71,8 +71,14 @@ void IStateLoadableFrom.LoadFromDto(SfuCallState dto, } // dto.CallState.Participants may not contain all participants - UpdateExtensions.TryAddUniqueTrackedObjects(_participants, - dto.Participants, cache.CallParticipants); + foreach (var dtoParticipant in dto.Participants) + { + var participant = cache.TryCreateOrUpdate(dtoParticipant); + if (!_participants.Contains(participant)) + { + _participants.Add(participant); + } + } ((IStateLoadableFrom)ParticipantCount).LoadFromDto( dto.ParticipantCount, cache); diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs index b7c94a71..7d9adb24 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamCall.cs @@ -337,5 +337,10 @@ Task QueryMembersAsync(IEnumerable filters /// /// True if participant is pinned remotely bool IsPinned(IStreamVideoCallParticipant participant); + + /// + /// Helper function to get the local participant object for the current user in this call + /// + IStreamVideoCallParticipant GetLocalParticipant(); } } \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs index e45a7903..5f3282b2 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs @@ -441,6 +441,49 @@ public Task UploadParticipantCustomDataAsync(IStreamVideoCallParticipant partici return UploadCustomDataAsync(); } + public IStreamVideoCallParticipant GetLocalParticipant() + { + if (Participants.Count == 0) + { + using (new StringBuilderPoolScope(out var tempSb)) + { + tempSb.AppendLine($"{nameof(GetLocalParticipant)} - no participants in the call."); + tempSb.AppendLine("Last operations leading to this state:"); + foreach (var log in _tempLogs.GetLogs()) + { + tempSb.AppendLine(log); + } + + Logs.Error(tempSb.ToString()); + } + throw new InvalidOperationException("No participants in the call."); + } + + var localParticipant = Participants.FirstOrDefault(p => p.IsLocalParticipant); + if (localParticipant == null) + { + using (new StringBuilderPoolScope(out var sb)) + { + var currentSessionId = LowLevelClient.RtcSession.SessionId; + sb.AppendLine($"Local participant not found. Local Session ID: {currentSessionId}. Participants in the call:"); + foreach (var p in Participants) + { + sb.AppendLine($" - UserId: {p.UserId}, SessionId: {p.SessionId}, IsLocalParticipant: {p.IsLocalParticipant}"); + } + + sb.AppendLine("Last operations leading to this state:"); + foreach (var log in _tempLogs.GetLogs()) + { + sb.AppendLine(log); + } + + Logs.Error(sb.ToString()); + } + } + + return localParticipant; + } + void IUpdateableFrom.UpdateFromDto(CallResponseInternalDTO dto, ICache cache) { @@ -463,6 +506,29 @@ void IUpdateableFrom.UpdateFromDto(CallResp Type = new StreamCallType(dto.Type); UpdatedAt = dto.UpdatedAt; + try + { + // Ignore the IDE warning, this can be null + if (dto.Session != null) + { + using (new StringBuilderPoolScope(out var tempSb)) + { + tempSb.Append($"`UpdateFromDto(CallResponseInternalDTO dto` - dto participants: {dto.Session.Participants?.Count}, call participants: {Session.Participants.Count}. Dto participants: "); + foreach (var p in dto.Session.Participants) + { + tempSb.Append($"[UserSessionId: {p.UserSessionId}, SessionId: {p.User?.Id}"); + } + + _tempLogs.Add(tempSb.ToString()); + } + } + + } + catch (Exception e) + { + Logs.Exception(e); + } + // Depends on Session.Participants so load as last LoadCustomData(dto.Custom); } @@ -531,6 +597,26 @@ internal void UpdateFromSfu(JoinResponse joinResponse) { ((IStateLoadableFrom)Session).LoadFromDto(joinResponse.CallState, Cache); UpdateServerPins(joinResponse.CallState.Pins); + + try + { + using (new StringBuilderPoolScope(out var tempSb)) + { + tempSb.Append("`UpdateFromSfu(JoinResponse joinResponse)` - joinResponse participants: "); + if(joinResponse.CallState !=null && joinResponse.CallState.Participants != null) + { + foreach (var p in joinResponse.CallState.Participants) + { + tempSb.Append($"[UserId: {p.UserId}, SessionId: {p.SessionId}, "); + } + } + _tempLogs.Add(tempSb.ToString()); + } + } + catch (Exception e) + { + Logs.Exception(e); + } } internal void UpdateFromSfu(ParticipantJoined participantJoined, ICache cache) @@ -750,6 +836,8 @@ private readonly List private readonly Dictionary> _capabilitiesByRole = new Dictionary>(); + private readonly DebugLogBuffer _tempLogs = new DebugLogBuffer(); + #endregion private readonly StreamCallType _type; diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs index 56098acd..10641560 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs @@ -23,7 +23,7 @@ internal sealed class StreamVideoCallParticipant : StreamStatefulModelBase UserSessionId == Client.InternalLowLevelClient.RtcSession.SessionId; + public bool IsLocalParticipant => SessionId == Client.InternalLowLevelClient.RtcSession.SessionId; public bool IsPinned { get; private set; } @@ -49,26 +49,6 @@ internal sealed class StreamVideoCallParticipant : StreamStatefulModelBase _userSessionId; - private set - { - Logs.WarningIfDebug( - $"UserSessionId set to: {value} for participant: {UserId} and Session ID: {SessionId}"); - _userSessionId = value; - - if (string.IsNullOrEmpty(SessionId)) - { - SessionId = value; - } - } - } - - private string _userSessionId; - #endregion #region Sfu State @@ -155,18 +135,13 @@ void IUpdateableFrom check if this is possible - //StreamTodo: verify if we're using every piece of data received from API/SFU responses to update this object } @@ -275,8 +250,8 @@ internal void NotifyTrackEnabled(TrackType type, bool enabled) protected override string InternalUniqueId { - get => UserSessionId; - set => UserSessionId = value; + get => SessionId; + set => SessionId = value; } protected override StreamVideoCallParticipant Self => this; diff --git a/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs new file mode 100644 index 00000000..49f07dc7 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace StreamVideo.Core.Utils +{ + internal class DebugLogBuffer + { + private const int MaxSize = 10; + private readonly string[] _buffer = new string[MaxSize]; + private int _index; + private int _count; + + public void Add(string msg) + { + _buffer[_index] = msg; + _index = (_index + 1) % MaxSize; + if (_count < MaxSize) _count++; + } + + public IEnumerable GetLogs() + { + for (var i = 0; i < _count; i++) + { + var idx = (_index - _count + i + MaxSize) % MaxSize; + yield return _buffer[idx]; + } + } + } +} \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs.meta b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs.meta new file mode 100644 index 00000000..6e737b95 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 739bff57312b4ac886f7e44d137822cc +timeCreated: 1763907152 \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs new file mode 100644 index 00000000..cd6472ce --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StreamVideo.Core.Utils +{ + internal static class StringBuilderPool + { + public static StringBuilder Rent() + { + if (Pool.Count > 0) + { + var sb = Pool.Pop(); + sb.Clear(); + return sb; + } + + return new StringBuilder(); + } + + public static void Release(StringBuilder sb) + { + if (sb == null) + { + throw new ArgumentNullException(nameof(sb)); + } + + sb.Clear(); + + if (Pool.Count < MaxPoolSize) + { + Pool.Push(sb); + } + } + + private const int MaxPoolSize = 128; + private static readonly Stack Pool = new Stack(); + } +} \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs.meta b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs.meta new file mode 100644 index 00000000..e45a3199 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPool.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1232b18c26cb4f8aa805b41dec9e5009 +timeCreated: 1763909310 \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs new file mode 100644 index 00000000..381d5f39 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs @@ -0,0 +1,24 @@ +using System; +using System.Text; + +namespace StreamVideo.Core.Utils +{ + internal readonly struct StringBuilderPoolScope : IDisposable + { + public StringBuilderPoolScope(out StringBuilder sb) + { + _sb = StringBuilderPool.Rent(); + sb = _sb; + } + + public void Dispose() + { + if (_sb != null) + { + StringBuilderPool.Release(_sb); + } + } + + private readonly StringBuilder _sb; + } +} \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs.meta b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs.meta new file mode 100644 index 00000000..80e4e0ab --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/StringBuilderPoolScope.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3aa3051826ba46978d388f6796567cf3 +timeCreated: 1763909332 \ No newline at end of file