@@ -3,6 +3,6 @@ | |||
public static class AudioExtensions | |||
{ | |||
public static AudioService Audio(this DiscordClient client, bool required = true) | |||
=> client.GetService<AudioService>(required); | |||
=> client.Services.Get<AudioService>(required); | |||
} | |||
} |
@@ -11,7 +11,7 @@ namespace Discord.Audio | |||
public readonly ulong ServerId; | |||
public VoiceDisconnectedEventArgs(ulong serverId, DisconnectedEventArgs e) | |||
: base(e.WasUnexpected, e.Error) | |||
: base(e.WasUnexpected, e.Exception) | |||
{ | |||
ServerId = serverId; | |||
} | |||
@@ -45,8 +45,8 @@ namespace Discord.Audio | |||
} | |||
public class AudioService : IService | |||
{ | |||
private DiscordAudioClient _defaultClient; | |||
{ | |||
private DiscordAudioClient _defaultClient; | |||
private ConcurrentDictionary<ulong, DiscordAudioClient> _voiceClients; | |||
private ConcurrentDictionary<User, bool> _talkingUsers; | |||
private int _nextClientId; | |||
@@ -94,8 +94,8 @@ namespace Discord.Audio | |||
_voiceClients = new ConcurrentDictionary<ulong, DiscordAudioClient>(); | |||
else | |||
{ | |||
var logger = Client.Log().CreateLogger("Voice"); | |||
_defaultClient = new DiscordAudioClient(this, 0, logger, _client.WebSocket); | |||
var logger = Client.Log.CreateLogger("Voice"); | |||
_defaultClient = new DiscordAudioClient(this, 0, logger, _client.GatewaySocket); | |||
} | |||
_talkingUsers = new ConcurrentDictionary<User, bool>(); | |||
@@ -147,7 +147,7 @@ namespace Discord.Audio | |||
var client = _voiceClients.GetOrAdd(server.Id, _ => | |||
{ | |||
int id = unchecked(++_nextClientId); | |||
var logger = Client.Log().CreateLogger($"Voice #{id}"); | |||
var logger = Client.Log.CreateLogger($"Voice #{id}"); | |||
GatewaySocket gatewaySocket = null; | |||
var voiceClient = new DiscordAudioClient(this, id, logger, gatewaySocket); | |||
voiceClient.SetServerId(server.Id); | |||
@@ -158,7 +158,7 @@ namespace Discord.Audio | |||
}; | |||
voiceClient.VoiceSocket.IsSpeaking += (s, e) => | |||
{ | |||
var user = Client.GetUser(server, e.UserId); | |||
var user = server.GetUser(e.UserId); | |||
RaiseUserIsSpeakingUpdated(user, e.IsSpeaking); | |||
}; | |||
@@ -1,6 +1,8 @@ | |||
using Discord.API; | |||
using Discord.API.Client.GatewaySocket; | |||
using Discord.Logging; | |||
using Discord.Net.WebSockets; | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Threading.Tasks; | |||
@@ -8,31 +10,33 @@ namespace Discord.Audio | |||
{ | |||
public partial class DiscordAudioClient | |||
{ | |||
private readonly int _id; | |||
public int Id => _id; | |||
private JsonSerializer _serializer; | |||
private readonly AudioService _service; | |||
private readonly Logger _logger; | |||
internal AudioService Service { get; } | |||
internal Logger Logger { get; } | |||
public int Id { get; } | |||
public GatewaySocket GatewaySocket { get; } | |||
public VoiceWebSocket VoiceSocket { get; } | |||
public GatewaySocket GatewaySocket => _gatewaySocket; | |||
private readonly GatewaySocket _gatewaySocket; | |||
public VoiceWebSocket VoiceSocket => _voiceSocket; | |||
private readonly VoiceWebSocket _voiceSocket; | |||
public string Token => _token; | |||
private string _token; | |||
public ulong? ServerId => _voiceSocket.ServerId; | |||
public ulong? ChannelId => _voiceSocket.ChannelId; | |||
public ulong? ServerId => VoiceSocket.ServerId; | |||
public ulong? ChannelId => VoiceSocket.ChannelId; | |||
public DiscordAudioClient(AudioService service, int id, Logger logger, GatewaySocket gatewaySocket) | |||
{ | |||
_service = service; | |||
_id = id; | |||
_logger = logger; | |||
_gatewaySocket = gatewaySocket; | |||
_voiceSocket = new VoiceWebSocket(service.Client, this, logger); | |||
Service = service; | |||
Id = id; | |||
Logger = logger; | |||
_serializer = new JsonSerializer(); | |||
_serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||
_serializer.Error += (s, e) => | |||
{ | |||
e.ErrorContext.Handled = true; | |||
Logger.Error("Serialization Failed", e.ErrorContext.Error); | |||
}; | |||
GatewaySocket = gatewaySocket; | |||
VoiceSocket = new VoiceWebSocket(service.Client, this, _serializer, logger); | |||
/*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
_voiceSocket.Disconnected += async (s, e) => | |||
@@ -68,7 +72,7 @@ namespace Discord.Audio | |||
_voiceSocket.ParentCancelToken = _cancelToken; | |||
};*/ | |||
_gatewaySocket.ReceivedDispatch += async (s, e) => | |||
GatewaySocket.ReceivedDispatch += async (s, e) => | |||
{ | |||
try | |||
{ | |||
@@ -76,15 +80,15 @@ namespace Discord.Audio | |||
{ | |||
case "VOICE_SERVER_UPDATE": | |||
{ | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_gatewaySocket.Serializer); | |||
var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
var serverId = data.GuildId; | |||
if (serverId == ServerId) | |||
{ | |||
var client = _service.Client; | |||
_token = data.Token; | |||
_voiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||
await _voiceSocket.Connect().ConfigureAwait(false); | |||
var client = Service.Client; | |||
VoiceSocket.Token = data.Token; | |||
VoiceSocket.Host = "wss://" + e.Payload.Value<string>("endpoint").Split(':')[0]; | |||
await VoiceSocket.Connect().ConfigureAwait(false); | |||
} | |||
} | |||
break; | |||
@@ -92,7 +96,7 @@ namespace Discord.Audio | |||
} | |||
catch (Exception ex) | |||
{ | |||
_gatewaySocket.Logger.Error($"Error handling {e.Type} event", ex); | |||
Logger.Error($"Error handling {e.Type} event", ex); | |||
} | |||
}; | |||
} | |||
@@ -100,7 +104,7 @@ namespace Discord.Audio | |||
internal void SetServerId(ulong serverId) | |||
{ | |||
_voiceSocket.ServerId = serverId; | |||
VoiceSocket.ServerId = serverId; | |||
} | |||
public async Task Join(Channel channel) | |||
{ | |||
@@ -110,14 +114,14 @@ namespace Discord.Audio | |||
throw new InvalidOperationException("Cannot join a channel on a different server than this voice client."); | |||
//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); | |||
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); | |||
} | |||
public Task Disconnect() => _voiceSocket.Disconnect(); | |||
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> | |||
@@ -129,7 +133,7 @@ namespace Discord.Audio | |||
//CheckReady(checkVoice: true); | |||
if (count != 0) | |||
_voiceSocket.SendPCMFrames(data, count); | |||
VoiceSocket.SendPCMFrames(data, count); | |||
} | |||
/// <summary> Clears the PCM buffer. </summary> | |||
@@ -137,7 +141,7 @@ namespace Discord.Audio | |||
{ | |||
//CheckReady(checkVoice: true); | |||
_voiceSocket.ClearPCMFrames(); | |||
VoiceSocket.ClearPCMFrames(); | |||
} | |||
/// <summary> Returns a task that completes once the voice output buffer is empty. </summary> | |||
@@ -145,7 +149,7 @@ namespace Discord.Audio | |||
{ | |||
//CheckReady(checkVoice: true); | |||
_voiceSocket.WaitForQueue(); | |||
VoiceSocket.WaitForQueue(); | |||
return TaskHelper.CompletedTask; | |||
} | |||
} | |||
@@ -4,6 +4,7 @@ using Discord.API.Client.VoiceSocket; | |||
using Discord.Audio; | |||
using Discord.Audio.Opus; | |||
using Discord.Audio.Sodium; | |||
using Discord.Logging; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
@@ -24,35 +25,33 @@ namespace Discord.Net.WebSockets | |||
private const int MaxOpusSize = 4000; | |||
private const string EncryptedMode = "xsalsa20_poly1305"; | |||
private const string UnencryptedMode = "plain"; | |||
//private readonly Random _rand; | |||
private readonly int _targetAudioBufferLength; | |||
private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
private readonly DiscordAudioClient _audioClient; | |||
private readonly AudioServiceConfig _config; | |||
private OpusEncoder _encoder; | |||
private Thread _sendThread, _receiveThread; | |||
private VoiceBuffer _sendBuffer; | |||
private OpusEncoder _encoder; | |||
private uint _ssrc; | |||
private ConcurrentDictionary<uint, ulong> _ssrcMapping; | |||
private VoiceBuffer _sendBuffer; | |||
private UdpClient _udp; | |||
private IPEndPoint _endpoint; | |||
private bool _isEncrypted; | |||
private byte[] _secretKey, _encodingBuffer; | |||
private ushort _sequence; | |||
private ulong? _serverId, _channelId; | |||
private string _encryptionMode; | |||
private int _ping; | |||
private Thread _sendThread, _receiveThread; | |||
public ulong? ServerId { get { return _serverId; } internal set { _serverId = value; } } | |||
public ulong? ChannelId { get { return _channelId; } internal set { _channelId = value; } } | |||
public int Ping => _ping; | |||
private int _ping; | |||
public string Token { get; internal set; } | |||
public ulong? ServerId { get; internal set; } | |||
public ulong? ChannelId { get; internal set; } | |||
public int Ping => _ping; | |||
internal VoiceBuffer OutputBuffer => _sendBuffer; | |||
public VoiceWebSocket(DiscordClient client, DiscordAudioClient audioClient, Logger logger) | |||
: base(client, logger) | |||
public VoiceWebSocket(DiscordClient client, DiscordAudioClient audioClient, JsonSerializer serializer, Logger logger) | |||
: base(client, serializer, logger) | |||
{ | |||
_audioClient = audioClient; | |||
_config = client.Audio().Config; | |||
@@ -84,7 +83,7 @@ namespace Discord.Net.WebSockets | |||
catch (OperationCanceledException) { throw; } | |||
catch (Exception ex) | |||
{ | |||
_logger.Error("Reconnect failed", ex); | |||
Logger.Error("Reconnect failed", ex); | |||
//Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
} | |||
@@ -101,14 +100,14 @@ namespace Discord.Net.WebSockets | |||
List<Task> tasks = new List<Task>(); | |||
if ((_config.Mode & AudioMode.Outgoing) != 0) | |||
{ | |||
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | |||
_sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(CancelToken))); | |||
_sendThread.IsBackground = true; | |||
_sendThread.Start(); | |||
} | |||
if ((_config.Mode & AudioMode.Incoming) != 0) | |||
{ | |||
_receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(_cancelToken))); | |||
_receiveThread.IsBackground = true; | |||
_receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(CancelToken))); | |||
_receiveThread.IsBackground = true; | |||
_receiveThread.Start(); | |||
} | |||
@@ -117,8 +116,8 @@ namespace Discord.Net.WebSockets | |||
#if !DOTNET5_4 | |||
tasks.Add(WatcherAsync()); | |||
#endif | |||
tasks.AddRange(_engine.GetTasks(_cancelToken)); | |||
tasks.Add(HeartbeatAsync(_cancelToken)); | |||
tasks.AddRange(_engine.GetTasks(CancelToken)); | |||
tasks.Add(HeartbeatAsync(CancelToken)); | |||
await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||
} | |||
protected override Task Cleanup() | |||
@@ -179,7 +178,7 @@ namespace Discord.Net.WebSockets | |||
if (packetLength > 0 && endpoint.Equals(_endpoint)) | |||
{ | |||
if (_state != ConnectionState.Connected) | |||
if (State != ConnectionState.Connected) | |||
{ | |||
if (packetLength != 70) | |||
return; | |||
@@ -235,7 +234,7 @@ namespace Discord.Net.WebSockets | |||
ulong userId; | |||
if (_ssrcMapping.TryGetValue(ssrc, out userId)) | |||
RaiseOnPacket(userId, _channelId.Value, result, resultOffset, resultLength); | |||
RaiseOnPacket(userId, ChannelId.Value, result, resultOffset, resultLength); | |||
} | |||
} | |||
} | |||
@@ -249,7 +248,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
try | |||
{ | |||
while (!cancelToken.IsCancellationRequested && _state != ConnectionState.Connected) | |||
while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected) | |||
Thread.Sleep(1); | |||
if (cancelToken.IsCancellationRequested) | |||
@@ -353,7 +352,7 @@ namespace Discord.Net.WebSockets | |||
} | |||
catch (SocketException ex) | |||
{ | |||
_logger.Error("Failed to send UDP packet.", ex); | |||
Logger.Error("Failed to send UDP packet.", ex); | |||
} | |||
hasFrame = false; | |||
} | |||
@@ -385,11 +384,7 @@ namespace Discord.Net.WebSockets | |||
#if !DOTNET5_4 | |||
//Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken | |||
private Task WatcherAsync() | |||
{ | |||
var cancelToken = _cancelToken; | |||
return cancelToken.Wait() | |||
.ContinueWith(_ => _udp.Close()); | |||
} | |||
=> CancelToken.Wait().ContinueWith(_ => _udp.Close()); | |||
#endif | |||
protected override async Task ProcessMessage(string json) | |||
@@ -401,7 +396,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
case OpCodes.Ready: | |||
{ | |||
if (_state != ConnectionState.Connected) | |||
if (State != ConnectionState.Connected) | |||
{ | |||
var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
_heartbeatInterval = payload.HeartbeatInterval; | |||
@@ -460,24 +455,23 @@ namespace Discord.Net.WebSockets | |||
} | |||
break; | |||
default: | |||
if (_logger.Level >= LogSeverity.Warning) | |||
_logger.Warning($"Unknown Opcode: {opCode}"); | |||
Logger.Warning($"Unknown Opcode: {opCode}"); | |||
break; | |||
} | |||
} | |||
public void SendPCMFrames(byte[] data, int bytes) | |||
{ | |||
_sendBuffer.Push(data, bytes, _cancelToken); | |||
_sendBuffer.Push(data, bytes, CancelToken); | |||
} | |||
public void ClearPCMFrames() | |||
{ | |||
_sendBuffer.Clear(_cancelToken); | |||
_sendBuffer.Clear(CancelToken); | |||
} | |||
public void WaitForQueue() | |||
{ | |||
_sendBuffer.Wait(_cancelToken); | |||
_sendBuffer.Wait(CancelToken); | |||
} | |||
public Task WaitForConnection(int timeout) | |||
{ | |||
@@ -485,7 +479,7 @@ namespace Discord.Net.WebSockets | |||
{ | |||
try | |||
{ | |||
if (!_connectedEvent.Wait(timeout, _cancelToken)) | |||
if (!_connectedEvent.Wait(timeout, CancelToken)) | |||
throw new TimeoutException(); | |||
} | |||
catch (OperationCanceledException) | |||
@@ -498,9 +492,11 @@ namespace Discord.Net.WebSockets | |||
public override void SendHeartbeat() | |||
=> QueueMessage(new HeartbeatCommand()); | |||
public void SendIdentify() | |||
=> QueueMessage(new IdentifyCommand { GuildId = _serverId.Value, UserId = _client.UserId.Value, SessionId = _client.SessionId, Token = _audioClient.Token }); | |||
=> QueueMessage(new IdentifyCommand { GuildId = ServerId.Value, UserId = _client.CurrentUser.Id, | |||
SessionId = _client.SessionId, Token = Token }); | |||
public void SendSelectProtocol(string externalAddress, int externalPort) | |||
=> QueueMessage(new SelectProtocolCommand { Protocol = "udp", ExternalAddress = externalAddress, ExternalPort = externalPort, EncryptionMode = _encryptionMode }); | |||
=> QueueMessage(new SelectProtocolCommand { Protocol = "udp", ExternalAddress = externalAddress, | |||
ExternalPort = externalPort, EncryptionMode = _encryptionMode }); | |||
public void SendSetSpeaking(bool value) | |||
=> QueueMessage(new SetSpeakingCommand { IsSpeaking = value, Delay = 0 }); | |||
@@ -56,6 +56,8 @@ namespace Discord | |||
internal User PrivateUser { get; private set; } | |||
/// <summary> Gets information about the current logged-in account. </summary> | |||
public Profile CurrentUser { get; private set; } | |||
/// <summary> Gets the session id for the current connection. </summary> | |||
public string SessionId { get; private set; } | |||
/// <summary> Gets the status of the current user. </summary> | |||
public UserStatus Status { get; private set; } | |||
/// <summary> Gets the game this current user is reported as playing. </summary> | |||
@@ -448,7 +450,7 @@ namespace Discord | |||
case "READY": //Resync | |||
{ | |||
var data = e.Payload.ToObject<ReadyEvent>(_serializer); | |||
//SessionId = data.SessionId; | |||
SessionId = data.SessionId; | |||
PrivateUser = new User(this, data.User.Id, null); | |||
PrivateUser.Update(data.User); | |||
CurrentUser.Update(data.User); | |||
@@ -22,7 +22,7 @@ namespace Discord.Net.WebSockets | |||
private DateTime _lastHeartbeat; | |||
/// <summary> Gets the logger used for this client. </summary> | |||
internal Logger Logger { get; } | |||
protected internal Logger Logger { get; } | |||
public CancellationToken CancelToken { get; private set; } | |||