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