@@ -1,43 +1,19 @@ | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public abstract class AudioInStream : Stream | |||
public abstract class AudioInStream : AudioStream | |||
{ | |||
public override bool CanRead => true; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
public abstract int AvailableFrames { get; } | |||
public override bool CanRead => true; | |||
public override bool CanWrite => true; | |||
public abstract Task<RTPFrame> ReadFrameAsync(CancellationToken cancelToken); | |||
public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); | |||
public RTPFrame ReadFrame() | |||
{ | |||
return ReadFrameAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override int Read(byte[] buffer, int offset, int count) | |||
{ | |||
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override void Write(byte[] buffer, int offset, int count) | |||
{ | |||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override void Flush() { throw new NotSupportedException(); } | |||
public override long Length { get { throw new NotSupportedException(); } } | |||
public override long Position | |||
{ | |||
get { throw new NotSupportedException(); } | |||
set { throw new NotSupportedException(); } | |||
} | |||
public override void SetLength(long value) { throw new NotSupportedException(); } | |||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } | |||
public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); } | |||
} | |||
} |
@@ -1,39 +1,12 @@ | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public abstract class AudioOutStream : Stream | |||
public abstract class AudioOutStream : AudioStream | |||
{ | |||
public override bool CanRead => false; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
public override void Write(byte[] buffer, int offset, int count) | |||
{ | |||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override void Flush() | |||
{ | |||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public void Clear() | |||
{ | |||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } | |||
//public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); } | |||
public override long Length { get { throw new NotSupportedException(); } } | |||
public override long Position | |||
{ | |||
get { throw new NotSupportedException(); } | |||
set { throw new NotSupportedException(); } | |||
} | |||
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } | |||
public override void SetLength(long value) { throw new NotSupportedException(); } | |||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } | |||
@@ -0,0 +1,40 @@ | |||
using System; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public abstract class AudioStream : Stream | |||
{ | |||
public override bool CanRead => false; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => false; | |||
public override void Write(byte[] buffer, int offset, int count) | |||
{ | |||
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override void Flush() | |||
{ | |||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public void Clear() | |||
{ | |||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } | |||
public override long Length { get { throw new NotSupportedException(); } } | |||
public override long Position | |||
{ | |||
get { throw new NotSupportedException(); } | |||
set { throw new NotSupportedException(); } | |||
} | |||
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } | |||
public override void SetLength(long value) { throw new NotSupportedException(); } | |||
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } | |||
} | |||
} |
@@ -42,12 +42,12 @@ namespace Discord.Audio | |||
private string _url, _sessionId, _token; | |||
private ulong _userId; | |||
private uint _ssrc; | |||
private byte[] _secretKey; | |||
public SocketGuild Guild { get; } | |||
public DiscordVoiceAPIClient ApiClient { get; private set; } | |||
public int Latency { get; private set; } | |||
public ulong ChannelId { get; internal set; } | |||
internal byte[] SecretKey { get; private set; } | |||
private DiscordSocketClient Discord => Guild.Discord; | |||
public ConnectionState ConnectionState => _connection.State; | |||
@@ -134,7 +134,7 @@ namespace Discord.Audio | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var outputStream = new OutputStream(ApiClient); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); | |||
var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); | |||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); | |||
return new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); | |||
} | |||
@@ -142,14 +142,14 @@ namespace Discord.Audio | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var outputStream = new OutputStream(ApiClient); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||
return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); | |||
} | |||
public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var outputStream = new OutputStream(ApiClient); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); | |||
var bufferedStream = new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); | |||
return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); | |||
@@ -158,7 +158,7 @@ namespace Discord.Audio | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var outputStream = new OutputStream(ApiClient); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); | |||
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); | |||
var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); | |||
return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); | |||
} | |||
@@ -175,8 +175,10 @@ namespace Discord.Audio | |||
if (!_streams.ContainsKey(userId)) | |||
{ | |||
var readerStream = new InputStream(); | |||
var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); | |||
_streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); | |||
var opusDecoder = new OpusDecodeStream(readerStream); | |||
var rtpReader = new RTPReadStream(readerStream, opusDecoder); | |||
var decryptStream = new SodiumDecryptStream(rtpReader, this); | |||
_streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); | |||
await _streamCreatedEvent.InvokeAsync(userId, readerStream); | |||
} | |||
} | |||
@@ -238,7 +240,7 @@ namespace Discord.Audio | |||
if (data.Mode != DiscordVoiceAPIClient.Mode) | |||
throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); | |||
_secretKey = data.SecretKey; | |||
SecretKey = data.SecretKey; | |||
await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); | |||
var _ = _connection.CompleteAsync(); | |||
@@ -335,7 +337,7 @@ namespace Discord.Audio | |||
await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); | |||
return; | |||
} | |||
await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); | |||
//await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); | |||
} | |||
} | |||
@@ -26,7 +26,7 @@ namespace Discord.Audio.Streams | |||
private static readonly byte[] _silenceFrame = new byte[0]; | |||
private readonly AudioClient _client; | |||
private readonly AudioOutStream _next; | |||
private readonly AudioStream _next; | |||
private readonly CancellationTokenSource _cancelTokenSource; | |||
private readonly CancellationToken _cancelToken; | |||
private readonly Task _task; | |||
@@ -38,9 +38,9 @@ namespace Discord.Audio.Streams | |||
private bool _isPreloaded; | |||
private int _silenceFrames; | |||
public BufferedWriteStream(AudioOutStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) | |||
public BufferedWriteStream(AudioStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) | |||
: this(next, client as AudioClient, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } | |||
internal BufferedWriteStream(AudioOutStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) | |||
internal BufferedWriteStream(AudioStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) | |||
{ | |||
//maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms | |||
_next = next; | |||
@@ -8,11 +8,11 @@ namespace Discord.Audio.Streams | |||
{ | |||
public const int SampleRate = OpusEncodeStream.SampleRate; | |||
private readonly AudioOutStream _next; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _buffer; | |||
private readonly OpusDecoder _decoder; | |||
public OpusDecodeStream(AudioOutStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) | |||
public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) | |||
{ | |||
_next = next; | |||
_buffer = new byte[bufferSize]; | |||
@@ -9,7 +9,7 @@ namespace Discord.Audio.Streams | |||
{ | |||
public const int SampleRate = 48000; | |||
private readonly AudioOutStream _next; | |||
private readonly AudioStream _next; | |||
private readonly OpusEncoder _encoder; | |||
private readonly byte[] _buffer; | |||
@@ -17,7 +17,7 @@ namespace Discord.Audio.Streams | |||
private byte[] _partialFrameBuffer; | |||
private int _partialFramePos; | |||
public OpusEncodeStream(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) | |||
public OpusEncodeStream(AudioStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) | |||
{ | |||
_next = next; | |||
_encoder = new OpusEncoder(SampleRate, channels, bitrate, application); | |||
@@ -9,20 +9,19 @@ namespace Discord.Audio.Streams | |||
public class RTPReadStream : AudioOutStream | |||
{ | |||
private readonly InputStream _queue; | |||
private readonly AudioOutStream _next; | |||
private readonly byte[] _buffer, _nonce, _secretKey; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _buffer, _nonce; | |||
public override bool CanRead => true; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
public RTPReadStream(InputStream queue, byte[] secretKey, int bufferSize = 4000) | |||
: this(queue, null, secretKey, bufferSize) { } | |||
public RTPReadStream(InputStream queue, AudioOutStream next, byte[] secretKey, int bufferSize = 4000) | |||
public RTPReadStream(InputStream queue, int bufferSize = 4000) | |||
: this(queue, null, bufferSize) { } | |||
public RTPReadStream(InputStream queue, AudioStream next, int bufferSize = 4000) | |||
{ | |||
_queue = queue; | |||
_next = next; | |||
_secretKey = secretKey; | |||
_buffer = new byte[bufferSize]; | |||
_nonce = new byte[24]; | |||
} | |||
@@ -7,14 +7,14 @@ namespace Discord.Audio.Streams | |||
///<summary> Wraps data in an RTP frame </summary> | |||
public class RTPWriteStream : AudioOutStream | |||
{ | |||
private readonly AudioOutStream _next; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _header; | |||
private int _samplesPerFrame; | |||
private uint _ssrc, _timestamp = 0; | |||
protected readonly byte[] _buffer; | |||
public RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) | |||
public RTPWriteStream(AudioStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) | |||
{ | |||
_next = next; | |||
_samplesPerFrame = samplesPerFrame; | |||
@@ -7,18 +7,18 @@ namespace Discord.Audio.Streams | |||
///<summary> Decrypts an RTP frame using libsodium </summary> | |||
public class SodiumDecryptStream : AudioOutStream | |||
{ | |||
private readonly AudioOutStream _next; | |||
private readonly byte[] _buffer, _nonce, _secretKey; | |||
private readonly AudioClient _client; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _nonce; | |||
public override bool CanRead => true; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
public SodiumDecryptStream(AudioOutStream next, byte[] secretKey, int bufferSize = 4000) | |||
public SodiumDecryptStream(AudioStream next, IAudioClient client) | |||
{ | |||
_next = next; | |||
_secretKey = secretKey; | |||
_buffer = new byte[bufferSize]; | |||
_client = (AudioClient)client; | |||
_nonce = new byte[24]; | |||
} | |||
@@ -26,11 +26,11 @@ namespace Discord.Audio.Streams | |||
{ | |||
cancelToken.ThrowIfCancellationRequested(); | |||
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce | |||
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); | |||
if (_client.SecretKey == null) | |||
return; | |||
var newBuffer = new byte[count]; | |||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); | |||
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); | |||
await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); | |||
} | |||
@@ -7,16 +7,14 @@ namespace Discord.Audio.Streams | |||
///<summary> Encrypts an RTP frame using libsodium </summary> | |||
public class SodiumEncryptStream : AudioOutStream | |||
{ | |||
private readonly AudioOutStream _next; | |||
private readonly byte[] _nonce, _secretKey; | |||
private readonly AudioClient _client; | |||
private readonly AudioStream _next; | |||
private readonly byte[] _nonce; | |||
//protected readonly byte[] _buffer; | |||
public SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/) | |||
public SodiumEncryptStream(AudioStream next, IAudioClient client) | |||
{ | |||
_next = next; | |||
_secretKey = secretKey; | |||
//_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt? | |||
_client = (AudioClient)client; | |||
_nonce = new byte[24]; | |||
} | |||
@@ -24,8 +22,11 @@ namespace Discord.Audio.Streams | |||
{ | |||
cancelToken.ThrowIfCancellationRequested(); | |||
if (_client.SecretKey == 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, _secretKey); | |||
count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); | |||
await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); | |||
} | |||