| @@ -21,11 +21,11 @@ namespace Discord | |||
| 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 | |||
| @@ -35,7 +35,7 @@ namespace Discord | |||
| public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) | |||
| : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.Permissions, baseArgs.Args) | |||
| { | |||
| this.Exception = ex; | |||
| Exception = ex; | |||
| } | |||
| } | |||
| public partial class DiscordBotClient : DiscordClient | |||
| @@ -103,6 +103,9 @@ | |||
| <Compile Include="..\Discord.Net\Audio\Opus.cs"> | |||
| <Link>Audio\Opus.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Audio\OpusDecoder.cs"> | |||
| <Link>Audio\OpusDecoder.cs</Link> | |||
| </Compile> | |||
| <Compile Include="..\Discord.Net\Audio\OpusEncoder.cs"> | |||
| <Link>Audio\OpusEncoder.cs</Link> | |||
| </Compile> | |||
| @@ -3,21 +3,21 @@ using System.Runtime.InteropServices; | |||
| namespace Discord.Audio | |||
| { | |||
| internal static unsafe class Opus | |||
| internal unsafe static class Opus | |||
| { | |||
| [DllImport("lib/opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] | |||
| public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out Error error); | |||
| [DllImport("lib/opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] | |||
| public static extern void DestroyEncoder(IntPtr encoder); | |||
| [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)] | |||
| public static extern void DestroyDecoder(IntPtr decoder); | |||
| [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)] | |||
| 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> | |||
| internal class OpusEncoder : IDisposable | |||
| { | |||
| private readonly IntPtr _encoderPtr; | |||
| private readonly IntPtr _ptr; | |||
| /// <summary> Gets the bit rate of the encoder. </summary> | |||
| public const int BitRate = 16; | |||
| @@ -48,7 +48,7 @@ namespace Discord.Audio | |||
| FrameSize = SamplesPerFrame * SampleSize; | |||
| 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) | |||
| 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> | |||
| /// <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> | |||
| public unsafe int EncodeFrame(byte[] pcmSamples, int offset, byte[] outputBuffer) | |||
| public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) | |||
| { | |||
| if (disposed) | |||
| throw new ObjectDisposedException("OpusEncoder"); | |||
| throw new ObjectDisposedException(nameof(OpusEncoder)); | |||
| 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) | |||
| throw new Exception("Encoding failed: " + ((Opus.Error)result).ToString()); | |||
| @@ -79,9 +78,9 @@ namespace Discord.Audio | |||
| public void SetForwardErrorCorrection(bool value) | |||
| { | |||
| 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) | |||
| throw new Exception("Encoder error: " + ((Opus.Error)result).ToString()); | |||
| } | |||
| @@ -95,8 +94,8 @@ namespace Discord.Audio | |||
| GC.SuppressFinalize(this); | |||
| if (_encoderPtr != IntPtr.Zero) | |||
| Opus.DestroyEncoder(_encoderPtr); | |||
| if (_ptr != IntPtr.Zero) | |||
| Opus.DestroyEncoder(_ptr); | |||
| disposed = true; | |||
| } | |||
| @@ -2,14 +2,25 @@ | |||
| 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) | |||
| { | |||
| this.WasUnexpected = wasUnexpected; | |||
| this.Error = error; | |||
| WasUnexpected = wasUnexpected; | |||
| Error = error; | |||
| } | |||
| } | |||
| public sealed class LogMessageEventArgs : EventArgs | |||
| @@ -40,9 +40,9 @@ namespace Discord | |||
| 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 string ServerId => Server.Id; | |||
| internal ServerEventArgs(Server server) { this.Server = server; } | |||
| internal ServerEventArgs(Server server) { Server = server; } | |||
| } | |||
| public sealed class ChannelEventArgs : EventArgs | |||
| { | |||
| @@ -60,14 +60,14 @@ namespace Discord | |||
| public Server Server => Channel.Server; | |||
| public string ServerId => Channel.ServerId; | |||
| internal ChannelEventArgs(Channel channel) { this.Channel = channel; } | |||
| internal ChannelEventArgs(Channel channel) { Channel = channel; } | |||
| } | |||
| public sealed class UserEventArgs : EventArgs | |||
| { | |||
| public User User { get; } | |||
| public string UserId => User.Id; | |||
| internal UserEventArgs(User user) { this.User = user; } | |||
| internal UserEventArgs(User user) { User = user; } | |||
| } | |||
| public sealed class MessageEventArgs : EventArgs | |||
| { | |||
| @@ -81,7 +81,7 @@ namespace Discord | |||
| public User User => Member.User; | |||
| public string UserId => Message.UserId; | |||
| internal MessageEventArgs(Message msg) { this.Message = msg; } | |||
| internal MessageEventArgs(Message msg) { Message = msg; } | |||
| } | |||
| public sealed class RoleEventArgs : EventArgs | |||
| { | |||
| @@ -90,7 +90,7 @@ namespace Discord | |||
| public Server Server => Role.Server; | |||
| public string ServerId => Role.ServerId; | |||
| internal RoleEventArgs(Role role) { this.Role = role; } | |||
| internal RoleEventArgs(Role role) { Role = role; } | |||
| } | |||
| public sealed class BanEventArgs : EventArgs | |||
| { | |||
| @@ -101,9 +101,9 @@ namespace Discord | |||
| 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 | |||
| @@ -114,7 +114,7 @@ namespace Discord | |||
| public Server Server => Member.Server; | |||
| public string ServerId => Member.ServerId; | |||
| internal MemberEventArgs(Member member) { this.Member = member; } | |||
| internal MemberEventArgs(Member member) { Member = member; } | |||
| } | |||
| public sealed class UserTypingEventArgs : EventArgs | |||
| { | |||
| @@ -127,8 +127,8 @@ namespace Discord | |||
| internal UserTypingEventArgs(User user, Channel channel) | |||
| { | |||
| this.User = user; | |||
| this.Channel = channel; | |||
| User = user; | |||
| Channel = channel; | |||
| } | |||
| } | |||
| public sealed class UserIsSpeakingEventArgs : EventArgs | |||
| @@ -144,10 +144,26 @@ namespace Discord | |||
| 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 | |||
| { | |||
| @@ -340,5 +356,12 @@ namespace Discord | |||
| if (VoiceDisconnected != null) | |||
| 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 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); | |||
| 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); | |||
| _voiceSocket.SetChannel(server, channel); | |||
| _dataSocket.SendJoinVoice(server.Id, channel.Id); | |||
| 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) | |||
| { | |||
| @@ -39,11 +40,11 @@ namespace Discord | |||
| if (_voiceSocket.State != WebSocketState.Disconnected) | |||
| { | |||
| var serverId = _voiceSocket.CurrentVoiceServerId; | |||
| if (serverId != null) | |||
| var server = _voiceSocket.CurrentVoiceServer; | |||
| if (server != null) | |||
| { | |||
| 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> | |||
| public User CurrentUser => _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> | |||
| public Server CurrentVoiceServer => _servers[_voiceSocket.CurrentVoiceServerId]; | |||
| public Server CurrentVoiceServer => _voiceSocket.CurrentVoiceServer; | |||
| /// <summary> Returns the current connection state of this client. </summary> | |||
| public DiscordClientState State => (DiscordClientState)_state; | |||
| @@ -103,7 +101,7 @@ namespace Discord | |||
| if (e.WasUnexpected) | |||
| await _dataSocket.Reconnect(_token); | |||
| }; | |||
| if (_config.EnableVoice) | |||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||
| { | |||
| _voiceSocket = new VoiceWebSocket(this); | |||
| _voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||
| @@ -125,7 +123,7 @@ namespace Discord | |||
| { | |||
| if (_voiceSocket.State == WebSocketState.Connected) | |||
| { | |||
| var member = _members[e.UserId, _voiceSocket.CurrentVoiceServerId]; | |||
| var member = _members[e.UserId, _voiceSocket.CurrentVoiceServer.Id]; | |||
| bool value = e.IsSpeaking; | |||
| if (member.IsSpeaking != value) | |||
| { | |||
| @@ -147,14 +145,14 @@ namespace Discord | |||
| _users = new Users(this, cacheLock); | |||
| _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); | |||
| if (_config.LogLevel >= LogMessageSeverity.Info) | |||
| { | |||
| _dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | |||
| _dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | |||
| //_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.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.VoiceWebSocket, "Disconnected"); | |||
| @@ -535,28 +533,6 @@ namespace Discord | |||
| } | |||
| } | |||
| 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": | |||
| { | |||
| var data = e.Payload.ToObject<TypingStartEvent>(_serializer); | |||
| @@ -586,13 +562,35 @@ namespace Discord | |||
| break; | |||
| //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": | |||
| { | |||
| var data = e.Payload.ToObject<VoiceServerUpdateEvent>(_serializer); | |||
| if (data.GuildId == _voiceSocket.CurrentVoiceServerId) | |||
| if (data.GuildId == _voiceSocket.CurrentVoiceServer.Id) | |||
| { | |||
| var server = _servers[data.GuildId]; | |||
| if (_config.EnableVoice) | |||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||
| { | |||
| _voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0]; | |||
| await _voiceSocket.Login(_currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false); | |||
| @@ -770,7 +768,7 @@ namespace Discord | |||
| _wasDisconnectUnexpected = false; | |||
| await _dataSocket.Disconnect().ConfigureAwait(false); | |||
| if (_config.EnableVoice) | |||
| if (_config.VoiceMode != DiscordVoiceMode.Disabled) | |||
| await _voiceSocket.Disconnect().ConfigureAwait(false); | |||
| if (_config.UseMessageQueue) | |||
| @@ -817,7 +815,7 @@ namespace Discord | |||
| 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."); | |||
| } | |||
| private void RaiseEvent(string name, Action action) | |||
| @@ -2,6 +2,15 @@ | |||
| namespace Discord | |||
| { | |||
| [Flags] | |||
| public enum DiscordVoiceMode | |||
| { | |||
| Disabled = 0x00, | |||
| Incoming = 0x01, | |||
| Outgoing = 0x02, | |||
| Both = Outgoing | Incoming | |||
| } | |||
| 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> | |||
| @@ -33,15 +42,19 @@ namespace Discord | |||
| //Experimental Features | |||
| #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> | |||
| 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 | |||
| internal bool EnableVoice => false; | |||
| internal DiscordVoiceMode VoiceMode => DiscordVoiceMode.Disabled; | |||
| internal bool EnableVoiceEncryption => false; | |||
| internal bool EnableVoiceMultiserver => false; | |||
| #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> | |||
| public bool UseMessageQueue { get { return _useMessageQueue; } set { SetValue(ref _useMessageQueue, value); } } | |||
| @@ -17,6 +17,7 @@ namespace Discord | |||
| private readonly DiscordClient _client; | |||
| private ConcurrentDictionary<string, bool> _messages; | |||
| private ConcurrentDictionary<uint, string> _ssrcMapping; | |||
| /// <summary> Returns the unique identifier for this channel. </summary> | |||
| public string Id { get; } | |||
| @@ -69,6 +70,8 @@ namespace Discord | |||
| { | |||
| Name = model.Name; | |||
| Type = model.Type; | |||
| if (Type == ChannelTypes.Voice && _ssrcMapping == null) | |||
| _ssrcMapping = new ConcurrentDictionary<uint, string>(); | |||
| } | |||
| internal void Update(API.ChannelInfo model) | |||
| { | |||
| @@ -101,5 +104,12 @@ namespace Discord | |||
| bool 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) | |||
| 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 Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| @@ -16,46 +17,51 @@ namespace Discord.WebSockets.Voice | |||
| { | |||
| 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 readonly int _targetAudioBufferLength; | |||
| private readonly Random _rand; | |||
| private readonly int _targetAudioBufferLength; | |||
| private OpusEncoder _encoder; | |||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
| private ManualResetEventSlim _connectWaitOnLogin; | |||
| private uint _ssrc; | |||
| private readonly Random _rand = new Random(); | |||
| private OpusEncoder _encoder; | |||
| private ConcurrentQueue<byte[]> _sendQueue; | |||
| private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _endpoint; | |||
| private bool _isClearing, _isEncrypted; | |||
| private byte[] _secretKey; | |||
| private byte[] _secretKey, _encodingBuffer; | |||
| 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 | |||
| private Thread _sendThread; | |||
| #endif | |||
| public string CurrentVoiceServerId => _serverId; | |||
| public Server CurrentVoiceServer => _server; | |||
| public VoiceWebSocket(DiscordClient client) | |||
| : base(client) | |||
| { | |||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||
| _rand = new Random(); | |||
| _connectWaitOnLogin = new ManualResetEventSlim(false); | |||
| _decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||
| _sendQueue = new ConcurrentQueue<byte[]>(); | |||
| _sendQueueWait = 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 | |||
| } | |||
| _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) | |||
| { | |||
| @@ -69,7 +75,7 @@ namespace Discord.WebSockets.Voice | |||
| _userId = userId; | |||
| _sessionId = sessionId; | |||
| _token = token; | |||
| await Connect().ConfigureAwait(false); | |||
| } | |||
| public async Task Reconnect() | |||
| @@ -107,26 +113,29 @@ namespace Discord.WebSockets.Voice | |||
| #endif | |||
| LoginCommand msg = new LoginCommand(); | |||
| msg.Payload.ServerId = _serverId; | |||
| msg.Payload.ServerId = _server.Id; | |||
| msg.Payload.SessionId = _sessionId; | |||
| msg.Payload.Token = _token; | |||
| msg.Payload.UserId = _userId; | |||
| QueueMessage(msg); | |||
| #if USE_THREAD | |||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_disconnectToken))); | |||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(_cancelToken))); | |||
| _sendThread.Start(); | |||
| #if !DNXCORE50 | |||
| return new Task[] { WatcherAsync() }.Concat(base.Run()).ToArray(); | |||
| #else | |||
| return base.Run(); | |||
| #endif | |||
| return new Task[] | |||
| { | |||
| #else //!USE_THREAD | |||
| return new Task[] { Task.WhenAll( | |||
| ReceiveVoiceAsync(), | |||
| #if !USE_THREAD | |||
| SendVoiceAsync(), | |||
| #endif | |||
| #if !DNXCORE50 | |||
| WatcherAsync() | |||
| #endif | |||
| }.Concat(base.Run()).ToArray(); | |||
| )}.Concat(base.Run()).ToArray(); | |||
| #endif | |||
| } | |||
| protected override Task Cleanup() | |||
| { | |||
| @@ -134,6 +143,13 @@ namespace Discord.WebSockets.Voice | |||
| _sendThread.Join(); | |||
| _sendThread = null; | |||
| #endif | |||
| OpusDecoder decoder; | |||
| foreach (var pair in _decoders) | |||
| { | |||
| if (_decoders.TryRemove(pair.Key, out decoder)) | |||
| decoder.Dispose(); | |||
| } | |||
| ClearPCMFrames(); | |||
| if (!_wasDisconnectUnexpected) | |||
| @@ -147,39 +163,137 @@ namespace Discord.WebSockets.Voice | |||
| return base.Cleanup(); | |||
| } | |||
| private async Task ReceiveVoiceAsync() | |||
| #if USE_THREAD | |||
| private void ReceiveVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| #else | |||
| private Task ReceiveVoiceAsync() | |||
| { | |||
| 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 | |||
| 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); | |||
| #endif | |||
| } | |||
| #if USE_THREAD | |||
| private void SendVoiceAsync(CancellationTokenSource cancelSource) | |||
| private void SendVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| var cancelToken = cancelSource.Token; | |||
| #else | |||
| private Task SendVoiceAsync() | |||
| { | |||
| @@ -189,103 +303,114 @@ namespace Discord.WebSockets.Voice | |||
| { | |||
| #endif | |||
| byte[] packet; | |||
| try | |||
| try | |||
| { | |||
| while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||
| { | |||
| while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||
| { | |||
| #if USE_THREAD | |||
| Thread.Sleep(1); | |||
| Thread.Sleep(1); | |||
| #else | |||
| await Task.Delay(1); | |||
| await Task.Delay(1); | |||
| #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 | |||
| _udp.Send(rtpPacket, packet.Length + 12); | |||
| _udp.Send(result, rtpPacketLength); | |||
| #else | |||
| await _udp.SendAsync(rtpPacket, packet.Length + 12).ConfigureAwait(false); | |||
| await _udp.SendAsync(rtpPacket, rtpPacketLength).ConfigureAwait(false); | |||
| #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 | |||
| Thread.Sleep(1); | |||
| #else | |||
| await Task.Delay(1).ConfigureAwait(false); | |||
| await Task.Delay(1).ConfigureAwait(false); | |||
| #endif | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| #if !USE_THREAD | |||
| }); | |||
| }).ConfigureAwait(false); | |||
| #endif | |||
| } | |||
| #if !DNXCORE50 | |||
| @@ -331,16 +456,12 @@ namespace Discord.WebSockets.Voice | |||
| _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | |||
| //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; | |||
| @@ -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) | |||
| { | |||
| int frameSize = _encoder.FrameSize; | |||
| @@ -491,17 +520,11 @@ namespace Discord.WebSockets.Voice | |||
| //Wipe the end of the buffer | |||
| for (int j = lastFrameSize; j < frameSize; j++) | |||
| data[j] = 0; | |||
| } | |||
| //Encode the frame | |||
| int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | |||
| //TODO: Handle Encryption | |||
| if (_isEncrypted) | |||
| { | |||
| } | |||
| //Copy result to the queue | |||
| payload = new byte[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() | |||
| { | |||
| @@ -88,9 +88,9 @@ namespace Discord.WebSockets | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token; | |||
| else | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
| _lastHeartbeat = DateTime.UtcNow; | |||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
| _state = (int)WebSocketState.Connecting; | |||
| _runTask = RunTasks(); | |||