Added a limit to BufferedAudioTarget's internal buffer.
This commit is contained in:
@@ -22,7 +22,7 @@ namespace Discord.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Stream CreateOpusStream(int samplesPerFrame);
|
Stream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.
|
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,7 +35,7 @@ namespace Discord.Audio
|
|||||||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
|
||||||
/// <param name="bitrate"></param>
|
/// <param name="bitrate"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null);
|
Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.
|
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Discord.Audio
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
public class AudioClient : IAudioClient, IDisposable
|
internal class AudioClient : IAudioClient, IDisposable
|
||||||
{
|
{
|
||||||
public event Func<Task> Connected
|
public event Func<Task> Connected
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,7 @@ namespace Discord.Audio
|
|||||||
|
|
||||||
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
||||||
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false);
|
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false);
|
||||||
ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
|
//ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
|
||||||
ApiClient.ReceivedEvent += ProcessMessageAsync;
|
ApiClient.ReceivedEvent += ProcessMessageAsync;
|
||||||
ApiClient.ReceivedPacket += ProcessPacketAsync;
|
ApiClient.ReceivedPacket += ProcessPacketAsync;
|
||||||
ApiClient.Disconnected += async ex =>
|
ApiClient.Disconnected += async ex =>
|
||||||
@@ -170,10 +170,10 @@ namespace Discord.Audio
|
|||||||
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false);
|
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream CreateOpusStream(int samplesPerFrame)
|
public Stream CreateOpusStream(int samplesPerFrame, int bufferMillis)
|
||||||
{
|
{
|
||||||
CheckSamplesPerFrame(samplesPerFrame);
|
CheckSamplesPerFrame(samplesPerFrame);
|
||||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token);
|
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token);
|
||||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
||||||
}
|
}
|
||||||
public Stream CreateDirectOpusStream(int samplesPerFrame)
|
public Stream CreateDirectOpusStream(int samplesPerFrame)
|
||||||
@@ -182,13 +182,13 @@ namespace Discord.Audio
|
|||||||
var target = new DirectAudioTarget(ApiClient);
|
var target = new DirectAudioTarget(ApiClient);
|
||||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
||||||
}
|
}
|
||||||
public Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null)
|
public Stream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
|
||||||
{
|
{
|
||||||
CheckSamplesPerFrame(samplesPerFrame);
|
CheckSamplesPerFrame(samplesPerFrame);
|
||||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token);
|
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token);
|
||||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
||||||
}
|
}
|
||||||
public Stream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null)
|
public Stream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate)
|
||||||
{
|
{
|
||||||
CheckSamplesPerFrame(samplesPerFrame);
|
CheckSamplesPerFrame(samplesPerFrame);
|
||||||
var target = new DirectAudioTarget(ApiClient);
|
var target = new DirectAudioTarget(ApiClient);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -8,62 +7,98 @@ namespace Discord.Audio
|
|||||||
{
|
{
|
||||||
internal class BufferedAudioTarget : IAudioTarget, IDisposable
|
internal class BufferedAudioTarget : IAudioTarget, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly byte[] _silencePacket = new byte[] { 0xF8, 0xFF, 0xFE };
|
private struct Frame
|
||||||
|
{
|
||||||
|
public Frame(byte[] buffer, int bytes)
|
||||||
|
{
|
||||||
|
Buffer = buffer;
|
||||||
|
Bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly byte[] Buffer;
|
||||||
|
public readonly int Bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly byte[] _silenceFrame = new byte[] { 0xF8, 0xFF, 0xFE };
|
||||||
|
|
||||||
private Task _task;
|
private Task _task;
|
||||||
private DiscordVoiceAPIClient _client;
|
private DiscordVoiceAPIClient _client;
|
||||||
private CancellationTokenSource _cancelTokenSource;
|
private CancellationTokenSource _cancelTokenSource;
|
||||||
private ConcurrentQueue<byte[]> _queue;
|
private CancellationToken _cancelToken;
|
||||||
|
private ConcurrentQueue<Frame> _queuedFrames;
|
||||||
|
private ConcurrentQueue<byte[]> _bufferPool;
|
||||||
|
private SemaphoreSlim _queueLock;
|
||||||
|
private int _ticksPerFrame;
|
||||||
|
|
||||||
internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, CancellationToken cancelToken)
|
internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
long ticksPerFrame = samplesPerFrame / 48;
|
_ticksPerFrame = samplesPerFrame / 48;
|
||||||
|
int queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up
|
||||||
|
|
||||||
_cancelTokenSource = new CancellationTokenSource();
|
_cancelTokenSource = new CancellationTokenSource();
|
||||||
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
|
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
|
||||||
_queue = new ConcurrentQueue<byte[]>(); //TODO: We need a better queue
|
_queuedFrames = new ConcurrentQueue<Frame>();
|
||||||
|
_bufferPool = new ConcurrentQueue<byte[]>();
|
||||||
|
for (int i = 0; i < queueLength; i++)
|
||||||
|
_bufferPool.Enqueue(new byte[1275]);
|
||||||
|
_queueLock = new SemaphoreSlim(queueLength, queueLength);
|
||||||
|
|
||||||
_task = Run(ticksPerFrame, cancelToken);
|
_task = Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task Run(long ticksPerFrame, CancellationToken cancelToken)
|
private Task Run()
|
||||||
{
|
{
|
||||||
return Task.Run(async () =>
|
return Task.Run(async () =>
|
||||||
{
|
{
|
||||||
long nextTick = Environment.TickCount;
|
try
|
||||||
while (!cancelToken.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
long tick = Environment.TickCount;
|
long nextTick = Environment.TickCount;
|
||||||
long dist = nextTick - tick;
|
while (!_cancelToken.IsCancellationRequested)
|
||||||
if (dist <= 0)
|
|
||||||
{
|
{
|
||||||
byte[] buffer;
|
long tick = Environment.TickCount;
|
||||||
if (_queue.TryDequeue(out buffer))
|
long dist = nextTick - tick;
|
||||||
await _client.SendAsync(buffer, buffer.Length).ConfigureAwait(false);
|
if (dist <= 0)
|
||||||
else
|
{
|
||||||
await _client.SendAsync(_silencePacket, _silencePacket.Length).ConfigureAwait(false);
|
Frame frame;
|
||||||
nextTick += ticksPerFrame;
|
if (_queuedFrames.TryDequeue(out frame))
|
||||||
|
{
|
||||||
|
#if NETSTANDARD1_3
|
||||||
|
Console.WriteLine("Pop");
|
||||||
|
#endif
|
||||||
|
await _client.SendAsync(frame.Buffer, frame.Bytes).ConfigureAwait(false);
|
||||||
|
_bufferPool.Enqueue(frame.Buffer);
|
||||||
|
_queueLock.Release();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await _client.SendAsync(_silenceFrame, _silenceFrame.Length).ConfigureAwait(false);
|
||||||
|
nextTick += _ticksPerFrame;
|
||||||
|
}
|
||||||
|
else if (dist > 1)
|
||||||
|
await Task.Delay((int)dist).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (dist > 1)
|
|
||||||
await Task.Delay((int)dist).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendAsync(byte[] buffer, int count)
|
public async Task SendAsync(byte[] data, int count)
|
||||||
{
|
{
|
||||||
byte[] newBuffer = new byte[count];
|
await _queueLock.WaitAsync(-1, _cancelToken).ConfigureAwait(false);
|
||||||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, count);
|
#if NETSTANDARD1_3
|
||||||
_queue.Enqueue(newBuffer);
|
Console.WriteLine("Push");
|
||||||
return Task.Delay(0);
|
#endif
|
||||||
|
byte[] buffer;
|
||||||
|
_bufferPool.TryDequeue(out buffer);
|
||||||
|
Buffer.BlockCopy(data, 0, buffer, 0, count);
|
||||||
|
_queuedFrames.Enqueue(new Frame(buffer, count));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FlushAsync()
|
public async Task FlushAsync()
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_queue.Count == 0)
|
if (_queuedFrames.Count == 0)
|
||||||
return;
|
return;
|
||||||
await Task.Delay(250).ConfigureAwait(false);
|
await Task.Delay(250).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace Discord.WebSocket
|
|||||||
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
|
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
|
||||||
private ImmutableArray<GuildEmoji> _emojis;
|
private ImmutableArray<GuildEmoji> _emojis;
|
||||||
private ImmutableArray<string> _features;
|
private ImmutableArray<string> _features;
|
||||||
|
private AudioClient _audioClient;
|
||||||
internal bool _available;
|
internal bool _available;
|
||||||
|
|
||||||
public string Name { get; private set; }
|
public string Name { get; private set; }
|
||||||
@@ -42,7 +43,6 @@ namespace Discord.WebSocket
|
|||||||
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
|
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; }
|
||||||
public int MemberCount { get; set; }
|
public int MemberCount { get; set; }
|
||||||
public int DownloadedMemberCount { get; private set; }
|
public int DownloadedMemberCount { get; private set; }
|
||||||
public AudioClient AudioClient { get; private set; }
|
|
||||||
|
|
||||||
public ulong? AFKChannelId { get; private set; }
|
public ulong? AFKChannelId { get; private set; }
|
||||||
public ulong? EmbedChannelId { get; private set; }
|
public ulong? EmbedChannelId { get; private set; }
|
||||||
@@ -59,6 +59,7 @@ namespace Discord.WebSocket
|
|||||||
public bool IsSynced => _syncPromise.Task.IsCompleted;
|
public bool IsSynced => _syncPromise.Task.IsCompleted;
|
||||||
public Task SyncPromise => _syncPromise.Task;
|
public Task SyncPromise => _syncPromise.Task;
|
||||||
public Task DownloaderPromise => _downloaderPromise.Task;
|
public Task DownloaderPromise => _downloaderPromise.Task;
|
||||||
|
public IAudioClient AudioClient => _audioClient;
|
||||||
public SocketGuildUser CurrentUser
|
public SocketGuildUser CurrentUser
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -69,7 +70,6 @@ namespace Discord.WebSocket
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SocketRole EveryoneRole => GetRole(Id);
|
public SocketRole EveryoneRole => GetRole(Id);
|
||||||
public IReadOnlyCollection<SocketGuildChannel> Channels
|
public IReadOnlyCollection<SocketGuildChannel> Channels
|
||||||
{
|
{
|
||||||
@@ -476,9 +476,9 @@ namespace Discord.WebSocket
|
|||||||
{
|
{
|
||||||
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
|
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
|
||||||
_audioConnectPromise = null;
|
_audioConnectPromise = null;
|
||||||
if (AudioClient != null)
|
if (_audioClient != null)
|
||||||
await AudioClient.DisconnectAsync().ConfigureAwait(false);
|
await _audioClient.DisconnectAsync().ConfigureAwait(false);
|
||||||
AudioClient = null;
|
_audioClient = null;
|
||||||
}
|
}
|
||||||
internal async Task FinishConnectAudio(int id, string url, string token)
|
internal async Task FinishConnectAudio(int id, string url, string token)
|
||||||
{
|
{
|
||||||
@@ -487,7 +487,7 @@ namespace Discord.WebSocket
|
|||||||
await _audioLock.WaitAsync().ConfigureAwait(false);
|
await _audioLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (AudioClient == null)
|
if (_audioClient == null)
|
||||||
{
|
{
|
||||||
var audioClient = new AudioClient(this, id);
|
var audioClient = new AudioClient(this, id);
|
||||||
var promise = _audioConnectPromise;
|
var promise = _audioConnectPromise;
|
||||||
@@ -497,7 +497,7 @@ namespace Discord.WebSocket
|
|||||||
if (!promise.Task.IsCompleted)
|
if (!promise.Task.IsCompleted)
|
||||||
{
|
{
|
||||||
try { audioClient.Dispose(); } catch { }
|
try { audioClient.Dispose(); } catch { }
|
||||||
AudioClient = null;
|
_audioClient = null;
|
||||||
if (ex != null)
|
if (ex != null)
|
||||||
await promise.TrySetExceptionAsync(ex);
|
await promise.TrySetExceptionAsync(ex);
|
||||||
else
|
else
|
||||||
@@ -535,10 +535,10 @@ namespace Discord.WebSocket
|
|||||||
_audioLock.Release();
|
_audioLock.Release();
|
||||||
}*/
|
}*/
|
||||||
};
|
};
|
||||||
AudioClient = audioClient;
|
_audioClient = audioClient;
|
||||||
}
|
}
|
||||||
await AudioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
|
await _audioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
|
||||||
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false);
|
await _audioConnectPromise.TrySetResultAsync(_audioClient).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user