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)