@@ -79,6 +79,8 @@ internal sealed class RtcSession : IMediaInputProvider, IDisposable
7979 public const bool UseNativeAudioBindings = false ;
8080#endif
8181
82+ public const int MaxParticipantsForVideoAutoSubscription = 5 ;
83+
8284 public event Action < bool > PublisherAudioTrackIsEnabledChanged ;
8385 public event Action < bool > PublisherVideoTrackIsEnabledChanged ;
8486
@@ -214,7 +216,7 @@ public Camera VideoSceneInput
214216 }
215217
216218 #endregion
217-
219+
218220 public bool ShouldSfuAttemptToReconnect ( )
219221 {
220222 if ( CallState != CallingState . Joined && CallState != CallingState . Joining )
@@ -344,6 +346,8 @@ private void ValidateCallCredentialsOrThrow(IStreamCall call)
344346 }
345347 }
346348
349+ public void SetCallingState ( CallingState newState ) => CallState = newState ;
350+
347351 public async Task StartAsync ( StreamCall call , CancellationToken cancellationToken = default )
348352 {
349353 if ( ActiveCall != null )
@@ -360,7 +364,7 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke
360364 try
361365 {
362366 _logs . Info ( $ "Start joining a call: type={ call . Type } , id={ call . Id } ") ;
363-
367+
364368 //StreamTodo: perhaps not necessary here
365369 ClearSession ( ) ;
366370
@@ -422,6 +426,12 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke
422426 _sfuWebSocket . SetSessionData ( SessionId , offer . sdp , sfuUrl , sfuToken ) ;
423427 await _sfuWebSocket . ConnectAsync ( cancellationToken ) ;
424428
429+ #if STREAM_TESTS_ENABLED && UNITY_EDITOR
430+ // Simulate a bit of delay for tests so we can test killing the operation in progress
431+ //StreamTOdo: we could add fake delays in multiple places and this way control exiting from every step in tests
432+ await Task . Delay ( 100 ) ;
433+ #endif
434+
425435 // Wait for call to be joined with timeout
426436 const int joinTimeoutSeconds = 30 ;
427437 var joinStartTime = _timeService . Time ;
@@ -453,6 +463,11 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke
453463 WebRTC . StartAudioPlayback ( AudioOutputSampleRate , AudioOutputChannels ) ;
454464#endif
455465 }
466+
467+ foreach ( var p in ActiveCall . Participants )
468+ {
469+ NotifyParticipantJoined ( p . SessionId ) ;
470+ }
456471
457472 //StreamTodo: validate when this state should set
458473 CallState = CallingState . Joined ;
@@ -465,12 +480,12 @@ public async Task StartAsync(StreamCall call, CancellationToken cancellationToke
465480 _videoAudioSyncBenchmark ? . Init ( call ) ;
466481#endif
467482 }
468- catch ( OperationCanceledException )
483+ catch ( OperationCanceledException )
469484 {
470485 ClearSession ( ) ;
471486 throw ;
472487 }
473- catch ( Exception e )
488+ catch ( Exception e )
474489 {
475490 _logs . Exception ( e ) ;
476491 ClearSession ( ) ;
@@ -494,7 +509,7 @@ public async Task StopAsync(string reason = "")
494509 WebRTC . StopAudioPlayback ( ) ;
495510#endif
496511 }
497-
512+
498513 if ( CallState == CallingState . Leaving || CallState == CallingState . Offline )
499514 {
500515 //StreamTODO: should this return a task of the ongoing stop?
@@ -537,6 +552,14 @@ public async Task StopAsync(string reason = "")
537552 }
538553 }
539554 }
555+ catch ( HttpRequestException httpEx )
556+ {
557+ _logs . Info ( $ "Network unavailable during final stats send: { httpEx . Message } ") ;
558+ }
559+ catch ( OperationCanceledException )
560+ {
561+ _logs . Info ( "Final stats send timed out." ) ;
562+ }
540563 catch ( Exception e )
541564 {
542565 _logs . Warning ( $ "Failed to send final stats on leave: { e . Message } ") ;
@@ -581,6 +604,42 @@ public void UpdateRequestedVideoResolution(string participantSessionId, VideoRes
581604 QueueTracksSubscriptionRequest ( ) ;
582605 }
583606
607+ public void UpdateIncomingVideoRequested ( string participantSessionId , bool isRequested )
608+ {
609+ _incomingVideoRequestedByParticipantSessionId [ participantSessionId ] = isRequested ;
610+ QueueTracksSubscriptionRequest ( ) ;
611+ }
612+
613+ public void UpdateIncomingAudioRequested ( string participantSessionId , bool isRequested )
614+ {
615+ _incomingAudioRequestedByParticipantSessionId [ participantSessionId ] = isRequested ;
616+ QueueTracksSubscriptionRequest ( ) ;
617+ }
618+
619+ // Let's request video for the first 10 participants that join
620+ public void NotifyParticipantJoined ( string participantSessionId )
621+ {
622+ if ( ActiveCall == null )
623+ {
624+ return ;
625+ }
626+
627+ var participantCount = ActiveCall . Participants ? . Count ?? 0 ;
628+ var requestVideo = participantCount <= MaxParticipantsForVideoAutoSubscription ;
629+ var requestAudio = true ; // No limit by default
630+
631+ _incomingVideoRequestedByParticipantSessionId . TryAdd ( participantSessionId , requestVideo ) ;
632+ _incomingAudioRequestedByParticipantSessionId . TryAdd ( participantSessionId , requestAudio ) ;
633+ }
634+
635+ public void NotifyParticipantLeft ( string participantSessionId )
636+ {
637+ _videoResolutionByParticipantSessionId . Remove ( participantSessionId ) ;
638+ _incomingVideoRequestedByParticipantSessionId . Remove ( participantSessionId ) ;
639+ _incomingAudioRequestedByParticipantSessionId . Remove ( participantSessionId ) ;
640+ QueueTracksSubscriptionRequest ( ) ;
641+ }
642+
584643 public void SetAudioRecordingDevice ( MicrophoneDeviceInfo device )
585644 {
586645 _logs . WarningIfDebug ( "RtcSession.SetAudioRecordingDevice device: " + device ) ;
@@ -659,6 +718,12 @@ public void ResumeAndroidAudioPlayback()
659718 private readonly Dictionary < string , VideoResolution > _videoResolutionByParticipantSessionId
660719 = new Dictionary < string , VideoResolution > ( ) ;
661720
721+ private readonly Dictionary < string , bool > _incomingVideoRequestedByParticipantSessionId
722+ = new Dictionary < string , bool > ( ) ;
723+
724+ private readonly Dictionary < string , bool > _incomingAudioRequestedByParticipantSessionId
725+ = new Dictionary < string , bool > ( ) ;
726+
662727 private HttpClient _httpClient ;
663728 private CallingState _callState ;
664729
@@ -686,6 +751,8 @@ private void ClearSession()
686751
687752 _pendingIceTrickleRequests . Clear ( ) ;
688753 _videoResolutionByParticipantSessionId . Clear ( ) ;
754+ _incomingVideoRequestedByParticipantSessionId . Clear ( ) ;
755+ _incomingAudioRequestedByParticipantSessionId . Clear ( ) ;
689756 _tracerManager ? . Clear ( ) ;
690757
691758 Subscriber ? . Dispose ( ) ;
@@ -843,33 +910,47 @@ private IEnumerable<TrackSubscriptionDetails> GetDesiredTracksDetails()
843910 continue ;
844911 }
845912
846- var requestedVideoResolution = GetRequestedVideoResolution ( participant ) ;
847-
848- foreach ( var trackType in trackTypes )
913+ var userId = GetUserId ( participant ) ;
914+ if ( string . IsNullOrEmpty ( userId ) )
849915 {
850- //StreamTODO: UserId is sometimes null here
851- //This was before changing the IUpdateableFrom<CallParticipantResponseInternalDTO, StreamVideoCallParticipant>.UpdateFromDto
852- //to extract UserId from User obj
916+ _logs . Error (
917+ $ "Cannot subscribe to any tracks - participant UserId is null or empty. SessionID: { participant . SessionId } ") ;
918+ continue ;
919+ }
853920
854- var userId = GetUserId ( participant ) ;
855- if ( string . IsNullOrEmpty ( userId ) )
921+ var shouldConsumeAudio = ShouldSubscribeToAudioTrack ( participant ) ;
922+ if ( shouldConsumeAudio )
923+ {
924+ yield return new TrackSubscriptionDetails
856925 {
857- _logs . Error (
858- $ "Cannot subscribe to { trackType } - participant UserId is null or empty. SessionID: { participant . SessionId } ") ;
859- continue ;
860- }
926+ UserId = userId ,
927+ SessionId = participant . SessionId ,
928+ TrackType = SfuTrackType . Audio ,
929+ } ;
930+ }
931+
932+ var shouldConsumeVideo = ShouldSubscribeToVideoTrack ( participant ) ;
933+ if ( shouldConsumeVideo )
934+ {
935+ var requestedVideoResolution = GetRequestedVideoResolution ( participant ) ;
861936
862937 yield return new TrackSubscriptionDetails
863938 {
864939 UserId = userId ,
865940 SessionId = participant . SessionId ,
866- TrackType = trackType ,
941+ TrackType = SfuTrackType . Video ,
867942 Dimension = requestedVideoResolution . ToVideoDimension ( )
868943 } ;
869944 }
870945 }
871946 }
872947
948+ private bool ShouldSubscribeToVideoTrack ( IStreamVideoCallParticipant participant )
949+ => _incomingVideoRequestedByParticipantSessionId . GetValueOrDefault ( participant . SessionId , false ) ;
950+
951+ private bool ShouldSubscribeToAudioTrack ( IStreamVideoCallParticipant participant )
952+ => _incomingAudioRequestedByParticipantSessionId . GetValueOrDefault ( participant . SessionId , false ) ;
953+
873954 //StreamTodo: remove this, this is a workaround to Null UserId error
874955 private string GetUserId ( IStreamVideoCallParticipant participant )
875956 {
@@ -963,7 +1044,7 @@ private void UpdateAudioRecording()
9631044 private void OnSfuJoinResponse ( JoinResponse joinResponse )
9641045 {
9651046 //StreamTODO: what if left the call and started a new one but the JoinResponse belongs to the previous session?
966-
1047+
9671048 _sfuTracer ? . Trace ( PeerConnectionTraceKey . JoinRequest , joinResponse ) ;
9681049 _logs . InfoIfDebug ( $ "Handle Sfu { nameof ( JoinResponse ) } ") ;
9691050 ActiveCall . UpdateFromSfu ( joinResponse ) ;
@@ -1029,7 +1110,7 @@ private async void OnSfuSubscriberOffer(SubscriberOffer subscriberOffer)
10291110 {
10301111 return ;
10311112 }
1032-
1113+
10331114 //StreamTodo: handle subscriberOffer.iceRestart
10341115 var rtcSessionDescription = new RTCSessionDescription
10351116 {
@@ -1039,7 +1120,8 @@ private async void OnSfuSubscriberOffer(SubscriberOffer subscriberOffer)
10391120
10401121 try
10411122 {
1042- await Subscriber . SetRemoteDescriptionAsync ( rtcSessionDescription , GetCurrentCancellationTokenOrDefault ( ) ) ;
1123+ await Subscriber . SetRemoteDescriptionAsync ( rtcSessionDescription ,
1124+ GetCurrentCancellationTokenOrDefault ( ) ) ;
10431125 Subscriber . ThrowDisposedDuringOperationIfNull ( ) ;
10441126 }
10451127 catch ( Exception e )
@@ -1099,14 +1181,16 @@ private void OnSfuTrackUnpublished(TrackUnpublished trackUnpublished)
10991181 var sessionId = trackUnpublished . SessionId ;
11001182 var type = trackUnpublished . Type . ToPublicEnum ( ) ;
11011183 var cause = trackUnpublished . Cause ;
1102-
1184+
11031185 // StreamTODO: test if this works well with other user muting this user
1104- var updateLocalParticipantState = cause != TrackUnpublishReason . Unspecified && cause != TrackUnpublishReason . UserMuted ;
1186+ var updateLocalParticipantState
1187+ = cause != TrackUnpublishReason . Unspecified && cause != TrackUnpublishReason . UserMuted ;
11051188
11061189 // Optionally available. Read TrackUnpublished.participant comment in events.proto
11071190 var participantSfuDto = trackUnpublished . Participant ;
11081191
1109- UpdateParticipantTracksState ( userId , sessionId , type , isEnabled : false , updateLocalParticipantState , out var participant ) ;
1192+ UpdateParticipantTracksState ( userId , sessionId , type , isEnabled : false , updateLocalParticipantState ,
1193+ out var participant ) ;
11101194
11111195 if ( participantSfuDto != null && participant != null )
11121196 {
@@ -1127,7 +1211,8 @@ private void OnSfuTrackPublished(TrackPublished trackPublished)
11271211 // Optionally available. Read TrackUnpublished.participant comment in events.proto
11281212 var participantSfuDto = trackPublished . Participant ;
11291213
1130- UpdateParticipantTracksState ( userId , sessionId , type , isEnabled : true , updateLocalParticipantState : true , out var participant ) ;
1214+ UpdateParticipantTracksState ( userId , sessionId , type , isEnabled : true , updateLocalParticipantState : true ,
1215+ out var participant ) ;
11311216
11321217 if ( participantSfuDto != null && participant != null )
11331218 {
@@ -1498,7 +1583,7 @@ private async void OnPublisherNegotiationNeeded()
14981583 $ "{ nameof ( Publisher . SignalingState ) } state is not stable, current state: { Publisher . SignalingState } ") ;
14991584 }
15001585
1501- if ( GetCurrentCancellationTokenOrDefault ( ) . IsCancellationRequested )
1586+ if ( GetCurrentCancellationTokenOrDefault ( ) . IsCancellationRequested )
15021587 {
15031588 return ;
15041589 }
@@ -1885,7 +1970,7 @@ private void OnPublisherVideoTrackChanged(VideoStreamTrack videoTrack)
18851970 {
18861971 PublisherVideoTrackChanged ? . Invoke ( ) ;
18871972 }
1888-
1973+
18891974 void PublisherOnDisconnected ( )
18901975 {
18911976 if ( CallState == CallingState . Joined || CallState == CallingState . Joining )
@@ -1912,7 +1997,7 @@ private static bool AssertCallIdMatch(IStreamCall activeCall, string callId, ILo
19121997
19131998 return true ;
19141999 }
1915-
2000+
19162001
19172002 private void SubscribeToSfuEvents ( )
19182003 {
0 commit comments