Skip to content

Commit

Permalink
Refactor VoiceExtension API
Browse files Browse the repository at this point in the history
  • Loading branch information
Quahu committed Aug 3, 2024
1 parent faba794 commit f34d4ef
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 21 deletions.
9 changes: 8 additions & 1 deletion examples/Voice/BasicVoice/AudioPlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ public override async Task StopAsync(CancellationToken cancellationToken)
{
await base.StopAsync(cancellationToken);

var voiceExtension = Bot.GetRequiredExtension<VoiceExtension>();

await _semaphore.WaitAsync(cancellationToken);
try
{
foreach (var (_, player) in _players)
foreach (var (guildID, player) in _players)
{
player.Stop();
await player.DisposeAsync();

await voiceExtension.DisconnectAsync(guildID);
}

_players.Clear();
Expand Down Expand Up @@ -111,6 +115,9 @@ public async Task DisposePlayerAsync(Snowflake guildId)
{
player.Stop();
await player.DisposeAsync();

var voiceExtension = Bot.GetRequiredExtension<VoiceExtension>();
await voiceExtension.DisconnectAsync(guildId);
}
}
finally
Expand Down
120 changes: 100 additions & 20 deletions src/Disqord.Extensions.Voice/VoiceExtension.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Disqord.Gateway;
using Disqord.Utilities.Threading;
using Disqord.Voice;
using Microsoft.Extensions.Logging;
using Qommon;
using Qommon.Collections.ReadOnly;
using Qommon.Collections.ThreadSafe;

namespace Disqord.Extensions.Voice;

// TODO: keyed semaphore
public class VoiceExtension : DiscordClientExtension
{
public IReadOnlyDictionary<Snowflake, IVoiceConnection> Connections => _connections.ReadOnly();

private readonly IVoiceConnectionFactory _connectionFactory;

private readonly IThreadSafeDictionary<Snowflake, IVoiceConnection> _connections;
private readonly IThreadSafeDictionary<Snowflake, VoiceConnectionInfo> _connections;

public VoiceExtension(
ILogger<VoiceExtension> logger,
Expand All @@ -25,7 +26,7 @@ public VoiceExtension(
{
_connectionFactory = connectionFactory;

_connections = ThreadSafeDictionary.Monitor.Create<Snowflake, IVoiceConnection>();
_connections = ThreadSafeDictionary.Monitor.Create<Snowflake, VoiceConnectionInfo>();
}

/// <inheritdoc/>
Expand All @@ -39,28 +40,59 @@ protected override ValueTask InitializeAsync(CancellationToken cancellationToken

private Task VoiceServerUpdatedAsync(object? sender, VoiceServerUpdatedEventArgs e)
{
if (_connections.TryGetValue(e.GuildId, out var connection))
{
connection.OnVoiceServerUpdate(e.Token, e.Endpoint);
}

GetConnection(e.GuildId)?.OnVoiceServerUpdate(e.Token, e.Endpoint);
return Task.CompletedTask;
}

private Task VoiceStateUpdatedAsync(object? sender, VoiceStateUpdatedEventArgs e)
{
if (Client.CurrentUser.Id != e.NewVoiceState.MemberId)
return Task.CompletedTask;

if (_connections.TryGetValue(e.GuildId, out var connection))
{
var voiceState = e.NewVoiceState;
connection.OnVoiceStateUpdate(voiceState.ChannelId, voiceState.SessionId);
return Task.CompletedTask;
}

var voiceState = e.NewVoiceState;
GetConnection(e.GuildId)?.OnVoiceStateUpdate(voiceState.ChannelId, voiceState.SessionId);
return Task.CompletedTask;
}

/// <summary>
/// Gets the voice connection for the guild with the given ID.
/// </summary>
/// <param name="guildId"> The ID of the guild. </param>
/// <returns>
/// The voice connection or <see langword="null"/> if the connection does not exist.
/// </returns>
public IVoiceConnection? GetConnection(Snowflake guildId)
{
return _connections.TryGetValue(guildId, out var connectionInfo)
? connectionInfo.Connection
: null;
}

/// <summary>
/// Gets all maintained voice connections.
/// </summary>
/// <returns>
/// A dictionary of all connections keyed by the IDs of the guilds.
/// </returns>
public IReadOnlyDictionary<Snowflake, IVoiceConnection> GetConnections()
{
return _connections.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Connection);
}

/// <summary>
/// Connects to the channel with the given ID.
/// </summary>
/// <remarks>
/// To disconnect the voice connection, use <see cref="DisconnectAsync"/>.
/// </remarks>
/// <param name="guildId"> The ID of the guild the channel is in. </param>
/// <param name="channelId"> The ID of the channel. </param>
/// <param name="cancellationToken"> The cancellation token to observe. This is only used for the initial connection. </param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> with the result being the created voice connection.
/// </returns>
public async ValueTask<IVoiceConnection> ConnectAsync(Snowflake guildId, Snowflake channelId, CancellationToken cancellationToken = default)
{
var connection = _connectionFactory.Create(guildId, channelId, Client.CurrentUser.Id,
Expand All @@ -74,12 +106,60 @@ public async ValueTask<IVoiceConnection> ConnectAsync(Snowflake guildId, Snowfla
return new(shard.SetVoiceStateAsync(guildId, channelId, false, true, cancellationToken));
});

_connections[guildId] = connection;
var connectionInfo = new VoiceConnectionInfo(connection, Cts.Linked(Client.StoppingToken));
_connections[guildId] = connectionInfo;
try
{
var readyTask = connection.WaitUntilReadyAsync(cancellationToken);
_ = connection.RunAsync(connectionInfo.Cts.Token);

var readyTask = connection.WaitUntilReadyAsync(cancellationToken);
_ = connection.RunAsync(Client.StoppingToken);
await readyTask.ConfigureAwait(false);
}
catch
{
_connections.Remove(guildId);
await connectionInfo.DisposeAsync();
throw;
}

await readyTask.ConfigureAwait(false);
return connection;
}

/// <summary>
/// Disconnects the voice connection, if one exists, for the guild with the given ID.
/// </summary>
/// <remarks>
/// This will render the relevant voice connection unusable.
/// Use <see cref="ConnectAsync"/> to obtain a new connection afterward.
/// </remarks>
/// <param name="guildId"> The ID of the guild. </param>
public ValueTask DisconnectAsync(Snowflake guildId)
{
if (!_connections.TryRemove(guildId, out var connectionInfo))
{
return default;
}

return connectionInfo.DisposeAsync();
}

private readonly struct VoiceConnectionInfo : IAsyncDisposable
{
public IVoiceConnection Connection { get; }

public Cts Cts { get; }

public VoiceConnectionInfo(IVoiceConnection connection, Cts cts)
{
Connection = connection;
Cts = cts;
}

public async ValueTask DisposeAsync()
{
Cts.Cancel();
Cts.Dispose();
await Connection.DisposeAsync();
}
}
}

0 comments on commit f34d4ef

Please sign in to comment.