| @@ -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); | |||||
| } | |||||
| } | |||||
| } | |||||