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