| @@ -49,7 +49,7 @@ namespace Discord.Audio | |||
| private DiscordAudioClient _defaultClient; | |||
| private ConcurrentDictionary<ulong, DiscordAudioClient> _voiceClients; | |||
| private ConcurrentDictionary<User, bool> _talkingUsers; | |||
| private int _nextClientId; | |||
| //private int _nextClientId; | |||
| internal DiscordClient Client => _client; | |||
| private DiscordClient _client; | |||
| @@ -143,13 +143,14 @@ namespace Discord.Audio | |||
| _defaultClient.SetServerId(server.Id); | |||
| return Task.FromResult(_defaultClient); | |||
| } | |||
| else | |||
| throw new InvalidOperationException("Multiserver voice is not currently supported"); | |||
| var client = _voiceClients.GetOrAdd(server.Id, _ => | |||
| /*var client = _voiceClients.GetOrAdd(server.Id, _ => | |||
| { | |||
| int id = unchecked(++_nextClientId); | |||
| var logger = Client.Log.CreateLogger($"Voice #{id}"); | |||
| GatewaySocket gatewaySocket = null; | |||
| var voiceClient = new DiscordAudioClient(this, id, logger, gatewaySocket); | |||
| var voiceClient = new DiscordAudioClient(this, id, logger, Client.GatewaySocket); | |||
| voiceClient.SetServerId(server.Id); | |||
| voiceClient.VoiceSocket.OnPacket += (s, e) => | |||
| @@ -165,7 +166,7 @@ namespace Discord.Audio | |||
| return voiceClient; | |||
| }); | |||
| //await client.Connect(gatewaySocket.Host, _client.Token).ConfigureAwait(false); | |||
| return Task.FromResult(client); | |||
| return Task.FromResult(client);*/ | |||
| } | |||
| public async Task<DiscordAudioClient> Join(Channel channel) | |||
| @@ -1,18 +1,20 @@ | |||
| using Discord.API; | |||
| using Discord.API.Client.GatewaySocket; | |||
| using Discord.API.Client.GatewaySocket; | |||
| using Discord.Logging; | |||
| using Discord.Net.WebSockets; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Audio | |||
| { | |||
| public partial class DiscordAudioClient | |||
| { | |||
| private JsonSerializer _serializer; | |||
| { | |||
| private readonly Semaphore _connectionLock; | |||
| private readonly JsonSerializer _serializer; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| internal AudioService Service { get; } | |||
| internal AudioService Service { get; } | |||
| internal Logger Logger { get; } | |||
| public int Id { get; } | |||
| public GatewaySocket GatewaySocket { get; } | |||
| @@ -26,6 +28,9 @@ namespace Discord.Audio | |||
| Service = service; | |||
| Id = id; | |||
| Logger = logger; | |||
| GatewaySocket = gatewaySocket; | |||
| _connectionLock = new Semaphore(1, 1); | |||
| _serializer = new JsonSerializer(); | |||
| _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||
| @@ -35,7 +40,6 @@ namespace Discord.Audio | |||
| Logger.Error("Serialization Failed", e.ErrorContext.Error); | |||
| }; | |||
| GatewaySocket = gatewaySocket; | |||
| VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger); | |||
| /*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
| @@ -72,33 +76,7 @@ namespace Discord.Audio | |||
| _voiceSocket.ParentCancelToken = _cancelToken; | |||
| };*/ | |||
| GatewaySocket.ReceivedDispatch += async (s, e) => | |||
| { | |||
| try | |||
| { | |||
| switch (e.Type) | |||
| { | |||
| case "VOICE_SERVER_UPDATE": | |||
| { | |||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
| var serverId = data.GuildId; | |||
| if (serverId == ServerId) | |||
| { | |||
| var client = Service.Client; | |||
| VoiceSocket.Token = data.Token; | |||
| VoiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||
| await VoiceSocket.Connect().ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error($"Error handling {e.Type} event", ex); | |||
| } | |||
| }; | |||
| GatewaySocket.ReceivedDispatch += OnReceivedDispatch; | |||
| } | |||
| @@ -106,22 +84,71 @@ namespace Discord.Audio | |||
| { | |||
| VoiceSocket.ServerId = serverId; | |||
| } | |||
| public async Task Join(Channel channel) | |||
| public Task Join(Channel channel) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| ulong? serverId = channel.Server?.Id; | |||
| if (serverId != ServerId) | |||
| throw new InvalidOperationException("Cannot join a channel on a different server than this voice client."); | |||
| //CheckReady(checkVoice: true); | |||
| //CheckReady(checkVoice: true); | |||
| await VoiceSocket.Disconnect().ConfigureAwait(false); | |||
| VoiceSocket.ChannelId = channel.Id; | |||
| GatewaySocket.SendUpdateVoice(channel.Server.Id, channel.Id, | |||
| (Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
| (Service.Config.Mode | AudioMode.Incoming) == 0); | |||
| await VoiceSocket.WaitForConnection(Service.Config.ConnectionTimeout).ConfigureAwait(false); | |||
| return Task.Run(async () => | |||
| { | |||
| _connectionLock.WaitOne(); | |||
| try | |||
| { | |||
| await VoiceSocket.Disconnect().ConfigureAwait(false); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| var cancelToken = _cancelTokenSource.Token; | |||
| VoiceSocket.ParentCancelToken = cancelToken; | |||
| VoiceSocket.ChannelId = channel.Id; | |||
| GatewaySocket.SendUpdateVoice(channel.Server.Id, channel.Id, | |||
| (Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
| (Service.Config.Mode | AudioMode.Incoming) == 0); | |||
| VoiceSocket.WaitForConnection(cancelToken); | |||
| } | |||
| finally | |||
| { | |||
| _connectionLock.Release(); | |||
| } | |||
| }); | |||
| } | |||
| public Task Disconnect() | |||
| { | |||
| GatewaySocket.ReceivedDispatch -= OnReceivedDispatch; | |||
| return VoiceSocket.Disconnect(); | |||
| } | |||
| private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e) | |||
| { | |||
| try | |||
| { | |||
| switch (e.Type) | |||
| { | |||
| case "VOICE_SERVER_UPDATE": | |||
| { | |||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
| var serverId = data.GuildId; | |||
| if (serverId == ServerId) | |||
| { | |||
| var client = Service.Client; | |||
| VoiceSocket.Token = data.Token; | |||
| VoiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||
| await VoiceSocket.Connect().ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error($"Error handling {e.Type} event", ex); | |||
| } | |||
| } | |||
| public Task Disconnect() => VoiceSocket.Disconnect(); | |||
| /// <summary> Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. </summary> | |||
| /// <param name="data">PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. </param> | |||
| @@ -63,10 +63,8 @@ namespace Discord.Net.WebSockets | |||
| _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||
| } | |||
| public async Task Connect() | |||
| { | |||
| await BeginConnect().ConfigureAwait(false); | |||
| } | |||
| public Task Connect() | |||
| => BeginConnect(); | |||
| public async Task Reconnect() | |||
| { | |||
| try | |||
| @@ -473,21 +471,6 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| _sendBuffer.Wait(CancelToken); | |||
| } | |||
| public Task WaitForConnection(int timeout) | |||
| { | |||
| return Task.Run(() => | |||
| { | |||
| try | |||
| { | |||
| if (!_connectedEvent.Wait(timeout, CancelToken)) | |||
| throw new TimeoutException(); | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| _taskManager.ThrowException(); | |||
| } | |||
| }); | |||
| } | |||
| public override void SendHeartbeat() | |||
| => QueueMessage(new HeartbeatCommand()); | |||
| @@ -6,7 +6,7 @@ namespace Discord.API.Client.GatewaySocket | |||
| [JsonObject(MemberSerialization.OptIn)] | |||
| public sealed class UpdateVoiceCommand : IWebSocketMessage | |||
| { | |||
| int IWebSocketMessage.OpCode => (int)OpCodes.StatusUpdate; | |||
| int IWebSocketMessage.OpCode => (int)OpCodes.VoiceStateUpdate; | |||
| object IWebSocketMessage.Payload => this; | |||
| bool IWebSocketMessage.IsPrivate => false; | |||
| @@ -126,11 +126,6 @@ namespace Discord | |||
| if (Config.UseMessageQueue) | |||
| MessageQueue = new MessageQueue(this, Log.CreateLogger("MessageQueue")); | |||
| Connected += async (s, e) => | |||
| { | |||
| ClientAPI.CancelToken = CancelToken; | |||
| await SendStatus().ConfigureAwait(false); | |||
| }; | |||
| //Extensibility | |||
| Services = new ServiceManager(this); | |||
| @@ -172,6 +167,10 @@ namespace Discord | |||
| State = ConnectionState.Connecting; | |||
| _disconnectedEvent.Reset(); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| CancelToken = _cancelTokenSource.Token; | |||
| GatewaySocket.ParentCancelToken = CancelToken; | |||
| await Login(email, password, token).ConfigureAwait(false); | |||
| await GatewaySocket.Connect().ConfigureAwait(false); | |||
| @@ -196,10 +195,6 @@ namespace Discord | |||
| } | |||
| private async Task Login(string email, string password, string token) | |||
| { | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| CancelToken = _cancelTokenSource.Token; | |||
| GatewaySocket.ParentCancelToken = CancelToken; | |||
| bool useCache = Config.CacheToken; | |||
| while (true) | |||
| { | |||
| @@ -261,6 +256,9 @@ namespace Discord | |||
| { | |||
| State = ConnectionState.Connected; | |||
| _connectedEvent.Set(); | |||
| ClientAPI.CancelToken = CancelToken; | |||
| SendStatus(); | |||
| OnConnected(); | |||
| } | |||
| @@ -293,21 +291,19 @@ namespace Discord | |||
| _disconnectedEvent.Set(); | |||
| } | |||
| public Task SetStatus(UserStatus status) | |||
| public void SetStatus(UserStatus status) | |||
| { | |||
| if (status == null) throw new ArgumentNullException(nameof(status)); | |||
| if (status != UserStatus.Online && status != UserStatus.Idle) | |||
| throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | |||
| Status = status; | |||
| return SendStatus(); | |||
| } | |||
| public Task SetGame(string game) | |||
| public void SetGame(string game) | |||
| { | |||
| CurrentGame = game; | |||
| return SendStatus(); | |||
| } | |||
| private Task SendStatus() | |||
| private void SendStatus() | |||
| { | |||
| PrivateUser.Status = Status; | |||
| PrivateUser.CurrentGame = CurrentGame; | |||
| @@ -321,7 +317,6 @@ namespace Discord | |||
| } | |||
| } | |||
| GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame); | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| #region Channels | |||
| @@ -5,6 +5,7 @@ using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Net.WebSockets | |||
| @@ -176,5 +177,9 @@ namespace Discord.Net.WebSockets | |||
| => QueueMessage(new UpdateVoiceCommand { GuildId = serverId, ChannelId = channelId, IsSelfMuted = isSelfMuted, IsSelfDeafened = isSelfDeafened }); | |||
| public void SendRequestMembers(ulong serverId, string query, int limit) | |||
| => QueueMessage(new RequestMembersCommand { GuildId = serverId, Query = query, Limit = limit }); | |||
| } | |||
| //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached | |||
| public override void WaitForConnection(CancellationToken cancelToken) | |||
| => base.WaitForConnection(CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token); | |||
| } | |||
| } | |||
| @@ -173,12 +173,10 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| public abstract void SendHeartbeat(); | |||
| public void WaitForConnection(CancellationToken cancelToken) | |||
| public virtual void WaitForConnection(CancellationToken cancelToken) | |||
| { | |||
| try | |||
| { | |||
| //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached | |||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token; | |||
| if (!_connectedEvent.Wait(_client.Config.ConnectionTimeout, cancelToken)) | |||
| throw new TimeoutException(); | |||
| } | |||