| @@ -1,5 +1,4 @@ | |||
| using Discord.API; | |||
| using Discord.API.Client; | |||
| using Discord.API.Client; | |||
| using Discord.API.Client.VoiceSocket; | |||
| using Discord.Audio; | |||
| using Discord.Audio.Opus; | |||
| @@ -20,150 +19,150 @@ using System.Threading.Tasks; | |||
| namespace Discord.Net.WebSockets | |||
| { | |||
| public partial class VoiceWebSocket : WebSocket | |||
| { | |||
| private const int MaxOpusSize = 4000; | |||
| public partial class VoiceWebSocket : WebSocket | |||
| { | |||
| private const int MaxOpusSize = 4000; | |||
| private const string EncryptedMode = "xsalsa20_poly1305"; | |||
| private const string UnencryptedMode = "plain"; | |||
| private readonly int _targetAudioBufferLength; | |||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
| private readonly AudioClient _audioClient; | |||
| private const string UnencryptedMode = "plain"; | |||
| private readonly int _targetAudioBufferLength; | |||
| private readonly ConcurrentDictionary<uint, OpusDecoder> _decoders; | |||
| private readonly AudioClient _audioClient; | |||
| private readonly AudioServiceConfig _config; | |||
| private Thread _sendThread, _receiveThread; | |||
| private VoiceBuffer _sendBuffer; | |||
| private OpusEncoder _encoder; | |||
| private uint _ssrc; | |||
| private ConcurrentDictionary<uint, ulong> _ssrcMapping; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _endpoint; | |||
| private bool _isEncrypted; | |||
| private byte[] _secretKey, _encodingBuffer; | |||
| private ushort _sequence; | |||
| private string _encryptionMode; | |||
| private int _ping; | |||
| private uint _ssrc; | |||
| private ConcurrentDictionary<uint, ulong> _ssrcMapping; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _endpoint; | |||
| private bool _isEncrypted; | |||
| private byte[] _secretKey, _encodingBuffer; | |||
| private ushort _sequence; | |||
| private string _encryptionMode; | |||
| private int _ping; | |||
| public string Token { get; internal set; } | |||
| public Server Server { get; internal set; } | |||
| public Channel Channel { get; internal set; } | |||
| public Channel Channel { get; internal set; } | |||
| public int Ping => _ping; | |||
| internal VoiceBuffer OutputBuffer => _sendBuffer; | |||
| internal VoiceBuffer OutputBuffer => _sendBuffer; | |||
| internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, JsonSerializer serializer, Logger logger) | |||
| : base(client, serializer, logger) | |||
| { | |||
| internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, JsonSerializer serializer, Logger logger) | |||
| : base(client, serializer, logger) | |||
| { | |||
| _audioClient = audioClient; | |||
| _config = client.Audio().Config; | |||
| _decoders = new ConcurrentDictionary<uint, OpusDecoder>(); | |||
| _targetAudioBufferLength = _config.BufferLength / 20; //20 ms frames | |||
| _encodingBuffer = new byte[MaxOpusSize]; | |||
| _ssrcMapping = new ConcurrentDictionary<uint, ulong>(); | |||
| _encoder = new OpusEncoder(48000, _config.Channels, 20, _config.Bitrate, OpusApplication.Audio); | |||
| _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||
| _targetAudioBufferLength = _config.BufferLength / 20; //20 ms frames | |||
| _encodingBuffer = new byte[MaxOpusSize]; | |||
| _ssrcMapping = new ConcurrentDictionary<uint, ulong>(); | |||
| _encoder = new OpusEncoder(48000, _config.Channels, 20, _config.Bitrate, OpusApplication.Audio); | |||
| _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); | |||
| } | |||
| public Task Connect() | |||
| public Task Connect() | |||
| => BeginConnect(); | |||
| private async Task Reconnect() | |||
| { | |||
| try | |||
| { | |||
| var cancelToken = ParentCancelToken.Value; | |||
| private async Task Reconnect() | |||
| { | |||
| try | |||
| { | |||
| var cancelToken = ParentCancelToken.Value; | |||
| await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| try | |||
| { | |||
| await Connect().ConfigureAwait(false); | |||
| break; | |||
| } | |||
| catch (OperationCanceledException) { throw; } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error("Reconnect failed", ex); | |||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||
| await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| } | |||
| public Task Disconnect() => _taskManager.Stop(); | |||
| protected override async Task Run() | |||
| { | |||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
| List<Task> tasks = new List<Task>(); | |||
| if ((_config.Mode & AudioMode.Outgoing) != 0) | |||
| { | |||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(CancelToken))); | |||
| _sendThread.IsBackground = true; | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| try | |||
| { | |||
| await Connect().ConfigureAwait(false); | |||
| break; | |||
| } | |||
| catch (OperationCanceledException) { throw; } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error("Reconnect failed", ex); | |||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||
| await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| } | |||
| public Task Disconnect() => _taskManager.Stop(true); | |||
| protected override async Task Run() | |||
| { | |||
| _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | |||
| List<Task> tasks = new List<Task>(); | |||
| if ((_config.Mode & AudioMode.Outgoing) != 0) | |||
| { | |||
| _sendThread = new Thread(new ThreadStart(() => SendVoiceAsync(CancelToken))); | |||
| _sendThread.IsBackground = true; | |||
| _sendThread.Start(); | |||
| } | |||
| } | |||
| /*if ((_config.Mode & AudioMode.Incoming) != 0) | |||
| {*/ | |||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(CancelToken))); | |||
| _receiveThread.IsBackground = true; | |||
| _receiveThread.Start(); | |||
| _receiveThread = new Thread(new ThreadStart(() => ReceiveVoiceAsync(CancelToken))); | |||
| _receiveThread.IsBackground = true; | |||
| _receiveThread.Start(); | |||
| /*} | |||
| else | |||
| tasks.Add(Task.Run(() => ReceiveVoiceAsync(CancelToken)));*/ | |||
| SendIdentify(); | |||
| SendIdentify(); | |||
| #if !DOTNET5_4 | |||
| tasks.Add(WatcherAsync()); | |||
| tasks.Add(WatcherAsync()); | |||
| #endif | |||
| tasks.AddRange(_engine.GetTasks(CancelToken)); | |||
| tasks.Add(HeartbeatAsync(CancelToken)); | |||
| await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||
| } | |||
| protected override Task Cleanup() | |||
| { | |||
| if (_sendThread != null) | |||
| _sendThread.Join(); | |||
| if (_receiveThread != null) | |||
| _receiveThread.Join(); | |||
| _sendThread = null; | |||
| _receiveThread = null; | |||
| OpusDecoder decoder; | |||
| foreach (var pair in _decoders) | |||
| { | |||
| if (_decoders.TryRemove(pair.Key, out decoder)) | |||
| decoder.Dispose(); | |||
| } | |||
| protected override Task Cleanup() | |||
| { | |||
| if (_sendThread != null) | |||
| _sendThread.Join(); | |||
| if (_receiveThread != null) | |||
| _receiveThread.Join(); | |||
| _sendThread = null; | |||
| _receiveThread = null; | |||
| OpusDecoder decoder; | |||
| foreach (var pair in _decoders) | |||
| { | |||
| if (_decoders.TryRemove(pair.Key, out decoder)) | |||
| decoder.Dispose(); | |||
| } | |||
| ClearPCMFrames(); | |||
| _udp = null; | |||
| return base.Cleanup(); | |||
| } | |||
| private void ReceiveVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| var closeTask = cancelToken.Wait(); | |||
| try | |||
| { | |||
| byte[] packet, decodingBuffer = null, nonce = null, result; | |||
| int packetLength, resultOffset, resultLength; | |||
| IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | |||
| if ((_config.Mode & AudioMode.Incoming) != 0) | |||
| { | |||
| decodingBuffer = new byte[MaxOpusSize]; | |||
| nonce = new byte[24]; | |||
| } | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| Thread.Sleep(1); | |||
| if (_udp.Available > 0) | |||
| { | |||
| ClearPCMFrames(); | |||
| _udp = null; | |||
| return base.Cleanup(); | |||
| } | |||
| private void ReceiveVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| var closeTask = cancelToken.Wait(); | |||
| try | |||
| { | |||
| byte[] packet, decodingBuffer = null, nonce = null, result; | |||
| int packetLength, resultOffset, resultLength; | |||
| IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | |||
| if ((_config.Mode & AudioMode.Incoming) != 0) | |||
| { | |||
| decodingBuffer = new byte[MaxOpusSize]; | |||
| nonce = new byte[24]; | |||
| } | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| Thread.Sleep(1); | |||
| if (_udp.Available > 0) | |||
| { | |||
| #if !DOTNET5_4 | |||
| packet = _udp.Receive(ref endpoint); | |||
| packet = _udp.Receive(ref endpoint); | |||
| #else | |||
| //TODO: Is this really the only way to end a Receive call in DOTNET5_4? | |||
| var receiveTask = _udp.ReceiveAsync(); | |||
| @@ -174,316 +173,326 @@ namespace Discord.Net.WebSockets | |||
| packet = udpPacket.Buffer; | |||
| endpoint = udpPacket.RemoteEndPoint; | |||
| #endif | |||
| packetLength = packet.Length; | |||
| packetLength = packet.Length; | |||
| if (packetLength > 0 && endpoint.Equals(_endpoint)) | |||
| { | |||
| if (State != ConnectionState.Connected) | |||
| { | |||
| if (packetLength != 70) | |||
| return; | |||
| string ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); | |||
| int port = packet[68] | packet[69] << 8; | |||
| SendSelectProtocol(ip, port); | |||
| if ((_config.Mode & AudioMode.Incoming) == 0) | |||
| return; //We dont need this thread anymore | |||
| } | |||
| else | |||
| { | |||
| //Parse RTP Data | |||
| if (packetLength < 12) return; | |||
| if (packet[0] != 0x80) return; //Flags | |||
| if (packet[1] != 0x78) return; //Payload Type | |||
| 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 = SecretBox.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 (State != ConnectionState.Connected) | |||
| { | |||
| if (packetLength != 70) | |||
| return; | |||
| string ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); | |||
| int port = packet[68] | packet[69] << 8; | |||
| SendSelectProtocol(ip, port); | |||
| if ((_config.Mode & AudioMode.Incoming) == 0) | |||
| return; //We dont need this thread anymore | |||
| } | |||
| else | |||
| { | |||
| //Parse RTP Data | |||
| if (packetLength < 12) return; | |||
| if (packet[0] != 0x80) return; //Flags | |||
| if (packet[1] != 0x78) return; //Payload Type | |||
| 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 = SecretBox.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.");*/ | |||
| ulong userId; | |||
| if (_ssrcMapping.TryGetValue(ssrc, out userId)) | |||
| RaiseOnPacket(userId, Channel.Id, result, resultOffset, resultLength); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (_ssrcMapping.TryGetValue(ssrc, out userId)) | |||
| RaiseOnPacket(userId, Channel.Id, result, resultOffset, resultLength); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| } | |||
| private void SendVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| try | |||
| { | |||
| while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected) | |||
| Thread.Sleep(1); | |||
| if (cancelToken.IsCancellationRequested) | |||
| return; | |||
| byte[] frame = new byte[_encoder.FrameSize]; | |||
| byte[] encodedFrame = new byte[MaxOpusSize]; | |||
| byte[] voicePacket, pingPacket, nonce = null; | |||
| uint timestamp = 0; | |||
| double nextTicks = 0.0, nextPingTicks = 0.0; | |||
| long ticksPerSeconds = Stopwatch.Frequency; | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| } | |||
| private void SendVoiceAsync(CancellationToken cancelToken) | |||
| { | |||
| try | |||
| { | |||
| while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected) | |||
| Thread.Sleep(1); | |||
| if (cancelToken.IsCancellationRequested) | |||
| return; | |||
| byte[] frame = new byte[_encoder.FrameSize]; | |||
| byte[] encodedFrame = new byte[MaxOpusSize]; | |||
| byte[] voicePacket, pingPacket, nonce = null; | |||
| uint timestamp = 0; | |||
| double nextTicks = 0.0, nextPingTicks = 0.0; | |||
| long ticksPerSeconds = Stopwatch.Frequency; | |||
| double ticksPerMillisecond = Stopwatch.Frequency / 1000.0; | |||
| double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; | |||
| double spinLockThreshold = 3 * ticksPerMillisecond; | |||
| uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||
| Stopwatch sw = Stopwatch.StartNew(); | |||
| if (_isEncrypted) | |||
| { | |||
| nonce = new byte[24]; | |||
| voicePacket = new byte[MaxOpusSize + 12 + 16]; | |||
| } | |||
| else | |||
| voicePacket = new byte[MaxOpusSize + 12]; | |||
| pingPacket = new byte[8]; | |||
| pingPacket[0] = 0x80; //Flags; | |||
| pingPacket[1] = 0xC9; //Payload Type | |||
| pingPacket[2] = 0x00; //Length | |||
| pingPacket[3] = 0x01; //Length (1*8 bytes) | |||
| pingPacket[4] = (byte)((_ssrc >> 24) & 0xFF); | |||
| pingPacket[5] = (byte)((_ssrc >> 16) & 0xFF); | |||
| pingPacket[6] = (byte)((_ssrc >> 8) & 0xFF); | |||
| pingPacket[7] = (byte)((_ssrc >> 0) & 0xFF); | |||
| if (_isEncrypted) | |||
| { | |||
| Buffer.BlockCopy(pingPacket, 0, nonce, 0, 8); | |||
| int ret = SecretBox.Encrypt(pingPacket, 8, encodedFrame, 0, nonce, _secretKey); | |||
| if (ret != 0) | |||
| throw new InvalidOperationException("Failed to encrypt ping packet"); | |||
| pingPacket = new byte[pingPacket.Length + 16]; | |||
| Buffer.BlockCopy(encodedFrame, 0, pingPacket, 0, pingPacket.Length); | |||
| Array.Clear(nonce, 0, nonce.Length); | |||
| double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; | |||
| double spinLockThreshold = 3 * ticksPerMillisecond; | |||
| uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; | |||
| Stopwatch sw = Stopwatch.StartNew(); | |||
| if (_isEncrypted) | |||
| { | |||
| nonce = new byte[24]; | |||
| voicePacket = new byte[MaxOpusSize + 12 + 16]; | |||
| } | |||
| else | |||
| voicePacket = new byte[MaxOpusSize + 12]; | |||
| pingPacket = new byte[8]; | |||
| pingPacket[0] = 0x80; //Flags; | |||
| pingPacket[1] = 0xC9; //Payload Type | |||
| pingPacket[2] = 0x00; //Length | |||
| pingPacket[3] = 0x01; //Length (1*8 bytes) | |||
| pingPacket[4] = (byte)((_ssrc >> 24) & 0xFF); | |||
| pingPacket[5] = (byte)((_ssrc >> 16) & 0xFF); | |||
| pingPacket[6] = (byte)((_ssrc >> 8) & 0xFF); | |||
| pingPacket[7] = (byte)((_ssrc >> 0) & 0xFF); | |||
| if (_isEncrypted) | |||
| { | |||
| Buffer.BlockCopy(pingPacket, 0, nonce, 0, 8); | |||
| int ret = SecretBox.Encrypt(pingPacket, 8, encodedFrame, 0, nonce, _secretKey); | |||
| if (ret != 0) | |||
| throw new InvalidOperationException("Failed to encrypt ping packet"); | |||
| pingPacket = new byte[pingPacket.Length + 16]; | |||
| Buffer.BlockCopy(encodedFrame, 0, pingPacket, 0, pingPacket.Length); | |||
| Array.Clear(nonce, 0, nonce.Length); | |||
| } | |||
| int rtpPacketLength = 0; | |||
| voicePacket[0] = 0x80; //Flags; | |||
| voicePacket[1] = 0x78; //Payload Type | |||
| voicePacket[8] = (byte)((_ssrc >> 24) & 0xFF); | |||
| voicePacket[9] = (byte)((_ssrc >> 16) & 0xFF); | |||
| voicePacket[10] = (byte)((_ssrc >> 8) & 0xFF); | |||
| voicePacket[11] = (byte)((_ssrc >> 0) & 0xFF); | |||
| if (_isEncrypted) | |||
| Buffer.BlockCopy(voicePacket, 0, nonce, 0, 12); | |||
| bool hasFrame = false; | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| if (!hasFrame && _sendBuffer.Pop(frame)) | |||
| { | |||
| ushort sequence = unchecked(_sequence++); | |||
| voicePacket[2] = (byte)((sequence >> 8) & 0xFF); | |||
| voicePacket[3] = (byte)((sequence >> 0) & 0xFF); | |||
| voicePacket[4] = (byte)((timestamp >> 24) & 0xFF); | |||
| voicePacket[5] = (byte)((timestamp >> 16) & 0xFF); | |||
| voicePacket[6] = (byte)((timestamp >> 8) & 0xFF); | |||
| voicePacket[7] = (byte)((timestamp >> 0) & 0xFF); | |||
| //Encode | |||
| int encodedLength = _encoder.EncodeFrame(frame, 0, encodedFrame); | |||
| //Encrypt | |||
| if (_isEncrypted) | |||
| { | |||
| Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce | |||
| int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey); | |||
| if (ret != 0) | |||
| continue; | |||
| rtpPacketLength = encodedLength + 12 + 16; | |||
| } | |||
| else | |||
| { | |||
| Buffer.BlockCopy(encodedFrame, 0, voicePacket, 12, encodedLength); | |||
| rtpPacketLength = encodedLength + 12; | |||
| } | |||
| timestamp = unchecked(timestamp + samplesPerFrame); | |||
| hasFrame = true; | |||
| } | |||
| int rtpPacketLength = 0; | |||
| voicePacket[0] = 0x80; //Flags; | |||
| voicePacket[1] = 0x78; //Payload Type | |||
| voicePacket[8] = (byte)((_ssrc >> 24) & 0xFF); | |||
| voicePacket[9] = (byte)((_ssrc >> 16) & 0xFF); | |||
| voicePacket[10] = (byte)((_ssrc >> 8) & 0xFF); | |||
| voicePacket[11] = (byte)((_ssrc >> 0) & 0xFF); | |||
| if (_isEncrypted) | |||
| Buffer.BlockCopy(voicePacket, 0, nonce, 0, 12); | |||
| bool hasFrame = false; | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| if (!hasFrame && _sendBuffer.Pop(frame)) | |||
| { | |||
| ushort sequence = unchecked(_sequence++); | |||
| voicePacket[2] = (byte)((sequence >> 8) & 0xFF); | |||
| voicePacket[3] = (byte)((sequence >> 0) & 0xFF); | |||
| voicePacket[4] = (byte)((timestamp >> 24) & 0xFF); | |||
| voicePacket[5] = (byte)((timestamp >> 16) & 0xFF); | |||
| voicePacket[6] = (byte)((timestamp >> 8) & 0xFF); | |||
| voicePacket[7] = (byte)((timestamp >> 0) & 0xFF); | |||
| //Encode | |||
| int encodedLength = _encoder.EncodeFrame(frame, 0, encodedFrame); | |||
| //Encrypt | |||
| if (_isEncrypted) | |||
| { | |||
| Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce | |||
| int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey); | |||
| if (ret != 0) | |||
| continue; | |||
| rtpPacketLength = encodedLength + 12 + 16; | |||
| } | |||
| else | |||
| { | |||
| Buffer.BlockCopy(encodedFrame, 0, voicePacket, 12, encodedLength); | |||
| rtpPacketLength = encodedLength + 12; | |||
| } | |||
| timestamp = unchecked(timestamp + samplesPerFrame); | |||
| hasFrame = true; | |||
| } | |||
| long currentTicks = sw.ElapsedTicks; | |||
| double ticksToNextFrame = nextTicks - currentTicks; | |||
| if (ticksToNextFrame <= 0.0) | |||
| { | |||
| if (hasFrame) | |||
| { | |||
| try | |||
| { | |||
| _udp.Send(voicePacket, rtpPacketLength); | |||
| } | |||
| catch (SocketException ex) | |||
| { | |||
| Logger.Error("Failed to send UDP packet.", ex); | |||
| } | |||
| hasFrame = false; | |||
| } | |||
| nextTicks += ticksPerFrame; | |||
| //Is it time to send out another ping? | |||
| if (currentTicks > nextPingTicks) | |||
| { | |||
| _udp.Send(pingPacket, pingPacket.Length); | |||
| nextPingTicks = currentTicks + 5 * ticksPerSeconds; | |||
| double ticksToNextFrame = nextTicks - currentTicks; | |||
| if (ticksToNextFrame <= 0.0) | |||
| { | |||
| if (hasFrame) | |||
| { | |||
| try | |||
| { | |||
| _udp.Send(voicePacket, rtpPacketLength); | |||
| } | |||
| catch (SocketException ex) | |||
| { | |||
| Logger.Error("Failed to send UDP packet.", ex); | |||
| } | |||
| hasFrame = false; | |||
| } | |||
| nextTicks += ticksPerFrame; | |||
| //Is it time to send out another ping? | |||
| if (currentTicks > nextPingTicks) | |||
| { | |||
| _udp.Send(pingPacket, pingPacket.Length); | |||
| nextPingTicks = currentTicks + 5 * ticksPerSeconds; | |||
| } | |||
| } | |||
| else | |||
| { | |||
| if (hasFrame) | |||
| { | |||
| int time = (int)Math.Floor(ticksToNextFrame / ticksPerMillisecond); | |||
| if (time > 0) | |||
| Thread.Sleep(time); | |||
| } | |||
| else | |||
| Thread.Sleep(1); //Give as much time to the encrypter as possible | |||
| } | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| } | |||
| else | |||
| { | |||
| if (hasFrame) | |||
| { | |||
| int time = (int)Math.Floor(ticksToNextFrame / ticksPerMillisecond); | |||
| if (time > 0) | |||
| Thread.Sleep(time); | |||
| } | |||
| else | |||
| Thread.Sleep(1); //Give as much time to the encrypter as possible | |||
| } | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| catch (InvalidOperationException) { } //Includes ObjectDisposedException | |||
| } | |||
| #if !DOTNET5_4 | |||
| //Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken | |||
| private Task WatcherAsync() | |||
| //Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken | |||
| private Task WatcherAsync() | |||
| => CancelToken.Wait().ContinueWith(_ => _udp.Close()); | |||
| #endif | |||
| protected override async Task ProcessMessage(string json) | |||
| { | |||
| await base.ProcessMessage(json).ConfigureAwait(false); | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| var opCode = (OpCodes)msg.Operation; | |||
| protected override async Task ProcessMessage(string json) | |||
| { | |||
| await base.ProcessMessage(json).ConfigureAwait(false); | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| var opCode = (OpCodes)msg.Operation; | |||
| switch (opCode) | |||
| { | |||
| case OpCodes.Ready: | |||
| { | |||
| if (State != ConnectionState.Connected) | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| _ssrc = payload.SSRC; | |||
| { | |||
| case OpCodes.Ready: | |||
| { | |||
| if (State != ConnectionState.Connected) | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
| _heartbeatInterval = payload.HeartbeatInterval; | |||
| _ssrc = payload.SSRC; | |||
| var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); | |||
| _endpoint = new IPEndPoint(address, payload.Port); | |||
| if (_config.EnableEncryption) | |||
| { | |||
| if (payload.Modes.Contains(EncryptedMode)) | |||
| { | |||
| _encryptionMode = EncryptedMode; | |||
| _isEncrypted = true; | |||
| } | |||
| else | |||
| throw new InvalidOperationException("Unexpected encryption format."); | |||
| } | |||
| else | |||
| { | |||
| _encryptionMode = UnencryptedMode; | |||
| _isEncrypted = false; | |||
| if (_config.EnableEncryption) | |||
| { | |||
| if (payload.Modes.Contains(EncryptedMode)) | |||
| { | |||
| _encryptionMode = EncryptedMode; | |||
| _isEncrypted = true; | |||
| } | |||
| else | |||
| throw new InvalidOperationException("Unexpected encryption format."); | |||
| } | |||
| else | |||
| { | |||
| _encryptionMode = UnencryptedMode; | |||
| _isEncrypted = false; | |||
| } | |||
| _udp.Connect(_endpoint); | |||
| _sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue); | |||
| //No thread issue here because SendAsync doesn't start until _isReady is true | |||
| 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; | |||
| case OpCodes.Heartbeat: | |||
| { | |||
| long time = EpochTime.GetMilliseconds(); | |||
| var payload = (long)msg.Payload; | |||
| _ping = (int)(payload - time); | |||
| //TODO: Use this to estimate latency | |||
| } | |||
| break; | |||
| case OpCodes.SessionDescription: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<SessionDescriptionEvent>(_serializer); | |||
| _secretKey = payload.SecretKey; | |||
| SendSetSpeaking(true); | |||
| EndConnect(); | |||
| } | |||
| break; | |||
| case OpCodes.Speaking: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<SpeakingEvent>(_serializer); | |||
| RaiseIsSpeaking(payload.UserId, payload.IsSpeaking); | |||
| } | |||
| break; | |||
| default: | |||
| _sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue); | |||
| //No thread issue here because SendAsync doesn't start until _isReady is true | |||
| 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; | |||
| case OpCodes.Heartbeat: | |||
| { | |||
| long time = EpochTime.GetMilliseconds(); | |||
| var payload = (long)msg.Payload; | |||
| _ping = (int)(payload - time); | |||
| //TODO: Use this to estimate latency | |||
| } | |||
| break; | |||
| case OpCodes.SessionDescription: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<SessionDescriptionEvent>(_serializer); | |||
| _secretKey = payload.SecretKey; | |||
| SendSetSpeaking(true); | |||
| EndConnect(); | |||
| } | |||
| break; | |||
| case OpCodes.Speaking: | |||
| { | |||
| var payload = (msg.Payload as JToken).ToObject<SpeakingEvent>(_serializer); | |||
| RaiseIsSpeaking(payload.UserId, payload.IsSpeaking); | |||
| } | |||
| break; | |||
| default: | |||
| Logger.Warning($"Unknown Opcode: {opCode}"); | |||
| break; | |||
| } | |||
| } | |||
| public void SendPCMFrames(byte[] data, int bytes) | |||
| { | |||
| _sendBuffer.Push(data, bytes, CancelToken); | |||
| } | |||
| public void ClearPCMFrames() | |||
| { | |||
| _sendBuffer.Clear(CancelToken); | |||
| } | |||
| public void WaitForQueue() | |||
| { | |||
| _sendBuffer.Wait(CancelToken); | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| public void SendPCMFrames(byte[] data, int bytes) | |||
| { | |||
| _sendBuffer.Push(data, bytes, CancelToken); | |||
| } | |||
| public void ClearPCMFrames() | |||
| { | |||
| _sendBuffer.Clear(CancelToken); | |||
| } | |||
| public void WaitForQueue() | |||
| { | |||
| _sendBuffer.Wait(CancelToken); | |||
| } | |||
| public override void SendHeartbeat() | |||
| => QueueMessage(new HeartbeatCommand()); | |||
| public void SendIdentify() | |||
| => QueueMessage(new IdentifyCommand { GuildId = Server.Id, UserId = _client.CurrentUser.Id, | |||
| SessionId = _client.SessionId, Token = Token }); | |||
| => QueueMessage(new IdentifyCommand | |||
| { | |||
| GuildId = Server.Id, | |||
| UserId = _client.CurrentUser.Id, | |||
| SessionId = _client.SessionId, | |||
| Token = Token | |||
| }); | |||
| public void SendSelectProtocol(string externalAddress, int externalPort) | |||
| => QueueMessage(new SelectProtocolCommand { Protocol = "udp", ExternalAddress = externalAddress, | |||
| ExternalPort = externalPort, EncryptionMode = _encryptionMode }); | |||
| => QueueMessage(new SelectProtocolCommand | |||
| { | |||
| Protocol = "udp", | |||
| ExternalAddress = externalAddress, | |||
| ExternalPort = externalPort, | |||
| EncryptionMode = _encryptionMode | |||
| }); | |||
| public void SendSetSpeaking(bool value) | |||
| => QueueMessage(new SetSpeakingCommand { IsSpeaking = value, Delay = 0 }); | |||
| } | |||
| } | |||
| } | |||