General Voice improvements - encryption, initial decoding, more config options
This commit is contained in:
@@ -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);
|
||||
|
||||
105
src/Discord.Net/Audio/OpusDecoder.cs
Normal file
105
src/Discord.Net/Audio/OpusDecoder.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 () =>
|
||||
{
|
||||
try
|
||||
#endif
|
||||
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)
|
||||
{
|
||||
#if DNXCORE50
|
||||
if (_udp.Available > 0)
|
||||
{
|
||||
#endif
|
||||
var result = await _udp.ReceiveAsync().ConfigureAwait(false);
|
||||
ProcessUdpMessage(result);
|
||||
#if DNXCORE50
|
||||
}
|
||||
else
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
decodingBuffer = new byte[MaxOpusSize];
|
||||
nonce = new byte[24];
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (InvalidOperationException) { } //Includes ObjectDisposedException
|
||||
catch (Exception ex) { await DisconnectInternal(ex); }
|
||||
|
||||
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)
|
||||
{
|
||||
packet = _udp.Receive(ref endpoint);
|
||||
#else
|
||||
var msg = await _udp.ReceiveAsync().ConfigureAwait(false);
|
||||
endpoint = msg.Endpoint;
|
||||
receievedPacket = msg.Buffer;
|
||||
#endif
|
||||
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);
|
||||
}
|
||||
}
|
||||
#if USE_THREAD || DNXCORE50
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
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;
|
||||
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[] 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();
|
||||
|
||||
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)
|
||||
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)
|
||||
{
|
||||
nonce = new byte[24];
|
||||
Buffer.BlockCopy(rtpPacket, 0, nonce, 0, 12);
|
||||
}
|
||||
|
||||
while (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
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++);
|
||||
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);
|
||||
#if USE_THREAD
|
||||
_udp.Send(rtpPacket, packet.Length + 12);
|
||||
#else
|
||||
await _udp.SendAsync(rtpPacket, packet.Length + 12).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
timestamp = unchecked(timestamp + samplesPerFrame);
|
||||
nextTicks += ticksPerFrame;
|
||||
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 we have less than our target data buffered, request more
|
||||
int count = _sendQueue.Count;
|
||||
if (count == 0)
|
||||
if (_isEncrypted)
|
||||
{
|
||||
_sendQueueWait.Set();
|
||||
_sendQueueEmptyWait.Set();
|
||||
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 if (count < _targetAudioBufferLength)
|
||||
_sendQueueWait.Set();
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(queuedPacket, 0, result, 12, queuedPacket.Length);
|
||||
rtpPacketLength = queuedPacket.Length + 12;
|
||||
}
|
||||
#if USE_THREAD
|
||||
_udp.Send(result, rtpPacketLength);
|
||||
#else
|
||||
await _udp.SendAsync(rtpPacket, rtpPacketLength).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
//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();
|
||||
|
||||
Reference in New Issue
Block a user