| @@ -21,11 +21,11 @@ namespace Discord | |||||
| public CommandEventArgs(Message message, Command command, string commandText, int? permissions, string[] args) | public CommandEventArgs(Message message, Command command, string commandText, int? permissions, string[] args) | ||||
| { | { | ||||
| this.Message = message; | |||||
| this.Command = command; | |||||
| this.CommandText = commandText; | |||||
| this.Permissions = permissions; | |||||
| this.Args = args; | |||||
| Message = message; | |||||
| Command = command; | |||||
| CommandText = commandText; | |||||
| Permissions = permissions; | |||||
| Args = args; | |||||
| } | } | ||||
| } | } | ||||
| public class CommandErrorEventArgs : CommandEventArgs | public class CommandErrorEventArgs : CommandEventArgs | ||||
| @@ -35,7 +35,7 @@ namespace Discord | |||||
| public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) | public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) | ||||
| : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.Permissions, baseArgs.Args) | : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.Permissions, baseArgs.Args) | ||||
| { | { | ||||
| this.Exception = ex; | |||||
| Exception = ex; | |||||
| } | } | ||||
| } | } | ||||
| public partial class DiscordBotClient : DiscordClient | public partial class DiscordBotClient : DiscordClient | ||||
| @@ -103,6 +103,9 @@ | |||||
| <Compile Include="..\Discord.Net\Audio\Opus.cs"> | <Compile Include="..\Discord.Net\Audio\Opus.cs"> | ||||
| <Link>Audio\Opus.cs</Link> | <Link>Audio\Opus.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Audio\OpusDecoder.cs"> | |||||
| <Link>Audio\OpusDecoder.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Audio\OpusEncoder.cs"> | <Compile Include="..\Discord.Net\Audio\OpusEncoder.cs"> | ||||
| <Link>Audio\OpusEncoder.cs</Link> | <Link>Audio\OpusEncoder.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -3,21 +3,21 @@ using System.Runtime.InteropServices; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| internal static unsafe class Opus | |||||
| internal unsafe static class Opus | |||||
| { | { | ||||
| [DllImport("lib/opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out Error error); | public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out Error error); | ||||
| [DllImport("lib/opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern void DestroyEncoder(IntPtr encoder); | public static extern void DestroyEncoder(IntPtr encoder); | ||||
| [DllImport("lib/opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); | |||||
| public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); | |||||
| /*[DllImport("lib/opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] | |||||
| public static extern IntPtr CreateDecoder(int Fs, int channels, out Errors error); | |||||
| [DllImport("lib/opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] | |||||
| public static extern IntPtr CreateDecoder(int Fs, int channels, out Error error); | |||||
| [DllImport("lib/opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern void DestroyDecoder(IntPtr decoder); | public static extern void DestroyDecoder(IntPtr decoder); | ||||
| [DllImport("lib/opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern int Decode(IntPtr st, byte[] data, int len, IntPtr pcm, int frame_size, int decode_fec);*/ | |||||
| public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); | |||||
| [DllImport("lib/opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] | [DllImport("lib/opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] | ||||
| public static extern int EncoderCtl(IntPtr st, Ctl request, int value); | public static extern int EncoderCtl(IntPtr st, Ctl request, int value); | ||||
| @@ -0,0 +1,105 @@ | |||||
| using System; | |||||
| namespace Discord.Audio | |||||
| { | |||||
| /// <summary> Opus codec wrapper. </summary> | |||||
| internal class OpusDecoder : IDisposable | |||||
| { | |||||
| private readonly IntPtr _ptr; | |||||
| /// <summary> Gets the bit rate of the encoder. </summary> | |||||
| public const int BitRate = 16; | |||||
| /// <summary> Gets the input sampling rate of the encoder. </summary> | |||||
| public int InputSamplingRate { get; private set; } | |||||
| /// <summary> Gets the number of channels of the encoder. </summary> | |||||
| public int InputChannels { get; private set; } | |||||
| /// <summary> Gets the milliseconds per frame. </summary> | |||||
| public int FrameLength { get; private set; } | |||||
| /// <summary> Gets the number of samples per frame. </summary> | |||||
| public int SamplesPerFrame { get; private set; } | |||||
| /// <summary> Gets the bytes per sample. </summary> | |||||
| public int SampleSize { get; private set; } | |||||
| /// <summary> Gets the bytes per frame. </summary> | |||||
| public int FrameSize { get; private set; } | |||||
| /// <summary> Creates a new Opus encoder. </summary> | |||||
| /// <param name="samplingRate">Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000.</param> | |||||
| /// <param name="channels">Number of channels (1 or 2) in input signal.</param> | |||||
| /// <param name="frameLength">Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60</param> | |||||
| /// <param name="application">Coding mode.</param> | |||||
| /// <returns>A new <c>OpusEncoder</c></returns> | |||||
| public OpusDecoder(int samplingRate, int channels, int frameLength) | |||||
| { | |||||
| if (samplingRate != 8000 && samplingRate != 12000 && | |||||
| samplingRate != 16000 && samplingRate != 24000 && | |||||
| samplingRate != 48000) | |||||
| throw new ArgumentOutOfRangeException(nameof(samplingRate)); | |||||
| if (channels != 1 && channels != 2) | |||||
| throw new ArgumentOutOfRangeException(nameof(channels)); | |||||
| InputSamplingRate = samplingRate; | |||||
| InputChannels = channels; | |||||
| FrameLength = frameLength; | |||||
| SampleSize = (BitRate / 8) * channels; | |||||
| SamplesPerFrame = samplingRate / 1000 * FrameLength; | |||||
| FrameSize = SamplesPerFrame * SampleSize; | |||||
| Opus.Error error; | |||||
| _ptr = Opus.CreateDecoder(samplingRate, channels, out error); | |||||
| if (error != Opus.Error.OK) | |||||
| throw new InvalidOperationException($"Error occured while creating decoder: {error}"); | |||||
| SetForwardErrorCorrection(true); | |||||
| } | |||||
| /// <summary> Produces Opus encoded audio from PCM samples. </summary> | |||||
| /// <param name="input">PCM samples to encode.</param> | |||||
| /// <param name="inputOffset">Offset of the frame in pcmSamples.</param> | |||||
| /// <param name="output">Buffer to store the encoded frame.</param> | |||||
| /// <returns>Length of the frame contained in outputBuffer.</returns> | |||||
| public unsafe int DecodeFrame(byte[] input, int inputOffset, byte[] output) | |||||
| { | |||||
| if (disposed) | |||||
| throw new ObjectDisposedException(nameof(OpusDecoder)); | |||||
| int result = 0; | |||||
| fixed (byte* inPtr = input) | |||||
| result = Opus.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); | |||||
| if (result < 0) | |||||
| throw new Exception("Decoding failed: " + ((Opus.Error)result).ToString()); | |||||
| return result; | |||||
| } | |||||
| /// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||||
| public void SetForwardErrorCorrection(bool value) | |||||
| { | |||||
| if (disposed) | |||||
| throw new ObjectDisposedException(nameof(OpusDecoder)); | |||||
| var result = Opus.EncoderCtl(_ptr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0); | |||||
| if (result < 0) | |||||
| throw new Exception("Decoder error: " + ((Opus.Error)result).ToString()); | |||||
| } | |||||
| #region IDisposable | |||||
| private bool disposed; | |||||
| public void Dispose() | |||||
| { | |||||
| if (disposed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| if (_ptr != IntPtr.Zero) | |||||
| Opus.DestroyEncoder(_ptr); | |||||
| disposed = true; | |||||
| } | |||||
| ~OpusDecoder() | |||||
| { | |||||
| Dispose(); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| @@ -5,7 +5,7 @@ namespace Discord.Audio | |||||
| /// <summary> Opus codec wrapper. </summary> | /// <summary> Opus codec wrapper. </summary> | ||||
| internal class OpusEncoder : IDisposable | internal class OpusEncoder : IDisposable | ||||
| { | { | ||||
| private readonly IntPtr _encoderPtr; | |||||
| private readonly IntPtr _ptr; | |||||
| /// <summary> Gets the bit rate of the encoder. </summary> | /// <summary> Gets the bit rate of the encoder. </summary> | ||||
| public const int BitRate = 16; | public const int BitRate = 16; | ||||
| @@ -48,7 +48,7 @@ namespace Discord.Audio | |||||
| FrameSize = SamplesPerFrame * SampleSize; | FrameSize = SamplesPerFrame * SampleSize; | ||||
| Opus.Error error; | Opus.Error error; | ||||
| _encoderPtr = Opus.CreateEncoder(samplingRate, channels, (int)application, out error); | |||||
| _ptr = Opus.CreateEncoder(samplingRate, channels, (int)application, out error); | |||||
| if (error != Opus.Error.OK) | if (error != Opus.Error.OK) | ||||
| throw new InvalidOperationException($"Error occured while creating encoder: {error}"); | throw new InvalidOperationException($"Error occured while creating encoder: {error}"); | ||||
| @@ -56,19 +56,18 @@ namespace Discord.Audio | |||||
| } | } | ||||
| /// <summary> Produces Opus encoded audio from PCM samples. </summary> | /// <summary> Produces Opus encoded audio from PCM samples. </summary> | ||||
| /// <param name="pcmSamples">PCM samples to encode.</param> | |||||
| /// <param name="offset">Offset of the frame in pcmSamples.</param> | |||||
| /// <param name="outputBuffer">Buffer to store the encoded frame.</param> | |||||
| /// <param name="input">PCM samples to encode.</param> | |||||
| /// <param name="inputOffset">Offset of the frame in pcmSamples.</param> | |||||
| /// <param name="output">Buffer to store the encoded frame.</param> | |||||
| /// <returns>Length of the frame contained in outputBuffer.</returns> | /// <returns>Length of the frame contained in outputBuffer.</returns> | ||||
| public unsafe int EncodeFrame(byte[] pcmSamples, int offset, byte[] outputBuffer) | |||||
| public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) | |||||
| { | { | ||||
| if (disposed) | if (disposed) | ||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| throw new ObjectDisposedException(nameof(OpusEncoder)); | |||||
| int result = 0; | int result = 0; | ||||
| fixed (byte* inPtr = pcmSamples) | |||||
| fixed (byte* outPtr = outputBuffer) | |||||
| result = Opus.Encode(_encoderPtr, inPtr + offset, SamplesPerFrame, outPtr, outputBuffer.Length); | |||||
| fixed (byte* inPtr = input) | |||||
| result = Opus.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); | |||||
| if (result < 0) | if (result < 0) | ||||
| throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString()); | throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString()); | ||||
| @@ -79,9 +78,9 @@ namespace Discord.Audio | |||||
| public void SetForwardErrorCorrection(bool value) | public void SetForwardErrorCorrection(bool value) | ||||
| { | { | ||||
| if (disposed) | if (disposed) | ||||
| throw new ObjectDisposedException("OpusEncoder"); | |||||
| throw new ObjectDisposedException(nameof(OpusEncoder)); | |||||
| var result = Opus.EncoderCtl(_encoderPtr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0); | |||||
| var result = Opus.EncoderCtl(_ptr, Opus.Ctl.SetInbandFECRequest, value ? 1 : 0); | |||||
| if (result < 0) | if (result < 0) | ||||
| throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); | throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); | ||||
| } | } | ||||
| @@ -95,8 +94,8 @@ namespace Discord.Audio | |||||
| GC.SuppressFinalize(this); | GC.SuppressFinalize(this); | ||||
| if (_encoderPtr != IntPtr.Zero) | |||||
| Opus.DestroyEncoder(_encoderPtr); | |||||
| if (_ptr != IntPtr.Zero) | |||||
| Opus.DestroyEncoder(_ptr); | |||||
| disposed = true; | disposed = true; | ||||
| } | } | ||||
| @@ -2,14 +2,25 @@ | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| internal static class Sodium | |||||
| { | |||||
| [DllImport("lib/libsodium", EntryPoint = "crypto_stream_xor", CallingConvention = CallingConvention.Cdecl)] | |||||
| private static extern int StreamXOR(byte[] output, byte[] msg, long msgLength, byte[] nonce, byte[] secret); | |||||
| internal unsafe static class Sodium | |||||
| { | |||||
| [DllImport("lib/libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] | |||||
| private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); | |||||
| public static int Encrypt(byte[] buffer, int inputLength, byte[] output, byte[] nonce, byte[] secret) | |||||
| public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) | |||||
| { | { | ||||
| return StreamXOR(output, buffer, inputLength, nonce, secret); | |||||
| fixed (byte* outPtr = output) | |||||
| return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); | |||||
| } | |||||
| [DllImport("lib/libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] | |||||
| private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); | |||||
| public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) | |||||
| { | |||||
| fixed (byte* inPtr = input) | |||||
| return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -28,8 +28,8 @@ namespace Discord | |||||
| internal DisconnectedEventArgs(bool wasUnexpected, Exception error) | internal DisconnectedEventArgs(bool wasUnexpected, Exception error) | ||||
| { | { | ||||
| this.WasUnexpected = wasUnexpected; | |||||
| this.Error = error; | |||||
| WasUnexpected = wasUnexpected; | |||||
| Error = error; | |||||
| } | } | ||||
| } | } | ||||
| public sealed class LogMessageEventArgs : EventArgs | public sealed class LogMessageEventArgs : EventArgs | ||||
| @@ -40,9 +40,9 @@ namespace Discord | |||||
| internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg) | internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg) | ||||
| { | { | ||||
| this.Severity = severity; | |||||
| this.Source = source; | |||||
| this.Message = msg; | |||||
| Severity = severity; | |||||
| Source = source; | |||||
| Message = msg; | |||||
| } | } | ||||
| } | } | ||||
| @@ -51,7 +51,7 @@ namespace Discord | |||||
| public Server Server { get; } | public Server Server { get; } | ||||
| public string ServerId => Server.Id; | public string ServerId => Server.Id; | ||||
| internal ServerEventArgs(Server server) { this.Server = server; } | |||||
| internal ServerEventArgs(Server server) { Server = server; } | |||||
| } | } | ||||
| public sealed class ChannelEventArgs : EventArgs | public sealed class ChannelEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -60,14 +60,14 @@ namespace Discord | |||||
| public Server Server => Channel.Server; | public Server Server => Channel.Server; | ||||
| public string ServerId => Channel.ServerId; | public string ServerId => Channel.ServerId; | ||||
| internal ChannelEventArgs(Channel channel) { this.Channel = channel; } | |||||
| internal ChannelEventArgs(Channel channel) { Channel = channel; } | |||||
| } | } | ||||
| public sealed class UserEventArgs : EventArgs | public sealed class UserEventArgs : EventArgs | ||||
| { | { | ||||
| public User User { get; } | public User User { get; } | ||||
| public string UserId => User.Id; | public string UserId => User.Id; | ||||
| internal UserEventArgs(User user) { this.User = user; } | |||||
| internal UserEventArgs(User user) { User = user; } | |||||
| } | } | ||||
| public sealed class MessageEventArgs : EventArgs | public sealed class MessageEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -81,7 +81,7 @@ namespace Discord | |||||
| public User User => Member.User; | public User User => Member.User; | ||||
| public string UserId => Message.UserId; | public string UserId => Message.UserId; | ||||
| internal MessageEventArgs(Message msg) { this.Message = msg; } | |||||
| internal MessageEventArgs(Message msg) { Message = msg; } | |||||
| } | } | ||||
| public sealed class RoleEventArgs : EventArgs | public sealed class RoleEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -90,7 +90,7 @@ namespace Discord | |||||
| public Server Server => Role.Server; | public Server Server => Role.Server; | ||||
| public string ServerId => Role.ServerId; | public string ServerId => Role.ServerId; | ||||
| internal RoleEventArgs(Role role) { this.Role = role; } | |||||
| internal RoleEventArgs(Role role) { Role = role; } | |||||
| } | } | ||||
| public sealed class BanEventArgs : EventArgs | public sealed class BanEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -101,9 +101,9 @@ namespace Discord | |||||
| internal BanEventArgs(User user, string userId, Server server) | internal BanEventArgs(User user, string userId, Server server) | ||||
| { | { | ||||
| this.User = user; | |||||
| this.UserId = userId; | |||||
| this.Server = server; | |||||
| User = user; | |||||
| UserId = userId; | |||||
| Server = server; | |||||
| } | } | ||||
| } | } | ||||
| public sealed class MemberEventArgs : EventArgs | public sealed class MemberEventArgs : EventArgs | ||||
| @@ -114,7 +114,7 @@ namespace Discord | |||||
| public Server Server => Member.Server; | public Server Server => Member.Server; | ||||
| public string ServerId => Member.ServerId; | public string ServerId => Member.ServerId; | ||||
| internal MemberEventArgs(Member member) { this.Member = member; } | |||||
| internal MemberEventArgs(Member member) { Member = member; } | |||||
| } | } | ||||
| public sealed class UserTypingEventArgs : EventArgs | public sealed class UserTypingEventArgs : EventArgs | ||||
| { | { | ||||
| @@ -127,8 +127,8 @@ namespace Discord | |||||
| internal UserTypingEventArgs(User user, Channel channel) | internal UserTypingEventArgs(User user, Channel channel) | ||||
| { | { | ||||
| this.User = user; | |||||
| this.Channel = channel; | |||||
| User = user; | |||||
| Channel = channel; | |||||
| } | } | ||||
| } | } | ||||
| public sealed class UserIsSpeakingEventArgs : EventArgs | public sealed class UserIsSpeakingEventArgs : EventArgs | ||||
| @@ -144,10 +144,26 @@ namespace Discord | |||||
| internal UserIsSpeakingEventArgs(Member member, bool isSpeaking) | internal UserIsSpeakingEventArgs(Member member, bool isSpeaking) | ||||
| { | { | ||||
| this.Member = member; | |||||
| this.IsSpeaking = isSpeaking; | |||||
| Member = member; | |||||
| IsSpeaking = isSpeaking; | |||||
| } | } | ||||
| } | } | ||||
| public sealed class VoicePacketEventArgs | |||||
| { | |||||
| public string UserId { get; } | |||||
| public string ChannelId { get; } | |||||
| public byte[] Buffer { get; } | |||||
| public int Offset { get; } | |||||
| public int Count { get; } | |||||
| internal VoicePacketEventArgs(string userId, string channelId, byte[] buffer, int offset, int count) | |||||
| { | |||||
| UserId = userId; | |||||
| Buffer = buffer; | |||||
| Offset = offset; | |||||
| Count = count; | |||||
| } | |||||
| } | |||||
| public partial class DiscordClient | public partial class DiscordClient | ||||
| { | { | ||||
| @@ -340,5 +356,12 @@ namespace Discord | |||||
| if (VoiceDisconnected != null) | if (VoiceDisconnected != null) | ||||
| RaiseEvent(nameof(UserIsSpeaking), () => VoiceDisconnected(this, e)); | RaiseEvent(nameof(UserIsSpeaking), () => VoiceDisconnected(this, e)); | ||||
| } | } | ||||
| public event EventHandler<VoicePacketEventArgs> OnVoicePacket; | |||||
| internal void RaiseOnVoicePacket(VoicePacketEventArgs e) | |||||
| { | |||||
| if (OnVoicePacket != null) | |||||
| OnVoicePacket(this, e); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -8,25 +8,26 @@ namespace Discord | |||||
| public partial class DiscordClient | public partial class DiscordClient | ||||
| { | { | ||||
| public Task JoinVoiceServer(Channel channel) | public Task JoinVoiceServer(Channel channel) | ||||
| => JoinVoiceServer(channel.ServerId, channel.Id); | |||||
| public async Task JoinVoiceServer(string serverId, string channelId) | |||||
| => JoinVoiceServer(channel?.Server, channel); | |||||
| public Task JoinVoiceServer(string serverId, string channelId) | |||||
| => JoinVoiceServer(_servers[serverId], _channels[channelId]); | |||||
| public Task JoinVoiceServer(Server server, string channelId) | |||||
| => JoinVoiceServer(server, _channels[channelId]); | |||||
| private async Task JoinVoiceServer(Server server, Channel channel) | |||||
| { | { | ||||
| CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
| if (serverId == null) throw new ArgumentNullException(nameof(serverId)); | |||||
| if (channelId == null) throw new ArgumentNullException(nameof(channelId)); | |||||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||||
| await LeaveVoiceServer().ConfigureAwait(false); | await LeaveVoiceServer().ConfigureAwait(false); | ||||
| _voiceSocket.SetChannel(server, channel); | |||||
| _dataSocket.SendJoinVoice(server.Id, channel.Id); | |||||
| try | try | ||||
| { | { | ||||
| await Task.Run(() => | |||||
| { | |||||
| _voiceSocket.SetServer(serverId); | |||||
| _dataSocket.SendJoinVoice(serverId, channelId); | |||||
| _voiceSocket.WaitForConnection(); | |||||
| }) | |||||
| .Timeout(_config.ConnectionTimeout) | |||||
| .ConfigureAwait(false); | |||||
| await Task.Run(() => _voiceSocket.WaitForConnection()) | |||||
| .Timeout(_config.ConnectionTimeout) | |||||
| .ConfigureAwait(false); | |||||
| } | } | ||||
| catch (TaskCanceledException) | catch (TaskCanceledException) | ||||
| { | { | ||||
| @@ -39,11 +40,11 @@ namespace Discord | |||||
| if (_voiceSocket.State != WebSocketState.Disconnected) | if (_voiceSocket.State != WebSocketState.Disconnected) | ||||
| { | { | ||||
| var serverId = _voiceSocket.CurrentVoiceServerId; | |||||
| if (serverId != null) | |||||
| var server = _voiceSocket.CurrentVoiceServer; | |||||
| if (server != null) | |||||
| { | { | ||||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
| _dataSocket.SendLeaveVoice(serverId); | |||||
| _dataSocket.SendLeaveVoice(server.Id); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -45,10 +45,8 @@ namespace Discord | |||||
| /// <summary> Returns the current logged-in user. </summary> | /// <summary> Returns the current logged-in user. </summary> | ||||
| public User CurrentUser => _currentUser; | public User CurrentUser => _currentUser; | ||||
| private User _currentUser; | private User _currentUser; | ||||
| /// <summary> Returns the id of the server this user is currently connected to for voice. </summary> | |||||
| public string CurrentVoiceServerId => _voiceSocket.CurrentVoiceServerId; | |||||
| /// <summary> Returns the server this user is currently connected to for voice. </summary> | /// <summary> Returns the server this user is currently connected to for voice. </summary> | ||||
| public Server CurrentVoiceServer => _servers[_voiceSocket.CurrentVoiceServerId]; | |||||
| public Server CurrentVoiceServer => _voiceSocket.CurrentVoiceServer; | |||||
| /// <summary> Returns the current connection state of this client. </summary> | /// <summary> Returns the current connection state of this client. </summary> | ||||
| public DiscordClientState State => (DiscordClientState)_state; | public DiscordClientState State => (DiscordClientState)_state; | ||||
| @@ -103,7 +101,7 @@ namespace Discord | |||||
| if (e.WasUnexpected) | if (e.WasUnexpected) | ||||
| await _dataSocket.Reconnect(_token); | await _dataSocket.Reconnect(_token); | ||||
| }; | }; | ||||
| if (_config.EnableVoice) | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | { | ||||
| _voiceSocket = new VoiceWebSocket(this); | _voiceSocket = new VoiceWebSocket(this); | ||||
| _voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | _voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | ||||
| @@ -125,7 +123,7 @@ namespace Discord | |||||
| { | { | ||||
| if (_voiceSocket.State == WebSocketState.Connected) | if (_voiceSocket.State == WebSocketState.Connected) | ||||
| { | { | ||||
| var member = _members[e.UserId, _voiceSocket.CurrentVoiceServerId]; | |||||
| var member = _members[e.UserId, _voiceSocket.CurrentVoiceServer.Id]; | |||||
| bool value = e.IsSpeaking; | bool value = e.IsSpeaking; | ||||
| if (member.IsSpeaking != value) | if (member.IsSpeaking != value) | ||||
| { | { | ||||
| @@ -147,14 +145,14 @@ namespace Discord | |||||
| _users = new Users(this, cacheLock); | _users = new Users(this, cacheLock); | ||||
| _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | _dataSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.DataWebSocket, e.Message); | ||||
| if (_config.EnableVoice) | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | ||||
| if (_config.LogLevel >= LogMessageSeverity.Info) | if (_config.LogLevel >= LogMessageSeverity.Info) | ||||
| { | { | ||||
| _dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | _dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | ||||
| _dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | _dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | ||||
| //_dataSocket.ReceivedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Received {e.Type}"); | //_dataSocket.ReceivedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Received {e.Type}"); | ||||
| if (_config.EnableVoice) | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | { | ||||
| _voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected"); | _voiceSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Connected"); | ||||
| _voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected"); | _voiceSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected"); | ||||
| @@ -535,28 +533,6 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| case "VOICE_STATE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||||
| var member = _members[data.UserId, data.GuildId]; | |||||
| /*if (_config.TrackActivity) | |||||
| { | |||||
| var user = _users[data.User.Id]; | |||||
| if (user != null) | |||||
| user.UpdateActivity(DateTime.UtcNow); | |||||
| }*/ | |||||
| if (member != null) | |||||
| { | |||||
| member.Update(data); | |||||
| if (member.IsSpeaking) | |||||
| { | |||||
| member.IsSpeaking = false; | |||||
| RaiseUserIsSpeaking(member, false); | |||||
| } | |||||
| RaiseUserVoiceStateUpdated(member); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "TYPING_START": | case "TYPING_START": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<TypingStartEvent>(_serializer); | var data = e.Payload.ToObject<TypingStartEvent>(_serializer); | ||||
| @@ -586,13 +562,35 @@ namespace Discord | |||||
| break; | break; | ||||
| //Voice | //Voice | ||||
| case "VOICE_STATE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||||
| var member = _members[data.UserId, data.GuildId]; | |||||
| /*if (_config.TrackActivity) | |||||
| { | |||||
| var user = _users[data.User.Id]; | |||||
| if (user != null) | |||||
| user.UpdateActivity(DateTime.UtcNow); | |||||
| }*/ | |||||
| if (member != null) | |||||
| { | |||||
| member.Update(data); | |||||
| if (member.IsSpeaking) | |||||
| { | |||||
| member.IsSpeaking = false; | |||||
| RaiseUserIsSpeaking(member, false); | |||||
| } | |||||
| RaiseUserVoiceStateUpdated(member); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "VOICE_SERVER_UPDATE": | case "VOICE_SERVER_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | ||||
| if (data.GuildId == _voiceSocket.CurrentVoiceServerId) | |||||
| if (data.GuildId == _voiceSocket.CurrentVoiceServer.Id) | |||||
| { | { | ||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| if (_config.EnableVoice) | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| { | { | ||||
| _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | ||||
| await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | ||||
| @@ -770,7 +768,7 @@ namespace Discord | |||||
| _wasDisconnectUnexpected = false; | _wasDisconnectUnexpected = false; | ||||
| await _dataSocket.Disconnect().ConfigureAwait(false); | await _dataSocket.Disconnect().ConfigureAwait(false); | ||||
| if (_config.EnableVoice) | |||||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
| if (_config.UseMessageQueue) | if (_config.UseMessageQueue) | ||||
| @@ -817,7 +815,7 @@ namespace Discord | |||||
| throw new InvalidOperationException("The client is connecting."); | throw new InvalidOperationException("The client is connecting."); | ||||
| } | } | ||||
| if (checkVoice && !_config.EnableVoice) | |||||
| if (checkVoice && _config.VoiceMode == DiscordVoiceMode.Disabled) | |||||
| throw new InvalidOperationException("Voice is not enabled for this client."); | throw new InvalidOperationException("Voice is not enabled for this client."); | ||||
| } | } | ||||
| private void RaiseEvent(string name, Action action) | private void RaiseEvent(string name, Action action) | ||||
| @@ -2,6 +2,15 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [Flags] | |||||
| public enum DiscordVoiceMode | |||||
| { | |||||
| Disabled = 0x00, | |||||
| Incoming = 0x01, | |||||
| Outgoing = 0x02, | |||||
| Both = Outgoing | Incoming | |||||
| } | |||||
| public class DiscordClientConfig | public class DiscordClientConfig | ||||
| { | { | ||||
| /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | ||||
| @@ -33,15 +42,19 @@ namespace Discord | |||||
| //Experimental Features | //Experimental Features | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| /// <summary> (Experimental) Enables the voice websocket and UDP client. This option requires the opus .dll or .so be in the local lib/ folder. </summary> | |||||
| public bool EnableVoice { get { return _enableVoice; } set { SetValue(ref _enableVoice, value); } } | |||||
| private bool _enableVoice = false; | |||||
| /// <summary> (Experimental) Enables the voice websocket and UDP client and specifies how it will be used. Any option other than Disabled requires the opus .dll or .so be in the local lib/ folder. </summary> | |||||
| public DiscordVoiceMode VoiceMode { get { return _voiceMode; } set { SetValue(ref _voiceMode, value); } } | |||||
| private DiscordVoiceMode _voiceMode = DiscordVoiceMode.Disabled; | |||||
| /// <summary> (Experimental) Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local lib/ folder. </summary> | /// <summary> (Experimental) Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local lib/ folder. </summary> | ||||
| public bool EnableVoiceEncryption { get { return _enableVoiceEncryption; } set { SetValue(ref _enableVoiceEncryption, value); } } | public bool EnableVoiceEncryption { get { return _enableVoiceEncryption; } set { SetValue(ref _enableVoiceEncryption, value); } } | ||||
| private bool _enableVoiceEncryption = false; | |||||
| private bool _enableVoiceEncryption = true; | |||||
| /// <summary> (Experimental) Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). </summary> | |||||
| public bool EnableVoiceMultiserver { get { return _enableVoiceMultiserver; } set { SetValue(ref _enableVoiceMultiserver, value); } } | |||||
| private bool _enableVoiceMultiserver = false; | |||||
| #else | #else | ||||
| internal bool EnableVoice => false; | |||||
| internal DiscordVoiceMode VoiceMode => DiscordVoiceMode.Disabled; | |||||
| internal bool EnableVoiceEncryption => false; | internal bool EnableVoiceEncryption => false; | ||||
| internal bool EnableVoiceMultiserver => false; | |||||
| #endif | #endif | ||||
| /// <summary> (Experimental) Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. </summary> | /// <summary> (Experimental) Enables or disables the internal message queue. This will allow SendMessage to return immediately and handle messages internally. Messages will set the IsQueued and HasFailed properties to show their progress. </summary> | ||||
| public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } | public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } | ||||
| @@ -17,6 +17,7 @@ namespace Discord | |||||
| private readonly DiscordClient _client; | private readonly DiscordClient _client; | ||||
| private ConcurrentDictionary<string, bool> _messages; | private ConcurrentDictionary<string, bool> _messages; | ||||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||||
| /// <summary> Returns the unique identifier for this channel. </summary> | /// <summary> Returns the unique identifier for this channel. </summary> | ||||
| public string Id { get; } | public string Id { get; } | ||||
| @@ -69,6 +70,8 @@ namespace Discord | |||||
| { | { | ||||
| Name = model.Name; | Name = model.Name; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| if (Type == ChannelTypes.Voice && _ssrcMapping == null) | |||||
| _ssrcMapping = new ConcurrentDictionary<uint, string>(); | |||||
| } | } | ||||
| internal void Update(API.ChannelInfo model) | internal void Update(API.ChannelInfo model) | ||||
| { | { | ||||
| @@ -101,5 +104,12 @@ namespace Discord | |||||
| bool ignored; | bool ignored; | ||||
| return _messages.TryRemove(messageId, out ignored); | return _messages.TryRemove(messageId, out ignored); | ||||
| } | } | ||||
| internal string GetUserId(uint ssrc) | |||||
| { | |||||
| string userId = null; | |||||
| _ssrcMapping.TryGetValue(ssrc, out userId); | |||||
| return userId; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -21,5 +21,12 @@ namespace Discord.WebSockets.Voice | |||||
| if (IsSpeaking != null) | if (IsSpeaking != null) | ||||
| IsSpeaking(this, new IsTalkingEventArgs(userId, isSpeaking)); | IsSpeaking(this, new IsTalkingEventArgs(userId, isSpeaking)); | ||||
| } | } | ||||
| public event EventHandler<VoicePacketEventArgs> OnPacket; | |||||
| internal void RaiseOnPacket(string userId, string channelId, byte[] buffer, int offset, int count) | |||||
| { | |||||
| if (OnPacket != null) | |||||
| OnPacket(this, new VoicePacketEventArgs(userId, channelId, buffer, offset, count)); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.Audio; | |||||
| #define USE_THREAD | |||||
| using Discord.Audio; | |||||
| using Discord.Helpers; | using Discord.Helpers; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| @@ -16,46 +17,51 @@ namespace Discord.WebSockets.Voice | |||||
| { | { | ||||
| internal partial class VoiceWebSocket : WebSocket | internal partial class VoiceWebSocket : WebSocket | ||||
| { | { | ||||
| private const string EncryptedMode = "xsalsa20_poly1305"; | |||||
| private const int MaxOpusSize = 4000; | |||||
| private const string EncryptedMode = "xsalsa20_poly1305"; | |||||
| private const string UnencryptedMode = "plain"; | private const string UnencryptedMode = "plain"; | ||||
| private readonly int _targetAudioBufferLength; | |||||
| private readonly Random _rand; | |||||
| private readonly int _targetAudioBufferLength; | |||||
| private OpusEncoder _encoder; | |||||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||||
| private ManualResetEventSlim _connectWaitOnLogin; | private ManualResetEventSlim _connectWaitOnLogin; | ||||
| private uint _ssrc; | private uint _ssrc; | ||||
| private readonly Random _rand = new Random(); | |||||
| private OpusEncoder _encoder; | |||||
| private ConcurrentQueue<byte[]> _sendQueue; | private ConcurrentQueue<byte[]> _sendQueue; | ||||
| private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | ||||
| private UdpClient _udp; | private UdpClient _udp; | ||||
| private IPEndPoint _endpoint; | private IPEndPoint _endpoint; | ||||
| private bool _isClearing, _isEncrypted; | private bool _isClearing, _isEncrypted; | ||||
| private byte[] _secretKey; | |||||
| private byte[] _secretKey, _encodingBuffer; | |||||
| private ushort _sequence; | private ushort _sequence; | ||||
| private byte[] _encodingBuffer; | |||||
| private string _serverId, _userId, _sessionId, _token, _encryptionMode; | |||||
| private string _userId, _sessionId, _token, _encryptionMode; | |||||
| private Server _server; | |||||
| private Channel _channel; | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| private Thread _sendThread; | private Thread _sendThread; | ||||
| #endif | #endif | ||||
| public string CurrentVoiceServerId => _serverId; | |||||
| public Server CurrentVoiceServer => _server; | |||||
| public VoiceWebSocket(DiscordClient client) | public VoiceWebSocket(DiscordClient client) | ||||
| : base(client) | : base(client) | ||||
| { | { | ||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||||
| _rand = new Random(); | |||||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||||
| _decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||||
| _sendQueue = new ConcurrentQueue<byte[]>(); | _sendQueue = new ConcurrentQueue<byte[]>(); | ||||
| _sendQueueWait = new ManualResetEventSlim(true); | _sendQueueWait = new ManualResetEventSlim(true); | ||||
| _sendQueueEmptyWait = new ManualResetEventSlim(true); | _sendQueueEmptyWait = new ManualResetEventSlim(true); | ||||
| _encoder = new OpusEncoder(48000, 1, 20, Opus.Application.Audio); | |||||
| _encodingBuffer = new byte[4000]; | |||||
| _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | ||||
| } | |||||
| _encodingBuffer = new byte[MaxOpusSize]; | |||||
| } | |||||
| public void SetServer(string serverId) | |||||
| public void SetChannel(Server server, Channel channel) | |||||
| { | { | ||||
| _serverId = serverId; | |||||
| _server = server; | |||||
| _channel = channel; | |||||
| } | } | ||||
| public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken) | public async Task Login(string userId, string sessionId, string token, CancellationToken cancelToken) | ||||
| { | { | ||||
| @@ -69,7 +75,7 @@ namespace Discord.WebSockets.Voice | |||||
| _userId = userId; | _userId = userId; | ||||
| _sessionId = sessionId; | _sessionId = sessionId; | ||||
| _token = token; | _token = token; | ||||
| await Connect().ConfigureAwait(false); | await Connect().ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task Reconnect() | public async Task Reconnect() | ||||
| @@ -107,26 +113,29 @@ namespace Discord.WebSockets.Voice | |||||
| #endif | #endif | ||||
| LoginCommand msg = new LoginCommand(); | LoginCommand msg = new LoginCommand(); | ||||
| msg.Payload.ServerId = _serverId; | |||||
| msg.Payload.ServerId = _server.Id; | |||||
| msg.Payload.SessionId = _sessionId; | msg.Payload.SessionId = _sessionId; | ||||
| msg.Payload.Token = _token; | msg.Payload.Token = _token; | ||||
| msg.Payload.UserId = _userId; | msg.Payload.UserId = _userId; | ||||
| QueueMessage(msg); | QueueMessage(msg); | ||||
| #if USE_THREAD | #if USE_THREAD | ||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_disconnectToken))); | |||||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | |||||
| _sendThread.Start(); | _sendThread.Start(); | ||||
| #if !DNXCORE50 | |||||
| return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray(); | |||||
| #else | |||||
| return base.Run(); | |||||
| #endif | #endif | ||||
| return new Task[] | |||||
| { | |||||
| #else //!USE_THREAD | |||||
| return new Task[] { Task.WhenAll( | |||||
| ReceiveVoiceAsync(), | ReceiveVoiceAsync(), | ||||
| #if !USE_THREAD | |||||
| SendVoiceAsync(), | SendVoiceAsync(), | ||||
| #endif | |||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| WatcherAsync() | WatcherAsync() | ||||
| #endif | #endif | ||||
| }.Concat(base.Run()).ToArray(); | |||||
| )}.Concat(base.Run()).ToArray(); | |||||
| #endif | |||||
| } | } | ||||
| protected override Task Cleanup() | protected override Task Cleanup() | ||||
| { | { | ||||
| @@ -134,6 +143,13 @@ namespace Discord.WebSockets.Voice | |||||
| _sendThread.Join(); | _sendThread.Join(); | ||||
| _sendThread = null; | _sendThread = null; | ||||
| #endif | #endif | ||||
| OpusDecoder decoder; | |||||
| foreach (var pair in _decoders) | |||||
| { | |||||
| if (_decoders.TryRemove(pair.Key, out decoder)) | |||||
| decoder.Dispose(); | |||||
| } | |||||
| ClearPCMFrames(); | ClearPCMFrames(); | ||||
| if (!_wasDisconnectUnexpected) | if (!_wasDisconnectUnexpected) | ||||
| @@ -147,39 +163,137 @@ namespace Discord.WebSockets.Voice | |||||
| return base.Cleanup(); | return base.Cleanup(); | ||||
| } | } | ||||
| private async Task ReceiveVoiceAsync() | |||||
| #if USE_THREAD | |||||
| private void ReceiveVoiceAsync(CancellationToken cancelToken) | |||||
| { | |||||
| #else | |||||
| private Task ReceiveVoiceAsync() | |||||
| { | { | ||||
| var cancelToken = _cancelToken; | var cancelToken = _cancelToken; | ||||
| await Task.Run(async () => | |||||
| return Task.Run(async () => | |||||
| { | |||||
| #endif | |||||
| try | |||||
| { | { | ||||
| try | |||||
| byte[] packet, decodingBuffer = null, nonce = null, result; | |||||
| int packetLength, resultOffset, resultLength; | |||||
| IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | |||||
| if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) != 0) | |||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested) | |||||
| decodingBuffer = new byte[MaxOpusSize]; | |||||
| nonce = new byte[24]; | |||||
| } | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| #if USE_THREAD | |||||
| Thread.Sleep(1); | |||||
| #elif DNXCORE50 | |||||
| await Task.Delay(1).ConfigureAwait(false); | |||||
| #endif | |||||
| #if USE_THREAD || DNXCORE50 | |||||
| if (_udp.Available > 0) | |||||
| { | { | ||||
| #if DNXCORE50 | |||||
| if (_udp.Available > 0) | |||||
| { | |||||
| packet = _udp.Receive(ref endpoint); | |||||
| #else | |||||
| var msg = await _udp.ReceiveAsync().ConfigureAwait(false); | |||||
| endpoint = msg.Endpoint; | |||||
| receievedPacket = msg.Buffer; | |||||
| #endif | #endif | ||||
| var result = await _udp.ReceiveAsync().ConfigureAwait(false); | |||||
| ProcessUdpMessage(result); | |||||
| #if DNXCORE50 | |||||
| packetLength = packet.Length; | |||||
| if (packetLength > 0 && endpoint.Equals(_endpoint)) | |||||
| { | |||||
| if (_state != (int)WebSocketState.Connected) | |||||
| { | |||||
| if (packetLength != 70) | |||||
| return; | |||||
| int port = packet[68] | packet[69] << 8; | |||||
| string ip = Encoding.ASCII.GetString(packet, 4, 70 - 6).TrimEnd('\0'); | |||||
| CompleteConnect(); | |||||
| var login2 = new Login2Command(); | |||||
| login2.Payload.Protocol = "udp"; | |||||
| login2.Payload.SocketData.Address = ip; | |||||
| login2.Payload.SocketData.Mode = _encryptionMode; | |||||
| login2.Payload.SocketData.Port = port; | |||||
| QueueMessage(login2); | |||||
| if ((_client.Config.VoiceMode & DiscordVoiceMode.Incoming) == 0) | |||||
| return; | |||||
| } | |||||
| else | |||||
| { | |||||
| //Parse RTP Data | |||||
| if (packetLength < 12) | |||||
| return; | |||||
| byte flags = packet[0]; | |||||
| if (flags != 0x80) | |||||
| return; | |||||
| byte payloadType = packet[1]; | |||||
| if (payloadType != 0x78) | |||||
| return; | |||||
| ushort sequenceNumber = (ushort)((packet[2] << 8) | | |||||
| packet[3] << 0); | |||||
| uint timestamp = (uint)((packet[4] << 24) | | |||||
| (packet[5] << 16) | | |||||
| (packet[6] << 8) | | |||||
| (packet[7] << 0)); | |||||
| uint ssrc = (uint)((packet[8] << 24) | | |||||
| (packet[9] << 16) | | |||||
| (packet[10] << 8) | | |||||
| (packet[11] << 0)); | |||||
| //Decrypt | |||||
| if (_isEncrypted) | |||||
| { | |||||
| if (packetLength < 28) //12 + 16 (RTP + Poly1305 MAC) | |||||
| return; | |||||
| Buffer.BlockCopy(packet, 0, nonce, 0, 12); | |||||
| int ret = Sodium.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey); | |||||
| if (ret != 0) | |||||
| continue; | |||||
| result = decodingBuffer; | |||||
| resultOffset = 0; | |||||
| resultLength = packetLength - 28; | |||||
| } | |||||
| else //Plain | |||||
| { | |||||
| result = packet; | |||||
| resultOffset = 12; | |||||
| resultLength = packetLength - 12; | |||||
| } | |||||
| /*if (_logLevel >= LogMessageSeverity.Debug) | |||||
| RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/ | |||||
| string userId = _channel.GetUserId(ssrc); | |||||
| if (userId != null) | |||||
| RaiseOnPacket(userId, _channel.Id, result, resultOffset, resultLength); | |||||
| } | |||||
| } | } | ||||
| else | |||||
| await Task.Delay(1).ConfigureAwait(false); | |||||
| #endif | |||||
| #if USE_THREAD || DNXCORE50 | |||||
| } | } | ||||
| #endif | |||||
| } | } | ||||
| catch (OperationCanceledException) { } | |||||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||||
| catch (Exception ex) { await DisconnectInternal(ex); } | |||||
| } | |||||
| catch (OperationCanceledException) { } | |||||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||||
| #if !USE_THREAD | |||||
| }).ConfigureAwait(false); | }).ConfigureAwait(false); | ||||
| #endif | |||||
| } | } | ||||
| #if USE_THREAD | #if USE_THREAD | ||||
| private void SendVoiceAsync(CancellationTokenSource cancelSource) | |||||
| private void SendVoiceAsync(CancellationToken cancelToken) | |||||
| { | { | ||||
| var cancelToken = cancelSource.Token; | |||||
| #else | #else | ||||
| private Task SendVoiceAsync() | private Task SendVoiceAsync() | ||||
| { | { | ||||
| @@ -189,103 +303,114 @@ namespace Discord.WebSockets.Voice | |||||
| { | { | ||||
| #endif | #endif | ||||
| byte[] packet; | |||||
| try | |||||
| try | |||||
| { | |||||
| while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||||
| { | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| Thread.Sleep(1); | |||||
| Thread.Sleep(1); | |||||
| #else | #else | ||||
| await Task.Delay(1); | |||||
| await Task.Delay(1); | |||||
| #endif | #endif | ||||
| } | |||||
| } | |||||
| if (cancelToken.IsCancellationRequested) | |||||
| return; | |||||
| uint timestamp = 0; | |||||
| double nextTicks = 0.0; | |||||
| double ticksPerMillisecond = Stopwatch.Frequency / 1000.0; | |||||
| double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; | |||||
| double spinLockThreshold = 1.5 * ticksPerMillisecond; | |||||
| uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||||
| Stopwatch sw = Stopwatch.StartNew(); | |||||
| byte[] rtpPacket = new byte[_encodingBuffer.Length + 12]; | |||||
| byte[] nonce = null; | |||||
| rtpPacket[0] = 0x80; //Flags; | |||||
| rtpPacket[1] = 0x78; //Payload Type | |||||
| rtpPacket[8] = (byte)((_ssrc >> 24) & 0xFF); | |||||
| rtpPacket[9] = (byte)((_ssrc >> 16) & 0xFF); | |||||
| rtpPacket[10] = (byte)((_ssrc >> 8) & 0xFF); | |||||
| rtpPacket[11] = (byte)((_ssrc >> 0) & 0xFF); | |||||
| if (_isEncrypted) | |||||
| { | |||||
| nonce = new byte[24]; | |||||
| Buffer.BlockCopy(rtpPacket, 0, nonce, 0, 12); | |||||
| } | |||||
| if (cancelToken.IsCancellationRequested) | |||||
| return; | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| byte[] queuedPacket, result, nonce = null; | |||||
| uint timestamp = 0; | |||||
| double nextTicks = 0.0; | |||||
| double ticksPerMillisecond = Stopwatch.Frequency / 1000.0; | |||||
| double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; | |||||
| double spinLockThreshold = 1.5 * ticksPerMillisecond; | |||||
| uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||||
| Stopwatch sw = Stopwatch.StartNew(); | |||||
| if (_isEncrypted) | |||||
| { | |||||
| nonce = new byte[24]; | |||||
| result = new byte[MaxOpusSize + 12 + 16]; | |||||
| } | |||||
| else | |||||
| result = new byte[MaxOpusSize + 12]; | |||||
| int rtpPacketLength = 0; | |||||
| result[0] = 0x80; //Flags; | |||||
| result[1] = 0x78; //Payload Type | |||||
| result[8] = (byte)((_ssrc >> 24) & 0xFF); | |||||
| result[9] = (byte)((_ssrc >> 16) & 0xFF); | |||||
| result[10] = (byte)((_ssrc >> 8) & 0xFF); | |||||
| result[11] = (byte)((_ssrc >> 0) & 0xFF); | |||||
| if (_isEncrypted) | |||||
| Buffer.BlockCopy(result, 0, nonce, 0, 12); | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| double ticksToNextFrame = nextTicks - sw.ElapsedTicks; | |||||
| if (ticksToNextFrame <= 0.0) | |||||
| { | { | ||||
| double ticksToNextFrame = nextTicks - sw.ElapsedTicks; | |||||
| if (ticksToNextFrame <= 0.0) | |||||
| while (sw.ElapsedTicks > nextTicks) | |||||
| { | { | ||||
| while (sw.ElapsedTicks > nextTicks) | |||||
| if (!_isClearing) | |||||
| { | { | ||||
| if (!_isClearing) | |||||
| if (_sendQueue.TryDequeue(out queuedPacket)) | |||||
| { | { | ||||
| if (_sendQueue.TryDequeue(out packet)) | |||||
| ushort sequence = unchecked(_sequence++); | |||||
| result[2] = (byte)((sequence >> 8) & 0xFF); | |||||
| result[3] = (byte)((sequence >> 0) & 0xFF); | |||||
| result[4] = (byte)((timestamp >> 24) & 0xFF); | |||||
| result[5] = (byte)((timestamp >> 16) & 0xFF); | |||||
| result[6] = (byte)((timestamp >> 8) & 0xFF); | |||||
| result[7] = (byte)((timestamp >> 0) & 0xFF); | |||||
| if (_isEncrypted) | |||||
| { | |||||
| Buffer.BlockCopy(result, 2, nonce, 2, 6); //Update nonce | |||||
| int ret = Sodium.Encrypt(queuedPacket, queuedPacket.Length, result, 12, nonce, _secretKey); | |||||
| if (ret != 0) | |||||
| continue; | |||||
| rtpPacketLength = queuedPacket.Length + 12 + 16; | |||||
| } | |||||
| else | |||||
| { | { | ||||
| ushort sequence = unchecked(_sequence++); | |||||
| rtpPacket[2] = (byte)((sequence >> 8) & 0xFF); | |||||
| rtpPacket[3] = (byte)((sequence >> 0) & 0xFF); | |||||
| rtpPacket[4] = (byte)((timestamp >> 24) & 0xFF); | |||||
| rtpPacket[5] = (byte)((timestamp >> 16) & 0xFF); | |||||
| rtpPacket[6] = (byte)((timestamp >> 8) & 0xFF); | |||||
| rtpPacket[7] = (byte)((timestamp >> 0) & 0xFF); | |||||
| if (_isEncrypted) | |||||
| { | |||||
| Buffer.BlockCopy(rtpPacket, 2, nonce, 2, 6); //Update nonce | |||||
| int ret = Sodium.Encrypt(packet, packet.Length, packet, nonce, _secretKey); | |||||
| if (ret != 0) | |||||
| continue; | |||||
| } | |||||
| Buffer.BlockCopy(packet, 0, rtpPacket, 12, packet.Length); | |||||
| Buffer.BlockCopy(queuedPacket, 0, result, 12, queuedPacket.Length); | |||||
| rtpPacketLength = queuedPacket.Length + 12; | |||||
| } | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| _udp.Send(rtpPacket, packet.Length + 12); | |||||
| _udp.Send(result, rtpPacketLength); | |||||
| #else | #else | ||||
| await _udp.SendAsync(rtpPacket, packet.Length + 12).ConfigureAwait(false); | |||||
| await _udp.SendAsync(rtpPacket, rtpPacketLength).ConfigureAwait(false); | |||||
| #endif | #endif | ||||
| } | |||||
| timestamp = unchecked(timestamp + samplesPerFrame); | |||||
| nextTicks += ticksPerFrame; | |||||
| } | |||||
| timestamp = unchecked(timestamp + samplesPerFrame); | |||||
| nextTicks += ticksPerFrame; | |||||
| //If we have less than our target data buffered, request more | |||||
| int count = _sendQueue.Count; | |||||
| if (count == 0) | |||||
| { | |||||
| _sendQueueWait.Set(); | |||||
| _sendQueueEmptyWait.Set(); | |||||
| } | |||||
| else if (count < _targetAudioBufferLength) | |||||
| _sendQueueWait.Set(); | |||||
| //If we have less than our target data buffered, request more | |||||
| int count = _sendQueue.Count; | |||||
| if (count == 0) | |||||
| { | |||||
| _sendQueueWait.Set(); | |||||
| _sendQueueEmptyWait.Set(); | |||||
| } | } | ||||
| else if (count < _targetAudioBufferLength) | |||||
| _sendQueueWait.Set(); | |||||
| } | } | ||||
| } | } | ||||
| //Dont sleep for 1 millisecond if we need to output audio in the next 1.5 | |||||
| else if (_sendQueue.Count == 0 || ticksToNextFrame >= spinLockThreshold) | |||||
| } | |||||
| //Dont sleep for 1 millisecond if we need to output audio in the next 1.5 | |||||
| else if (_sendQueue.Count == 0 || ticksToNextFrame >= spinLockThreshold) | |||||
| #if USE_THREAD | #if USE_THREAD | ||||
| Thread.Sleep(1); | Thread.Sleep(1); | ||||
| #else | #else | ||||
| await Task.Delay(1).ConfigureAwait(false); | |||||
| await Task.Delay(1).ConfigureAwait(false); | |||||
| #endif | #endif | ||||
| } | |||||
| } | } | ||||
| catch (OperationCanceledException) { } | |||||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||||
| } | |||||
| catch (OperationCanceledException) { } | |||||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||||
| #if !USE_THREAD | #if !USE_THREAD | ||||
| }); | |||||
| }).ConfigureAwait(false); | |||||
| #endif | #endif | ||||
| } | } | ||||
| #if !DNXCORE50 | #if !DNXCORE50 | ||||
| @@ -331,16 +456,12 @@ namespace Discord.WebSockets.Voice | |||||
| _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | ||||
| //No thread issue here because SendAsync doesn't start until _isReady is true | //No thread issue here because SendAsync doesn't start until _isReady is true | ||||
| await _udp.SendAsync(new byte[70] { | |||||
| (byte)((_ssrc >> 24) & 0xFF), | |||||
| (byte)((_ssrc >> 16) & 0xFF), | |||||
| (byte)((_ssrc >> 8) & 0xFF), | |||||
| (byte)((_ssrc >> 0) & 0xFF), | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | |||||
| 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, 70).ConfigureAwait(false); | |||||
| byte[] packet = new byte[70]; | |||||
| packet[0] = (byte)((_ssrc >> 24) & 0xFF); | |||||
| packet[1] = (byte)((_ssrc >> 16) & 0xFF); | |||||
| packet[2] = (byte)((_ssrc >> 8) & 0xFF); | |||||
| packet[3] = (byte)((_ssrc >> 0) & 0xFF); | |||||
| await _udp.SendAsync(packet, 70).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -365,98 +486,6 @@ namespace Discord.WebSockets.Voice | |||||
| } | } | ||||
| } | } | ||||
| private void ProcessUdpMessage(UdpReceiveResult msg) | |||||
| { | |||||
| if (msg.Buffer.Length > 0 && msg.RemoteEndPoint.Equals(_endpoint)) | |||||
| { | |||||
| byte[] buffer = msg.Buffer; | |||||
| int length = msg.Buffer.Length; | |||||
| if (_state != (int)WebSocketState.Connected) | |||||
| { | |||||
| if (length != 70) | |||||
| { | |||||
| if (_logLevel >= LogMessageSeverity.Warning) | |||||
| RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected message length. Expected 70, got {length}."); | |||||
| return; | |||||
| } | |||||
| int port = buffer[68] | buffer[69] << 8; | |||||
| string ip = Encoding.ASCII.GetString(buffer, 4, 70 - 6).TrimEnd('\0'); | |||||
| CompleteConnect(); | |||||
| var login2 = new Login2Command(); | |||||
| login2.Payload.Protocol = "udp"; | |||||
| login2.Payload.SocketData.Address = ip; | |||||
| login2.Payload.SocketData.Mode = _encryptionMode; | |||||
| login2.Payload.SocketData.Port = port; | |||||
| QueueMessage(login2); | |||||
| } | |||||
| else | |||||
| { | |||||
| //Parse RTP Data | |||||
| if (length < 12) | |||||
| { | |||||
| if (_logLevel >= LogMessageSeverity.Warning) | |||||
| RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected message length. Expected >= 12, got {length}."); | |||||
| return; | |||||
| } | |||||
| byte flags = buffer[0]; | |||||
| if (flags != 0x80) | |||||
| { | |||||
| if (_logLevel >= LogMessageSeverity.Warning) | |||||
| RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected Flags: {flags}"); | |||||
| return; | |||||
| } | |||||
| byte payloadType = buffer[1]; | |||||
| if (payloadType != 0x78) | |||||
| { | |||||
| if (_logLevel >= LogMessageSeverity.Warning) | |||||
| RaiseOnLog(LogMessageSeverity.Warning, $"Unexpected Payload Type: {payloadType}"); | |||||
| return; | |||||
| } | |||||
| ushort sequenceNumber = (ushort)((buffer[2] << 8) | | |||||
| buffer[3] << 0); | |||||
| uint timestamp = (uint)((buffer[4] << 24) | | |||||
| (buffer[5] << 16) | | |||||
| (buffer[6] << 8) | | |||||
| (buffer[7] << 0)); | |||||
| uint ssrc = (uint)((buffer[8] << 24) | | |||||
| (buffer[9] << 16) | | |||||
| (buffer[10] << 8) | | |||||
| (buffer[11] << 0)); | |||||
| //Decrypt | |||||
| /*if (_mode == "xsalsa20_poly1305") | |||||
| { | |||||
| if (length < 36) //12 + 24 | |||||
| throw new Exception($"Unexpected message length. Expected >= 36, got {length}."); | |||||
| byte[] nonce = new byte[24]; //16 bytes static, 8 bytes incrementing? | |||||
| Buffer.BlockCopy(buffer, 12, nonce, 0, 24); | |||||
| byte[] cipherText = new byte[buffer.Length - 36]; | |||||
| Buffer.BlockCopy(buffer, 36, cipherText, 0, cipherText.Length); | |||||
| Sodium.SecretBox.Open(cipherText, nonce, _secretKey); | |||||
| } | |||||
| else //Plain | |||||
| { | |||||
| byte[] newBuffer = new byte[buffer.Length - 12]; | |||||
| Buffer.BlockCopy(buffer, 12, newBuffer, 0, newBuffer.Length); | |||||
| buffer = newBuffer; | |||||
| }*/ | |||||
| if (_logLevel >= LogMessageSeverity.Debug) | |||||
| RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes."); | |||||
| //TODO: Use Voice Data | |||||
| } | |||||
| } | |||||
| } | |||||
| public void SendPCMFrames(byte[] data, int bytes) | public void SendPCMFrames(byte[] data, int bytes) | ||||
| { | { | ||||
| int frameSize = _encoder.FrameSize; | int frameSize = _encoder.FrameSize; | ||||
| @@ -491,17 +520,11 @@ namespace Discord.WebSockets.Voice | |||||
| //Wipe the end of the buffer | //Wipe the end of the buffer | ||||
| for (int j = lastFrameSize; j < frameSize; j++) | for (int j = lastFrameSize; j < frameSize; j++) | ||||
| data[j] = 0; | data[j] = 0; | ||||
| } | } | ||||
| //Encode the frame | //Encode the frame | ||||
| int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | ||||
| //TODO: Handle Encryption | |||||
| if (_isEncrypted) | |||||
| { | |||||
| } | |||||
| //Copy result to the queue | //Copy result to the queue | ||||
| payload = new byte[encodedLength]; | payload = new byte[encodedLength]; | ||||
| Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength); | Buffer.BlockCopy(_encodingBuffer, 0, payload, 0, encodedLength); | ||||
| @@ -515,8 +538,8 @@ namespace Discord.WebSockets.Voice | |||||
| } | } | ||||
| } | } | ||||
| if (_logLevel >= LogMessageSeverity.Debug) | |||||
| RaiseOnLog(LogMessageSeverity.Debug, $"Queued {bytes} bytes for voice output."); | |||||
| /*if (_logLevel >= LogMessageSeverity.Debug) | |||||
| RaiseOnLog(LogMessageSeverity.Debug, $"Queued {bytes} bytes for voice output.");*/ | |||||
| } | } | ||||
| public void ClearPCMFrames() | public void ClearPCMFrames() | ||||
| { | { | ||||
| @@ -88,9 +88,9 @@ namespace Discord.WebSockets | |||||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token; | _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token; | ||||
| else | else | ||||
| _cancelToken = _cancelTokenSource.Token; | _cancelToken = _cancelTokenSource.Token; | ||||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||||
| _lastHeartbeat = DateTime.UtcNow; | _lastHeartbeat = DateTime.UtcNow; | ||||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||||
| _state = (int)WebSocketState.Connecting; | _state = (int)WebSocketState.Connecting; | ||||
| _runTask = RunTasks(); | _runTask = RunTasks(); | ||||