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