| @@ -27,8 +27,8 @@ namespace Discord.API | |||||
| public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | 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>>(); | 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); } } | 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>>(); | 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) | if (payload != null) | ||||
| bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | ||||
| await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); | 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 | //Auth | ||||
| @@ -11,28 +11,37 @@ using System.IO.Compression; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Net.Sockets; | |||||
| using System.Net; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| public class DiscordVoiceAPIClient | public class DiscordVoiceAPIClient | ||||
| { | { | ||||
| public const int MaxBitrate = 128; | 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); } } | 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>>(); | 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); } } | 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>>(); | 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); } } | 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 AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly IWebSocketClient _gatewayClient; | |||||
| private readonly IWebSocketClient _webSocketClient; | |||||
| private readonly SemaphoreSlim _connectionLock; | private readonly SemaphoreSlim _connectionLock; | ||||
| private CancellationTokenSource _connectCancelToken; | private CancellationTokenSource _connectCancelToken; | ||||
| private UdpClient _udp; | |||||
| private IPEndPoint _udpEndpoint; | |||||
| private Task _udpRecieveTask; | |||||
| private bool _isDisposed; | private bool _isDisposed; | ||||
| public ulong GuildId { get; } | public ulong GuildId { get; } | ||||
| @@ -42,10 +51,11 @@ namespace Discord.Audio | |||||
| { | { | ||||
| GuildId = guildId; | GuildId = guildId; | ||||
| _connectionLock = new SemaphoreSlim(1, 1); | _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.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 compressed = new MemoryStream(data, index + 2, count - 2)) | ||||
| using (var decompressed = new MemoryStream()) | 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); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | ||||
| await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); | ||||
| }; | }; | ||||
| _gatewayClient.Closed += async ex => | |||||
| _webSocketClient.Closed += async ex => | |||||
| { | { | ||||
| await DisconnectAsync().ConfigureAwait(false); | await DisconnectAsync().ConfigureAwait(false); | ||||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | ||||
| @@ -80,21 +90,29 @@ namespace Discord.Audio | |||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| _connectCancelToken?.Dispose(); | _connectCancelToken?.Dispose(); | ||||
| (_gatewayClient as IDisposable)?.Dispose(); | |||||
| (_webSocketClient as IDisposable)?.Dispose(); | |||||
| } | } | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| public void Dispose() => Dispose(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; | byte[] bytes = null; | ||||
| payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; | payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; | ||||
| if (payload != null) | if (payload != null) | ||||
| bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | 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 | //WebSocket | ||||
| @@ -102,36 +120,56 @@ namespace Discord.Audio | |||||
| { | { | ||||
| await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); | 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 | await SendAsync(VoiceOpCode.Identify, new IdentifyParams | ||||
| { | { | ||||
| GuildId = guildId, | |||||
| GuildId = GuildId, | |||||
| UserId = userId, | UserId = userId, | ||||
| SessionId = sessionId, | SessionId = sessionId, | ||||
| Token = token | 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); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); | |||||
| await ConnectInternalAsync(url).ConfigureAwait(false); | |||||
| } | } | ||||
| finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
| } | } | ||||
| private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) | |||||
| private async Task ConnectInternalAsync(string url) | |||||
| { | { | ||||
| ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
| try | try | ||||
| { | { | ||||
| _connectCancelToken = new CancellationTokenSource(); | _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; | ConnectionState = ConnectionState.Connected; | ||||
| } | } | ||||
| @@ -159,11 +197,43 @@ namespace Discord.Audio | |||||
| try { _connectCancelToken?.Cancel(false); } | try { _connectCancelToken?.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| await _gatewayClient.DisconnectAsync().ConfigureAwait(false); | |||||
| //Wait for tasks to complete | |||||
| await _udpRecieveTask.ConfigureAwait(false); | |||||
| await _webSocketClient.DisconnectAsync().ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Disconnected; | 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 | //Helpers | ||||
| private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); | ||||
| private string SerializeJson(object value) | 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.Logging; | ||||
| using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | using System; | ||||
| using System.Linq; | |||||
| using System.Net; | |||||
| using System.Text; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | 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 AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>(); | ||||
| private readonly ILogger _webSocketLogger, _udpLogger; | |||||
| private readonly ILogger _audioLogger; | |||||
| #if BENCHMARK | #if BENCHMARK | ||||
| private readonly ILogger _benchmarkLogger; | private readonly ILogger _benchmarkLogger; | ||||
| #endif | #endif | ||||
| @@ -42,6 +46,8 @@ namespace Discord.Audio | |||||
| private long _heartbeatTime; | private long _heartbeatTime; | ||||
| private string _url; | private string _url; | ||||
| private bool _isDisposed; | private bool _isDisposed; | ||||
| private uint _ssrc; | |||||
| private byte[] _secretKey; | |||||
| public CachedGuild Guild { get; } | public CachedGuild Guild { get; } | ||||
| public DiscordVoiceAPIClient ApiClient { get; private set; } | public DiscordVoiceAPIClient ApiClient { get; private set; } | ||||
| @@ -51,12 +57,11 @@ namespace Discord.Audio | |||||
| private DiscordSocketClient Discord => Guild.Discord; | private DiscordSocketClient Discord => Guild.Discord; | ||||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | /// <summary> Creates a new REST/WebSocket discord client. </summary> | ||||
| internal AudioClient(CachedGuild guild) | |||||
| internal AudioClient(CachedGuild guild, int id) | |||||
| { | { | ||||
| Guild = guild; | Guild = guild; | ||||
| _webSocketLogger = Discord.LogManager.CreateLogger("Audio"); | |||||
| _udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); | |||||
| _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); | |||||
| #if BENCHMARK | #if BENCHMARK | ||||
| _benchmarkLogger = logManager.CreateLogger("Benchmark"); | _benchmarkLogger = logManager.CreateLogger("Benchmark"); | ||||
| #endif | #endif | ||||
| @@ -66,20 +71,22 @@ namespace Discord.Audio | |||||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | ||||
| _serializer.Error += (s, e) => | _serializer.Error += (s, e) => | ||||
| { | { | ||||
| _webSocketLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||||
| _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||||
| e.ErrorContext.Handled = true; | e.ErrorContext.Handled = true; | ||||
| }; | }; | ||||
| ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); | 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.ReceivedEvent += ProcessMessageAsync; | ||||
| ApiClient.ReceivedPacket += ProcessPacketAsync; | |||||
| ApiClient.Disconnected += async ex => | ApiClient.Disconnected += async ex => | ||||
| { | { | ||||
| if (ex != null) | if (ex != null) | ||||
| await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | |||||
| await _audioLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); | |||||
| else | 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); | await DisconnectInternalAsync(null).ConfigureAwait(false); | ||||
| ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
| await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||||
| await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false); | |||||
| try | try | ||||
| { | { | ||||
| _url = url; | _url = url; | ||||
| _connectTask = new TaskCompletionSource<bool>(); | _connectTask = new TaskCompletionSource<bool>(); | ||||
| _cancelToken = new CancellationTokenSource(); | _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 _connectTask.Task.ConfigureAwait(false); | ||||
| await _connectedEvent.InvokeAsync().ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Connected; | ConnectionState = ConnectionState.Connected; | ||||
| await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); | |||||
| await _audioLogger.InfoAsync("Connected").ConfigureAwait(false); | |||||
| } | } | ||||
| catch (Exception) | catch (Exception) | ||||
| { | { | ||||
| @@ -143,7 +151,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| if (ConnectionState == ConnectionState.Disconnected) return; | if (ConnectionState == ConnectionState.Disconnected) return; | ||||
| ConnectionState = ConnectionState.Disconnecting; | ConnectionState = ConnectionState.Disconnecting; | ||||
| await _webSocketLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | |||||
| await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | |||||
| //Signal tasks to complete | //Signal tasks to complete | ||||
| try { _cancelToken.Cancel(); } catch { } | try { _cancelToken.Cancel(); } catch { } | ||||
| @@ -158,7 +166,7 @@ namespace Discord.Audio | |||||
| _heartbeatTask = null; | _heartbeatTask = null; | ||||
| ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
| await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||||
| await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -174,25 +182,49 @@ namespace Discord.Audio | |||||
| { | { | ||||
| switch (opCode) | 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); | 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; | _heartbeatTime = 0; | ||||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | _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: | case VoiceOpCode.HeartbeatAck: | ||||
| { | { | ||||
| await _webSocketLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||||
| await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||||
| var heartbeatTime = _heartbeatTime; | var heartbeatTime = _heartbeatTime; | ||||
| if (heartbeatTime != 0) | if (heartbeatTime != 0) | ||||
| { | { | ||||
| int latency = (int)(Environment.TickCount - _heartbeatTime); | int latency = (int)(Environment.TickCount - _heartbeatTime); | ||||
| _heartbeatTime = 0; | _heartbeatTime = 0; | ||||
| await _webSocketLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||||
| await _audioLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||||
| int before = Latency; | int before = Latency; | ||||
| Latency = latency; | Latency = latency; | ||||
| @@ -202,13 +234,13 @@ namespace Discord.Audio | |||||
| } | } | ||||
| break; | break; | ||||
| default: | default: | ||||
| await _webSocketLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); | |||||
| await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); | |||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| await _webSocketLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); | |||||
| await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); | |||||
| return; | return; | ||||
| } | } | ||||
| #if BENCHMARK | #if BENCHMARK | ||||
| @@ -222,6 +254,27 @@ namespace Discord.Audio | |||||
| #endif | #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) | private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) | ||||
| { | { | ||||
| //Clean this up when Discord's session patch is live | //Clean this up when Discord's session patch is live | ||||
| @@ -235,7 +288,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| if (ConnectionState == ConnectionState.Connected) | 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); | await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -35,8 +35,7 @@ namespace Discord | |||||
| public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
| /// <summary> Creates a new REST-only discord client. </summary> | /// <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> | /// <summary> Creates a new REST-only discord client. </summary> | ||||
| public DiscordClient(DiscordConfig config) | public DiscordClient(DiscordConfig config) | ||||
| { | { | ||||
| @@ -35,6 +35,7 @@ namespace Discord | |||||
| private bool _isReconnecting; | private bool _isReconnecting; | ||||
| private int _unavailableGuilds; | private int _unavailableGuilds; | ||||
| private long _lastGuildAvailableTime; | private long _lastGuildAvailableTime; | ||||
| private int _nextAudioId; | |||||
| /// <summary> Gets the shard if of this client. </summary> | /// <summary> Gets the shard if of this client. </summary> | ||||
| public int ShardId { get; } | public int ShardId { get; } | ||||
| @@ -74,6 +75,7 @@ namespace Discord | |||||
| LargeThreshold = config.LargeThreshold; | LargeThreshold = config.LargeThreshold; | ||||
| AudioMode = config.AudioMode; | AudioMode = config.AudioMode; | ||||
| WebSocketProvider = config.WebSocketProvider; | WebSocketProvider = config.WebSocketProvider; | ||||
| _nextAudioId = 1; | |||||
| _gatewayLogger = LogManager.CreateLogger("Gateway"); | _gatewayLogger = LogManager.CreateLogger("Gateway"); | ||||
| #if BENCHMARK | #if BENCHMARK | ||||
| @@ -87,7 +89,7 @@ namespace Discord | |||||
| e.ErrorContext.Handled = true; | 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.ReceivedGatewayEvent += ProcessMessageAsync; | ||||
| ApiClient.Disconnected += async ex => | ApiClient.Disconnected += async ex => | ||||
| { | { | ||||
| @@ -1173,8 +1175,8 @@ namespace Discord | |||||
| var guild = DataStore.GetGuild(data.GuildId); | var guild = DataStore.GetGuild(data.GuildId); | ||||
| if (guild != null) | 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 | else | ||||
| { | { | ||||
| @@ -261,7 +261,7 @@ namespace Discord | |||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task ConnectAudio(string url, string token) | |||||
| public async Task ConnectAudio(int id, string url, string token) | |||||
| { | { | ||||
| AudioClient audioClient; | AudioClient audioClient; | ||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | await _audioLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -271,7 +271,7 @@ namespace Discord | |||||
| audioClient = AudioClient; | audioClient = AudioClient; | ||||
| if (audioClient == null) | if (audioClient == null) | ||||
| { | { | ||||
| audioClient = new AudioClient(this); | |||||
| audioClient = new AudioClient(this, id); | |||||
| audioClient.Disconnected += async ex => | audioClient.Disconnected += async ex => | ||||
| { | { | ||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | await _audioLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -26,6 +26,8 @@ | |||||
| "System.IO.Compression": "4.1.0", | "System.IO.Compression": "4.1.0", | ||||
| "System.IO.FileSystem": "4.0.1", | "System.IO.FileSystem": "4.0.1", | ||||
| "System.Net.Http": "4.1.0", | "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.Net.WebSockets.Client": "4.0.0", | ||||
| "System.Reflection.Extensions": "4.0.1", | "System.Reflection.Extensions": "4.0.1", | ||||
| "System.Runtime.InteropServices": "4.1.0", | "System.Runtime.InteropServices": "4.1.0", | ||||