@@ -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", | ||||