Skip to content

Commit 5dc533b

Browse files
authored
Merge pull request #189 from GetStream/feature/uni-140-add-support-for-controlling-track-subscriptions
Feature/uni 140 add support for controlling track subscriptions
2 parents 47871c2 + 40b793b commit 5dc533b

File tree

8 files changed

+178
-33
lines changed

8 files changed

+178
-33
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,7 @@ Assets/LocalOnlyTools_53854/
103103
Assets/LocalOnlyTools_53854.meta
104104
last_build_version_4983432740324322.txt
105105
last_build_version_4983432740324322.meta
106+
replace_imported_sample_scene_credentials.bat
107+
replace_imported_sample_scene_credentials.ps1
108+
replace_imported_sample_scene_credentials.sh
106109

Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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
{

Packages/StreamVideo/Runtime/Core/LowLevelClient/StreamPeerConnection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ public async Task<RTCSessionDescription> CreateAnswerAsync(CancellationToken can
223223
public void Update()
224224
{
225225
//StreamTodo: investigate if this Blit is necessary
226+
// One reason was to easy control target resolution -> we don't accept every target resolution because small res can crash Android video encoder
227+
// We should check if WebCamTexture allows setting any resolution
226228
if (_publisherVideoTrackTexture != null && _mediaInputProvider.VideoInput != null)
227229
{
228230
Graphics.Blit(_mediaInputProvider.VideoInput, _publisherVideoTrackTexture);
@@ -273,6 +275,11 @@ public void Dispose()
273275

274276
if (_publisherVideoTrackTexture != null)
275277
{
278+
// Unity gives warning when releasing an active texture
279+
if (RenderTexture.active == _publisherVideoTrackTexture)
280+
{
281+
RenderTexture.active = null;
282+
}
276283
_publisherVideoTrackTexture.Release();
277284
_publisherVideoTrackTexture = null;
278285
}

Packages/StreamVideo/Runtime/Core/StatefulModels/IStreamVideoCallParticipant.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,17 @@ public interface IStreamVideoCallParticipant : IStreamStatefulModel, IHasCustomD
8585
/// </summary>
8686
/// <param name="videoResolution">Video resolution you wish to receive for this participant. You can use a predefined size or pick a predefined one from <see cref="VideoResolution"/></param>
8787
void UpdateRequestedVideoResolution(VideoResolution videoResolution);
88+
89+
/// <summary>
90+
/// Should video track of this participant be received.
91+
/// </summary>
92+
/// <param name="enabled">If enabled, the video stream will be requested from the server</param>
93+
void SetIncomingVideoEnabled(bool enabled);
94+
95+
/// <summary>
96+
/// Should audio track of this participant be received
97+
/// </summary>
98+
/// <param name="enabled">If enabled, the audio stream will be requested from the server</param>
99+
void SetIncomingAudioEnabled(bool enabled);
88100
}
89101
}

Packages/StreamVideo/Runtime/Core/StatefulModels/StreamCall.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,9 @@ internal void UpdateFromSfu(JoinResponse joinResponse)
658658
internal void UpdateFromSfu(ParticipantJoined participantJoined, ICache cache)
659659
{
660660
var participant = Session.UpdateFromSfu(participantJoined, cache);
661+
LowLevelClient.RtcSession.NotifyParticipantJoined(participantJoined.Participant.SessionId);
661662
UpdateSortedParticipants();
663+
662664
ParticipantJoined?.Invoke(participant);
663665
}
664666

@@ -678,6 +680,8 @@ internal void UpdateFromSfu(ParticipantLeft participantLeft, ICache cache)
678680
Logs.Warning("Error when generating debug log: " + e.Message);
679681
}
680682

683+
LowLevelClient.RtcSession.NotifyParticipantLeft(participantLeft.Participant.SessionId);
684+
681685
var participant = Session.UpdateFromSfu(participantLeft, cache);
682686

683687
_localPinsSessionIds.RemoveAll(participant.sessionId);

Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ public IEnumerable<IStreamTrack> GetTracks()
9494
yield return _screenShareTrack;
9595
}
9696
}
97+
98+
public void SetIncomingVideoEnabled(bool enabled)
99+
=> LowLevelClient.RtcSession.UpdateIncomingVideoRequested(SessionId, enabled);
100+
101+
public void SetIncomingAudioEnabled(bool enabled)
102+
=> LowLevelClient.RtcSession.UpdateIncomingAudioRequested(SessionId, enabled);
97103

98104
public void UpdateRequestedVideoResolution(VideoResolution videoResolution)
99105
=> LowLevelClient.RtcSession.UpdateRequestedVideoResolution(SessionId, videoResolution);

0 commit comments

Comments
 (0)