Browse Source

General Voice improvements - encryption, initial decoding, more config options

tags/docs-0.9
RogueException 9 years ago
parent
commit
a965ec4a23
14 changed files with 521 additions and 328 deletions
  1. +6
    -6
      src/Discord.Net.Commands/DiscordBotClient.Events.cs
  2. +3
    -0
      src/Discord.Net.Net45/Discord.Net.csproj
  3. +5
    -5
      src/Discord.Net/Audio/Opus.cs
  4. +105
    -0
      src/Discord.Net/Audio/OpusDecoder.cs
  5. +13
    -14
      src/Discord.Net/Audio/OpusEncoder.cs
  6. +17
    -6
      src/Discord.Net/Audio/Sodium.cs
  7. +41
    -18
      src/Discord.Net/DiscordClient.Events.cs
  8. +16
    -15
      src/Discord.Net/DiscordClient.Voice.cs
  9. +31
    -33
      src/Discord.Net/DiscordClient.cs
  10. +18
    -5
      src/Discord.Net/DiscordClientConfig.cs
  11. +10
    -0
      src/Discord.Net/Models/Channel.cs
  12. +7
    -0
      src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs
  13. +247
    -224
      src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs
  14. +2
    -2
      src/Discord.Net/WebSockets/WebSocket.cs

+ 6
- 6
src/Discord.Net.Commands/DiscordBotClient.Events.cs View File

@@ -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


+ 3
- 0
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -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>


+ 5
- 5
src/Discord.Net/Audio/Opus.cs View File

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


+ 105
- 0
src/Discord.Net/Audio/OpusDecoder.cs View File

@@ -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
}
}

+ 13
- 14
src/Discord.Net/Audio/OpusEncoder.cs View File

@@ -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;
} }


+ 17
- 6
src/Discord.Net/Audio/Sodium.cs View File

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

+ 41
- 18
src/Discord.Net/DiscordClient.Events.cs View File

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

+ 16
- 15
src/Discord.Net/DiscordClient.Voice.cs View File

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


+ 31
- 33
src/Discord.Net/DiscordClient.cs View File

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


+ 18
- 5
src/Discord.Net/DiscordClientConfig.cs View File

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


+ 10
- 0
src/Discord.Net/Models/Channel.cs View File

@@ -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;
}
} }
} }

+ 7
- 0
src/Discord.Net/WebSockets/Voice/VoiceWebSocket.Events.cs View File

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

+ 247
- 224
src/Discord.Net/WebSockets/Voice/VoiceWebSocket.cs View File

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


+ 2
- 2
src/Discord.Net/WebSockets/WebSocket.cs View File

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


Loading…
Cancel
Save