From d81fb12b262eda1c5e04384fe55a04f95d537896 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 30 Dec 2015 22:47:10 -0400 Subject: [PATCH] Fixed several voice stability issues, redesigned single-server voice --- .../Discord.Net.Audio.csproj | 10 +- .../{DiscordAudioClient.cs => AudioClient.cs} | 158 ++++++++++-------- src/Discord.Net.Audio/AudioService.cs | 91 +++++----- src/Discord.Net.Audio/IAudioClient.cs | 23 +++ .../Net/WebSockets/VoiceWebSocket.cs | 14 +- src/Discord.Net.Audio/SimpleAudioClient.cs | 79 +++++++++ src/Discord.Net.Audio/Sodium/SecretBox.cs | 2 +- 7 files changed, 255 insertions(+), 122 deletions(-) rename src/Discord.Net.Audio/{DiscordAudioClient.cs => AudioClient.cs} (54%) create mode 100644 src/Discord.Net.Audio/IAudioClient.cs create mode 100644 src/Discord.Net.Audio/SimpleAudioClient.cs 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]