Skip to content

Commit e123447

Browse files
Add a Bitrate Controller (#1347)
* fix stream retrieval bug RTC updates * add translations for tray app and installer * fix for ptz serialization use wyze camera name from url * fix bug in g722 codec * Only get stream format once Add timeout to ice gathering Add prefer H264 flag for compatible formats * revert * Optimise SendVideo and SendAudio (only get SendingFormat once) Add sanity check for ICE Gathering timeout * Optimise SendVideo and SendAudio (only get SendingFormat once) Add sanity check for ICE Gathering timeout Fix bug in g722 codec * remove turn folder * Remove turn folder * Move gather timeout to config * Fix rounding bug in SendAudioFrame Fix bug where duplicate durations were being added to local track timestamp in SendAudioFrame Ignore H264 formats that use unsupported packetization modes Clean up logic in AreMatch * remove comment marker * Check for null * Fix bug with HasVideo and HasAudio * Fix locking issue * fix lock * Fix stuttering on connect * Fix DtlsSrtpTransport bug * Fix issues with buffer over-runs * Add TWCC header extension support * Add support for TWCC Fix SDP parsing of audio m fields with ports Return null if stream not matched (was causing issues with RTP processing) Use PrimaryStream to unprotect incoming RTP packets Fix logic bug in SendRtpRaw Prevent some buffer over-reads var videoExtensions = new Dictionary<int, RTPHeaderExtension>(); videoExtensions.Add(extensionId, RTPHeaderExtension.GetRTPHeaderExtension(extensionId++, TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, SDPMediaTypesEnum.video)); MediaStreamTrack videoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, formats.Select(x => new SDPAudioVideoMediaFormat(x)).ToList(), MediaStreamStatusEnum.SendOnly, null, videoExtensions ); in OnReceiveReport access rr.TWCCFeedback * Use feedback type parser * Add notes * Fix limit check * Fix issues with parsing TWCC packet parsing * ignore invalid chunkType * remove console.writeline * Update src/net/RTCP/RTCPCompoundPacket.cs Co-authored-by: Paulo Morgado <[email protected]> * Update src/net/RTCP/RTCPCompoundPacket.cs Co-authored-by: Paulo Morgado <[email protected]> * Update for code review * remove announcement portcount (move to new PR) * Add SRTP fallback * bug fix * add controller class * add comments * move file --------- Co-authored-by: Paulo Morgado <[email protected]>
1 parent 829957a commit e123447

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

src/core/TWCCBitrateController.cs

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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

Comments
 (0)