Concrete class prototype

This commit is contained in:
RogueException
2016-09-22 21:15:37 -03:00
parent ab42129eb9
commit 6319933ed0
394 changed files with 3648 additions and 3224 deletions

View File

@@ -0,0 +1,331 @@
using Discord.API.Voice;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.WebSocket;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
internal class AudioClient : IAudioClient, IDisposable
{
public event Func<Task> Connected
{
add { _connectedEvent.Add(value); }
remove { _connectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
public event Func<Exception, Task> Disconnected
{
add { _disconnectedEvent.Add(value); }
remove { _disconnectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
public event Func<int, int, Task> LatencyUpdated
{
add { _latencyUpdatedEvent.Add(value); }
remove { _latencyUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>();
private readonly ILogger _audioLogger;
#if BENCHMARK
private readonly ILogger _benchmarkLogger;
#endif
internal readonly SemaphoreSlim _connectionLock;
private readonly JsonSerializer _serializer;
private TaskCompletionSource<bool> _connectTask;
private CancellationTokenSource _cancelToken;
private Task _heartbeatTask;
private long _heartbeatTime;
private string _url;
private bool _isDisposed;
private uint _ssrc;
private byte[] _secretKey;
public SocketGuild Guild { get; }
public DiscordVoiceAPIClient ApiClient { get; private set; }
public ConnectionState ConnectionState { get; private set; }
public int Latency { get; private set; }
private DiscordSocketClient Discord => Guild.Discord;
/// <summary> Creates a new REST/WebSocket discord client. </summary>
public AudioClient(SocketGuild guild, int id)
{
Guild = guild;
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}");
#if BENCHMARK
_benchmarkLogger = logManager.CreateLogger("Benchmark");
#endif
_connectionLock = new SemaphoreSlim(1, 1);
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
_serializer.Error += (s, e) =>
{
_audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
e.ErrorContext.Handled = true;
};
ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider);
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false);
ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
ApiClient.ReceivedEvent += ProcessMessageAsync;
ApiClient.ReceivedPacket += ProcessPacketAsync;
ApiClient.Disconnected += async ex =>
{
if (ex != null)
await _audioLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false);
else
await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
};
LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ConnectAsync(string url, ulong userId, string sessionId, string token)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token)
{
var state = ConnectionState;
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
await DisconnectInternalAsync(null).ConfigureAwait(false);
ConnectionState = ConnectionState.Connecting;
await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false);
try
{
_url = url;
_connectTask = new TaskCompletionSource<bool>();
_cancelToken = new CancellationTokenSource();
await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false);
await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false);
await _connectTask.Task.ConfigureAwait(false);
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Connected;
await _audioLogger.InfoAsync("Connected").ConfigureAwait(false);
}
catch (Exception)
{
await DisconnectInternalAsync(null).ConfigureAwait(false);
throw;
}
}
/// <inheritdoc />
public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(null).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectAsync(Exception ex)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(ex).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex)
{
if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false);
//Signal tasks to complete
try { _cancelToken.Cancel(); } catch { }
//Disconnect from server
await ApiClient.DisconnectAsync().ConfigureAwait(false);
//Wait for tasks to complete
var heartbeatTask = _heartbeatTask;
if (heartbeatTask != null)
await heartbeatTask.ConfigureAwait(false);
_heartbeatTask = null;
ConnectionState = ConnectionState.Disconnected;
await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
}
public void Send(byte[] data, int count)
{
//TODO: Queue these?
ApiClient.SendAsync(data, count).ConfigureAwait(false);
}
public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000)
{
return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000);
}
public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null,
OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000)
{
return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, application, bufferSize);
}
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
{
#if BENCHMARK
Stopwatch stopwatch = Stopwatch.StartNew();
try
{
#endif
try
{
switch (opCode)
{
case VoiceOpCode.Ready:
{
await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false);
var data = (payload as JToken).ToObject<ReadyEvent>(_serializer);
_ssrc = data.SSRC;
if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode))
throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}");
_heartbeatTime = 0;
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token);
var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false);
ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port));
await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false);
}
break;
case VoiceOpCode.SessionDescription:
{
await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false);
var data = (payload as JToken).ToObject<SessionDescriptionEvent>(_serializer);
if (data.Mode != DiscordVoiceAPIClient.Mode)
throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}");
_secretKey = data.SecretKey;
await ApiClient.SendSetSpeaking(true).ConfigureAwait(false);
var _ = _connectTask.TrySetResultAsync(true);
}
break;
case VoiceOpCode.HeartbeatAck:
{
await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false);
var heartbeatTime = _heartbeatTime;
if (heartbeatTime != 0)
{
int latency = (int)(Environment.TickCount - _heartbeatTime);
_heartbeatTime = 0;
int before = Latency;
Latency = latency;
await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false);
}
}
break;
default:
await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false);
return;
}
}
catch (Exception ex)
{
await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false);
return;
}
#if BENCHMARK
}
finally
{
stopwatch.Stop();
double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false);
}
#endif
}
private async Task ProcessPacketAsync(byte[] packet)
{
if (!_connectTask.Task.IsCompleted)
{
if (packet.Length == 70)
{
string ip;
int port;
try
{
ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0');
port = packet[69] | (packet[68] << 8);
}
catch { return; }
await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false);
await ApiClient.SendSelectProtocol(ip, port);
}
}
}
private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken)
{
//Clean this up when Discord's session patch is live
try
{
while (!cancelToken.IsCancellationRequested)
{
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
if (_heartbeatTime != 0) //Server never responded to our last heartbeat
{
if (ConnectionState == ConnectionState.Connected)
{
await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false);
await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false);
return;
}
}
else
_heartbeatTime = Environment.TickCount;
await ApiClient.SendHeartbeatAsync().ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
}
internal virtual void Dispose(bool disposing)
{
if (!_isDisposed)
_isDisposed = true;
ApiClient.Dispose();
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Discord.Audio
{
[Flags]
public enum AudioMode : byte
{
Disabled = 0,
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}
}

View File

@@ -0,0 +1,51 @@
using System;
namespace Discord.Audio
{
internal abstract class OpusConverter : IDisposable
{
protected IntPtr _ptr;
/// <summary> Gets the bit rate of this converter. </summary>
public const int BitsPerSample = 16;
/// <summary> Gets the bytes per sample. </summary>
public const int SampleSize = (BitsPerSample / 8) * MaxChannels;
/// <summary> Gets the maximum amount of channels this encoder supports. </summary>
public const int MaxChannels = 2;
/// <summary> Gets the input sampling rate of this converter. </summary>
public int SamplingRate { get; }
/// <summary> Gets the number of samples per second for this stream. </summary>
public int Channels { get; }
protected OpusConverter(int samplingRate, int channels)
{
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));
SamplingRate = samplingRate;
Channels = channels;
}
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
disposedValue = true;
}
~OpusConverter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Discord.Audio
{
internal enum OpusCtl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
internal unsafe class OpusDecoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec);
public OpusDecoder(int samplingRate, int channels)
: base(samplingRate, channels)
{
OpusError error;
_ptr = CreateDecoder(samplingRate, channels, out error);
if (error != OpusError.OK)
throw new Exception($"Opus Error: {error}");
}
/// <summary> Produces PCM samples from Opus-encoded audio. </summary>
/// <param name="input">PCM samples to decode.</param>
/// <param name="inputOffset">Offset of the frame in input.</param>
/// <param name="output">Buffer to store the decoded frame.</param>
public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset)
{
int result = 0;
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
return result;
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyDecoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
internal unsafe class OpusEncoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
/// <summary> Gets the coding mode of the encoder. </summary>
public OpusApplication Application { get; }
public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed)
: base(samplingRate, channels)
{
Application = application;
OpusError error;
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != OpusError.OK)
throw new Exception($"Opus Error: {error}");
}
/// <summary> Produces Opus encoded audio from PCM samples. </summary>
/// <param name="input">PCM samples to encode.</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[] input, int inputOffset, int inputCount, byte[] output, int outputOffset)
{
int result = 0;
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
return result;
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetBitrate(int value)
{
if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(value));
var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyEncoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Discord.Audio
{
internal enum OpusError : int
{
OK = 0,
BadArg = -1,
BufferToSmall = -2,
InternalError = -3,
InvalidPacket = -4,
Unimplemented = -5,
InvalidState = -6,
AllocFail = -7
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Runtime.InteropServices;
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);
[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);
public static int Encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, 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;
}
}
public static int Decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, 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;
}
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Discord.Audio
{
public class OpusDecodeStream : RTPReadStream
{
private readonly byte[] _buffer;
private readonly OpusDecoder _decoder;
internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate,
int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
: base(audioClient, secretKey)
{
_buffer = new byte[bufferSize];
_decoder = new OpusDecoder(samplingRate, channels);
}
public override int Read(byte[] buffer, int offset, int count)
{
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0);
return base.Read(_buffer, 0, count);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_decoder.Dispose();
}
}
}

View File

@@ -0,0 +1,35 @@
namespace Discord.Audio
{
public class OpusEncodeStream : RTPWriteStream
{
public int SampleRate = 48000;
public int Channels = 2;
private readonly OpusEncoder _encoder;
internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int? bitrate = null,
OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000)
: base(audioClient, secretKey, samplesPerFrame, ssrc, bufferSize)
{
_encoder = new OpusEncoder(SampleRate, Channels);
_encoder.SetForwardErrorCorrection(true);
if (bitrate != null)
_encoder.SetBitrate(bitrate.Value);
}
public override void Write(byte[] buffer, int offset, int count)
{
count = _encoder.EncodeFrame(buffer, offset, count, _buffer, 0);
base.Write(_buffer, 0, count);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_encoder.Dispose();
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Concurrent;
using System.IO;
namespace Discord.Audio
{
public class RTPReadStream : Stream
{
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
private readonly AudioClient _audioClient;
private readonly byte[] _buffer, _nonce, _secretKey;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
internal RTPReadStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000)
{
_audioClient = audioClient;
_secretKey = secretKey;
_buffer = new byte[bufferSize];
_queuedData = new BlockingCollection<byte[]>(100);
_nonce = new byte[24];
}
public override int Read(byte[] buffer, int offset, int count)
{
var queuedData = _queuedData.Take();
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
return queuedData.Length;
}
public override void Write(byte[] buffer, int offset, int count)
{
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12);
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);
var newBuffer = new byte[count];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
_queuedData.Add(newBuffer);
}
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(); }
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.IO;
namespace Discord.Audio
{
public class RTPWriteStream : Stream
{
private readonly AudioClient _audioClient;
private readonly byte[] _nonce, _secretKey;
private int _samplesPerFrame;
private uint _ssrc, _timestamp = 0;
protected readonly byte[] _buffer;
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000)
{
_audioClient = audioClient;
_secretKey = secretKey;
_samplesPerFrame = samplesPerFrame;
_ssrc = ssrc;
_buffer = new byte[bufferSize];
_nonce = new byte[24];
_nonce[0] = 0x80;
_nonce[1] = 0x78;
_nonce[8] = (byte)(_ssrc >> 24);
_nonce[9] = (byte)(_ssrc >> 16);
_nonce[10] = (byte)(_ssrc >> 8);
_nonce[11] = (byte)(_ssrc >> 0);
}
public override void Write(byte[] buffer, int offset, int count)
{
unchecked
{
if (_nonce[3]++ == byte.MaxValue)
_nonce[2]++;
_timestamp += (uint)_samplesPerFrame;
_nonce[4] = (byte)(_timestamp >> 24);
_nonce[5] = (byte)(_timestamp >> 16);
_nonce[6] = (byte)(_timestamp >> 8);
_nonce[7] = (byte)(_timestamp >> 0);
}
count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey);
Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer
_audioClient.Send(_buffer, count + 12);
}
public override void Flush() { }
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(); }
}
}