| @@ -17,7 +17,35 @@ namespace Discord.Audio | |||||
| Task DisconnectAsync(); | Task DisconnectAsync(); | ||||
| /// <summary> | |||||
| /// Creates a new outgoing stream accepting Opus-encoded data. | |||||
| /// </summary> | |||||
| /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||||
| /// <param name="bufferSize">The size of the internal buffer used for encryption.</param> | |||||
| /// <returns></returns> | |||||
| Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); | Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); | ||||
| /// <summary> | |||||
| /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. | |||||
| /// </summary> | |||||
| /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||||
| /// <param name="bufferSize">The size of the internal buffer used for encryption.</param> | |||||
| /// <returns></returns> | |||||
| Stream CreateDirectOpusStream(int samplesPerFrame, int bufferSize = 4000); | |||||
| /// <summary> | |||||
| /// Creates a new outgoing stream accepting PCM (raw) data. | |||||
| /// </summary> | |||||
| /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||||
| /// <param name="bitrate"></param> | |||||
| /// <param name="bufferSize">The size of the internal buffer used for encoding and encryption.</param> | |||||
| /// <returns></returns> | |||||
| Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000); | Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000); | ||||
| /// <summary> | |||||
| /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. | |||||
| /// </summary> | |||||
| /// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||||
| /// <param name="bitrate"></param> | |||||
| /// <param name="bufferSize">The size of the internal buffer used for encoding and encryption.</param> | |||||
| /// <returns></returns> | |||||
| Stream CreateDirectPCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000); | |||||
| } | } | ||||
| } | } | ||||
| @@ -39,7 +39,7 @@ namespace Discord.Audio | |||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private TaskCompletionSource<bool> _connectTask; | private TaskCompletionSource<bool> _connectTask; | ||||
| private CancellationTokenSource _cancelToken; | |||||
| private CancellationTokenSource _cancelTokenSource; | |||||
| private Task _heartbeatTask; | private Task _heartbeatTask; | ||||
| private long _heartbeatTime; | private long _heartbeatTime; | ||||
| private string _url; | private string _url; | ||||
| @@ -110,7 +110,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| _url = url; | _url = url; | ||||
| _connectTask = new TaskCompletionSource<bool>(); | _connectTask = new TaskCompletionSource<bool>(); | ||||
| _cancelToken = new CancellationTokenSource(); | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); | await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); | ||||
| await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); | await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); | ||||
| @@ -152,7 +152,7 @@ namespace Discord.Audio | |||||
| await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); | ||||
| //Signal tasks to complete | //Signal tasks to complete | ||||
| try { _cancelToken.Cancel(); } catch { } | |||||
| try { _cancelTokenSource.Cancel(); } catch { } | |||||
| //Disconnect from server | //Disconnect from server | ||||
| await ApiClient.DisconnectAsync().ConfigureAwait(false); | await ApiClient.DisconnectAsync().ConfigureAwait(false); | ||||
| @@ -169,19 +169,35 @@ namespace Discord.Audio | |||||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | ||||
| } | } | ||||
| public void Send(byte[] data, int count) | |||||
| public Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||||
| { | { | ||||
| //TODO: Queue these? | |||||
| ApiClient.SendAsync(data, count).ConfigureAwait(false); | |||||
| CheckSamplesPerFrame(samplesPerFrame); | |||||
| var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token); | |||||
| return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); | |||||
| } | } | ||||
| public Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||||
| public Stream CreateDirectOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||||
| { | { | ||||
| return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); | |||||
| CheckSamplesPerFrame(samplesPerFrame); | |||||
| var target = new DirectAudioTarget(ApiClient); | |||||
| return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); | |||||
| } | } | ||||
| public Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000) | public Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000) | ||||
| { | { | ||||
| return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, bufferSize); | |||||
| CheckSamplesPerFrame(samplesPerFrame); | |||||
| var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, _cancelTokenSource.Token); | |||||
| return new OpusEncodeStream(target, _secretKey, samplesPerFrame, _ssrc, bitrate, bufferSize); | |||||
| } | |||||
| public Stream CreateDirectPCMStream(int samplesPerFrame, int? bitrate = null, int bufferSize = 4000) | |||||
| { | |||||
| CheckSamplesPerFrame(samplesPerFrame); | |||||
| var target = new DirectAudioTarget(ApiClient); | |||||
| return new OpusEncodeStream(target, _secretKey, samplesPerFrame, _ssrc, bitrate, bufferSize); | |||||
| } | |||||
| private void CheckSamplesPerFrame(int samplesPerFrame) | |||||
| { | |||||
| if (samplesPerFrame != 120 && samplesPerFrame != 240 && samplesPerFrame != 480 && | |||||
| samplesPerFrame != 960 && samplesPerFrame != 1920 && samplesPerFrame != 2880) | |||||
| throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); | |||||
| } | } | ||||
| private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | ||||
| @@ -201,7 +217,7 @@ namespace Discord.Audio | |||||
| throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | ||||
| _heartbeatTime = 0; | _heartbeatTime = 0; | ||||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | |||||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelTokenSource.Token); | |||||
| ApiClient.SetUdpEndpoint(_url, data.Port); | ApiClient.SetUdpEndpoint(_url, data.Port); | ||||
| await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | ||||
| @@ -52,7 +52,7 @@ namespace Discord.Audio | |||||
| throw new Exception($"Opus Error: {(OpusError)result}"); | throw new Exception($"Opus Error: {(OpusError)result}"); | ||||
| } | } | ||||
| /// <summary> Gets or sets whether Forward Error Correction is enabled. </summary> | |||||
| /// <summary> Gets or sets the encoder's bitrate. </summary> | |||||
| public void SetBitrate(int value) | public void SetBitrate(int value) | ||||
| { | { | ||||
| if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) | if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) | ||||
| @@ -7,8 +7,8 @@ | |||||
| private readonly OpusEncoder _encoder; | private readonly OpusEncoder _encoder; | ||||
| internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int? bitrate = null, int bufferSize = 4000) | |||||
| : base(audioClient, secretKey, samplesPerFrame, ssrc, bufferSize) | |||||
| internal OpusEncodeStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc, int? bitrate = null, int bufferSize = 4000) | |||||
| : base(target, secretKey, samplesPerFrame, ssrc, bufferSize) | |||||
| { | { | ||||
| _encoder = new OpusEncoder(SampleRate, Channels); | _encoder = new OpusEncoder(SampleRate, Channels); | ||||
| @@ -5,7 +5,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| internal class RTPWriteStream : Stream | internal class RTPWriteStream : Stream | ||||
| { | { | ||||
| private readonly AudioClient _audioClient; | |||||
| private readonly IAudioTarget _target; | |||||
| private readonly byte[] _nonce, _secretKey; | private readonly byte[] _nonce, _secretKey; | ||||
| private int _samplesPerFrame; | private int _samplesPerFrame; | ||||
| private uint _ssrc, _timestamp = 0; | private uint _ssrc, _timestamp = 0; | ||||
| @@ -16,9 +16,9 @@ namespace Discord.Audio | |||||
| public override bool CanSeek => false; | public override bool CanSeek => false; | ||||
| public override bool CanWrite => true; | public override bool CanWrite => true; | ||||
| internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000) | |||||
| internal RTPWriteStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000) | |||||
| { | { | ||||
| _audioClient = audioClient; | |||||
| _target = target; | |||||
| _secretKey = secretKey; | _secretKey = secretKey; | ||||
| _samplesPerFrame = samplesPerFrame; | _samplesPerFrame = samplesPerFrame; | ||||
| _ssrc = ssrc; | _ssrc = ssrc; | ||||
| @@ -48,7 +48,7 @@ namespace Discord.Audio | |||||
| count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey); | 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 | Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer | ||||
| _audioClient.Send(_buffer, count + 12); | |||||
| _target.SendAsync(_buffer, count + 12).GetAwaiter().GetResult(); | |||||
| } | } | ||||
| public override void Flush() { } | public override void Flush() { } | ||||
| @@ -0,0 +1,79 @@ | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Diagnostics; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Audio | |||||
| { | |||||
| internal class BufferedAudioTarget : IAudioTarget, IDisposable | |||||
| { | |||||
| private static readonly byte[] _silencePacket = new byte[] { 0xF8, 0xFF, 0xFE }; | |||||
| private double _ticksPerFrame; | |||||
| private Task _task; | |||||
| private DiscordVoiceAPIClient _client; | |||||
| private CancellationTokenSource _cancelTokenSource; | |||||
| private ConcurrentQueue<byte[]> _queue; | |||||
| internal BufferedAudioTarget(DiscordVoiceAPIClient client, int samplesPerFrame, CancellationToken cancelToken) | |||||
| { | |||||
| _client = client; | |||||
| double milliseconds = samplesPerFrame / 48.0; | |||||
| double ticksPerFrame = Stopwatch.Frequency / 1000.0 * milliseconds; | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; | |||||
| _queue = new ConcurrentQueue<byte[]>(); //TODO: We need a better queue | |||||
| _task = Run(ticksPerFrame, cancelToken); | |||||
| } | |||||
| private Task Run(double ticksPerFrame, CancellationToken cancelToken) | |||||
| { | |||||
| return Task.Run(async () => | |||||
| { | |||||
| var stopwatch = Stopwatch.StartNew(); | |||||
| long lastTick = stopwatch.ElapsedTicks; | |||||
| double ticksPerMilli = Stopwatch.Frequency / 1000.0; | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| long thisTick = stopwatch.ElapsedTicks; | |||||
| double remaining = ticksPerFrame - (thisTick - lastTick); | |||||
| if (remaining <= 0) | |||||
| { | |||||
| byte[] buffer; | |||||
| if (_queue.TryDequeue(out buffer)) | |||||
| await _client.SendAsync(buffer, buffer.Length).ConfigureAwait(false); | |||||
| else | |||||
| await _client.SendAsync(_silencePacket, _silencePacket.Length).ConfigureAwait(false); | |||||
| lastTick = thisTick; | |||||
| } | |||||
| else if (remaining > 1) | |||||
| { | |||||
| int millis = (int)Math.Floor(remaining / ticksPerMilli); | |||||
| await Task.Delay(millis).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| public Task SendAsync(byte[] buffer, int count) | |||||
| { | |||||
| byte[] newBuffer = new byte[count]; | |||||
| Buffer.BlockCopy(buffer, 0, newBuffer, 0, count); | |||||
| _queue.Enqueue(newBuffer); | |||||
| return Task.Delay(0); | |||||
| } | |||||
| protected void Dispose(bool disposing) | |||||
| { | |||||
| if (disposing) | |||||
| _cancelTokenSource.Cancel(); | |||||
| } | |||||
| public void Dispose() | |||||
| { | |||||
| Dispose(true); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Audio | |||||
| { | |||||
| internal class DirectAudioTarget : IAudioTarget | |||||
| { | |||||
| private readonly DiscordVoiceAPIClient _client; | |||||
| public DirectAudioTarget(DiscordVoiceAPIClient client) | |||||
| { | |||||
| _client = client; | |||||
| } | |||||
| public Task SendAsync(byte[] buffer, int count) | |||||
| => _client.SendAsync(buffer, count); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Audio | |||||
| { | |||||
| internal interface IAudioTarget | |||||
| { | |||||
| Task SendAsync(byte[] buffer, int count); | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,4 @@ | |||||
| <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <Description>A core Discord.Net library containing the WebSocket client and models.</Description> | <Description>A core Discord.Net library containing the WebSocket client and models.</Description> | ||||
| <VersionPrefix>1.0.0-beta2</VersionPrefix> | <VersionPrefix>1.0.0-beta2</VersionPrefix> | ||||
| @@ -1563,10 +1563,10 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; | before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; | ||||
| after = guild.AddOrUpdateVoiceState(State, data); | after = guild.AddOrUpdateVoiceState(State, data); | ||||
| if (data.UserId == CurrentUser.Id) | |||||
| /*if (data.UserId == CurrentUser.Id) | |||||
| { | { | ||||
| var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); | var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); | ||||
| } | |||||
| }*/ | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -1,6 +1,10 @@ | |||||
| namespace Discord.WebSocket | |||||
| using Discord.Audio; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | { | ||||
| public interface ISocketAudioChannel : IAudioChannel | public interface ISocketAudioChannel : IAudioChannel | ||||
| { | { | ||||
| Task<IAudioClient> ConnectAsync(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.Rest; | |||||
| using Discord.Audio; | |||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -64,6 +65,11 @@ namespace Discord.WebSocket | |||||
| public Task LeaveAsync(RequestOptions options = null) | public Task LeaveAsync(RequestOptions options = null) | ||||
| => ChannelHelper.DeleteAsync(this, Discord, options); | => ChannelHelper.DeleteAsync(this, Discord, options); | ||||
| public Task<IAudioClient> ConnectAsync() | |||||
| { | |||||
| throw new NotSupportedException("Voice is not yet supported for group channels."); | |||||
| } | |||||
| //Messages | //Messages | ||||
| public SocketMessage GetCachedMessage(ulong id) | public SocketMessage GetCachedMessage(ulong id) | ||||
| => _messages?.Get(id); | => _messages?.Get(id); | ||||
| @@ -41,6 +41,17 @@ namespace Discord.WebSocket | |||||
| public Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null) | public Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null) | ||||
| => ChannelHelper.ModifyAsync(this, Discord, func, options); | => ChannelHelper.ModifyAsync(this, Discord, func, options); | ||||
| public async Task<IAudioClient> ConnectAsync() | |||||
| { | |||||
| var audioMode = Discord.AudioMode; | |||||
| if (audioMode == AudioMode.Disabled) | |||||
| throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); | |||||
| return await Guild.ConnectAudioAsync(Id, | |||||
| (audioMode & AudioMode.Incoming) == 0, | |||||
| (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); | |||||
| } | |||||
| public override SocketGuildUser GetUser(ulong id) | public override SocketGuildUser GetUser(ulong id) | ||||
| { | { | ||||
| var user = Guild.GetUser(id); | var user = Guild.GetUser(id); | ||||
| @@ -52,9 +63,6 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | ||||
| internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; | internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; | ||||
| //IVoiceChannel | |||||
| Task<IAudioClient> IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } | |||||
| //IGuildChannel | //IGuildChannel | ||||
| Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IGuildUser>(GetUser(id)); | => Task.FromResult<IGuildUser>(GetUser(id)); | ||||
| @@ -421,34 +421,64 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| //Audio | //Audio | ||||
| public async Task DisconnectAudioAsync(AudioClient client = null) | |||||
| internal async Task<IAudioClient> ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) | |||||
| { | { | ||||
| selfDeaf = false; | |||||
| selfMute = false; | |||||
| TaskCompletionSource<AudioClient> promise; | |||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | await _audioLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await DisconnectAudioInternalAsync(client).ConfigureAwait(false); | |||||
| await DisconnectAudioInternalAsync().ConfigureAwait(false); | |||||
| promise = new TaskCompletionSource<AudioClient>(); | |||||
| _audioConnectPromise = promise; | |||||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | |||||
| } | |||||
| catch (Exception) | |||||
| { | |||||
| await DisconnectAudioInternalAsync().ConfigureAwait(false); | |||||
| throw; | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _audioLock.Release(); | _audioLock.Release(); | ||||
| } | } | ||||
| try | |||||
| { | |||||
| var timeoutTask = Task.Delay(15000); | |||||
| if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) | |||||
| throw new TimeoutException(); | |||||
| return await promise.Task.ConfigureAwait(false); | |||||
| } | |||||
| catch (Exception) | |||||
| { | |||||
| await DisconnectAudioAsync().ConfigureAwait(false); | |||||
| throw; | |||||
| } | |||||
| } | } | ||||
| private async Task DisconnectAudioInternalAsync(AudioClient client = null) | |||||
| internal async Task DisconnectAudioAsync() | |||||
| { | { | ||||
| var oldClient = AudioClient; | |||||
| if (oldClient != null) | |||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | { | ||||
| if (client == null || oldClient == client) | |||||
| { | |||||
| _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection | |||||
| _audioConnectPromise = null; | |||||
| } | |||||
| if (oldClient == client) | |||||
| { | |||||
| AudioClient = null; | |||||
| await oldClient.DisconnectAsync().ConfigureAwait(false); | |||||
| } | |||||
| await DisconnectAudioInternalAsync().ConfigureAwait(false); | |||||
| } | } | ||||
| finally | |||||
| { | |||||
| _audioLock.Release(); | |||||
| } | |||||
| } | |||||
| private async Task DisconnectAudioInternalAsync() | |||||
| { | |||||
| _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection | |||||
| _audioConnectPromise = null; | |||||
| if (AudioClient != null) | |||||
| await AudioClient.DisconnectAsync().ConfigureAwait(false); | |||||
| AudioClient = null; | |||||
| } | } | ||||
| internal async Task FinishConnectAudio(int id, string url, string token) | internal async Task FinishConnectAudio(int id, string url, string token) | ||||
| { | { | ||||
| @@ -462,6 +492,14 @@ namespace Discord.WebSocket | |||||
| var audioClient = new AudioClient(this, id); | var audioClient = new AudioClient(this, id); | ||||
| audioClient.Disconnected += async ex => | audioClient.Disconnected += async ex => | ||||
| { | { | ||||
| //If the initial connection hasn't been made yet, reconnecting will lead to deadlocks | |||||
| if (!_audioConnectPromise.Task.IsCompleted) | |||||
| { | |||||
| try { audioClient.Dispose(); } catch { } | |||||
| AudioClient = null; | |||||
| return; | |||||
| } | |||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | await _audioLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| @@ -476,14 +514,14 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; | var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; | ||||
| if (voiceChannelId != null) | if (voiceChannelId != null) | ||||
| { | |||||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); | await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); | ||||
| return; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| else | |||||
| { | |||||
| try { AudioClient.Dispose(); } catch { } | |||||
| AudioClient = null; | |||||
| } | |||||
| try { audioClient.Dispose(); } catch { } | |||||
| AudioClient = null; | |||||
| } | } | ||||
| } | } | ||||
| finally | finally | ||||
| @@ -498,25 +536,12 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
| { | { | ||||
| await DisconnectAudioAsync().ConfigureAwait(false); | |||||
| await DisconnectAudioInternalAsync().ConfigureAwait(false); | |||||
| } | } | ||||
| catch (Exception e) | catch (Exception e) | ||||
| { | { | ||||
| await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); | await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); | ||||
| await DisconnectAudioAsync().ConfigureAwait(false); | |||||
| } | |||||
| finally | |||||
| { | |||||
| _audioLock.Release(); | |||||
| } | |||||
| } | |||||
| internal async Task FinishJoinAudioChannel() | |||||
| { | |||||
| await _audioLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| if (AudioClient != null) | |||||
| await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); | |||||
| await DisconnectAudioInternalAsync().ConfigureAwait(false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -22,6 +22,7 @@ namespace Discord.Net.Udp | |||||
| public DefaultUdpSocket() | public DefaultUdpSocket() | ||||
| { | { | ||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| } | } | ||||
| private void Dispose(bool disposing) | private void Dispose(bool disposing) | ||||
| { | { | ||||
| @@ -57,7 +58,7 @@ namespace Discord.Net.Udp | |||||
| _cancelTokenSource = new CancellationTokenSource(); | _cancelTokenSource = new CancellationTokenSource(); | ||||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | ||||
| _udp = new UdpClient(); | |||||
| _udp = new UdpClient(0); | |||||
| _task = RunAsync(_cancelToken); | _task = RunAsync(_cancelToken); | ||||
| } | } | ||||