@@ -45,6 +45,9 @@ | |||
<Reference Include="System" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Compile Include="..\Discord.Net.Audio\AudioClient.cs"> | |||
<Link>AudioClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\AudioExtensions.cs"> | |||
<Link>AudioExtensions.cs</Link> | |||
</Compile> | |||
@@ -54,8 +57,8 @@ | |||
<Compile Include="..\Discord.Net.Audio\AudioServiceConfig.cs"> | |||
<Link>AudioServiceConfig.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\DiscordAudioClient.cs"> | |||
<Link>DiscordAudioClient.cs</Link> | |||
<Compile Include="..\Discord.Net.Audio\IAudioClient.cs"> | |||
<Link>IAudioClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Net\WebSockets\VoiceWebSocket.cs"> | |||
<Link>Net\WebSockets\VoiceWebSocket.cs</Link> | |||
@@ -72,6 +75,9 @@ | |||
<Compile Include="..\Discord.Net.Audio\Opus\OpusEncoder.cs"> | |||
<Link>Opus\OpusEncoder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\SimpleAudioClient.cs"> | |||
<Link>SimpleAudioClient.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Audio\Sodium.cs"> | |||
<Link>Sodium.cs</Link> | |||
</Compile> | |||
@@ -8,7 +8,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public partial class DiscordAudioClient | |||
internal class AudioClient : IAudioClient | |||
{ | |||
private readonly Semaphore _connectionLock; | |||
private readonly JsonSerializer _serializer; | |||
@@ -20,19 +20,19 @@ namespace Discord.Audio | |||
public GatewaySocket GatewaySocket { get; } | |||
public VoiceWebSocket VoiceSocket { get; } | |||
public ulong? ServerId => VoiceSocket.ServerId; | |||
public ulong? ChannelId => VoiceSocket.ChannelId; | |||
public ConnectionState State => VoiceSocket.State; | |||
public Server Server => VoiceSocket.Server; | |||
public Channel Channel => VoiceSocket.Channel; | |||
public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket) | |||
public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, Logger logger) | |||
{ | |||
Service = service; | |||
Id = id; | |||
Logger = logger; | |||
Id = clientId; | |||
GatewaySocket = gatewaySocket; | |||
_connectionLock = new Semaphore(1, 1); | |||
Logger = logger; | |||
_connectionLock = new Semaphore(1, 1); | |||
_serializer = new JsonSerializer(); | |||
_serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||
_serializer.Error += (s, e) => | |||
@@ -41,7 +41,10 @@ namespace Discord.Audio | |||
Logger.Error("Serialization Failed", e.ErrorContext.Error); | |||
}; | |||
VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger); | |||
GatewaySocket.ReceivedDispatch += OnReceivedDispatch; | |||
VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger); | |||
VoiceSocket.Server = server; | |||
/*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
_voiceSocket.Disconnected += async (s, e) => | |||
@@ -76,58 +79,54 @@ namespace Discord.Audio | |||
{ | |||
_voiceSocket.ParentCancelToken = _cancelToken; | |||
};*/ | |||
} | |||
internal async Task SetServer(ulong serverId) | |||
{ | |||
if (serverId != VoiceSocket.ServerId) | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
VoiceSocket.ServerId = serverId; | |||
VoiceSocket.ChannelId = null; | |||
SendVoiceUpdate(); | |||
} | |||
} | |||
public Task JoinChannel(Channel channel) | |||
public async Task Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
var serverId = channel.Server?.Id; | |||
var channelId = channel.Id; | |||
if (serverId != ServerId) | |||
throw new InvalidOperationException("Cannot join a channel on a different server than this voice client."); | |||
if (channelId == VoiceSocket.ChannelId) | |||
return TaskHelper.CompletedTask; | |||
//CheckReady(checkVoice: true); | |||
return Task.Run(async () => | |||
{ | |||
_connectionLock.WaitOne(); | |||
GatewaySocket.ReceivedDispatch += OnReceivedDispatch; | |||
try | |||
{ | |||
if (State != ConnectionState.Disconnected) | |||
await Disconnect().ConfigureAwait(false); | |||
if (channel.Type != ChannelType.Voice) | |||
throw new ArgumentException("Channel must be a voice channel.", nameof(channel)); | |||
if (channel.Server != VoiceSocket.Server) | |||
throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); | |||
if (channel == VoiceSocket.Channel) return; | |||
if (VoiceSocket.Server == null) | |||
throw new InvalidOperationException("This client has been closed."); | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
var cancelToken = _cancelTokenSource.Token; | |||
VoiceSocket.ParentCancelToken = cancelToken; | |||
_connectionLock.WaitOne(); | |||
try | |||
{ | |||
_cancelTokenSource = new CancellationTokenSource(); | |||
var cancelToken = _cancelTokenSource.Token; | |||
VoiceSocket.ParentCancelToken = cancelToken; | |||
VoiceSocket.Channel = channel; | |||
VoiceSocket.ChannelId = channelId; | |||
await Task.Run(() => | |||
{ | |||
SendVoiceUpdate(); | |||
VoiceSocket.WaitForConnection(cancelToken); | |||
} | |||
finally | |||
{ | |||
GatewaySocket.ReceivedDispatch -= OnReceivedDispatch; | |||
_connectionLock.Release(); | |||
} | |||
}); | |||
}); | |||
} | |||
finally | |||
{ | |||
_connectionLock.Release(); | |||
} | |||
} | |||
public async Task Disconnect() | |||
{ | |||
_connectionLock.WaitOne(); | |||
try | |||
{ | |||
Service.RemoveClient(VoiceSocket.Server, this); | |||
VoiceSocket.Channel = null; | |||
SendVoiceUpdate(); | |||
await VoiceSocket.Disconnect(); | |||
} | |||
finally | |||
{ | |||
_connectionLock.Release(); | |||
} | |||
} | |||
public Task Disconnect() | |||
=> VoiceSocket.Disconnect(); | |||
private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e) | |||
{ | |||
@@ -135,12 +134,31 @@ namespace Discord.Audio | |||
{ | |||
switch (e.Type) | |||
{ | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||
if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) | |||
{ | |||
if (data.ChannelId == null) | |||
await Disconnect(); | |||
else | |||
{ | |||
var channel = Service.Client.GetChannel(data.ChannelId.Value); | |||
if (channel != null) | |||
VoiceSocket.Channel = channel; | |||
else | |||
{ | |||
Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); | |||
await Disconnect(); | |||
} | |||
} | |||
} | |||
} | |||
break; | |||
case "VOICE_SERVER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
var serverId = data.GuildId; | |||
if (serverId == ServerId) | |||
if (data.GuildId == VoiceSocket.Server?.Id) | |||
{ | |||
var client = Service.Client; | |||
VoiceSocket.Token = data.Token; | |||
@@ -164,34 +182,32 @@ namespace Discord.Audio | |||
{ | |||
if (data == null) throw new ArgumentException(nameof(data)); | |||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||
//CheckReady(checkVoice: true); | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
if (count != 0) | |||
if (count != 0) | |||
VoiceSocket.SendPCMFrames(data, count); | |||
} | |||
/// <summary> Clears the PCM buffer. </summary> | |||
public void Clear() | |||
{ | |||
//CheckReady(checkVoice: true); | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
/// <summary> Clears the PCM buffer. </summary> | |||
public void Clear() | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
/// <summary> Returns a task that completes once the voice output buffer is empty. </summary> | |||
public void Wait() | |||
{ | |||
//CheckReady(checkVoice: true); | |||
VoiceSocket.WaitForQueue(); | |||
{ | |||
if (VoiceSocket.Server == null) return; //Has been closed | |||
VoiceSocket.WaitForQueue(); | |||
} | |||
private void SendVoiceUpdate() | |||
{ | |||
var serverId = VoiceSocket.ServerId; | |||
var serverId = VoiceSocket.Server?.Id; | |||
if (serverId != null) | |||
{ | |||
GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.ChannelId, | |||
GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.Channel?.Id, | |||
(Service.Config.Mode | AudioMode.Outgoing) == 0, | |||
(Service.Config.Mode | AudioMode.Incoming) == 0); | |||
} |
@@ -46,8 +46,8 @@ namespace Discord.Audio | |||
public class AudioService : IService | |||
{ | |||
private DiscordAudioClient _defaultClient; | |||
private ConcurrentDictionary<ulong, DiscordAudioClient> _voiceClients; | |||
private AudioClient _defaultClient; | |||
private ConcurrentDictionary<ulong, IAudioClient> _voiceClients; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
//private int _nextClientId; | |||
@@ -91,11 +91,11 @@ namespace Discord.Audio | |||
{ | |||
_client = client; | |||
if (Config.EnableMultiserver) | |||
_voiceClients = new ConcurrentDictionary<ulong, DiscordAudioClient>(); | |||
_voiceClients = new ConcurrentDictionary<ulong, IAudioClient>(); | |||
else | |||
{ | |||
var logger = Client.Log.CreateLogger("Voice"); | |||
_defaultClient = new DiscordAudioClient(this, 0, logger, _client.GatewaySocket); | |||
_defaultClient = new SimpleAudioClient(this, 0, logger); | |||
} | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
@@ -118,34 +118,29 @@ namespace Discord.Audio | |||
}; | |||
} | |||
public DiscordAudioClient GetClient(Server server) | |||
public IAudioClient GetClient(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
if (!Config.EnableMultiserver) | |||
{ | |||
if (server.Id == _defaultClient.ServerId) | |||
return _defaultClient; | |||
else | |||
return null; | |||
} | |||
DiscordAudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
return null; | |||
if (!Config.EnableMultiserver) | |||
{ | |||
if (server == _defaultClient.Server) | |||
return (_defaultClient as SimpleAudioClient).CurrentClient; | |||
else | |||
return null; | |||
} | |||
else | |||
{ | |||
IAudioClient client; | |||
if (_voiceClients.TryGetValue(server.Id, out client)) | |||
return client; | |||
else | |||
return null; | |||
} | |||
} | |||
private async Task<DiscordAudioClient> CreateClient(Server server) | |||
private Task<IAudioClient> CreateClient(Server server) | |||
{ | |||
if (!Config.EnableMultiserver) | |||
{ | |||
await _defaultClient.SetServer(server.Id); | |||
return _defaultClient; | |||
} | |||
else | |||
throw new InvalidOperationException("Multiserver voice is not currently supported"); | |||
throw new NotImplementedException(); | |||
/*var client = _voiceClients.GetOrAdd(server.Id, _ => | |||
{ | |||
int id = unchecked(++_nextClientId); | |||
@@ -169,30 +164,44 @@ namespace Discord.Audio | |||
return Task.FromResult(client);*/ | |||
} | |||
public async Task<DiscordAudioClient> Join(Channel channel) | |||
//TODO: This isn't threadsafe | |||
internal void RemoveClient(Server server, IAudioClient client) | |||
{ | |||
if (Config.EnableMultiserver && server != null) | |||
_voiceClients.TryRemove(server.Id, out client); | |||
} | |||
public async Task<IAudioClient> Join(Channel channel) | |||
{ | |||
if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
//CheckReady(true); | |||
var client = await CreateClient(channel.Server).ConfigureAwait(false); | |||
await client.JoinChannel(channel).ConfigureAwait(false); | |||
IAudioClient client; | |||
if (!Config.EnableMultiserver) | |||
client = await (_defaultClient as SimpleAudioClient).Connect(channel).ConfigureAwait(false); | |||
else | |||
{ | |||
client = await CreateClient(channel.Server).ConfigureAwait(false); | |||
await client.Join(channel).ConfigureAwait(false); | |||
} | |||
return client; | |||
} | |||
public async Task Leave(Server server) | |||
{ | |||
if (server == null) throw new ArgumentNullException(nameof(server)); | |||
//CheckReady(true); | |||
if (Config.EnableMultiserver) | |||
{ | |||
//client.CheckReady(); | |||
DiscordAudioClient client; | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
else | |||
await _defaultClient.Disconnect().ConfigureAwait(false); | |||
if (Config.EnableMultiserver) | |||
{ | |||
IAudioClient client; | |||
if (_voiceClients.TryRemove(server.Id, out client)) | |||
await client.Disconnect().ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
IAudioClient client = GetClient(server); | |||
if (client != null) | |||
await (_defaultClient as SimpleAudioClient).Leave(client as SimpleAudioClient.VirtualClient).ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public interface IAudioClient | |||
{ | |||
ConnectionState State { get; } | |||
Channel Channel { get; } | |||
Server Server { get; } | |||
Task Join(Channel channel); | |||
Task 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> | |||
/// <param name="count">Number of bytes in this frame. </param> | |||
void Send(byte[] data, int count); | |||
/// <summary> Clears the PCM buffer. </summary> | |||
void Clear(); | |||
/// <summary> Blocks until the voice output buffer is empty. </summary> | |||
void Wait(); | |||
} | |||
} |
@@ -28,7 +28,7 @@ namespace Discord.Net.WebSockets | |||
private readonly int _targetAudioBufferLength; | |||
private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
private readonly DiscordAudioClient _audioClient; | |||
private readonly AudioClient _audioClient; | |||
private readonly AudioServiceConfig _config; | |||
private Thread _sendThread, _receiveThread; | |||
private VoiceBuffer _sendBuffer; | |||
@@ -44,13 +44,13 @@ namespace Discord.Net.WebSockets | |||
private int _ping; | |||
public string Token { get; internal set; } | |||
public ulong? ServerId { get; internal set; } | |||
public ulong? ChannelId { get; internal set; } | |||
public Server Server { get; internal set; } | |||
public Channel Channel { get; internal set; } | |||
public int Ping => _ping; | |||
internal VoiceBuffer OutputBuffer => _sendBuffer; | |||
public VoiceWebSocket(DiscordClient client, DiscordAudioClient audioClient, JsonSerializer serializer, Logger logger) | |||
internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, JsonSerializer serializer, Logger logger) | |||
: base(client, serializer, logger) | |||
{ | |||
_audioClient = audioClient; | |||
@@ -65,7 +65,7 @@ namespace Discord.Net.WebSockets | |||
public Task Connect() | |||
=> BeginConnect(); | |||
public async Task Reconnect() | |||
private async Task Reconnect() | |||
{ | |||
try | |||
{ | |||
@@ -234,7 +234,7 @@ namespace Discord.Net.WebSockets | |||
ulong userId; | |||
if (_ssrcMapping.TryGetValue(ssrc, out userId)) | |||
RaiseOnPacket(userId, ChannelId.Value, result, resultOffset, resultLength); | |||
RaiseOnPacket(userId, Channel.Id, result, resultOffset, resultLength); | |||
} | |||
} | |||
} | |||
@@ -477,7 +477,7 @@ namespace Discord.Net.WebSockets | |||
public override void SendHeartbeat() | |||
=> QueueMessage(new HeartbeatCommand()); | |||
public void SendIdentify() | |||
=> QueueMessage(new IdentifyCommand { GuildId = ServerId.Value, UserId = _client.CurrentUser.Id, | |||
=> QueueMessage(new IdentifyCommand { GuildId = Server.Id, UserId = _client.CurrentUser.Id, | |||
SessionId = _client.SessionId, Token = Token }); | |||
public void SendSelectProtocol(string externalAddress, int externalPort) | |||
=> QueueMessage(new SelectProtocolCommand { Protocol = "udp", ExternalAddress = externalAddress, | |||
@@ -0,0 +1,79 @@ | |||
using Discord.Logging; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class SimpleAudioClient : AudioClient | |||
{ | |||
internal class VirtualClient : IAudioClient | |||
{ | |||
private readonly SimpleAudioClient _client; | |||
ConnectionState IAudioClient.State => _client.VoiceSocket.State; | |||
Server IAudioClient.Server => _client.VoiceSocket.Server; | |||
Channel IAudioClient.Channel => _client.VoiceSocket.Channel; | |||
public VirtualClient(SimpleAudioClient client) | |||
{ | |||
_client = client; | |||
} | |||
Task IAudioClient.Disconnect() => _client.Leave(this); | |||
Task IAudioClient.Join(Channel channel) => _client.Join(channel); | |||
void IAudioClient.Send(byte[] data, int count) => _client.Send(data, count); | |||
void IAudioClient.Clear() => _client.Clear(); | |||
void IAudioClient.Wait() => _client.Wait(); | |||
} | |||
private readonly Semaphore _connectionLock; | |||
internal VirtualClient CurrentClient { get; private set; } | |||
public SimpleAudioClient(AudioService service, int id, Logger logger) | |||
: base(service, id, null, service.Client.GatewaySocket, logger) | |||
{ | |||
_connectionLock = new Semaphore(1, 1); | |||
} | |||
//Only disconnects if is current a member of this server | |||
public async Task Leave(VirtualClient client) | |||
{ | |||
_connectionLock.WaitOne(); | |||
try | |||
{ | |||
if (CurrentClient == client) | |||
{ | |||
CurrentClient = null; | |||
await Disconnect(); | |||
} | |||
} | |||
finally | |||
{ | |||
_connectionLock.Release(); | |||
} | |||
} | |||
internal async Task<IAudioClient> Connect(Channel channel) | |||
{ | |||
_connectionLock.WaitOne(); | |||
try | |||
{ | |||
bool changeServer = channel.Server != VoiceSocket.Server; | |||
if (changeServer || CurrentClient == null) | |||
{ | |||
await Disconnect().ConfigureAwait(false); | |||
CurrentClient = new VirtualClient(this); | |||
VoiceSocket.Server = channel.Server; | |||
} | |||
await Join(channel); | |||
return CurrentClient; | |||
} | |||
finally | |||
{ | |||
_connectionLock.Release(); | |||
} | |||
} | |||
} | |||
} |
@@ -3,7 +3,7 @@ using System.Security; | |||
namespace Discord.Audio.Sodium | |||
{ | |||
public unsafe static class SecretBox | |||
internal unsafe static class SecretBox | |||
{ | |||
#if NET45 | |||
[SuppressUnmanagedCodeSecurity] | |||