diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.10.0/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs b/Assets/Samples/Stream Video & Audio Chat SDK/0.10.0/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs index 333ceb35..6426ee18 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.10.0/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.10.0/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs @@ -97,6 +97,12 @@ private void FixVideoOrientation() // For local user, we don't have a video track, so we get the video rotation angle directly from WebCamTexture if (Participant != null && Participant.IsLocalParticipant && _video.texture is WebCamTexture sourceWebCamTexture) { + // WebCamTexture reports width=16 until fully initialized; reading videoRotationAngle before that logs a warning every frame + if (!sourceWebCamTexture.isPlaying || sourceWebCamTexture.width <= 16) + { + return; + } + _videoRectTransform.rotation = _baseVideoRotation * Quaternion.AngleAxis(-sourceWebCamTexture.videoRotationAngle, Vector3.forward); } } diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index c4aa112f..3fb2a3d0 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -2302,7 +2302,8 @@ private static bool TryParseSubscriberStreamId(string streamId, out string track trackTypeKey = null; var idParts = streamId.Split(':'); - if (idParts.Length != 2 || string.IsNullOrEmpty(idParts[0]) || string.IsNullOrEmpty(idParts[1])) + // SFU stream IDs are `{trackLookupPrefix}:{trackTypeKey}` with an optional track suffix, e.g. `:tR`. + if (idParts.Length < 2 || string.IsNullOrEmpty(idParts[0]) || string.IsNullOrEmpty(idParts[1])) { return false; } @@ -2638,12 +2639,24 @@ private async Task ClosePreviousSfuWebSocketAsync(ISfuWebSocket previousSfuWebSo private void OnPublisherAudioTrackChanged(AudioStreamTrack audioTrack) { + if (audioTrack != null && _publisherAudioTrackIsEnabled) + { + UpdateMuteStateAsync(TrackType.Audio, true).LogIfFailed(); + } + UpdateAudioRecording(); PublisherAudioTrackChanged?.Invoke(); } private void OnPublisherVideoTrackChanged(VideoStreamTrack videoTrack) { + // InternalExecuteSetPublisherVideoTrackEnabled runs before the track exists on first enable, + // so UpdateMuteStates must be sent once the publisher track is actually created. + if (videoTrack != null && _publisherVideoTrackIsEnabled) + { + UpdateMuteStateAsync(TrackType.Video, true).LogIfFailed(); + } + PublisherVideoTrackChanged?.Invoke(); } diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs index cd07958f..60ae7cae 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/StreamVideoCallParticipant.cs @@ -434,7 +434,13 @@ private void UploadLocalParticipantPublishedVideoRotationAngle() return; } - var angle = Client.InternalLowLevelClient.RtcSession.VideoInput.videoRotationAngle; + var videoInput = Client.InternalLowLevelClient.RtcSession.VideoInput; + if (!videoInput.isPlaying || videoInput.width <= 16) + { + return; + } + + var angle = videoInput.videoRotationAngle; var hasPrevAngle = CustomData.TryGet(VideoRotationAngleKey, out var prevAngle); if (!hasPrevAngle || Mathf.Abs(angle - prevAngle) > 0) diff --git a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs index 333ceb35..6426ee18 100644 --- a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs +++ b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs @@ -97,6 +97,12 @@ private void FixVideoOrientation() // For local user, we don't have a video track, so we get the video rotation angle directly from WebCamTexture if (Participant != null && Participant.IsLocalParticipant && _video.texture is WebCamTexture sourceWebCamTexture) { + // WebCamTexture reports width=16 until fully initialized; reading videoRotationAngle before that logs a warning every frame + if (!sourceWebCamTexture.isPlaying || sourceWebCamTexture.width <= 16) + { + return; + } + _videoRectTransform.rotation = _baseVideoRotation * Quaternion.AngleAxis(-sourceWebCamTexture.videoRotationAngle, Vector3.forward); } } diff --git a/Packages/StreamVideo/Tests/Editor/PendingRemoteTrackBufferTests.cs b/Packages/StreamVideo/Tests/Editor/PendingRemoteTrackBufferTests.cs index 3b884e9b..a1ac1ac5 100644 --- a/Packages/StreamVideo/Tests/Editor/PendingRemoteTrackBufferTests.cs +++ b/Packages/StreamVideo/Tests/Editor/PendingRemoteTrackBufferTests.cs @@ -181,6 +181,22 @@ private Task When_participant_already_has_prefix_expect_immediate_bind_Async() return Task.CompletedTask; } + [UnityTest] + public IEnumerator When_stream_id_has_track_suffix_expect_immediate_bind() + => When_stream_id_has_track_suffix_expect_immediate_bind_Async().RunAsIEnumerator(); + + private Task When_stream_id_has_track_suffix_expect_immediate_bind_Async() + { + SetupCallWithRemoteParticipant(trackLookupPrefix: TrackPrefix); + InvokeSubscriberStreamAdded(CreateMediaStream($"{TrackPrefix}:{VideoTrackTypeKey}:tR")); + + Assert.That(_session.BindAttempts, Has.Count.EqualTo(1), + "Stream IDs with an optional track suffix should still bind."); + Assert.That(_session.BindAttempts[0].TrackTypeKey, Is.EqualTo(VideoTrackTypeKey)); + Assert.That(GetPendingTrackCount(), Is.EqualTo(0)); + return Task.CompletedTask; + } + private StreamVideoCallParticipant SetupCallWithRemoteParticipant(string trackLookupPrefix) { var call = CreateCallWithRemoteParticipant(RemoteSessionId, trackLookupPrefix);