From 7c9aeef82301765b624eeb5f46cbb927aa488e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:29:09 +0100 Subject: [PATCH 1/7] Ignore timeout or network issues during send final stats --- .../StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index 68509a7e..7d9dcd7e 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -537,6 +537,14 @@ public async Task StopAsync(string reason = "") } } } + catch (HttpRequestException httpEx) + { + _logs.Info($"Network unavailable during final stats send: {httpEx.Message}"); + } + catch (OperationCanceledException) + { + _logs.Info("Final stats send timed out."); + } catch (Exception e) { _logs.Warning($"Failed to send final stats on leave: {e.Message}"); From 7a8769d0410876c43070747c915e24f0a97c60fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:29:34 +0100 Subject: [PATCH 2/7] Fix Unity warning regarding releasing active texture --- .../Runtime/Core/LowLevelClient/StreamPeerConnection.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/StreamPeerConnection.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/StreamPeerConnection.cs index e2c1aa66..4b07fc83 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/StreamPeerConnection.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/StreamPeerConnection.cs @@ -223,6 +223,8 @@ public async Task CreateAnswerAsync(CancellationToken can public void Update() { //StreamTodo: investigate if this Blit is necessary + // One reason was to easy control target resolution -> we don't accept every target resolution because small res can crash Android video encoder + // We should check if WebCamTexture allows setting any resolution if (_publisherVideoTrackTexture != null && _mediaInputProvider.VideoInput != null) { Graphics.Blit(_mediaInputProvider.VideoInput, _publisherVideoTrackTexture); @@ -273,6 +275,11 @@ public void Dispose() if (_publisherVideoTrackTexture != null) { + // Unity gives warning when releasing an active texture + if (RenderTexture.active == _publisherVideoTrackTexture) + { + RenderTexture.active = null; + } _publisherVideoTrackTexture.Release(); _publisherVideoTrackTexture = null; } From 673dce008aa033e136e209c6c7f572ab7a08e0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:30:16 +0100 Subject: [PATCH 3/7] Fix errors thrown when stats are gathered during disposal --- .../Core/Stats/UnityWebRtcStatsCollector.cs | 32 ++++++++++++++++--- .../Runtime/Core/Stats/WebRtcStatsSender.cs | 6 ++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Packages/StreamVideo/Runtime/Core/Stats/UnityWebRtcStatsCollector.cs b/Packages/StreamVideo/Runtime/Core/Stats/UnityWebRtcStatsCollector.cs index 0a47720f..c0c57d65 100644 --- a/Packages/StreamVideo/Runtime/Core/Stats/UnityWebRtcStatsCollector.cs +++ b/Packages/StreamVideo/Runtime/Core/Stats/UnityWebRtcStatsCollector.cs @@ -37,32 +37,44 @@ private class StatSnapshot public async Task GetPublisherStatsJsonAsync(CancellationToken cancellationToken) { + if (_rtcSession.Publisher == null) + { + return ConvertStatsToJson(new Dictionary()); + } + var report = await _rtcSession.Publisher.GetStatsReportAsync(cancellationToken); return ConvertStatsToJson(report.Stats); } public async Task GetSubscriberStatsJsonAsync(CancellationToken cancellationToken) { + if (_rtcSession.Subscriber == null) + { + return ConvertStatsToJson(new Dictionary()); + } + var report = await _rtcSession.Subscriber.GetStatsReportAsync(cancellationToken); return ConvertStatsToJson(report.Stats); } public async Task GetRtcStatsJsonAsync(CancellationToken cancellationToken) { - // Get both publisher and subscriber stats + if (_rtcSession.Publisher == null || _rtcSession.Subscriber == null) + { + var emptyTraces = new List(); + return _serializer.Serialize(emptyTraces, _serializationOptions); + } + var publisherReport = await _rtcSession.Publisher.GetStatsReportAsync(cancellationToken); var subscriberReport = await _rtcSession.Subscriber.GetStatsReportAsync(cancellationToken); - // Compute delta-compressed stats var publisherDelta = ComputeDeltaCompression(_previousPublisherStats, publisherReport.Stats); var subscriberDelta = ComputeDeltaCompression(_previousSubscriberStats, subscriberReport.Stats); - // Update previous stats for next delta - create snapshots to avoid holding native references _previousPublisherStats = CreateStatsSnapshot(publisherReport.Stats); _previousSubscriberStats = CreateStatsSnapshot(subscriberReport.Stats); - // Collect ALL tracer data from the TracerManager - // This includes all SFU events traced in RtcSession plus the getstats traces we'll add + //StreamTodo: check later if we can use fixed buffer e.g. list from pool var allTraces = new List(); foreach (var tracer in _tracerManager.GetTracers()) @@ -242,12 +254,22 @@ private Dictionary ComputeDeltaCompression( public async Task> GetEncodeStatsAsync(CancellationToken cancellationToken) { + if (_rtcSession.Publisher == null) + { + return new List(); + } + var report = await _rtcSession.Publisher.GetStatsReportAsync(cancellationToken); return ComputeEncodeStats(report.Stats); } public async Task> GetDecodeStatsAsync(CancellationToken cancellationToken) { + if (_rtcSession.Subscriber == null) + { + return new List(); + } + var report = await _rtcSession.Subscriber.GetStatsReportAsync(cancellationToken); return ComputeDecodeStats(report.Stats); } diff --git a/Packages/StreamVideo/Runtime/Core/Stats/WebRtcStatsSender.cs b/Packages/StreamVideo/Runtime/Core/Stats/WebRtcStatsSender.cs index b991150e..ed4a7148 100644 --- a/Packages/StreamVideo/Runtime/Core/Stats/WebRtcStatsSender.cs +++ b/Packages/StreamVideo/Runtime/Core/Stats/WebRtcStatsSender.cs @@ -87,6 +87,12 @@ private async Task CollectAndSend(CancellationToken cancellationToken) return; } + if(_rtcSession.Publisher == null || _rtcSession.Subscriber == null) + { + _logs.Warning("WebRtcStatsSender: Publisher or Subscriber is null, skipping stats collection."); + return; + } + var subscriberStatsJson = await _webRtcStatsCollector.GetSubscriberStatsJsonAsync(cancellationToken); var publisherStatsJson = await _webRtcStatsCollector.GetPublisherStatsJsonAsync(cancellationToken); var rtcStatsJson = await _webRtcStatsCollector.GetRtcStatsJsonAsync(cancellationToken); From 61ccf8ce2344a598fd8d636e91a0ac3560f50c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:39:41 +0100 Subject: [PATCH 4/7] exclude internal tools --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 269cba25..42abb982 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,7 @@ Assets/LocalOnlyTools_53854/ Assets/LocalOnlyTools_53854.meta last_build_version_4983432740324322.txt last_build_version_4983432740324322.meta +replace_imported_sample_scene_credentials.bat +replace_imported_sample_scene_credentials.ps1 +replace_imported_sample_scene_credentials.sh From ef8ef6937f25b874bdc7e91b07ceb9e9ea7c74ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:44:02 +0100 Subject: [PATCH 5/7] Add methods to control remote participants track subscriptions + only subscribe to 10 video tracks by default. Above this number, integrator must control the subscriptions --- .../Runtime/Core/LowLevelClient/RtcSession.cs | 128 ++++++++++++++---- .../IStreamVideoCallParticipant.cs | 12 ++ .../Runtime/Core/StatefulModels/StreamCall.cs | 4 + .../StreamVideoCallParticipant.cs | 6 + 4 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index 7d9dcd7e..ba745fb0 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -79,6 +79,8 @@ internal sealed class RtcSession : IMediaInputProvider, IDisposable public const bool UseNativeAudioBindings = false; #endif + public const int MaxParticipantsForVideoAutoSubscription = 10; + public event Action PublisherAudioTrackIsEnabledChanged; public event Action PublisherVideoTrackIsEnabledChanged; @@ -214,7 +216,7 @@ public Camera VideoSceneInput } #endregion - + public bool ShouldSfuAttemptToReconnect() { if (CallState != CallingState.Joined && CallState != CallingState.Joining) @@ -344,6 +346,8 @@ private void ValidateCallCredentialsOrThrow(IStreamCall call) } } + public void SetCallingState(CallingState newState) => CallState = newState; + public async Task StartAsync(StreamCall call, CancellationToken cancellationToken = default) { if (ActiveCall != null) @@ -360,7 +364,7 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke try { _logs.Info($"Start joining a call: type={call.Type}, id={call.Id}"); - + //StreamTodo: perhaps not necessary here ClearSession(); @@ -422,6 +426,12 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke _sfuWebSocket.SetSessionData(SessionId, offer.sdp, sfuUrl, sfuToken); await _sfuWebSocket.ConnectAsync(cancellationToken); +#if STREAM_TESTS_ENABLED && UNITY_EDITOR + // Simulate a bit of delay for tests so we can test killing the operation in progress + //StreamTOdo: we could add fake delays in multiple places and this way control exiting from every step in tests + await Task.Delay(100); +#endif + // Wait for call to be joined with timeout const int joinTimeoutSeconds = 30; var joinStartTime = _timeService.Time; @@ -465,12 +475,12 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke _videoAudioSyncBenchmark?.Init(call); #endif } - catch(OperationCanceledException) + catch (OperationCanceledException) { ClearSession(); throw; } - catch(Exception e) + catch (Exception e) { _logs.Exception(e); ClearSession(); @@ -494,7 +504,7 @@ public async Task StopAsync(string reason = "") WebRTC.StopAudioPlayback(); #endif } - + if (CallState == CallingState.Leaving || CallState == CallingState.Offline) { //StreamTODO: should this return a task of the ongoing stop? @@ -589,6 +599,42 @@ public void UpdateRequestedVideoResolution(string participantSessionId, VideoRes QueueTracksSubscriptionRequest(); } + public void UpdateIncomingVideoRequested(string participantSessionId, bool isRequested) + { + _incomingVideoRequestedByParticipantSessionId[participantSessionId] = isRequested; + QueueTracksSubscriptionRequest(); + } + + public void UpdateIncomingAudioRequested(string participantSessionId, bool isRequested) + { + _incomingAudioRequestedByParticipantSessionId[participantSessionId] = isRequested; + QueueTracksSubscriptionRequest(); + } + + // Let's request video for the first 10 participants that join + public void NotifyParticipantJoined(string participantSessionId) + { + if (ActiveCall == null) + { + return; + } + + var participantCount = ActiveCall.Participants?.Count ?? 0; + var requestVideo = participantCount <= MaxParticipantsForVideoAutoSubscription; + var requestAudio = true; // No limit by default + + _incomingVideoRequestedByParticipantSessionId.TryAdd(participantSessionId, requestVideo); + _incomingAudioRequestedByParticipantSessionId.TryAdd(participantSessionId, requestAudio); + } + + public void NotifyParticipantLeft(string participantSessionId) + { + _videoResolutionByParticipantSessionId.Remove(participantSessionId); + _incomingVideoRequestedByParticipantSessionId.Remove(participantSessionId); + _incomingAudioRequestedByParticipantSessionId.Remove(participantSessionId); + QueueTracksSubscriptionRequest(); + } + public void SetAudioRecordingDevice(MicrophoneDeviceInfo device) { _logs.WarningIfDebug("RtcSession.SetAudioRecordingDevice device: " + device); @@ -667,6 +713,12 @@ public void ResumeAndroidAudioPlayback() private readonly Dictionary _videoResolutionByParticipantSessionId = new Dictionary(); + private readonly Dictionary _incomingVideoRequestedByParticipantSessionId + = new Dictionary(); + + private readonly Dictionary _incomingAudioRequestedByParticipantSessionId + = new Dictionary(); + private HttpClient _httpClient; private CallingState _callState; @@ -694,6 +746,8 @@ private void ClearSession() _pendingIceTrickleRequests.Clear(); _videoResolutionByParticipantSessionId.Clear(); + _incomingVideoRequestedByParticipantSessionId.Clear(); + _incomingAudioRequestedByParticipantSessionId.Clear(); _tracerManager?.Clear(); Subscriber?.Dispose(); @@ -851,33 +905,47 @@ private IEnumerable GetDesiredTracksDetails() continue; } - var requestedVideoResolution = GetRequestedVideoResolution(participant); - - foreach (var trackType in trackTypes) + var userId = GetUserId(participant); + if (string.IsNullOrEmpty(userId)) { - //StreamTODO: UserId is sometimes null here - //This was before changing the IUpdateableFrom.UpdateFromDto - //to extract UserId from User obj + _logs.Error( + $"Cannot subscribe to any tracks - participant UserId is null or empty. SessionID: {participant.SessionId}"); + continue; + } - var userId = GetUserId(participant); - if (string.IsNullOrEmpty(userId)) + var shouldConsumeAudio = ShouldSubscribeToAudioTrack(participant); + if (shouldConsumeAudio) + { + yield return new TrackSubscriptionDetails { - _logs.Error( - $"Cannot subscribe to {trackType} - participant UserId is null or empty. SessionID: {participant.SessionId}"); - continue; - } + UserId = userId, + SessionId = participant.SessionId, + TrackType = SfuTrackType.Audio, + }; + } + + var shouldConsumeVideo = ShouldSubscribeToVideoTrack(participant); + if (shouldConsumeVideo) + { + var requestedVideoResolution = GetRequestedVideoResolution(participant); yield return new TrackSubscriptionDetails { UserId = userId, SessionId = participant.SessionId, - TrackType = trackType, + TrackType = SfuTrackType.Video, Dimension = requestedVideoResolution.ToVideoDimension() }; } } } + private bool ShouldSubscribeToVideoTrack(IStreamVideoCallParticipant participant) + => _incomingVideoRequestedByParticipantSessionId.GetValueOrDefault(participant.SessionId, false); + + private bool ShouldSubscribeToAudioTrack(IStreamVideoCallParticipant participant) + => _incomingAudioRequestedByParticipantSessionId.GetValueOrDefault(participant.SessionId, false); + //StreamTodo: remove this, this is a workaround to Null UserId error private string GetUserId(IStreamVideoCallParticipant participant) { @@ -971,7 +1039,7 @@ private void UpdateAudioRecording() private void OnSfuJoinResponse(JoinResponse joinResponse) { //StreamTODO: what if left the call and started a new one but the JoinResponse belongs to the previous session? - + _sfuTracer?.Trace(PeerConnectionTraceKey.JoinRequest, joinResponse); _logs.InfoIfDebug($"Handle Sfu {nameof(JoinResponse)}"); ActiveCall.UpdateFromSfu(joinResponse); @@ -1037,7 +1105,7 @@ private async void OnSfuSubscriberOffer(SubscriberOffer subscriberOffer) { return; } - + //StreamTodo: handle subscriberOffer.iceRestart var rtcSessionDescription = new RTCSessionDescription { @@ -1047,7 +1115,8 @@ private async void OnSfuSubscriberOffer(SubscriberOffer subscriberOffer) try { - await Subscriber.SetRemoteDescriptionAsync(rtcSessionDescription, GetCurrentCancellationTokenOrDefault()); + await Subscriber.SetRemoteDescriptionAsync(rtcSessionDescription, + GetCurrentCancellationTokenOrDefault()); Subscriber.ThrowDisposedDuringOperationIfNull(); } catch (Exception e) @@ -1107,14 +1176,16 @@ private void OnSfuTrackUnpublished(TrackUnpublished trackUnpublished) var sessionId = trackUnpublished.SessionId; var type = trackUnpublished.Type.ToPublicEnum(); var cause = trackUnpublished.Cause; - + // StreamTODO: test if this works well with other user muting this user - var updateLocalParticipantState = cause != TrackUnpublishReason.Unspecified && cause != TrackUnpublishReason.UserMuted; + var updateLocalParticipantState + = cause != TrackUnpublishReason.Unspecified && cause != TrackUnpublishReason.UserMuted; // Optionally available. Read TrackUnpublished.participant comment in events.proto var participantSfuDto = trackUnpublished.Participant; - UpdateParticipantTracksState(userId, sessionId, type, isEnabled: false, updateLocalParticipantState, out var participant); + UpdateParticipantTracksState(userId, sessionId, type, isEnabled: false, updateLocalParticipantState, + out var participant); if (participantSfuDto != null && participant != null) { @@ -1135,7 +1206,8 @@ private void OnSfuTrackPublished(TrackPublished trackPublished) // Optionally available. Read TrackUnpublished.participant comment in events.proto var participantSfuDto = trackPublished.Participant; - UpdateParticipantTracksState(userId, sessionId, type, isEnabled: true, updateLocalParticipantState: true, out var participant); + UpdateParticipantTracksState(userId, sessionId, type, isEnabled: true, updateLocalParticipantState: true, + out var participant); if (participantSfuDto != null && participant != null) { @@ -1506,7 +1578,7 @@ private async void OnPublisherNegotiationNeeded() $"{nameof(Publisher.SignalingState)} state is not stable, current state: {Publisher.SignalingState}"); } - if(GetCurrentCancellationTokenOrDefault().IsCancellationRequested) + if (GetCurrentCancellationTokenOrDefault().IsCancellationRequested) { return; } @@ -1893,7 +1965,7 @@ private void OnPublisherVideoTrackChanged(VideoStreamTrack videoTrack) { PublisherVideoTrackChanged?.Invoke(); } - + void PublisherOnDisconnected() { if (CallState == CallingState.Joined || CallState == CallingState.Joining) @@ -1920,7 +1992,7 @@ private static bool AssertCallIdMatch(IStreamCall activeCall, string callId, ILo return true; } - + private void SubscribeToSfuEvents() { diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs index d65dfca3..f21dde9e 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs @@ -85,5 +85,17 @@ public interface IStreamVideoCallParticipant : IStreamStatefulModel, IHasCustomD /// /// Video resolution you wish to receive for this participant. You can use a predefined size or pick a predefined one from void UpdateRequestedVideoResolution(VideoResolution videoResolution); + + /// + /// Should video track of this participant be received. By default, only few + /// + /// If enabled, the video stream will be requested from the server + void SetIncomingVideoEnabled(bool enabled); + + /// + /// Should audio track of this participant be received + /// + /// If enabled, the audio stream will be requested from the server + void SetIncomingAudioEnabled(bool enabled); } } \ 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 46d8cf86..52c750d3 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs @@ -658,7 +658,9 @@ internal void UpdateFromSfu(JoinResponse joinResponse) internal void UpdateFromSfu(ParticipantJoined participantJoined, ICache cache) { var participant = Session.UpdateFromSfu(participantJoined, cache); + LowLevelClient.RtcSession.NotifyParticipantJoined(participantJoined.Participant.SessionId); UpdateSortedParticipants(); + ParticipantJoined?.Invoke(participant); } @@ -678,6 +680,8 @@ internal void UpdateFromSfu(ParticipantLeft participantLeft, ICache cache) Logs.Warning("Error when generating debug log: " + e.Message); } + LowLevelClient.RtcSession.NotifyParticipantLeft(participantLeft.Participant.SessionId); + var participant = Session.UpdateFromSfu(participantLeft, cache); _localPinsSessionIds.RemoveAll(participant.sessionId); diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs index 10641560..1cd13543 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs @@ -94,6 +94,12 @@ public IEnumerable GetTracks() yield return _screenShareTrack; } } + + public void SetIncomingVideoEnabled(bool enabled) + => LowLevelClient.RtcSession.UpdateIncomingVideoRequested(SessionId, enabled); + + public void SetIncomingAudioEnabled(bool enabled) + => LowLevelClient.RtcSession.UpdateIncomingAudioRequested(SessionId, enabled); public void UpdateRequestedVideoResolution(VideoResolution videoResolution) => LowLevelClient.RtcSession.UpdateRequestedVideoResolution(SessionId, videoResolution); From a868ce8352ac254dbb5804c8bbace13cd41ffa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:06:15 +0100 Subject: [PATCH 6/7] Reduce the number of video auto subscriptions --- Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs | 2 +- .../Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index ba745fb0..ae59c99f 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -79,7 +79,7 @@ internal sealed class RtcSession : IMediaInputProvider, IDisposable public const bool UseNativeAudioBindings = false; #endif - public const int MaxParticipantsForVideoAutoSubscription = 10; + public const int MaxParticipantsForVideoAutoSubscription = 5; public event Action PublisherAudioTrackIsEnabledChanged; public event Action PublisherVideoTrackIsEnabledChanged; diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs index f21dde9e..a3fb2cbc 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs @@ -87,7 +87,7 @@ public interface IStreamVideoCallParticipant : IStreamStatefulModel, IHasCustomD void UpdateRequestedVideoResolution(VideoResolution videoResolution); /// - /// Should video track of this participant be received. By default, only few + /// Should video track of this participant be received. /// /// If enabled, the video stream will be requested from the server void SetIncomingVideoEnabled(bool enabled); From 40b793be7b5910363dedb6773d8b3cf75d5008e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:40:56 +0100 Subject: [PATCH 7/7] process participants already in the call --- .../StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index ae59c99f..185c64f4 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -463,6 +463,11 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke WebRTC.StartAudioPlayback(AudioOutputSampleRate, AudioOutputChannels); #endif } + + foreach(var p in ActiveCall.Participants) + { + NotifyParticipantJoined(p.SessionId); + } //StreamTodo: validate when this state should set CallState = CallingState.Joined;