From fe062df67f83e8603a1dea59adaa138392280279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:59:01 +0100 Subject: [PATCH 1/4] Implement handling SFU ChangePublishQuality --- .../LowLevelClient/PublisherPeerConnection.cs | 95 +++++++++++++++++++ .../Runtime/Core/LowLevelClient/RtcSession.cs | 13 ++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs index 2a41ff40..a3e6efb1 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs @@ -16,6 +16,7 @@ using StreamVideo.Libs.Utils; using StreamVideo.v1.Sfu.Models; using StreamVideo.v1.Sfu.Signal; +using SfuEvents = StreamVideo.v1.Sfu.Events; using Unity.WebRTC; using UnityEngine; using TrackType = StreamVideo.v1.Sfu.Models.TrackType; @@ -171,6 +172,100 @@ public override Task RestartIce() return Negotiate(iceRestart: true); } + /// + /// Apply quality changes requested by the SFU. Enables/disables simulcast layers and updates + /// encoding parameters (bitrate, framerate, resolution scale) to match current subscriber demands. + /// + public void ChangePublishQuality(SfuEvents.VideoSender videoSenderSettings) + { + if (videoSenderSettings.TrackType != TrackType.Video) + { + Logs.WarningIfDebug( + $"[{PeerType}] ChangePublishQuality: Ignoring unsupported track type: {videoSenderSettings.TrackType}"); + return; + } + + if (VideoSender == null) + { + Logs.WarningIfDebug($"[{PeerType}] ChangePublishQuality: VideoSender is null, skipping"); + return; + } + + var parameters = VideoSender.GetParameters(); + if (parameters.encodings == null || parameters.encodings.Length == 0) + { + Logs.WarningIfDebug( + $"[{PeerType}] ChangePublishQuality: No encodings found on video sender"); + return; + } + + var changed = false; + + foreach (var encoding in parameters.encodings) + { + var rid = string.IsNullOrEmpty(encoding.rid) ? "f" : encoding.rid; + + SfuEvents.VideoLayerSetting matchingLayer = null; + foreach (var layer in videoSenderSettings.Layers) + { + if (layer.Name == rid) + { + matchingLayer = layer; + break; + } + } + + var shouldBeActive = matchingLayer?.Active ?? false; + if (encoding.active != shouldBeActive) + { + encoding.active = shouldBeActive; + changed = true; + } + + if (matchingLayer == null || !matchingLayer.Active) + { + continue; + } + + if (matchingLayer.MaxBitrate > 0 && encoding.maxBitrate != (ulong)matchingLayer.MaxBitrate) + { + encoding.maxBitrate = (ulong)matchingLayer.MaxBitrate; + changed = true; + } + + if (matchingLayer.ScaleResolutionDownBy >= 1f + && encoding.scaleResolutionDownBy != (double)matchingLayer.ScaleResolutionDownBy) + { + encoding.scaleResolutionDownBy = (double)matchingLayer.ScaleResolutionDownBy; + changed = true; + } + + if (matchingLayer.MaxFramerate > 0 && encoding.maxFramerate != matchingLayer.MaxFramerate) + { + encoding.maxFramerate = matchingLayer.MaxFramerate; + changed = true; + } + } + + if (!changed) + { + Logs.InfoIfDebug($"[{PeerType}] ChangePublishQuality: No encoding changes needed"); + return; + } + + var error = VideoSender.SetParameters(parameters); + if (error.errorType != RTCErrorType.None) + { + Logs.Error( + $"[{PeerType}] ChangePublishQuality: Failed to set parameters: {error.errorType}"); + } + else + { + Logs.InfoIfDebug( + $"[{PeerType}] ChangePublishQuality: Successfully updated encoding parameters"); + } + } + private async Task Negotiate(bool iceRestart = false) { if (_isNegotiating) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index 145c89db..ca0db74a 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -1945,8 +1945,19 @@ private void OnSfuCallGrantsUpdated(CallGrantsUpdated callGrantsUpdated) private void OnSfuChangePublishQuality(ChangePublishQuality changePublishQuality) { + _logs.WarningIfDebug("---------- ChangePublishQuality -----------"); _sfuTracer?.Trace(PeerConnectionTraceKey.ChangePublishQuality, changePublishQuality); - // StreamTODO: Implement OnSfuChangePublishQuality + + if (Publisher == null) + { + _logs.WarningIfDebug("Received ChangePublishQuality but Publisher is null"); + return; + } + + foreach (var videoSender in changePublishQuality.VideoSenders) + { + Publisher.ChangePublishQuality(videoSender); + } } private void OnSfuConnectionQualityChanged(ConnectionQualityChanged connectionQualityChanged) From 2401a6622f7fd5b2dab58e4d2a5d8e28753b0dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:47:53 +0100 Subject: [PATCH 2/4] Add debug logs --- .../LowLevelClient/PublisherPeerConnection.cs | 18 +-- .../Core/Utils/PublishQualityDebugLogger.cs | 108 ++++++++++++++++++ .../Utils/PublishQualityDebugLogger.cs.meta | 11 ++ 3 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs create mode 100644 Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs.meta diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs index a3e6efb1..a4003c65 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs @@ -181,24 +181,26 @@ public void ChangePublishQuality(SfuEvents.VideoSender videoSenderSettings) if (videoSenderSettings.TrackType != TrackType.Video) { Logs.WarningIfDebug( - $"[{PeerType}] ChangePublishQuality: Ignoring unsupported track type: {videoSenderSettings.TrackType}"); + $"[{PeerType}] ChangePublishQuality: Ignoring track type: {videoSenderSettings.TrackType}"); return; } if (VideoSender == null) { - Logs.WarningIfDebug($"[{PeerType}] ChangePublishQuality: VideoSender is null, skipping"); + Logs.WarningIfDebug($"[{PeerType}] ChangePublishQuality: VideoSender null, skip"); return; } var parameters = VideoSender.GetParameters(); if (parameters.encodings == null || parameters.encodings.Length == 0) { - Logs.WarningIfDebug( - $"[{PeerType}] ChangePublishQuality: No encodings found on video sender"); + Logs.WarningIfDebug($"[{PeerType}] ChangePublishQuality: No encodings on sender"); return; } + PublishQualityDebugLogger.LogSfuRequest(Logs, PeerType.ToString(), videoSenderSettings); + PublishQualityDebugLogger.LogStateBefore(Logs, PeerType.ToString(), parameters); + var changed = false; foreach (var encoding in parameters.encodings) @@ -249,20 +251,18 @@ public void ChangePublishQuality(SfuEvents.VideoSender videoSenderSettings) if (!changed) { - Logs.InfoIfDebug($"[{PeerType}] ChangePublishQuality: No encoding changes needed"); + Logs.InfoIfDebug($"[{PeerType}] ChangePublishQuality: No changes needed"); return; } var error = VideoSender.SetParameters(parameters); if (error.errorType != RTCErrorType.None) { - Logs.Error( - $"[{PeerType}] ChangePublishQuality: Failed to set parameters: {error.errorType}"); + Logs.Error($"[{PeerType}] ChangePublishQuality: SetParameters FAILED: {error.errorType}"); } else { - Logs.InfoIfDebug( - $"[{PeerType}] ChangePublishQuality: Successfully updated encoding parameters"); + PublishQualityDebugLogger.LogStateAfter(Logs, PeerType.ToString(), parameters); } } diff --git a/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs b/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs new file mode 100644 index 00000000..51a93bb4 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs @@ -0,0 +1,108 @@ +using System.Text; +using StreamVideo.Libs.Logs; +using Unity.WebRTC; +using SfuEvents = StreamVideo.v1.Sfu.Events; + +namespace StreamVideo.Core.Utils +{ + /// + /// Compact debug logging for ChangePublishQuality events. + /// Log format per layer: "rid=ON/OFF br=300k fps=15 sc=4" + /// Abbreviations: br=bitrate, fps=framerate, sc=scaleResolutionDownBy + /// + internal static class PublishQualityDebugLogger + { + public static void LogSfuRequest(ILogs logs, string peerType, + SfuEvents.VideoSender videoSenderSettings) + { + var sb = new StringBuilder(); + sb.Append($"[{peerType}] ChangePublishQuality SFU req:"); + + foreach (var layer in videoSenderSettings.Layers) + { + sb.Append($" {layer.Name}="); + if (!layer.Active) + { + sb.Append("OFF |"); + continue; + } + + sb.Append("ON br="); + sb.Append(FormatBitrate(layer.MaxBitrate)); + sb.Append(" fps="); + sb.Append(layer.MaxFramerate); + sb.Append(" sc="); + sb.Append(layer.ScaleResolutionDownBy); + sb.Append(" |"); + } + + TrimTrailingSeparator(sb); + logs.Warning(sb.ToString()); + } + + public static void LogStateBefore(ILogs logs, string peerType, + RTCRtpSendParameters parameters) + { + logs.Warning($"[{peerType}] ChangePublishQuality before: {FormatEncodings(parameters)}"); + } + + public static void LogStateAfter(ILogs logs, string peerType, + RTCRtpSendParameters parameters) + { + logs.Warning($"[{peerType}] ChangePublishQuality after: {FormatEncodings(parameters)}"); + } + + private static string FormatEncodings(RTCRtpSendParameters parameters) + { + var sb = new StringBuilder(); + foreach (var encoding in parameters.encodings) + { + var rid = string.IsNullOrEmpty(encoding.rid) ? "f" : encoding.rid; + sb.Append($"{rid}="); + if (!encoding.active) + { + sb.Append("OFF | "); + continue; + } + + sb.Append("ON br="); + sb.Append(FormatBitrate(encoding.maxBitrate)); + sb.Append(" fps="); + sb.Append(encoding.maxFramerate?.ToString() ?? "?"); + sb.Append(" sc="); + sb.Append(encoding.scaleResolutionDownBy?.ToString("F0") ?? "?"); + sb.Append(" | "); + } + + if (sb.Length >= 3) + { + sb.Length -= 3; + } + + return sb.ToString(); + } + + private static string FormatBitrate(ulong? bps) + { + if (!bps.HasValue) return "?"; + return bps.Value >= 1_000_000 + ? $"{bps.Value / 1_000_000d:F1}m" + : $"{bps.Value / 1_000d:F0}k"; + } + + private static string FormatBitrate(int bps) + { + return bps >= 1_000_000 + ? $"{bps / 1_000_000d:F1}m" + : $"{bps / 1_000d:F0}k"; + } + + private static void TrimTrailingSeparator(StringBuilder sb) + { + if (sb.Length >= 2 && sb[sb.Length - 1] == '|') + { + sb.Length -= 2; + } + } + } +} diff --git a/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs.meta b/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs.meta new file mode 100644 index 00000000..6fcd66e1 --- /dev/null +++ b/Packages/StreamVideo/Runtime/Core/Utils/PublishQualityDebugLogger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba1bd0ab7df0bee4ea7824cad181a62b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From b734474c0a0874932042cec03ed7e7b2ec1a1afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:54:24 +0100 Subject: [PATCH 3/4] Fix double comparison --- .../Core/LowLevelClient/PublisherPeerConnection.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs index a4003c65..0821f9bc 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/PublisherPeerConnection.cs @@ -235,11 +235,15 @@ public void ChangePublishQuality(SfuEvents.VideoSender videoSenderSettings) changed = true; } - if (matchingLayer.ScaleResolutionDownBy >= 1f - && encoding.scaleResolutionDownBy != (double)matchingLayer.ScaleResolutionDownBy) + if (matchingLayer.ScaleResolutionDownBy >= 1f) { - encoding.scaleResolutionDownBy = (double)matchingLayer.ScaleResolutionDownBy; - changed = true; + var targetScale = (double)matchingLayer.ScaleResolutionDownBy; + if (encoding.scaleResolutionDownBy == null + || Math.Abs(encoding.scaleResolutionDownBy.Value - targetScale) > 0.001) + { + encoding.scaleResolutionDownBy = targetScale; + changed = true; + } } if (matchingLayer.MaxFramerate > 0 && encoding.maxFramerate != matchingLayer.MaxFramerate) From 097a1e7582f2d7c4751cd702f40c12e7dd5ee562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sierpi=C5=84ski?= <33436839+sierpinskid@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:58:06 +0100 Subject: [PATCH 4/4] Remove reduntant --- Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs index ca0db74a..0172e2b1 100644 --- a/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs +++ b/Packages/StreamVideo/Runtime/Core/LowLevelClient/RtcSession.cs @@ -1945,7 +1945,6 @@ private void OnSfuCallGrantsUpdated(CallGrantsUpdated callGrantsUpdated) private void OnSfuChangePublishQuality(ChangePublishQuality changePublishQuality) { - _logs.WarningIfDebug("---------- ChangePublishQuality -----------"); _sfuTracer?.Trace(PeerConnectionTraceKey.ChangePublishQuality, changePublishQuality); if (Publisher == null)