diff --git a/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs b/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs index af8f0b3a..9dcaf148 100644 --- a/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs +++ b/Packages/StreamVideo/Runtime/Core/IStreamVideoClient.cs @@ -106,5 +106,18 @@ Task JoinCallAsync(StreamCallType callType, string callId, bool cre void SetAudioProcessingModule(bool enabled, bool echoCancellationEnabled, bool autoGainEnabled, bool noiseSuppressionEnabled, int noiseSuppressionLevel); void GetAudioProcessingModuleConfig(out bool enabled, out bool echoCancellationEnabled, out bool autoGainEnabled, out bool noiseSuppressionEnabled, out int noiseSuppressionLevel); + + + /// + /// Temporary method (can be removed in the future) to pause audio playback on Android. + /// This will completely suspend playback of any audio coming from the StreamVideo SDK on the Android platform. + /// + void PauseAndroidAudioPlayback(); + + /// + /// Temporary method (can be removed in the future) to resume audio playback on Android. + /// Call this resume audio playback if it was previously paused using . + /// + void ResumeAndroidAudioPlayback(); } } \ 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 d3cb74e7..276d8451 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -213,7 +213,7 @@ public Camera VideoSceneInput #endregion - public string SessionId { get; private set; } + public string SessionId { get; private set; } = "(empty)"; public RtcSession(SfuWebSocket sfuWebSocket, Func httpClientFactory, ILogs logs, ISerializer serializer, ITimeService timeService, @@ -369,7 +369,7 @@ public async Task StartAsync(StreamCall call) //StreamTodo: validate when this state should set CallState = CallingState.Joined; - + #if STREAM_DEBUG_ENABLED _videoAudioSyncBenchmark?.Init(call); #endif @@ -434,7 +434,7 @@ public async Task StopAsync(string reason = "") { await _sfuWebSocket.DisconnectAsync(WebSocketCloseStatus.NormalClosure, reason); } - + CallState = CallingState.Offline; #if STREAM_DEBUG_ENABLED @@ -491,6 +491,30 @@ public void TryRestartAudioPlayback() #endif } + //StreamTODO: temp solution to allow stopping the audio when app is minimized. User tried disabling the AudioSource but the audio is handled natively so it has no effect + public void PauseAndroidAudioPlayback() + { +#if STREAM_NATIVE_AUDIO + WebRTC.StopAudioPlayback(); + _logs.Warning("Audio Playback is paused. This stops all audio coming from StreamVideo SDK on Android platform."); +#else + throw new NotSupportedException( + $"{nameof(PauseAndroidAudioPlayback)} is only supported on Android platform."); +#endif + } + + //StreamTODO: temp solution to allow stopping the audio when app is minimized. User tried disabling the AudioSource but the audio is handled natively so it has no effect + public void ResumeAndroidAudioPlayback() + { +#if STREAM_NATIVE_AUDIO + WebRTC.StartAudioPlayback(AudioOutputSampleRate, AudioOutputChannels); + _logs.Warning("Audio Playback is resumed. This resumes audio coming from StreamVideo SDK on Android platform."); +#else + throw new NotSupportedException( + $"{nameof(ResumeAndroidAudioPlayback)} is only supported on Android platform."); +#endif + } + /// /// Set Publisher Video track enabled/disabled, if track is available, or store the preference for when track becomes available /// diff --git a/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs b/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs index cd79e4cb..e14dec43 100644 --- a/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs +++ b/Packages/StreamVideo/Runtime/Core/Models/CallSession.cs @@ -6,9 +6,7 @@ using StreamVideo.Core.State; using StreamVideo.Core.State.Caches; using StreamVideo.Core.StatefulModels; -using StreamVideo.Core.Utils; using SfuCallState = StreamVideo.v1.Sfu.Models.CallState; -using SfuParticipant = StreamVideo.v1.Sfu.Models.Participant; using SfuParticipantCount = StreamVideo.v1.Sfu.Models.ParticipantCount; namespace StreamVideo.Core.Models @@ -56,6 +54,8 @@ void IStateLoadableFrom.LoadFromDto // CallSessionResponseInternalDTO usually (or always?) contains no participants. Participants are updated from the SFU join response // But SFU response can arrive before API response, so we can't override participants here because this clears the list + + foreach (var dtoParticipant in dto.Participants) { var participant = cache.TryCreateOrUpdate(dtoParticipant); @@ -117,7 +117,11 @@ internal void UpdateFromSfu(HealthCheckResponse healthCheckResponse, ICache cach internal (string sessionId, string userId) UpdateFromSfu(ParticipantLeft participantLeft, ICache cache) { var participant = cache.TryCreateOrUpdate(participantLeft.Participant); - _participants.Remove(participant); + + if (!participant.IsLocalParticipant) + { + _participants.Remove(participant); + } return (participantLeft.Participant.SessionId, participantLeft.Participant.UserId); } diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs index 5f3282b2..46d8cf86 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs @@ -62,7 +62,7 @@ internal sealed class StreamCall : StreamStatefulModelBase, //StreamTodo: Maybe add OtherParticipants -> All participants except for the local participant? public IReadOnlyList Participants => Session?.Participants; - + public ParticipantCount ParticipantCount => Session.ParticipantCount; public bool IsLocalUserOwner @@ -453,31 +453,41 @@ public IStreamVideoCallParticipant GetLocalParticipant() { tempSb.AppendLine(log); } - + Logs.Error(tempSb.ToString()); } - throw new InvalidOperationException("No participants in the call."); + + return null; } - + var localParticipant = Participants.FirstOrDefault(p => p.IsLocalParticipant); if (localParticipant == null) { - using (new StringBuilderPoolScope(out var sb)) + try { - var currentSessionId = LowLevelClient.RtcSession.SessionId; - sb.AppendLine($"Local participant not found. Local Session ID: {currentSessionId}. Participants in the call:"); - foreach (var p in Participants) + using (new StringBuilderPoolScope(out var sb)) { - sb.AppendLine($" - UserId: {p.UserId}, SessionId: {p.SessionId}, IsLocalParticipant: {p.IsLocalParticipant}"); - } + 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); + sb.AppendLine("Last operations leading to this state:"); + foreach (var log in _tempLogs.GetLogs()) + { + sb.AppendLine(log); + } + + Logs.Error(sb.ToString()); } - - Logs.Error(sb.ToString()); + } + catch (Exception e) + { + Logs.Warning($"Error while generating log for {nameof(GetLocalParticipant)}: " + e.Message); } } @@ -487,6 +497,8 @@ public IStreamVideoCallParticipant GetLocalParticipant() void IUpdateableFrom.UpdateFromDto(CallResponseInternalDTO dto, ICache cache) { + var wasBefore = IsLocalParticipantIncluded(); + Backstage = dto.Backstage; _blockedUserIds.TryReplaceValuesFromDto(dto.BlockedUserIds); Cid = dto.Cid; @@ -506,27 +518,33 @@ void IUpdateableFrom.UpdateFromDto(CallResp Type = new StreamCallType(dto.Type); UpdatedAt = dto.UpdatedAt; + var isAfter = IsLocalParticipantIncluded(); + try { + var localParticipantId = LowLevelClient.RtcSession.SessionId; // 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: "); + tempSb.Append( + $"`UpdateFromDto(CallResponseInternalDTO dto` - dto participants: {dto.Session.Participants?.Count}, call participants: {Session.Participants.Count}. "); + tempSb.Append( + $"IsLocalParticipantIncluded ({localParticipantId}) before: {wasBefore}, after: {isAfter}. "); + tempSb.Append("Dto participants:"); foreach (var p in dto.Session.Participants) { - tempSb.Append($"[UserSessionId: {p.UserSessionId}, SessionId: {p.User?.Id}"); + tempSb.Append($"[UserSessionId: {p.UserSessionId}, SessionId: {p.User?.Id}, "); } - + _tempLogs.Add(tempSb.ToString()); } } - } catch (Exception e) { - Logs.Exception(e); + Logs.Warning("Failed to log participants in UpdateFromDto: " + e.Message); } // Depends on Session.Participants so load as last @@ -595,21 +613,39 @@ internal StreamCall(string uniqueId, ICacheRepository repository, //StreamTodo: solve with a generic interface and best to be handled by cache layer internal void UpdateFromSfu(JoinResponse joinResponse) { + var wasBefore = IsLocalParticipantIncluded(); + ((IStateLoadableFrom)Session).LoadFromDto(joinResponse.CallState, Cache); UpdateServerPins(joinResponse.CallState.Pins); + var isAfter = IsLocalParticipantIncluded(); + try { + var localParticipantId = LowLevelClient.RtcSession.SessionId; using (new StringBuilderPoolScope(out var tempSb)) { + tempSb.Append("`UpdateFromSfu(JoinResponse joinResponse)` - "); + tempSb.Append( + $"IsLocalParticipantIncluded ({localParticipantId}) before: {wasBefore}, after: {isAfter}. "); tempSb.Append("`UpdateFromSfu(JoinResponse joinResponse)` - joinResponse participants: "); - if(joinResponse.CallState !=null && joinResponse.CallState.Participants != null) + if (joinResponse.CallState != null && joinResponse.CallState.Participants != null) { foreach (var p in joinResponse.CallState.Participants) { tempSb.Append($"[UserId: {p.UserId}, SessionId: {p.SessionId}, "); } } + else + { + tempSb.Append("joinResponse.CallState not null:"); + tempSb.Append(joinResponse.CallState != null); + tempSb.Append("joinResponse.CallState.Participants not null: "); + tempSb.Append(joinResponse.CallState?.Participants != null); + tempSb.Append("count: "); + tempSb.Append(joinResponse.CallState?.Participants?.Count); + } + _tempLogs.Add(tempSb.ToString()); } } @@ -628,6 +664,20 @@ internal void UpdateFromSfu(ParticipantJoined participantJoined, ICache cache) internal void UpdateFromSfu(ParticipantLeft participantLeft, ICache cache) { + try + { + var p = cache.TryCreateOrUpdate(participantLeft.Participant); + if (p.IsLocalParticipant) + { + _tempLogs.Add( + "`UpdateFromSfu(ParticipantLeft participantLeft)` - ERROR - local participant is leaving the call."); + } + } + catch (Exception e) + { + Logs.Warning("Error when generating debug log: " + e.Message); + } + var participant = Session.UpdateFromSfu(participantLeft, cache); _localPinsSessionIds.RemoveAll(participant.sessionId); @@ -666,23 +716,23 @@ internal void UpdateFromSfu(HealthCheckResponse healthCheckResponse, ICache cach { Session?.UpdateFromSfu(healthCheckResponse, cache); } - + internal void UpdateFromCoordinator(CallSessionParticipantCountsUpdatedEventInternalDTO eventData) { Session?.UpdateFromCoordinator(eventData, Client.InternalLowLevelClient.RtcSession.CallState); } - + internal void UpdateFromCoordinator(CallSessionParticipantJoinedEventInternalDTO eventData, ICache cache) { Session?.UpdateFromCoordinator(eventData, cache, Client.InternalLowLevelClient.RtcSession.CallState); - + //StreamTodo: we should extract AddParticipant logic from SFU and whatever is received first (SFU or Coordinator) should handle it } - + internal void UpdateFromCoordinator(CallSessionParticipantLeftEventInternalDTO eventData, ICache cache) { Session?.UpdateFromCoordinator(eventData, cache, Client.InternalLowLevelClient.RtcSession.CallState); - + //StreamTodo: we should extract RemoveParticipant logic from SFU and whatever is received first (SFU or Coordinator) should handle it } @@ -759,12 +809,14 @@ internal void InternalHandleCallRecordingStartedEvent(CallReactionEventInternalD //StreamTodo: NullReferenceException here because _client is never set var participant - = Client.InternalLowLevelClient.RtcSession.ActiveCall.Participants.FirstOrDefault(p => p.UserId == reaction.User.Id); + = Client.InternalLowLevelClient.RtcSession.ActiveCall.Participants.FirstOrDefault(p + => p.UserId == reaction.User.Id); if (participant == null) { Logs.ErrorIfDebug( $"Failed to find participant for reaction. UserId: {reaction.User.Id}, Participants: " + - string.Join(", ", Client.InternalLowLevelClient.RtcSession.ActiveCall.Participants.Select(p => p.UserId))); + string.Join(", ", + Client.InternalLowLevelClient.RtcSession.ActiveCall.Participants.Select(p => p.UserId))); return; } @@ -928,7 +980,7 @@ private void UpdateCapabilitiesByRole(Dictionary> capabilit tempRolesToRemove.Add(role); } } - + foreach (var role in tempRolesToRemove) { _capabilitiesByRole.Remove(role); @@ -987,5 +1039,15 @@ private void GetOrCreateParticipantsCustomDataSection(IStreamVideoCallParticipan participantCustomData = allParticipantsCustomData[participant.SessionId]; } + + private bool IsLocalParticipantIncluded() + { + if (Session == null || Session.Participants == null || Session.Participants.Count == 0) + { + return false; + } + + return Session.Participants.FirstOrDefault(p => p.IsLocalParticipant) != null; + } } } \ No newline at end of file diff --git a/Packages/StreamVideo/Runtime/Core/StreamVideoClient.cs b/Packages/StreamVideo/Runtime/Core/StreamVideoClient.cs index 35c9aea5..9c97c8c9 100644 --- a/Packages/StreamVideo/Runtime/Core/StreamVideoClient.cs +++ b/Packages/StreamVideo/Runtime/Core/StreamVideoClient.cs @@ -311,6 +311,10 @@ public void SetAudioProcessingModule(bool enabled, bool echoCancellationEnabled, #endif } + public void PauseAndroidAudioPlayback() => InternalLowLevelClient.RtcSession.PauseAndroidAudioPlayback(); + + public void ResumeAndroidAudioPlayback() => InternalLowLevelClient.RtcSession.ResumeAndroidAudioPlayback(); + #region IStreamVideoClientEventsListener event Action IStreamVideoClientEventsListener.Destroyed diff --git a/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs index 49f07dc7..47c94bd0 100644 --- a/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs +++ b/Packages/StreamVideo/Runtime/Core/Utils/DebugLogBuffer.cs @@ -4,7 +4,7 @@ namespace StreamVideo.Core.Utils { internal class DebugLogBuffer { - private const int MaxSize = 10; + private const int MaxSize = 15; private readonly string[] _buffer = new string[MaxSize]; private int _index; private int _count;