diff --git a/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj b/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj
index 30f43ad8b..d613a1650 100644
--- a/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj
+++ b/src/Discord.Net.Audio.Net5/Discord.Net.Audio.csproj
@@ -45,6 +45,9 @@
+
+ AudioClient.cs
+
AudioExtensions.cs
@@ -54,8 +57,8 @@
AudioServiceConfig.cs
-
- DiscordAudioClient.cs
+
+ IAudioClient.cs
Net\WebSockets\VoiceWebSocket.cs
@@ -72,6 +75,9 @@
Opus\OpusEncoder.cs
+
+ SimpleAudioClient.cs
+
Sodium.cs
diff --git a/src/Discord.Net.Audio/DiscordAudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs
similarity index 54%
rename from src/Discord.Net.Audio/DiscordAudioClient.cs
rename to src/Discord.Net.Audio/AudioClient.cs
index a3cde89e3..1566a3f99 100644
--- a/src/Discord.Net.Audio/DiscordAudioClient.cs
+++ b/src/Discord.Net.Audio/AudioClient.cs
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace Discord.Audio
{
- public partial class DiscordAudioClient
+ internal class AudioClient : IAudioClient
{
private readonly Semaphore _connectionLock;
private readonly JsonSerializer _serializer;
@@ -20,19 +20,19 @@ namespace Discord.Audio
public GatewaySocket GatewaySocket { get; }
public VoiceWebSocket VoiceSocket { get; }
- public ulong? ServerId => VoiceSocket.ServerId;
- public ulong? ChannelId => VoiceSocket.ChannelId;
public ConnectionState State => VoiceSocket.State;
+ public Server Server => VoiceSocket.Server;
+ public Channel Channel => VoiceSocket.Channel;
- public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket)
+ public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, Logger logger)
{
Service = service;
- Id = id;
- Logger = logger;
+ Id = clientId;
GatewaySocket = gatewaySocket;
-
- _connectionLock = new Semaphore(1, 1);
-
+ Logger = logger;
+
+ _connectionLock = new Semaphore(1, 1);
+
_serializer = new JsonSerializer();
_serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
_serializer.Error += (s, e) =>
@@ -41,7 +41,10 @@ namespace Discord.Audio
Logger.Error("Serialization Failed", e.ErrorContext.Error);
};
- VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger);
+ GatewaySocket.ReceivedDispatch += OnReceivedDispatch;
+
+ VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger);
+ VoiceSocket.Server = server;
/*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected();
_voiceSocket.Disconnected += async (s, e) =>
@@ -76,58 +79,54 @@ namespace Discord.Audio
{
_voiceSocket.ParentCancelToken = _cancelToken;
};*/
-
}
-
- internal async Task SetServer(ulong serverId)
- {
- if (serverId != VoiceSocket.ServerId)
- {
- await Disconnect().ConfigureAwait(false);
- VoiceSocket.ServerId = serverId;
- VoiceSocket.ChannelId = null;
- SendVoiceUpdate();
- }
- }
- public Task JoinChannel(Channel channel)
+ public async Task Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
- var serverId = channel.Server?.Id;
- var channelId = channel.Id;
- if (serverId != ServerId)
- throw new InvalidOperationException("Cannot join a channel on a different server than this voice client.");
- if (channelId == VoiceSocket.ChannelId)
- return TaskHelper.CompletedTask;
- //CheckReady(checkVoice: true);
-
- return Task.Run(async () =>
- {
- _connectionLock.WaitOne();
- GatewaySocket.ReceivedDispatch += OnReceivedDispatch;
- try
- {
- if (State != ConnectionState.Disconnected)
- await Disconnect().ConfigureAwait(false);
+ if (channel.Type != ChannelType.Voice)
+ throw new ArgumentException("Channel must be a voice channel.", nameof(channel));
+ if (channel.Server != VoiceSocket.Server)
+ throw new ArgumentException("This is channel is not part of the current server.", nameof(channel));
+ if (channel == VoiceSocket.Channel) return;
+ if (VoiceSocket.Server == null)
+ throw new InvalidOperationException("This client has been closed.");
- _cancelTokenSource = new CancellationTokenSource();
- var cancelToken = _cancelTokenSource.Token;
- VoiceSocket.ParentCancelToken = cancelToken;
+ _connectionLock.WaitOne();
+ try
+ {
+ _cancelTokenSource = new CancellationTokenSource();
+ var cancelToken = _cancelTokenSource.Token;
+ VoiceSocket.ParentCancelToken = cancelToken;
+ VoiceSocket.Channel = channel;
- VoiceSocket.ChannelId = channelId;
+ await Task.Run(() =>
+ {
SendVoiceUpdate();
-
VoiceSocket.WaitForConnection(cancelToken);
- }
- finally
- {
- GatewaySocket.ReceivedDispatch -= OnReceivedDispatch;
- _connectionLock.Release();
- }
- });
+ });
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+
+ public async Task Disconnect()
+ {
+ _connectionLock.WaitOne();
+ try
+ {
+ Service.RemoveClient(VoiceSocket.Server, this);
+ VoiceSocket.Channel = null;
+ SendVoiceUpdate();
+ await VoiceSocket.Disconnect();
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
}
- public Task Disconnect()
- => VoiceSocket.Disconnect();
private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e)
{
@@ -135,12 +134,31 @@ namespace Discord.Audio
{
switch (e.Type)
{
+ case "VOICE_STATE_UPDATE":
+ {
+ var data = e.Payload.ToObject(_serializer);
+ if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id)
+ {
+ if (data.ChannelId == null)
+ await Disconnect();
+ else
+ {
+ var channel = Service.Client.GetChannel(data.ChannelId.Value);
+ if (channel != null)
+ VoiceSocket.Channel = channel;
+ else
+ {
+ Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting.");
+ await Disconnect();
+ }
+ }
+ }
+ }
+ break;
case "VOICE_SERVER_UPDATE":
{
var data = e.Payload.ToObject(_serializer);
- var serverId = data.GuildId;
-
- if (serverId == ServerId)
+ if (data.GuildId == VoiceSocket.Server?.Id)
{
var client = Service.Client;
VoiceSocket.Token = data.Token;
@@ -164,34 +182,32 @@ namespace Discord.Audio
{
if (data == null) throw new ArgumentException(nameof(data));
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
- //CheckReady(checkVoice: true);
+ if (VoiceSocket.Server == null) return; //Has been closed
- if (count != 0)
+ if (count != 0)
VoiceSocket.SendPCMFrames(data, count);
}
- /// Clears the PCM buffer.
- public void Clear()
- {
- //CheckReady(checkVoice: true);
-
- VoiceSocket.ClearPCMFrames();
- }
+ /// Clears the PCM buffer.
+ public void Clear()
+ {
+ if (VoiceSocket.Server == null) return; //Has been closed
+ VoiceSocket.ClearPCMFrames();
+ }
/// Returns a task that completes once the voice output buffer is empty.
public void Wait()
- {
- //CheckReady(checkVoice: true);
-
- VoiceSocket.WaitForQueue();
+ {
+ if (VoiceSocket.Server == null) return; //Has been closed
+ VoiceSocket.WaitForQueue();
}
private void SendVoiceUpdate()
{
- var serverId = VoiceSocket.ServerId;
+ var serverId = VoiceSocket.Server?.Id;
if (serverId != null)
{
- GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.ChannelId,
+ GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.Channel?.Id,
(Service.Config.Mode | AudioMode.Outgoing) == 0,
(Service.Config.Mode | AudioMode.Incoming) == 0);
}
diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs
index b9f1241be..fcd2a43b7 100644
--- a/src/Discord.Net.Audio/AudioService.cs
+++ b/src/Discord.Net.Audio/AudioService.cs
@@ -46,8 +46,8 @@ namespace Discord.Audio
public class AudioService : IService
{
- private DiscordAudioClient _defaultClient;
- private ConcurrentDictionary _voiceClients;
+ private AudioClient _defaultClient;
+ private ConcurrentDictionary _voiceClients;
private ConcurrentDictionary _talkingUsers;
//private int _nextClientId;
@@ -91,11 +91,11 @@ namespace Discord.Audio
{
_client = client;
if (Config.EnableMultiserver)
- _voiceClients = new ConcurrentDictionary();
+ _voiceClients = new ConcurrentDictionary();
else
{
var logger = Client.Log.CreateLogger("Voice");
- _defaultClient = new DiscordAudioClient(this, 0, logger, _client.GatewaySocket);
+ _defaultClient = new SimpleAudioClient(this, 0, logger);
}
_talkingUsers = new ConcurrentDictionary();
@@ -118,34 +118,29 @@ namespace Discord.Audio
};
}
- public DiscordAudioClient GetClient(Server server)
+ public IAudioClient GetClient(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
- if (!Config.EnableMultiserver)
- {
- if (server.Id == _defaultClient.ServerId)
- return _defaultClient;
- else
- return null;
- }
-
- DiscordAudioClient client;
- if (_voiceClients.TryGetValue(server.Id, out client))
- return client;
- else
- return null;
+ if (!Config.EnableMultiserver)
+ {
+ if (server == _defaultClient.Server)
+ return (_defaultClient as SimpleAudioClient).CurrentClient;
+ else
+ return null;
+ }
+ else
+ {
+ IAudioClient client;
+ if (_voiceClients.TryGetValue(server.Id, out client))
+ return client;
+ else
+ return null;
+ }
}
- private async Task CreateClient(Server server)
+ private Task CreateClient(Server server)
{
- if (!Config.EnableMultiserver)
- {
- await _defaultClient.SetServer(server.Id);
- return _defaultClient;
- }
- else
- throw new InvalidOperationException("Multiserver voice is not currently supported");
-
+ throw new NotImplementedException();
/*var client = _voiceClients.GetOrAdd(server.Id, _ =>
{
int id = unchecked(++_nextClientId);
@@ -169,30 +164,44 @@ namespace Discord.Audio
return Task.FromResult(client);*/
}
- public async Task Join(Channel channel)
+ //TODO: This isn't threadsafe
+ internal void RemoveClient(Server server, IAudioClient client)
+ {
+ if (Config.EnableMultiserver && server != null)
+ _voiceClients.TryRemove(server.Id, out client);
+ }
+
+ public async Task Join(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
- //CheckReady(true);
- var client = await CreateClient(channel.Server).ConfigureAwait(false);
- await client.JoinChannel(channel).ConfigureAwait(false);
+ IAudioClient client;
+ if (!Config.EnableMultiserver)
+ client = await (_defaultClient as SimpleAudioClient).Connect(channel).ConfigureAwait(false);
+ else
+ {
+ client = await CreateClient(channel.Server).ConfigureAwait(false);
+ await client.Join(channel).ConfigureAwait(false);
+ }
return client;
}
public async Task Leave(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
- //CheckReady(true);
- if (Config.EnableMultiserver)
- {
- //client.CheckReady();
- DiscordAudioClient client;
- if (_voiceClients.TryRemove(server.Id, out client))
- await client.Disconnect().ConfigureAwait(false);
- }
- else
- await _defaultClient.Disconnect().ConfigureAwait(false);
+ if (Config.EnableMultiserver)
+ {
+ IAudioClient client;
+ if (_voiceClients.TryRemove(server.Id, out client))
+ await client.Disconnect().ConfigureAwait(false);
+ }
+ else
+ {
+ IAudioClient client = GetClient(server);
+ if (client != null)
+ await (_defaultClient as SimpleAudioClient).Leave(client as SimpleAudioClient.VirtualClient).ConfigureAwait(false);
+ }
}
}
}
diff --git a/src/Discord.Net.Audio/IAudioClient.cs b/src/Discord.Net.Audio/IAudioClient.cs
new file mode 100644
index 000000000..6d2ec8dcd
--- /dev/null
+++ b/src/Discord.Net.Audio/IAudioClient.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+
+namespace Discord.Audio
+{
+ public interface IAudioClient
+ {
+ ConnectionState State { get; }
+ Channel Channel { get; }
+ Server Server { get; }
+
+ Task Join(Channel channel);
+ Task Disconnect();
+
+ /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer.
+ /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames.
+ /// Number of bytes in this frame.
+ void Send(byte[] data, int count);
+ /// Clears the PCM buffer.
+ void Clear();
+ /// Blocks until the voice output buffer is empty.
+ void Wait();
+ }
+}
diff --git a/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs b/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs
index 7cfa45bfc..03d985dba 100644
--- a/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs
+++ b/src/Discord.Net.Audio/Net/WebSockets/VoiceWebSocket.cs
@@ -28,7 +28,7 @@ namespace Discord.Net.WebSockets
private readonly int _targetAudioBufferLength;
private readonly ConcurrentDictionary _decoders;
- private readonly DiscordAudioClient _audioClient;
+ private readonly AudioClient _audioClient;
private readonly AudioServiceConfig _config;
private Thread _sendThread, _receiveThread;
private VoiceBuffer _sendBuffer;
@@ -44,13 +44,13 @@ namespace Discord.Net.WebSockets
private int _ping;
public string Token { get; internal set; }
- public ulong? ServerId { get; internal set; }
- public ulong? ChannelId { get; internal set; }
+ public Server Server { get; internal set; }
+ public Channel Channel { get; internal set; }
public int Ping => _ping;
internal VoiceBuffer OutputBuffer => _sendBuffer;
- public VoiceWebSocket(DiscordClient client, DiscordAudioClient audioClient, JsonSerializer serializer, Logger logger)
+ internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, JsonSerializer serializer, Logger logger)
: base(client, serializer, logger)
{
_audioClient = audioClient;
@@ -65,7 +65,7 @@ namespace Discord.Net.WebSockets
public Task Connect()
=> BeginConnect();
- public async Task Reconnect()
+ private async Task Reconnect()
{
try
{
@@ -234,7 +234,7 @@ namespace Discord.Net.WebSockets
ulong userId;
if (_ssrcMapping.TryGetValue(ssrc, out userId))
- RaiseOnPacket(userId, ChannelId.Value, result, resultOffset, resultLength);
+ RaiseOnPacket(userId, Channel.Id, result, resultOffset, resultLength);
}
}
}
@@ -477,7 +477,7 @@ namespace Discord.Net.WebSockets
public override void SendHeartbeat()
=> QueueMessage(new HeartbeatCommand());
public void SendIdentify()
- => QueueMessage(new IdentifyCommand { GuildId = ServerId.Value, UserId = _client.CurrentUser.Id,
+ => QueueMessage(new IdentifyCommand { GuildId = Server.Id, UserId = _client.CurrentUser.Id,
SessionId = _client.SessionId, Token = Token });
public void SendSelectProtocol(string externalAddress, int externalPort)
=> QueueMessage(new SelectProtocolCommand { Protocol = "udp", ExternalAddress = externalAddress,
diff --git a/src/Discord.Net.Audio/SimpleAudioClient.cs b/src/Discord.Net.Audio/SimpleAudioClient.cs
new file mode 100644
index 000000000..71a76aa68
--- /dev/null
+++ b/src/Discord.Net.Audio/SimpleAudioClient.cs
@@ -0,0 +1,79 @@
+using Discord.Logging;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Discord.Audio
+{
+ internal class SimpleAudioClient : AudioClient
+ {
+ internal class VirtualClient : IAudioClient
+ {
+ private readonly SimpleAudioClient _client;
+
+ ConnectionState IAudioClient.State => _client.VoiceSocket.State;
+ Server IAudioClient.Server => _client.VoiceSocket.Server;
+ Channel IAudioClient.Channel => _client.VoiceSocket.Channel;
+
+ public VirtualClient(SimpleAudioClient client)
+ {
+ _client = client;
+ }
+
+ Task IAudioClient.Disconnect() => _client.Leave(this);
+ Task IAudioClient.Join(Channel channel) => _client.Join(channel);
+
+ void IAudioClient.Send(byte[] data, int count) => _client.Send(data, count);
+ void IAudioClient.Clear() => _client.Clear();
+ void IAudioClient.Wait() => _client.Wait();
+ }
+
+ private readonly Semaphore _connectionLock;
+
+ internal VirtualClient CurrentClient { get; private set; }
+
+ public SimpleAudioClient(AudioService service, int id, Logger logger)
+ : base(service, id, null, service.Client.GatewaySocket, logger)
+ {
+ _connectionLock = new Semaphore(1, 1);
+ }
+
+ //Only disconnects if is current a member of this server
+ public async Task Leave(VirtualClient client)
+ {
+ _connectionLock.WaitOne();
+ try
+ {
+ if (CurrentClient == client)
+ {
+ CurrentClient = null;
+ await Disconnect();
+ }
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+
+ internal async Task Connect(Channel channel)
+ {
+ _connectionLock.WaitOne();
+ try
+ {
+ bool changeServer = channel.Server != VoiceSocket.Server;
+ if (changeServer || CurrentClient == null)
+ {
+ await Disconnect().ConfigureAwait(false);
+ CurrentClient = new VirtualClient(this);
+ VoiceSocket.Server = channel.Server;
+ }
+ await Join(channel);
+ return CurrentClient;
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Audio/Sodium/SecretBox.cs b/src/Discord.Net.Audio/Sodium/SecretBox.cs
index da2287d8b..ac3218bde 100644
--- a/src/Discord.Net.Audio/Sodium/SecretBox.cs
+++ b/src/Discord.Net.Audio/Sodium/SecretBox.cs
@@ -3,7 +3,7 @@ using System.Security;
namespace Discord.Audio.Sodium
{
- public unsafe static class SecretBox
+ internal unsafe static class SecretBox
{
#if NET45
[SuppressUnmanagedCodeSecurity]