| @@ -27,8 +27,8 @@ namespace Discord.API | |||
| public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | |||
| public event Func<int, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<int, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<int, Task>>(); | |||
| public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>(); | |||
| public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>>(); | |||
| @@ -352,7 +352,7 @@ namespace Discord.API | |||
| if (payload != null) | |||
| bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | |||
| await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync((int)opCode).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | |||
| } | |||
| //Auth | |||
| @@ -11,28 +11,37 @@ using System.IO.Compression; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using System.Net.Sockets; | |||
| using System.Net; | |||
| namespace Discord.Audio | |||
| { | |||
| public class DiscordVoiceAPIClient | |||
| { | |||
| public const int MaxBitrate = 128; | |||
| private const string Mode = "xsalsa20_poly1305"; | |||
| public const string Mode = "xsalsa20_poly1305"; | |||
| public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | |||
| public event Func<int, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<int, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<int, Task>>(); | |||
| public event Func<VoiceOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<VoiceOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<VoiceOpCode, Task>>(); | |||
| public event Func<Task> SentDiscovery { add { _sentDiscoveryEvent.Add(value); } remove { _sentDiscoveryEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _sentDiscoveryEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<VoiceOpCode, object, Task> ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<VoiceOpCode, object, Task>> _receivedEvent = new AsyncEvent<Func<VoiceOpCode, object, Task>>(); | |||
| public event Func<byte[], Task> ReceivedPacket { add { _receivedPacketEvent.Add(value); } remove { _receivedPacketEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>(); | |||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| private readonly JsonSerializer _serializer; | |||
| private readonly IWebSocketClient _gatewayClient; | |||
| private readonly IWebSocketClient _webSocketClient; | |||
| private readonly SemaphoreSlim _connectionLock; | |||
| private CancellationTokenSource _connectCancelToken; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _udpEndpoint; | |||
| private Task _udpRecieveTask; | |||
| private bool _isDisposed; | |||
| public ulong GuildId { get; } | |||
| @@ -42,10 +51,11 @@ namespace Discord.Audio | |||
| { | |||
| GuildId = guildId; | |||
| _connectionLock = new SemaphoreSlim(1, 1); | |||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
| _gatewayClient = webSocketProvider(); | |||
| _webSocketClient = webSocketProvider(); | |||
| //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||
| _gatewayClient.BinaryMessage += async (data, index, count) => | |||
| _webSocketClient.BinaryMessage += async (data, index, count) => | |||
| { | |||
| using (var compressed = new MemoryStream(data, index + 2, count - 2)) | |||
| using (var decompressed = new MemoryStream()) | |||
| @@ -60,12 +70,12 @@ namespace Discord.Audio | |||
| } | |||
| } | |||
| }; | |||
| _gatewayClient.TextMessage += async text => | |||
| _webSocketClient.TextMessage += async text => | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | |||
| await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | |||
| }; | |||
| _gatewayClient.Closed += async ex => | |||
| _webSocketClient.Closed += async ex => | |||
| { | |||
| await DisconnectAsync().ConfigureAwait(false); | |||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||
| @@ -80,21 +90,29 @@ namespace Discord.Audio | |||
| if (disposing) | |||
| { | |||
| _connectCancelToken?.Dispose(); | |||
| (_gatewayClient as IDisposable)?.Dispose(); | |||
| (_webSocketClient as IDisposable)?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| public void Dispose() => Dispose(true); | |||
| public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) | |||
| public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) | |||
| { | |||
| byte[] bytes = null; | |||
| payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; | |||
| if (payload != null) | |||
| bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | |||
| //TODO: Send | |||
| return Task.CompletedTask; | |||
| await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync(opCode); | |||
| } | |||
| public async Task SendAsync(byte[] data, int bytes) | |||
| { | |||
| if (_udpEndpoint != null) | |||
| { | |||
| await _udp.SendAsync(data, bytes, _udpEndpoint).ConfigureAwait(false); | |||
| await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); | |||
| } | |||
| } | |||
| //WebSocket | |||
| @@ -102,36 +120,56 @@ namespace Discord.Audio | |||
| { | |||
| await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task SendIdentityAsync(ulong guildId, ulong userId, string sessionId, string token) | |||
| public async Task SendIdentityAsync(ulong userId, string sessionId, string token) | |||
| { | |||
| await SendAsync(VoiceOpCode.Identify, new IdentifyParams | |||
| { | |||
| GuildId = guildId, | |||
| GuildId = GuildId, | |||
| UserId = userId, | |||
| SessionId = sessionId, | |||
| Token = token | |||
| }); | |||
| } | |||
| public async Task SendSelectProtocol(string externalIp, int externalPort) | |||
| { | |||
| await SendAsync(VoiceOpCode.SelectProtocol, new SelectProtocolParams | |||
| { | |||
| Protocol = "udp", | |||
| Data = new UdpProtocolInfo | |||
| { | |||
| Address = externalIp, | |||
| Port = externalPort, | |||
| Mode = Mode | |||
| } | |||
| }); | |||
| } | |||
| public async Task SendSetSpeaking(bool value) | |||
| { | |||
| await SendAsync(VoiceOpCode.Speaking, new SpeakingParams | |||
| { | |||
| IsSpeaking = value, | |||
| Delay = 0 | |||
| }); | |||
| } | |||
| public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) | |||
| public async Task ConnectAsync(string url) | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||
| await ConnectInternalAsync(url).ConfigureAwait(false); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) | |||
| private async Task ConnectInternalAsync(string url) | |||
| { | |||
| ConnectionState = ConnectionState.Connecting; | |||
| try | |||
| { | |||
| _connectCancelToken = new CancellationTokenSource(); | |||
| _gatewayClient.SetCancelToken(_connectCancelToken.Token); | |||
| await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); | |||
| await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false); | |||
| _webSocketClient.SetCancelToken(_connectCancelToken.Token); | |||
| await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); | |||
| _udpRecieveTask = ReceiveAsync(_connectCancelToken.Token); | |||
| ConnectionState = ConnectionState.Connected; | |||
| } | |||
| @@ -159,11 +197,43 @@ namespace Discord.Audio | |||
| try { _connectCancelToken?.Cancel(false); } | |||
| catch { } | |||
| await _gatewayClient.DisconnectAsync().ConfigureAwait(false); | |||
| //Wait for tasks to complete | |||
| await _udpRecieveTask.ConfigureAwait(false); | |||
| await _webSocketClient.DisconnectAsync().ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Disconnected; | |||
| } | |||
| //Udp | |||
| public async Task SendDiscoveryAsync(uint ssrc) | |||
| { | |||
| var packet = new byte[70]; | |||
| packet[0] = (byte)(ssrc >> 24); | |||
| packet[1] = (byte)(ssrc >> 16); | |||
| packet[2] = (byte)(ssrc >> 8); | |||
| packet[3] = (byte)(ssrc >> 0); | |||
| await SendAsync(packet, 70).ConfigureAwait(false); | |||
| } | |||
| public void SetUdpEndpoint(IPEndPoint endpoint) | |||
| { | |||
| _udpEndpoint = endpoint; | |||
| } | |||
| private async Task ReceiveAsync(CancellationToken cancelToken) | |||
| { | |||
| var closeTask = Task.Delay(-1, cancelToken); | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| var receiveTask = _udp.ReceiveAsync(); | |||
| var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); | |||
| if (task == closeTask) | |||
| break; | |||
| await _receivedPacketEvent.InvokeAsync(receiveTask.Result.Buffer).ConfigureAwait(false); | |||
| } | |||
| } | |||
| //Helpers | |||
| private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | |||
| private string SerializeJson(object value) | |||
| @@ -0,0 +1,16 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class ReadyEvent | |||
| { | |||
| [JsonProperty("ssrc")] | |||
| public uint SSRC { get; set; } | |||
| [JsonProperty("port")] | |||
| public ushort Port { get; set; } | |||
| [JsonProperty("modes")] | |||
| public string[] Modes { get; set; } | |||
| [JsonProperty("heartbeat_interval")] | |||
| public int HeartbeatInterval { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SelectProtocolParams | |||
| { | |||
| [JsonProperty("protocol")] | |||
| public string Protocol { get; set; } | |||
| [JsonProperty("data")] | |||
| public UdpProtocolInfo Data { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SessionDescriptionEvent | |||
| { | |||
| [JsonProperty("secret_key")] | |||
| public byte[] SecretKey { get; set; } | |||
| [JsonProperty("mode")] | |||
| public string Mode { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class SpeakingParams | |||
| { | |||
| [JsonProperty("speaking")] | |||
| public bool IsSpeaking { get; set; } | |||
| [JsonProperty("delay")] | |||
| public int Delay { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Voice | |||
| { | |||
| public class UdpProtocolInfo | |||
| { | |||
| [JsonProperty("address")] | |||
| public string Address { get; set; } | |||
| [JsonProperty("port")] | |||
| public int Port { get; set; } | |||
| [JsonProperty("mode")] | |||
| public string Mode { get; set; } | |||
| } | |||
| } | |||
| @@ -2,7 +2,11 @@ | |||
| using Discord.Logging; | |||
| using Discord.Net.Converters; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Text; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| @@ -29,7 +33,7 @@ namespace Discord.Audio | |||
| } | |||
| private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>(); | |||
| private readonly ILogger _webSocketLogger, _udpLogger; | |||
| private readonly ILogger _audioLogger; | |||
| #if BENCHMARK | |||
| private readonly ILogger _benchmarkLogger; | |||
| #endif | |||
| @@ -42,6 +46,8 @@ namespace Discord.Audio | |||
| private long _heartbeatTime; | |||
| private string _url; | |||
| private bool _isDisposed; | |||
| private uint _ssrc; | |||
| private byte[] _secretKey; | |||
| public CachedGuild Guild { get; } | |||
| public DiscordVoiceAPIClient ApiClient { get; private set; } | |||
| @@ -51,12 +57,11 @@ namespace Discord.Audio | |||
| private DiscordSocketClient Discord => Guild.Discord; | |||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
| internal AudioClient(CachedGuild guild) | |||
| internal AudioClient(CachedGuild guild, int id) | |||
| { | |||
| Guild = guild; | |||
| _webSocketLogger = Discord.LogManager.CreateLogger("Audio"); | |||
| _udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); | |||
| _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); | |||
| #if BENCHMARK | |||
| _benchmarkLogger = logManager.CreateLogger("Benchmark"); | |||
| #endif | |||
| @@ -66,20 +71,22 @@ namespace Discord.Audio | |||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| _serializer.Error += (s, e) => | |||
| { | |||
| _webSocketLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||
| _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||
| e.ErrorContext.Handled = true; | |||
| }; | |||
| ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); | |||
| ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); | |||
| ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | |||
| ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); | |||
| ApiClient.ReceivedEvent += ProcessMessageAsync; | |||
| ApiClient.ReceivedPacket += ProcessPacketAsync; | |||
| ApiClient.Disconnected += async ex => | |||
| { | |||
| if (ex != null) | |||
| await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | |||
| await _audioLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | |||
| else | |||
| await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | |||
| await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); | |||
| }; | |||
| } | |||
| @@ -100,19 +107,20 @@ namespace Discord.Audio | |||
| await DisconnectInternalAsync(null).ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connecting; | |||
| await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||
| await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||
| try | |||
| { | |||
| _url = url; | |||
| _connectTask = new TaskCompletionSource<bool>(); | |||
| _cancelToken = new CancellationTokenSource(); | |||
| await ApiClient.ConnectAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||
| await _connectedEvent.InvokeAsync().ConfigureAwait(false); | |||
| await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); | |||
| await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); | |||
| await _connectTask.Task.ConfigureAwait(false); | |||
| await _connectedEvent.InvokeAsync().ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connected; | |||
| await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); | |||
| await _audioLogger.InfoAsync("Connected").ConfigureAwait(false); | |||
| } | |||
| catch (Exception) | |||
| { | |||
| @@ -143,7 +151,7 @@ namespace Discord.Audio | |||
| { | |||
| if (ConnectionState == ConnectionState.Disconnected) return; | |||
| ConnectionState = ConnectionState.Disconnecting; | |||
| await _webSocketLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | |||
| await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | |||
| //Signal tasks to complete | |||
| try { _cancelToken.Cancel(); } catch { } | |||
| @@ -158,7 +166,7 @@ namespace Discord.Audio | |||
| _heartbeatTask = null; | |||
| ConnectionState = ConnectionState.Disconnected; | |||
| await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||
| await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||
| } | |||
| @@ -174,25 +182,49 @@ namespace Discord.Audio | |||
| { | |||
| switch (opCode) | |||
| { | |||
| /*case VoiceOpCode.Ready: | |||
| case VoiceOpCode.Ready: | |||
| { | |||
| await _webSocketLogger.DebugAsync("Received Ready").ConfigureAwait(false); | |||
| await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| _ssrc = data.SSRC; | |||
| if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) | |||
| throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | |||
| _heartbeatTime = 0; | |||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | |||
| var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false); | |||
| ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port)); | |||
| await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | |||
| } | |||
| break;*/ | |||
| break; | |||
| case VoiceOpCode.SessionDescription: | |||
| { | |||
| await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false); | |||
| var data = (payload as JToken).ToObject<SessionDescriptionEvent>(_serializer); | |||
| if (data.Mode != DiscordVoiceAPIClient.Mode) | |||
| throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); | |||
| _secretKey = data.SecretKey; | |||
| await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); | |||
| _connectTask.TrySetResult(true); | |||
| } | |||
| break; | |||
| case VoiceOpCode.HeartbeatAck: | |||
| { | |||
| await _webSocketLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||
| await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||
| var heartbeatTime = _heartbeatTime; | |||
| if (heartbeatTime != 0) | |||
| { | |||
| int latency = (int)(Environment.TickCount - _heartbeatTime); | |||
| _heartbeatTime = 0; | |||
| await _webSocketLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||
| await _audioLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||
| int before = Latency; | |||
| Latency = latency; | |||
| @@ -202,13 +234,13 @@ namespace Discord.Audio | |||
| } | |||
| break; | |||
| default: | |||
| await _webSocketLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); | |||
| await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| await _webSocketLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); | |||
| await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); | |||
| return; | |||
| } | |||
| #if BENCHMARK | |||
| @@ -222,6 +254,27 @@ namespace Discord.Audio | |||
| #endif | |||
| } | |||
| private async Task ProcessPacketAsync(byte[] packet) | |||
| { | |||
| if (!_connectTask.Task.IsCompleted) | |||
| { | |||
| if (packet.Length == 70) | |||
| { | |||
| string ip; | |||
| int port; | |||
| try | |||
| { | |||
| ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); | |||
| port = packet[68] | packet[69] << 8; | |||
| } | |||
| catch { return; } | |||
| await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); | |||
| await ApiClient.SendSelectProtocol(ip, port); | |||
| } | |||
| } | |||
| } | |||
| private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) | |||
| { | |||
| //Clean this up when Discord's session patch is live | |||
| @@ -235,7 +288,7 @@ namespace Discord.Audio | |||
| { | |||
| if (ConnectionState == ConnectionState.Connected) | |||
| { | |||
| await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | |||
| await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | |||
| await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); | |||
| return; | |||
| } | |||
| @@ -35,8 +35,7 @@ namespace Discord | |||
| public LoginState LoginState { get; private set; } | |||
| /// <summary> Creates a new REST-only discord client. </summary> | |||
| public DiscordClient() | |||
| : this(new DiscordConfig()) { } | |||
| public DiscordClient() : this(new DiscordConfig()) { } | |||
| /// <summary> Creates a new REST-only discord client. </summary> | |||
| public DiscordClient(DiscordConfig config) | |||
| { | |||
| @@ -35,6 +35,7 @@ namespace Discord | |||
| private bool _isReconnecting; | |||
| private int _unavailableGuilds; | |||
| private long _lastGuildAvailableTime; | |||
| private int _nextAudioId; | |||
| /// <summary> Gets the shard if of this client. </summary> | |||
| public int ShardId { get; } | |||
| @@ -74,6 +75,7 @@ namespace Discord | |||
| LargeThreshold = config.LargeThreshold; | |||
| AudioMode = config.AudioMode; | |||
| WebSocketProvider = config.WebSocketProvider; | |||
| _nextAudioId = 1; | |||
| _gatewayLogger = LogManager.CreateLogger("Gateway"); | |||
| #if BENCHMARK | |||
| @@ -87,7 +89,7 @@ namespace Discord | |||
| e.ErrorContext.Handled = true; | |||
| }; | |||
| ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}").ConfigureAwait(false); | |||
| ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); | |||
| ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; | |||
| ApiClient.Disconnected += async ex => | |||
| { | |||
| @@ -1173,8 +1175,8 @@ namespace Discord | |||
| var guild = DataStore.GetGuild(data.GuildId); | |||
| if (guild != null) | |||
| { | |||
| string endpoint = "wss://" + data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); | |||
| await guild.ConnectAudio(endpoint, data.Token).ConfigureAwait(false); | |||
| string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); | |||
| var _ = guild.ConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| @@ -261,7 +261,7 @@ namespace Discord | |||
| return null; | |||
| } | |||
| public async Task ConnectAudio(string url, string token) | |||
| public async Task ConnectAudio(int id, string url, string token) | |||
| { | |||
| AudioClient audioClient; | |||
| await _audioLock.WaitAsync().ConfigureAwait(false); | |||
| @@ -271,7 +271,7 @@ namespace Discord | |||
| audioClient = AudioClient; | |||
| if (audioClient == null) | |||
| { | |||
| audioClient = new AudioClient(this); | |||
| audioClient = new AudioClient(this, id); | |||
| audioClient.Disconnected += async ex => | |||
| { | |||
| await _audioLock.WaitAsync().ConfigureAwait(false); | |||
| @@ -26,6 +26,8 @@ | |||
| "System.IO.Compression": "4.1.0", | |||
| "System.IO.FileSystem": "4.0.1", | |||
| "System.Net.Http": "4.1.0", | |||
| "System.Net.NameResolution": "4.0.0", | |||
| "System.Net.Sockets": "4.1.0", | |||
| "System.Net.WebSockets.Client": "4.0.0", | |||
| "System.Reflection.Extensions": "4.0.1", | |||
| "System.Runtime.InteropServices": "4.1.0", | |||