@@ -12,6 +12,7 @@ using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Collections.Generic; | |||
using System.Runtime.InteropServices; | |||
namespace Discord.Audio | |||
{ | |||
@@ -45,14 +46,17 @@ namespace Discord.Audio | |||
private string _url, _sessionId, _token; | |||
private ulong _userId; | |||
private uint _ssrc; | |||
private byte[] _secretKey; | |||
private GCHandle _secretKeyHandle; | |||
private bool _isSpeaking; | |||
private bool _isDisposed; | |||
public SocketGuild Guild { get; } | |||
public DiscordVoiceAPIClient ApiClient { get; private set; } | |||
public int Latency { get; private set; } | |||
public int UdpLatency { get; private set; } | |||
public ulong ChannelId { get; internal set; } | |||
internal byte[] SecretKey { get; private set; } | |||
internal IntPtr SecretKeyPtr { get; private set; } | |||
private DiscordSocketClient Discord => Guild.Discord; | |||
public ConnectionState ConnectionState => _connection.State; | |||
@@ -93,6 +97,20 @@ namespace Discord.Audio | |||
UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); | |||
} | |||
internal void Dispose(bool disposing) | |||
{ | |||
if (disposing && !_isDisposed) | |||
{ | |||
StopAsync().GetAwaiter().GetResult(); | |||
ApiClient.Dispose(); | |||
if (_secretKeyHandle.IsAllocated) | |||
_secretKeyHandle.Free(); | |||
_isDisposed = true; | |||
} | |||
} | |||
/// <inheritdoc /> | |||
public void Dispose() => Dispose(true); | |||
internal async Task StartAsync(string url, ulong userId, string sessionId, string token) | |||
{ | |||
_url = url; | |||
@@ -242,7 +260,12 @@ namespace Discord.Audio | |||
if (data.Mode != DiscordVoiceAPIClient.Mode) | |||
throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); | |||
SecretKey = data.SecretKey; | |||
_secretKey = data.SecretKey; | |||
if (_secretKeyHandle != null) | |||
_secretKeyHandle.Free(); | |||
_secretKeyHandle = GCHandle.Alloc(data.SecretKey, GCHandleType.Pinned); | |||
SecretKeyPtr = _secretKeyHandle.AddrOfPinnedObject(); | |||
_isSpeaking = false; | |||
await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); | |||
_keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); | |||
@@ -386,7 +409,7 @@ namespace Discord.Audio | |||
await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
var now = Environment.TickCount; | |||
int now = Environment.TickCount; | |||
//Did server respond to our last heartbeat? | |||
if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && | |||
@@ -427,7 +450,7 @@ namespace Discord.Audio | |||
await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); | |||
while (!cancelToken.IsCancellationRequested) | |||
{ | |||
var now = Environment.TickCount; | |||
int now = Environment.TickCount; | |||
try | |||
{ | |||
@@ -474,16 +497,5 @@ namespace Discord.Audio | |||
return buffer; | |||
return new byte[OpusConverter.FrameBytes]; | |||
} | |||
internal void Dispose(bool disposing) | |||
{ | |||
if (disposing) | |||
{ | |||
StopAsync().GetAwaiter().GetResult(); | |||
ApiClient.Dispose(); | |||
} | |||
} | |||
/// <inheritdoc /> | |||
public void Dispose() => Dispose(true); | |||
} | |||
} |
@@ -20,12 +20,9 @@ namespace Discord.Audio | |||
CheckError(error); | |||
} | |||
public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset, bool decodeFEC) | |||
public unsafe int DecodeFrame(byte* inPtr, int inputOffset, int inputCount, byte* outPtr, int outputOffset, bool decodeFEC) | |||
{ | |||
int result = 0; | |||
fixed (byte* inPtr = input) | |||
fixed (byte* outPtr = output) | |||
result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); | |||
int result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); | |||
CheckError(result); | |||
return result * SampleBytes; | |||
} | |||
@@ -53,12 +53,9 @@ namespace Discord.Audio | |||
CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); | |||
} | |||
public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int outputOffset) | |||
public unsafe int EncodeFrame(byte* inPtr, int inputOffset, byte* outPtr, int outputOffset, int count) | |||
{ | |||
int result = 0; | |||
fixed (byte* inPtr = input) | |||
fixed (byte* outPtr = output) | |||
result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, output.Length - outputOffset); | |||
int result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, count); | |||
CheckError(result); | |||
return result; | |||
} | |||
@@ -6,31 +6,23 @@ namespace Discord.Audio | |||
public unsafe static class SecretBox | |||
{ | |||
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] | |||
private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); | |||
private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte* nonce, byte* secret); | |||
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] | |||
private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); | |||
private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte* nonce, byte* secret); | |||
public static int Encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) | |||
public static int Encrypt(byte* inPtr, int inputOffset, int inputLength, byte* outPtr, int outputOffset, byte* nonce, byte* secret) | |||
{ | |||
fixed (byte* inPtr = input) | |||
fixed (byte* outPtr = output) | |||
{ | |||
int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
if (error != 0) | |||
throw new Exception($"Sodium Error: {error}"); | |||
return inputLength + 16; | |||
} | |||
int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
if (error != 0) | |||
throw new Exception($"Sodium Error: {error}"); | |||
return inputLength + 16; | |||
} | |||
public static int Decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) | |||
public static int Decrypt(byte* inPtr, int inputOffset, int inputLength, byte* outPtr, int outputOffset, byte* nonce, byte* secret) | |||
{ | |||
fixed (byte* inPtr = input) | |||
fixed (byte* outPtr = output) | |||
{ | |||
int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
if (error != 0) | |||
throw new Exception($"Sodium Error: {error}"); | |||
return inputLength - 16; | |||
} | |||
int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); | |||
if (error != 0) | |||
throw new Exception($"Sodium Error: {error}"); | |||
return inputLength - 16; | |||
} | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -12,15 +13,31 @@ namespace Discord.Audio.Streams | |||
private readonly AudioStream _next; | |||
private readonly OpusDecoder _decoder; | |||
private readonly byte[] _buffer; | |||
private readonly GCHandle _bufferHandle; | |||
private readonly IntPtr _bufferPtr; | |||
private bool _nextMissed; | |||
private bool _hasHeader; | |||
private bool _isDisposed; | |||
public OpusDecodeStream(AudioStream next) | |||
{ | |||
_next = next; | |||
_buffer = new byte[OpusConverter.FrameBytes]; | |||
_bufferHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned); | |||
_bufferPtr = _bufferHandle.AddrOfPinnedObject(); | |||
_decoder = new OpusDecoder(); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing && !_isDisposed) | |||
{ | |||
_decoder.Dispose(); | |||
_bufferHandle.Free(); | |||
_isDisposed = true; | |||
} | |||
} | |||
public override void WriteHeader(ushort seq, uint timestamp, bool missed) | |||
{ | |||
@@ -39,17 +56,28 @@ namespace Discord.Audio.Streams | |||
if (!_nextMissed) | |||
{ | |||
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); | |||
unsafe | |||
{ | |||
fixed (byte* inPtr = buffer) | |||
count = _decoder.DecodeFrame(inPtr, offset, count, (byte*)_bufferPtr, 0, false); | |||
} | |||
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||
} | |||
else if (count > 0) | |||
{ | |||
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); | |||
unsafe | |||
{ | |||
fixed(byte* inPtr = buffer) | |||
count = _decoder.DecodeFrame(inPtr, offset, count, (byte*)_bufferPtr, 0, true); | |||
} | |||
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); | |||
unsafe | |||
{ | |||
count = _decoder.DecodeFrame(null, 0, 0, (byte*)_bufferPtr, 0, true); | |||
} | |||
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
@@ -62,13 +90,5 @@ namespace Discord.Audio.Streams | |||
{ | |||
await _next.ClearAsync(cancelToken).ConfigureAwait(false); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing) | |||
_decoder.Dispose(); | |||
} | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -12,26 +13,48 @@ namespace Discord.Audio.Streams | |||
private readonly AudioStream _next; | |||
private readonly OpusEncoder _encoder; | |||
private readonly byte[] _buffer; | |||
private readonly GCHandle _bufferHandle; | |||
private readonly IntPtr _bufferPtr; | |||
private int _partialFramePos; | |||
private ushort _seq; | |||
private uint _timestamp; | |||
private bool _isDisposed; | |||
public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) | |||
{ | |||
_next = next; | |||
_encoder = new OpusEncoder(bitrate, application, packetLoss); | |||
_buffer = new byte[OpusConverter.FrameBytes]; | |||
_bufferHandle = GCHandle.Alloc(_buffer, GCHandleType.Pinned); | |||
_bufferPtr = _bufferHandle.AddrOfPinnedObject(); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing && !_isDisposed) | |||
{ | |||
_encoder.Dispose(); | |||
_bufferHandle.Free(); | |||
_isDisposed = true; | |||
} | |||
} | |||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||
{ | |||
//Assume threadsafe | |||
int encFrameSize = 0; | |||
while (count > 0) | |||
{ | |||
if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) | |||
{ | |||
//We have enough data and no partial frames. Pass the buffer directly to the encoder | |||
int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); | |||
unsafe | |||
{ | |||
fixed (byte* inPtr = buffer) | |||
encFrameSize = _encoder.EncodeFrame(inPtr, offset, (byte*)_bufferPtr, 0, OpusConverter.FrameBytes); | |||
} | |||
_next.WriteHeader(_seq, _timestamp, false); | |||
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); | |||
@@ -45,7 +68,10 @@ namespace Discord.Audio.Streams | |||
//We have enough data to complete a previous partial frame. | |||
int partialSize = OpusConverter.FrameBytes - _partialFramePos; | |||
Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); | |||
int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); | |||
unsafe | |||
{ | |||
encFrameSize = _encoder.EncodeFrame((byte*)_bufferPtr, 0, (byte*)_bufferPtr, 0, OpusConverter.FrameBytes); | |||
} | |||
_next.WriteHeader(_seq, _timestamp, false); | |||
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); | |||
@@ -86,13 +112,5 @@ namespace Discord.Audio.Streams | |||
{ | |||
await _next.ClearAsync(cancelToken).ConfigureAwait(false); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing) | |||
_encoder.Dispose(); | |||
} | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -10,6 +11,10 @@ namespace Discord.Audio.Streams | |||
private readonly AudioClient _client; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _nonce; | |||
private readonly GCHandle _nonceHandle; | |||
private readonly IntPtr _noncePtr; | |||
private bool _isDisposed; | |||
public override bool CanRead => true; | |||
public override bool CanSeek => false; | |||
@@ -20,17 +25,32 @@ namespace Discord.Audio.Streams | |||
_next = next; | |||
_client = (AudioClient)client; | |||
_nonce = new byte[24]; | |||
_nonceHandle = GCHandle.Alloc(_nonce, GCHandleType.Pinned); | |||
_noncePtr = _nonceHandle.AddrOfPinnedObject(); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing && !_isDisposed) | |||
{ | |||
_nonceHandle.Free(); | |||
_isDisposed = true; | |||
} | |||
} | |||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) | |||
{ | |||
cancelToken.ThrowIfCancellationRequested(); | |||
if (_client.SecretKey == null) | |||
if (_client.SecretKeyPtr == null) | |||
return; | |||
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce | |||
count = SecretBox.Decrypt(buffer, offset + 12, count - 12, buffer, offset + 12, _nonce, _client.SecretKey); | |||
unsafe | |||
{ | |||
fixed (byte* ptr = buffer) | |||
count = SecretBox.Decrypt(ptr, offset + 12, count - 12, ptr, offset + 12, (byte*)_noncePtr, (byte*)_client.SecretKeyPtr); | |||
} | |||
await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); | |||
} | |||
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
@@ -10,17 +11,32 @@ namespace Discord.Audio.Streams | |||
private readonly AudioClient _client; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _nonce; | |||
private readonly GCHandle _nonceHandle; | |||
private readonly IntPtr _noncePtr; | |||
private bool _hasHeader; | |||
private ushort _nextSeq; | |||
private uint _nextTimestamp; | |||
private bool _isDisposed; | |||
public SodiumEncryptStream(AudioStream next, IAudioClient client) | |||
{ | |||
_next = next; | |||
_client = (AudioClient)client; | |||
_nonce = new byte[24]; | |||
_nonceHandle = GCHandle.Alloc(_nonce, GCHandleType.Pinned); | |||
_noncePtr = _nonceHandle.AddrOfPinnedObject(); | |||
} | |||
protected override void Dispose(bool disposing) | |||
{ | |||
base.Dispose(disposing); | |||
if (disposing && !_isDisposed) | |||
{ | |||
_nonceHandle.Free(); | |||
_isDisposed = true; | |||
} | |||
} | |||
public override void WriteHeader(ushort seq, uint timestamp, bool missed) | |||
{ | |||
if (_hasHeader) | |||
@@ -37,11 +53,15 @@ namespace Discord.Audio.Streams | |||
throw new InvalidOperationException("Received payload without an RTP header"); | |||
_hasHeader = false; | |||
if (_client.SecretKey == null) | |||
if (_client.SecretKeyPtr == null) | |||
return; | |||
Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header | |||
count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); | |||
unsafe | |||
{ | |||
fixed (byte* ptr = buffer) | |||
count = SecretBox.Encrypt(ptr, offset + 12, count - 12, ptr, 12, (byte*)_noncePtr, (byte*)_client.SecretKeyPtr); | |||
} | |||
_next.WriteHeader(_nextSeq, _nextTimestamp, false); | |||
await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); | |||
} | |||