diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index bea44fcf4..149905654 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -8,7 +8,9 @@ namespace Discord.Audio event Func Connected; event Func Disconnected; event Func LatencyUpdated; - + event Func StreamCreated; + event Func StreamDestroyed; + /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index 6c9507299..afb81d92f 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,6 +1,12 @@ -namespace Discord +using Discord.Audio; +using System; +using System.Threading.Tasks; + +namespace Discord { public interface IAudioChannel : IChannel { + /// Connects to this audio channel. + Task ConnectAsync(Action configAction = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 80c90e4bd..e2a2ad8eb 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using System; +using System; using System.Threading.Tasks; namespace Discord @@ -13,7 +12,5 @@ namespace Discord /// Modifies this voice channel. Task ModifyAsync(Action func, RequestOptions options = null); - /// Connects to this voice channel. - Task ConnectAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index a4b49b118..e3ba4e94b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Audio; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -145,6 +146,9 @@ namespace Discord.Rest IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index e5330f29e..300ebd08d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -40,8 +40,8 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 504bf8670..c365ad4ff 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -112,6 +113,9 @@ namespace Discord.Rpc IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs index 3d5acfda9..067da6764 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -42,7 +42,7 @@ namespace Discord.Rpc private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 226d8eb7f..bb5a62438 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -47,6 +47,18 @@ namespace Discord.Audio remove { _latencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func StreamCreated + { + add { _streamCreated.Add(value); } + remove { _streamCreated.Remove(value); } + } + private readonly AsyncEvent> _streamCreated = new AsyncEvent>(); + public event Func StreamDestroyed + { + add { _streamDestroyed.Add(value); } + remove { _streamDestroyed.Remove(value); } + } + private readonly AsyncEvent> _streamDestroyed = new AsyncEvent>(); private readonly Logger _audioLogger; private readonly JsonSerializer _serializer; @@ -182,7 +194,7 @@ namespace Discord.Audio throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); } - internal void CreateInputStream(ulong userId) + internal async Task CreateInputStreamAsync(ulong userId) { //Assume Thread-safe if (!_streams.ContainsKey(userId)) @@ -190,6 +202,7 @@ namespace Discord.Audio var readerStream = new InputStream(); var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); _streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); + await _streamCreated.InvokeAsync(userId, readerStream); } } internal AudioInStream GetInputStream(ulong id) @@ -199,14 +212,18 @@ namespace Discord.Audio return streamPair.Reader; return null; } - internal void RemoveInputStream(ulong userId) + internal async Task RemoveInputStreamAsync(ulong userId) { - _streams.TryRemove(userId, out var ignored); + if (_streams.TryRemove(userId, out var ignored)) + await _streamDestroyed.InvokeAsync(userId).ConfigureAwait(false); } - internal void ClearInputStreams() + internal async Task ClearInputStreamsAsync() { - foreach (var pair in _streams.Values) - pair.Reader.Dispose(); + foreach (var pair in _streams) + { + pair.Value.Reader.Dispose(); + await _streamDestroyed.InvokeAsync(pair.Key).ConfigureAwait(false); + } _ssrcMap.Clear(); _streams.Clear(); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 30828d88b..8307b4a36 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1414,7 +1414,7 @@ namespace Discord.WebSocket if (data.ChannelId != null) { before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = guild.AddOrUpdateVoiceState(State, data); + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); /*if (data.UserId == CurrentUser.Id) { var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); @@ -1471,7 +1471,7 @@ namespace Discord.WebSocket if (guild != null) { string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); - var _ = guild.FinishConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false); + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); } else { @@ -1725,6 +1725,8 @@ namespace Discord.WebSocket } } + internal int GetAudioId() => _nextAudioId++; + //IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7b9bf07f0..c15eaf17b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -5,6 +5,5 @@ namespace Discord.WebSocket { public interface ISocketAudioChannel : IAudioChannel { - Task ConnectAsync(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index ceba50a6e..e6c875e5a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -212,6 +212,9 @@ namespace Discord.WebSocket IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 9ec0da72a..e8a669845 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -40,9 +40,9 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); - public async Task ConnectAsync() + public async Task ConnectAsync(Action configAction = null) { - return await Guild.ConnectAudioAsync(Id, false, false).ConfigureAwait(false); + return await Guild.ConnectAudioAsync(Id, false, false, configAction).ConfigureAwait(false); } public override SocketGuildUser GetUser(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 7de5eda77..d240aac1e 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -423,7 +423,7 @@ namespace Discord.WebSocket } //Voice States - internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; @@ -433,12 +433,12 @@ namespace Discord.WebSocket if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) { if (model.UserId == CurrentUser.Id) - RepopulateAudioStreams(); + await RepopulateAudioStreamsAsync().ConfigureAwait(false); else { - _audioClient.RemoveInputStream(model.UserId); //User changed channels, end their stream + await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) - _audioClient.CreateInputStream(model.UserId); + await _audioClient.CreateInputStreamAsync(model.UserId).ConfigureAwait(false); } } @@ -464,7 +464,7 @@ namespace Discord.WebSocket { return _audioClient?.GetInputStream(userId); } - internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) + internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute, Action configAction) { selfDeaf = false; selfMute = false; @@ -477,6 +477,32 @@ namespace Discord.WebSocket await DisconnectAudioInternalAsync().ConfigureAwait(false); promise = new TaskCompletionSource(); _audioConnectPromise = promise; + + if (_audioClient == null) + { + var audioClient = new AudioClient(this, Discord.GetAudioId()); + audioClient.Disconnected += async ex => + { + if (!promise.Task.IsCompleted) + { + try { audioClient.Dispose(); } catch { } + _audioClient = null; + if (ex != null) + await promise.TrySetExceptionAsync(ex); + else + await promise.TrySetCanceledAsync(); + return; + } + }; + audioClient.Connected += () => + { + var _ = promise.TrySetResultAsync(_audioClient); + return Task.Delay(0); + }; + configAction?.Invoke(audioClient); + _audioClient = audioClient; + } + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); } catch (Exception) @@ -523,7 +549,7 @@ namespace Discord.WebSocket await _audioClient.StopAsync().ConfigureAwait(false); _audioClient = null; } - internal async Task FinishConnectAudio(int id, string url, string token) + internal async Task FinishConnectAudio(string url, string token) { //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; @@ -531,31 +557,7 @@ namespace Discord.WebSocket await _audioLock.WaitAsync().ConfigureAwait(false); try { - var promise = _audioConnectPromise; - if (_audioClient == null) - { - var audioClient = new AudioClient(this, id); - audioClient.Disconnected += async ex => - { - if (!promise.Task.IsCompleted) - { - try { audioClient.Dispose(); } catch { } - _audioClient = null; - if (ex != null) - await promise.TrySetExceptionAsync(ex); - else - await promise.TrySetCanceledAsync(); - return; - } - }; - _audioClient = audioClient; - RepopulateAudioStreams(); - } - _audioClient.Connected += () => - { - var _ = promise.TrySetResultAsync(_audioClient); - return Task.Delay(0); - }; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) @@ -573,15 +575,15 @@ namespace Discord.WebSocket } } - internal void RepopulateAudioStreams() + internal async Task RepopulateAudioStreamsAsync() { - _audioClient.ClearInputStreams(); //We changed channels, end all current streams + await _audioClient.ClearInputStreamsAsync().ConfigureAwait(false); //We changed channels, end all current streams if (CurrentUser.VoiceChannel != null) { foreach (var pair in _voiceStates) { - if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) - _audioClient.CreateInputStream(pair.Key); + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id && pair.Key != CurrentUser.Id) + await _audioClient.CreateInputStreamAsync(pair.Key).ConfigureAwait(false); } } }