@@ -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(); | ||||