@@ -1,5 +1,4 @@ | |||
using Discord.API.Client.GatewaySocket; | |||
using Discord.API.Client.Rest; | |||
using Discord.Logging; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
@@ -13,7 +12,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class AudioClient : IAudioClient | |||
internal class AudioClient : IAudioClient | |||
{ | |||
private class OutStream : Stream | |||
{ | |||
@@ -50,7 +49,7 @@ namespace Discord.Audio | |||
private ConnectionState _gatewayState; | |||
internal Logger Logger { get; } | |||
public int Id { get; } | |||
public AudioService Service { get; } | |||
public AudioServiceConfig Config { get; } | |||
@@ -59,7 +58,7 @@ namespace Discord.Audio | |||
public VoiceSocket VoiceSocket { get; } | |||
public JsonSerializer Serializer { get; } | |||
public Stream OutputStream { get; } | |||
public CancellationToken CancelToken { get; private set; } | |||
public string SessionId => GatewaySocket.SessionId; | |||
@@ -68,7 +67,7 @@ namespace Discord.Audio | |||
public Channel Channel => VoiceSocket.Channel; | |||
public AudioClient(DiscordClient client, Server server, int id) | |||
{ | |||
{ | |||
Id = id; | |||
Service = client.GetService<AudioService>(); | |||
Config = Service.Config; | |||
@@ -84,40 +83,8 @@ namespace Discord.Audio | |||
CancelToken = new CancellationToken(true); | |||
//Networking | |||
if (Config.EnableMultiserver) | |||
{ | |||
//TODO: We can remove this hack when official API launches | |||
var baseConfig = client.Config; | |||
var builder = new DiscordConfigBuilder | |||
{ | |||
AppName = baseConfig.AppName, | |||
AppUrl = baseConfig.AppUrl, | |||
AppVersion = baseConfig.AppVersion, | |||
CacheToken = baseConfig.CacheDir != null, | |||
ConnectionTimeout = baseConfig.ConnectionTimeout, | |||
EnablePreUpdateEvents = false, | |||
FailedReconnectDelay = baseConfig.FailedReconnectDelay, | |||
LargeThreshold = 1, | |||
LogLevel = baseConfig.LogLevel, | |||
MessageCacheSize = 0, | |||
ReconnectDelay = baseConfig.ReconnectDelay, | |||
UsePermissionsCache = false | |||
}; | |||
_config = builder.Build(); | |||
ClientAPI = new JsonRestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}")); | |||
GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}")); | |||
GatewaySocket.Connected += (s, e) => | |||
{ | |||
if (_gatewayState == ConnectionState.Connecting) | |||
EndGatewayConnect(); | |||
}; | |||
} | |||
else | |||
{ | |||
_config = client.Config; | |||
GatewaySocket = client.GatewaySocket; | |||
} | |||
_config = client.Config; | |||
GatewaySocket = client.GatewaySocket; | |||
GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | |||
VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}")); | |||
VoiceSocket.Server = server; | |||
@@ -126,14 +93,9 @@ namespace Discord.Audio | |||
public async Task Connect() | |||
{ | |||
if (Config.EnableMultiserver) | |||
await BeginGatewayConnect().ConfigureAwait(false); | |||
else | |||
{ | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
} | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
} | |||
private async Task BeginGatewayConnect() | |||
{ | |||
@@ -154,7 +116,7 @@ namespace Discord.Audio | |||
var cancelSource = new CancellationTokenSource(); | |||
CancelToken = cancelSource.Token; | |||
ClientAPI.CancelToken = CancelToken; | |||
await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); | |||
await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); | |||
@@ -178,7 +140,7 @@ namespace Discord.Audio | |||
{ | |||
_gatewayState = ConnectionState.Connected; | |||
} | |||
public async Task Disconnect() | |||
{ | |||
await _taskManager.Stop(true).ConfigureAwait(false); | |||
@@ -188,28 +150,13 @@ namespace Discord.Audio | |||
var oldState = _gatewayState; | |||
_gatewayState = ConnectionState.Disconnecting; | |||
if (Config.EnableMultiserver) | |||
{ | |||
if (oldState == ConnectionState.Connected) | |||
{ | |||
try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } | |||
catch (OperationCanceledException) { } | |||
} | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
ClientAPI.Token = null; | |||
} | |||
var server = VoiceSocket.Server; | |||
VoiceSocket.Server = null; | |||
VoiceSocket.Channel = null; | |||
if (Config.EnableMultiserver) | |||
await Service.RemoveClient(server, this).ConfigureAwait(false); | |||
await Service.RemoveClient(server, this).ConfigureAwait(false); | |||
SendVoiceUpdate(server.Id, null); | |||
await VoiceSocket.Disconnect().ConfigureAwait(false); | |||
if (Config.EnableMultiserver) | |||
await GatewaySocket.Disconnect().ConfigureAwait(false); | |||
_gatewayState = (int)ConnectionState.Disconnected; | |||
} | |||
@@ -222,7 +169,7 @@ namespace Discord.Audio | |||
if (channel == VoiceSocket.Channel) return; | |||
var server = channel.Server; | |||
if (server != VoiceSocket.Server) | |||
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); | |||
throw new ArgumentException("This channel is not part of the current server.", nameof(channel)); | |||
if (VoiceSocket.Server == null) | |||
throw new InvalidOperationException("This client has been closed."); | |||
@@ -282,26 +229,26 @@ namespace Discord.Audio | |||
} | |||
public void Send(byte[] data, int offset, int count) | |||
{ | |||
{ | |||
if (data == null) throw new ArgumentException(nameof(data)); | |||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
if (count == 0) return; | |||
VoiceSocket.SendPCMFrames(data, offset, count); | |||
} | |||
VoiceSocket.SendPCMFrames(data, offset, count); | |||
} | |||
public void Clear() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
public void Wait() | |||
public void Wait() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.WaitForQueue(); | |||
} | |||
} | |||
public void SendVoiceUpdate(ulong? serverId, ulong? channelId) | |||
{ | |||
@@ -309,5 +256,5 @@ namespace Discord.Audio | |||
(Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
(Service.Config.Mode | AudioMode.Incoming) == 0); | |||
} | |||
} | |||
} | |||
} |
@@ -1,22 +1,19 @@ | |||
using Nito.AsyncEx; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public class AudioService : IService | |||
public class AudioService : IService | |||
{ | |||
private readonly AsyncLock _asyncLock; | |||
private AudioClient _defaultClient; //Only used for single server | |||
private VirtualClient _currentClient; //Only used for single server | |||
private ConcurrentDictionary<ulong, AudioClient> _voiceClients; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
public DiscordClient Client { get; private set; } | |||
public AudioServiceConfig Config { get; } | |||
public AudioServiceConfig Config { get; } | |||
public event EventHandler Connected = delegate { }; | |||
public event EventHandler<VoiceDisconnectedEventArgs> Disconnected = delegate { }; | |||
@@ -24,9 +21,9 @@ namespace Discord.Audio | |||
private void OnConnected() | |||
=> Connected(this, EventArgs.Empty); | |||
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) | |||
private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) | |||
=> Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex)); | |||
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) | |||
private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) | |||
=> UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking)); | |||
public AudioService() | |||
@@ -38,72 +35,41 @@ namespace Discord.Audio | |||
{ | |||
} | |||
public AudioService(AudioServiceConfig config) | |||
{ | |||
{ | |||
Config = config; | |||
_asyncLock = new AsyncLock(); | |||
} | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
if (Config.EnableMultiserver) | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
else | |||
{ | |||
var logger = Client.Log.CreateLogger("Voice"); | |||
_defaultClient = new AudioClient(Client, null, 0); | |||
} | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
client.GatewaySocket.Disconnected += async (s, e) => | |||
{ | |||
if (Config.EnableMultiserver) | |||
{ | |||
var tasks = _voiceClients | |||
.Select(x => | |||
{ | |||
var val = x.Value; | |||
if (val != null) | |||
return x.Value.Disconnect(); | |||
else | |||
return TaskHelper.CompletedTask; | |||
}) | |||
.ToArray(); | |||
await Task.WhenAll(tasks).ConfigureAwait(false); | |||
_voiceClients.Clear(); | |||
} | |||
foreach (var member in _talkingUsers) | |||
{ | |||
bool ignored; | |||
if (_talkingUsers.TryRemove(member.Key, out ignored)) | |||
OnUserIsSpeakingUpdated(member.Key, false); | |||
} | |||
}; | |||
} | |||
public IAudioClient GetClient(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (Config.EnableMultiserver) | |||
void IService.Install(DiscordClient client) | |||
{ | |||
Client = client; | |||
_voiceClients = new ConcurrentDictionary<ulong, AudioClient>(); | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
client.GatewaySocket.Disconnected += (s, e) => | |||
{ | |||
AudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
return null; | |||
} | |||
foreach (var member in _talkingUsers) | |||
{ | |||
bool ignored; | |||
if (_talkingUsers.TryRemove(member.Key, out ignored)) | |||
OnUserIsSpeakingUpdated(member.Key, false); | |||
} | |||
}; | |||
} | |||
public IAudioClient GetClient(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
AudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
{ | |||
if (server == _currentClient.Server) | |||
return _currentClient; | |||
else | |||
return null; | |||
} | |||
} | |||
//Called from AudioClient.Disconnect | |||
return null; | |||
} | |||
//Called from AudioClient.Cleanup | |||
internal async Task RemoveClient(Server server, AudioClient client) | |||
{ | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
@@ -113,81 +79,40 @@ namespace Discord.Audio | |||
} | |||
} | |||
public async Task<IAudioClient> Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
public async Task<IAudioClient> Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
var server = channel.Server; | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
if (Config.EnableMultiserver) | |||
{ | |||
AudioClient client; | |||
if (!_voiceClients.TryGetValue(server.Id, out client)) | |||
{ | |||
client = new AudioClient(Client, server, unchecked(++_nextClientId)); | |||
_voiceClients[server.Id] = client; | |||
await client.Connect().ConfigureAwait(false); | |||
/*voiceClient.VoiceSocket.FrameReceived += (s, e) => | |||
{ | |||
OnFrameReceieved(e); | |||
}; | |||
voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => | |||
{ | |||
var user = server.GetUser(e.UserId); | |||
OnUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
};*/ | |||
} | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
else | |||
AudioClient client; | |||
if (!_voiceClients.TryGetValue(server.Id, out client)) | |||
{ | |||
if (_defaultClient.Server != server) | |||
{ | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
_defaultClient.VoiceSocket.Server = server; | |||
await _defaultClient.Connect().ConfigureAwait(false); | |||
} | |||
var client = new VirtualClient(_defaultClient, server); | |||
_currentClient = client; | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
client = new AudioClient(Client, server, unchecked(++_nextClientId)); | |||
_voiceClients[server.Id] = client; | |||
await client.Connect().ConfigureAwait(false); | |||
} | |||
await client.Join(channel).ConfigureAwait(false); | |||
return client; | |||
} | |||
} | |||
} | |||
public Task Leave(Server server) => Leave(server, null); | |||
public Task Leave(Server server) => Leave(server, null); | |||
public Task Leave(Channel channel) => Leave(channel.Server, channel); | |||
private async Task Leave(Server server, Channel channel) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (Config.EnableMultiserver) | |||
AudioClient client; | |||
//Potential race condition if changing channels during this call, but that's acceptable | |||
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) | |||
{ | |||
AudioClient client; | |||
//Potential race condition if changing channels during this call, but that's acceptable | |||
if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) | |||
{ | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
else | |||
{ | |||
using (await _asyncLock.LockAsync().ConfigureAwait(false)) | |||
{ | |||
var client = GetClient(server) as VirtualClient; | |||
if (client != null && client.Channel == channel) | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
} | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
} |