|
|
@@ -0,0 +1,249 @@ |
|
|
|
using Discord.Logging; |
|
|
|
using System; |
|
|
|
using System.Collections.Concurrent; |
|
|
|
using System.Threading; |
|
|
|
using System.Threading.Tasks; |
|
|
|
|
|
|
|
namespace Discord.Audio.Streams |
|
|
|
{ |
|
|
|
///<summary> Wraps another stream with a timed buffer and packet loss detection. </summary> |
|
|
|
public class JitterBuffer : AudioOutStream |
|
|
|
{ |
|
|
|
private struct Frame |
|
|
|
{ |
|
|
|
public Frame(byte[] buffer, int bytes, ushort sequence, uint timestamp) |
|
|
|
{ |
|
|
|
Buffer = buffer; |
|
|
|
Bytes = bytes; |
|
|
|
Sequence = sequence; |
|
|
|
Timestamp = timestamp; |
|
|
|
} |
|
|
|
|
|
|
|
public readonly byte[] Buffer; |
|
|
|
public readonly int Bytes; |
|
|
|
public readonly ushort Sequence; |
|
|
|
public readonly uint Timestamp; |
|
|
|
} |
|
|
|
|
|
|
|
private static readonly byte[] _silenceFrame = new byte[0]; |
|
|
|
|
|
|
|
private readonly AudioStream _next; |
|
|
|
private readonly CancellationTokenSource _cancelTokenSource; |
|
|
|
private readonly CancellationToken _cancelToken; |
|
|
|
private readonly Task _task; |
|
|
|
private readonly ConcurrentQueue<Frame> _queuedFrames; |
|
|
|
private readonly ConcurrentQueue<byte[]> _bufferPool; |
|
|
|
private readonly SemaphoreSlim _queueLock; |
|
|
|
private readonly Logger _logger; |
|
|
|
private readonly int _ticksPerFrame, _queueLength; |
|
|
|
private bool _isPreloaded, _hasHeader; |
|
|
|
|
|
|
|
private ushort _seq, _nextSeq; |
|
|
|
private uint _timestamp, _nextTimestamp; |
|
|
|
private bool _isFirst; |
|
|
|
|
|
|
|
public JitterBuffer(AudioStream next, int bufferMillis = 60, int maxFrameSize = 1500) |
|
|
|
: this(next, null, bufferMillis, maxFrameSize) { } |
|
|
|
internal JitterBuffer(AudioStream next, Logger logger, int bufferMillis = 60, int maxFrameSize = 1500) |
|
|
|
{ |
|
|
|
//maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms |
|
|
|
_next = next; |
|
|
|
_ticksPerFrame = OpusEncoder.FrameMillis; |
|
|
|
_logger = logger; |
|
|
|
_queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up |
|
|
|
|
|
|
|
_cancelTokenSource = new CancellationTokenSource(); |
|
|
|
_cancelToken = _cancelTokenSource.Token; |
|
|
|
_queuedFrames = new ConcurrentQueue<Frame>(); |
|
|
|
_bufferPool = new ConcurrentQueue<byte[]>(); |
|
|
|
for (int i = 0; i < _queueLength; i++) |
|
|
|
_bufferPool.Enqueue(new byte[maxFrameSize]); |
|
|
|
_queueLock = new SemaphoreSlim(_queueLength, _queueLength); |
|
|
|
|
|
|
|
_isFirst = true; |
|
|
|
_task = Run(); |
|
|
|
} |
|
|
|
protected override void Dispose(bool disposing) |
|
|
|
{ |
|
|
|
if (disposing) |
|
|
|
_cancelTokenSource.Cancel(); |
|
|
|
base.Dispose(disposing); |
|
|
|
} |
|
|
|
|
|
|
|
private Task Run() |
|
|
|
{ |
|
|
|
return Task.Run(async () => |
|
|
|
{ |
|
|
|
try |
|
|
|
{ |
|
|
|
long nextTick = Environment.TickCount; |
|
|
|
int silenceFrames = 0; |
|
|
|
while (!_cancelToken.IsCancellationRequested) |
|
|
|
{ |
|
|
|
long tick = Environment.TickCount; |
|
|
|
long dist = nextTick - tick; |
|
|
|
if (dist > 0) |
|
|
|
{ |
|
|
|
await Task.Delay((int)dist).ConfigureAwait(false); |
|
|
|
continue; |
|
|
|
} |
|
|
|
nextTick += _ticksPerFrame; |
|
|
|
if (!_isPreloaded) |
|
|
|
{ |
|
|
|
await Task.Delay(_ticksPerFrame).ConfigureAwait(false); |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
Frame frame; |
|
|
|
if (_queuedFrames.TryPeek(out frame)) |
|
|
|
{ |
|
|
|
silenceFrames = 0; |
|
|
|
uint distance = (uint)(frame.Timestamp - _timestamp); |
|
|
|
bool restartSeq = _isFirst; |
|
|
|
if (!_isFirst) |
|
|
|
{ |
|
|
|
if (distance > uint.MaxValue - (OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps |
|
|
|
{ |
|
|
|
_queuedFrames.TryDequeue(out frame); |
|
|
|
_bufferPool.Enqueue(frame.Buffer); |
|
|
|
_queueLock.Release(); |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Dropped frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); |
|
|
|
#endif |
|
|
|
continue; //This is a missed packet less than five seconds old, ignore it |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (distance == 0 || restartSeq) |
|
|
|
{ |
|
|
|
//This is the frame we expected |
|
|
|
_seq = frame.Sequence; |
|
|
|
_timestamp = frame.Timestamp; |
|
|
|
_isFirst = false; |
|
|
|
silenceFrames = 0; |
|
|
|
|
|
|
|
_next.WriteHeader(_seq++, _timestamp, false); |
|
|
|
await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); |
|
|
|
_queuedFrames.TryDequeue(out frame); |
|
|
|
_bufferPool.Enqueue(frame.Buffer); |
|
|
|
_queueLock.Release(); |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Read frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); |
|
|
|
#endif |
|
|
|
} |
|
|
|
else if (distance == OpusEncoder.FrameSamplesPerChannel) |
|
|
|
{ |
|
|
|
//Missed this frame, but the next queued one might have FEC info |
|
|
|
_next.WriteHeader(_seq++, _timestamp, true); |
|
|
|
await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Recreated Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); |
|
|
|
#endif |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Missed this frame and we have no FEC data to work with |
|
|
|
_next.WriteHeader(_seq++, _timestamp, true); |
|
|
|
await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); |
|
|
|
#endif |
|
|
|
} |
|
|
|
} |
|
|
|
else if (!_isFirst) |
|
|
|
{ |
|
|
|
//Missed this frame and we have no FEC data to work with |
|
|
|
_next.WriteHeader(_seq++, _timestamp, true); |
|
|
|
await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); |
|
|
|
if (silenceFrames < 5) |
|
|
|
silenceFrames++; |
|
|
|
else |
|
|
|
{ |
|
|
|
_isFirst = true; |
|
|
|
_isPreloaded = false; |
|
|
|
} |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); |
|
|
|
#endif |
|
|
|
} |
|
|
|
_timestamp += OpusEncoder.FrameSamplesPerChannel; |
|
|
|
} |
|
|
|
} |
|
|
|
catch (OperationCanceledException) { } |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
public override void WriteHeader(ushort seq, uint timestamp, bool missed) |
|
|
|
{ |
|
|
|
if (_hasHeader) |
|
|
|
throw new InvalidOperationException("Header received with no payload"); |
|
|
|
_nextSeq = seq; |
|
|
|
_nextTimestamp = timestamp; |
|
|
|
_hasHeader = true; |
|
|
|
} |
|
|
|
public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) |
|
|
|
{ |
|
|
|
if (cancelToken.CanBeCanceled) |
|
|
|
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; |
|
|
|
else |
|
|
|
cancelToken = _cancelToken; |
|
|
|
|
|
|
|
if (!_hasHeader) |
|
|
|
throw new InvalidOperationException("Received payload without an RTP header"); |
|
|
|
_hasHeader = false; |
|
|
|
|
|
|
|
uint distance = (uint)(_nextTimestamp - _timestamp); |
|
|
|
if (!_isFirst && (distance == 0 || distance > OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps |
|
|
|
{ |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Frame {_nextTimestamp} was {distance} samples off. Ignoring."); |
|
|
|
#endif |
|
|
|
return; //This is an old frame, ignore |
|
|
|
} |
|
|
|
|
|
|
|
byte[] buffer; |
|
|
|
if (!await _queueLock.WaitAsync(0).ConfigureAwait(false)) |
|
|
|
{ |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Buffer overflow"); |
|
|
|
#endif |
|
|
|
return; |
|
|
|
} |
|
|
|
_bufferPool.TryDequeue(out buffer); |
|
|
|
|
|
|
|
Buffer.BlockCopy(data, offset, buffer, 0, count); |
|
|
|
#if DEBUG |
|
|
|
{ |
|
|
|
var _ = _logger?.DebugAsync($"Queued Frame {_nextTimestamp}."); |
|
|
|
} |
|
|
|
#endif |
|
|
|
_queuedFrames.Enqueue(new Frame(buffer, count, _nextSeq, _nextTimestamp)); |
|
|
|
if (!_isPreloaded && _queuedFrames.Count >= _queueLength) |
|
|
|
{ |
|
|
|
#if DEBUG |
|
|
|
var _ = _logger?.DebugAsync($"Preloaded"); |
|
|
|
#endif |
|
|
|
_isPreloaded = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public override async Task FlushAsync(CancellationToken cancelToken) |
|
|
|
{ |
|
|
|
while (true) |
|
|
|
{ |
|
|
|
cancelToken.ThrowIfCancellationRequested(); |
|
|
|
if (_queuedFrames.Count == 0) |
|
|
|
return; |
|
|
|
await Task.Delay(250, cancelToken).ConfigureAwait(false); |
|
|
|
} |
|
|
|
} |
|
|
|
public override Task ClearAsync(CancellationToken cancelToken) |
|
|
|
{ |
|
|
|
Frame ignored; |
|
|
|
do |
|
|
|
cancelToken.ThrowIfCancellationRequested(); |
|
|
|
while (_queuedFrames.TryDequeue(out ignored)); |
|
|
|
return Task.Delay(0); |
|
|
|
} |
|
|
|
} |
|
|
|
} |