|
| 1 | +/* |
| 2 | +* Filename: TWCCBitrateController.cs |
| 3 | +* |
| 4 | +* Description: |
| 5 | +* Uses TWCC Reports to adjust bitrates and framerates for video streams. |
| 6 | +* |
| 7 | +* Author: Sean Tearney |
| 8 | +* Date: 2025 - 03 - 05 |
| 9 | +* |
| 10 | +* License: BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. |
| 11 | +* |
| 12 | +* Change Log: |
| 13 | +* 2025-03-05 Initial creation. |
| 14 | +*/ |
| 15 | + |
| 16 | +using System; |
| 17 | +using SIPSorcery.Net; |
| 18 | +using SIPSorceryMedia.Abstractions; |
| 19 | + |
| 20 | +namespace SIPSorcery.core |
| 21 | +{ |
| 22 | + |
| 23 | + public class BitrateUpdateEventArgs : EventArgs |
| 24 | + { |
| 25 | + public int Bitrate { get; set; } |
| 26 | + public int Framerate { get; set; } |
| 27 | + |
| 28 | + public BitrateUpdateEventArgs(double bitrate, double framerate) |
| 29 | + { |
| 30 | + Bitrate = (int)bitrate; |
| 31 | + Framerate = (int)framerate; |
| 32 | + } |
| 33 | + } |
| 34 | + /// <summary> |
| 35 | + /// A controller that uses TWCC feedback reports |
| 36 | + /// to adjust the encoder bitrate based on measured network conditions |
| 37 | + /// over a rolling window using an exponential moving average (EMA). |
| 38 | + /// </summary> |
| 39 | + public class TWCCBitrateController |
| 40 | + { |
| 41 | + public event EventHandler<BitrateUpdateEventArgs> OnBitrateChange; |
| 42 | + private double _currentBitrate; // in bits per second |
| 43 | + private readonly double _minBitrate = 60000; |
| 44 | + private double _maxBitrate; |
| 45 | + |
| 46 | + // EMA for delay (µs) and loss rate (0 to 1). |
| 47 | + private double _rollingAvgDelay; |
| 48 | + private double _rollingLossRate; |
| 49 | + private bool _isFirstFeedback = true; |
| 50 | + |
| 51 | + // Smoothing factor for EMA. Smaller alpha => slower reaction. |
| 52 | + private const double Alpha = 0.1; |
| 53 | + |
| 54 | + // Interval (ms) at which we update the encoder bitrate. |
| 55 | + private const int UpdateIntervalMs = 1000; // 1 second |
| 56 | + |
| 57 | + private DateTime _lastUpdateTime; |
| 58 | + |
| 59 | + // For accumulating feedback in the current interval. |
| 60 | + private int _accumReceivedCount; |
| 61 | + private int _accumLostCount; |
| 62 | + private double _accumDelaySum; |
| 63 | + private double _maxFramerate; |
| 64 | + private double _minFramerate = 2; |
| 65 | + private double _framerate; |
| 66 | + private bool _inited = false; |
| 67 | + |
| 68 | + // Thread-safety lock if needed |
| 69 | + private readonly object _lock = new object(); |
| 70 | + |
| 71 | + |
| 72 | + public TWCCBitrateController() |
| 73 | + { |
| 74 | + _lastUpdateTime = DateTime.UtcNow; |
| 75 | + } |
| 76 | + |
| 77 | + /// <summary> |
| 78 | + /// Add a new feedback report. We accumulate the stats, |
| 79 | + /// but only update the actual bitrate on a fixed interval. |
| 80 | + /// </summary> |
| 81 | + public void ProcessFeedback(RTCPTWCCFeedback feedback) |
| 82 | + { |
| 83 | + if (feedback == null || !_inited) { return; } |
| 84 | + |
| 85 | + lock (_lock) |
| 86 | + { |
| 87 | + foreach (var ps in feedback.PacketStatuses) |
| 88 | + { |
| 89 | + switch (ps.Status) |
| 90 | + { |
| 91 | + case TWCCPacketStatusType.NotReceived: |
| 92 | + _accumLostCount++; |
| 93 | + break; |
| 94 | + case TWCCPacketStatusType.ReceivedSmallDelta: |
| 95 | + case TWCCPacketStatusType.ReceivedLargeDelta: |
| 96 | + if (ps.Delta.HasValue) |
| 97 | + { |
| 98 | + // Add the delay (µs). |
| 99 | + _accumDelaySum += Math.Abs(ps.Delta.Value); |
| 100 | + _accumReceivedCount++; |
| 101 | + } |
| 102 | + break; |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + var now = DateTime.UtcNow; |
| 107 | + double elapsedMs = (now - _lastUpdateTime).TotalMilliseconds; |
| 108 | + |
| 109 | + // Check if it's time to update the bitrate. |
| 110 | + if (elapsedMs >= UpdateIntervalMs) |
| 111 | + { |
| 112 | + // Compute average stats over this interval |
| 113 | + double intervalAvgDelay = 0.0; |
| 114 | + double intervalLossRate = 0.0; |
| 115 | + |
| 116 | + int totalPackets = _accumReceivedCount + _accumLostCount; |
| 117 | + if (totalPackets > 0) |
| 118 | + { |
| 119 | + intervalAvgDelay = _accumDelaySum / _accumReceivedCount; |
| 120 | + intervalLossRate = (double)_accumLostCount / totalPackets; |
| 121 | + } |
| 122 | + |
| 123 | + // Reset accumulators for the next interval. |
| 124 | + _accumDelaySum = 0.0; |
| 125 | + _accumReceivedCount = 0; |
| 126 | + _accumLostCount = 0; |
| 127 | + |
| 128 | + // Update rolling averages (EMA). |
| 129 | + if (_isFirstFeedback) |
| 130 | + { |
| 131 | + _rollingAvgDelay = intervalAvgDelay; |
| 132 | + _rollingLossRate = intervalLossRate; |
| 133 | + _isFirstFeedback = false; |
| 134 | + } |
| 135 | + else |
| 136 | + { |
| 137 | + _rollingAvgDelay = (1 - Alpha) * _rollingAvgDelay + Alpha * intervalAvgDelay; |
| 138 | + _rollingLossRate = (1 - Alpha) * _rollingLossRate + Alpha * intervalLossRate; |
| 139 | + } |
| 140 | + |
| 141 | + // Adjust the bitrate with a simple AIMD logic. |
| 142 | + // Example thresholds (tune to your environment!): |
| 143 | + const double highDelayThreshold = 100000.0; // 100 ms in microseconds |
| 144 | + const double superHighDelayThreshold = 200000.0; // 200 ms in microseconds |
| 145 | + const double lossThreshold = 0.05; // 5% loss |
| 146 | + const double superHighLossThreshold = 0.15; // 15% loss |
| 147 | + |
| 148 | + if (_rollingAvgDelay > highDelayThreshold || _rollingLossRate > lossThreshold) |
| 149 | + { |
| 150 | + if (_rollingAvgDelay > superHighDelayThreshold || _rollingLossRate > superHighLossThreshold) |
| 151 | + { |
| 152 | + // Super high delay: more drastic reduction. |
| 153 | + _currentBitrate *= 0.5; |
| 154 | + _framerate *= 0.5; |
| 155 | + } |
| 156 | + else |
| 157 | + { |
| 158 | + // Congestion: decrease bitrate by 15%. |
| 159 | + _currentBitrate *= 0.85; |
| 160 | + _framerate *= 0.85; |
| 161 | + } |
| 162 | + } |
| 163 | + else |
| 164 | + { |
| 165 | + // Good network: increase by 20 kbps. |
| 166 | + _currentBitrate += 20000; |
| 167 | + _framerate *= 1.1; |
| 168 | + } |
| 169 | + |
| 170 | + // Clamp |
| 171 | + _currentBitrate = Math.Max(_minBitrate, Math.Min(_currentBitrate, _maxBitrate)); |
| 172 | + _framerate = Math.Max(_minFramerate, Math.Min(_framerate, _maxFramerate)); |
| 173 | + |
| 174 | + // Update the encoder bitrate |
| 175 | + OnBitrateChange?.Invoke(this, new BitrateUpdateEventArgs(_currentBitrate, _framerate)); |
| 176 | + |
| 177 | + // Log for debugging |
| 178 | + //Debug.WriteLine( |
| 179 | + // $"[TWCCBitrateController] IntervalAvgDelay: {intervalAvgDelay:F1}µs, IntervalLossRate: {intervalLossRate:P1}, " + |
| 180 | + // $"RollingAvgDelay: {_rollingAvgDelay:F1}µs, RollingLossRate: {_rollingLossRate:P1}, MaxBitRate: {_maxBitrate:F0}, NewBitrate: {_currentBitrate:F0}bps"); |
| 181 | + |
| 182 | + _lastUpdateTime = now; |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + public void CalculateMaxBitrate(int width, int height, int framerate, VideoCodecsEnum codec) |
| 188 | + { |
| 189 | + // Base bitrates in kbps at 1fps |
| 190 | + int baseBitratePerFrame; |
| 191 | + |
| 192 | + if (codec == VideoCodecsEnum.H264) |
| 193 | + { |
| 194 | + if (width * height <= 352 * 288) |
| 195 | + { |
| 196 | + // CIF or smaller |
| 197 | + baseBitratePerFrame = 13; |
| 198 | + } |
| 199 | + else if (width * height <= 640 * 480) |
| 200 | + { |
| 201 | + // VGA |
| 202 | + baseBitratePerFrame = 35; |
| 203 | + } |
| 204 | + else if (width * height <= 1280 * 720) |
| 205 | + { |
| 206 | + // 720p |
| 207 | + baseBitratePerFrame = 87; |
| 208 | + } |
| 209 | + else if (width * height <= 1920 * 1080) |
| 210 | + { |
| 211 | + // 1080p |
| 212 | + baseBitratePerFrame = 173; |
| 213 | + } |
| 214 | + else |
| 215 | + { |
| 216 | + // 4K and above |
| 217 | + baseBitratePerFrame = 347; |
| 218 | + } |
| 219 | + } |
| 220 | + else |
| 221 | + { |
| 222 | + if (width * height <= 352 * 288) |
| 223 | + { |
| 224 | + // CIF or smaller |
| 225 | + baseBitratePerFrame = 10; |
| 226 | + } |
| 227 | + else if (width * height <= 640 * 480) |
| 228 | + { |
| 229 | + // VGA |
| 230 | + baseBitratePerFrame = 27; |
| 231 | + } |
| 232 | + else if (width * height <= 1280 * 720) |
| 233 | + { |
| 234 | + // 720p |
| 235 | + baseBitratePerFrame = 67; |
| 236 | + } |
| 237 | + else if (width * height <= 1920 * 1080) |
| 238 | + { |
| 239 | + // 1080p |
| 240 | + baseBitratePerFrame = 133; |
| 241 | + } |
| 242 | + else |
| 243 | + { |
| 244 | + // 4K and above |
| 245 | + baseBitratePerFrame = 267; |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + _maxBitrate = baseBitratePerFrame * framerate * 1000; // Convert to bps |
| 250 | + |
| 251 | + if (!_inited) |
| 252 | + { |
| 253 | + _currentBitrate = _maxBitrate/4; |
| 254 | + _maxFramerate = _framerate = framerate; |
| 255 | + _inited = true; |
| 256 | + } |
| 257 | + } |
| 258 | + } |
| 259 | +} |
0 commit comments