Finished AudioClient connection handshake
This commit is contained in:
@@ -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); } }
|
public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
|
||||||
private readonly AsyncEvent<Func<int, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<int, Task>>();
|
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); } }
|
public event Func<VoiceOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
|
||||||
private readonly AsyncEvent<Func<int, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<int, Task>>();
|
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
|
await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false);
|
||||||
return Task.CompletedTask;
|
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);
|
_webSocketClient.SetCancelToken(_connectCancelToken.Token);
|
||||||
await _gatewayClient.ConnectAsync(url).ConfigureAwait(false);
|
await _webSocketClient.ConnectAsync(url).ConfigureAwait(false);
|
||||||
|
_udpRecieveTask = ReceiveAsync(_connectCancelToken.Token);
|
||||||
await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
16
src/Discord.Net/API/Voice/ReadyEvent.cs
Normal file
16
src/Discord.Net/API/Voice/ReadyEvent.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Discord.Net/API/Voice/SelectProtocolParams.cs
Normal file
12
src/Discord.Net/API/Voice/SelectProtocolParams.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Discord.Net/API/Voice/SessionDescriptionEvent.cs
Normal file
12
src/Discord.Net/API/Voice/SessionDescriptionEvent.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Discord.Net/API/Voice/SpeakingParams.cs
Normal file
12
src/Discord.Net/API/Voice/SpeakingParams.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Discord.Net/API/Voice/UdpProtocolInfo.cs
Normal file
14
src/Discord.Net/API/Voice/UdpProtocolInfo.cs
Normal file
@@ -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");
|
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}");
|
||||||
_udpLogger = Discord.LogManager.CreateLogger("AudioUDP");
|
|
||||||
#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()
|
public DiscordClient() : this(new DiscordConfig()) { }
|
||||||
: 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(':'));
|
string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':'));
|
||||||
await guild.ConnectAudio(endpoint, data.Token).ConfigureAwait(false);
|
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",
|
||||||
|
|||||||
Reference in New Issue
Block a user