Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Disqord.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5D89D2F8
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{F3EE9740-ECB6-4224-807F-521AC1672A04}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicVoiceReceive", "examples\Voice\BasicVoiceReceive\BasicVoiceReceive.csproj", "{A402D4B7-22FD-4AAC-8175-0D378501600B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disqord.Tests", "Disqord.Tests\Disqord.Tests.csproj", "{13E985F4-4DC2-4B7B-BB8E-49A95E720C71}"
EndProject
Global
Expand Down Expand Up @@ -281,6 +283,18 @@ Global
{13E985F4-4DC2-4B7B-BB8E-49A95E720C71}.Release|x64.Build.0 = Release|Any CPU
{13E985F4-4DC2-4B7B-BB8E-49A95E720C71}.Release|x86.ActiveCfg = Release|Any CPU
{13E985F4-4DC2-4B7B-BB8E-49A95E720C71}.Release|x86.Build.0 = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|x64.ActiveCfg = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|x64.Build.0 = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|x86.ActiveCfg = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Debug|x86.Build.0 = Debug|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|Any CPU.Build.0 = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|x64.ActiveCfg = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|x64.Build.0 = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|x86.ActiveCfg = Release|Any CPU
{A402D4B7-22FD-4AAC-8175-0D378501600B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -315,6 +329,7 @@ Global
{4063DDC5-4595-4209-8AC8-B27F00B0C4B2} = {90A6850B-FB75-4ABA-9233-9E59AE6D4C98}
{EE5D060B-0555-4712-89EE-F1B1295D76F7} = {4063DDC5-4595-4209-8AC8-B27F00B0C4B2}
{13E985F4-4DC2-4B7B-BB8E-49A95E720C71} = {5D89D2F8-EC16-46AD-8D9C-362EDABC4383}
{A402D4B7-22FD-4AAC-8175-0D378501600B} = {4063DDC5-4595-4209-8AC8-B27F00B0C4B2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EFD09C91-3B86-4BB5-ABCD-4E8DD454F224}
Expand Down
12 changes: 0 additions & 12 deletions examples/Voice/BasicVoice/Audio/AudioMetadataKeys.cs

This file was deleted.

208 changes: 67 additions & 141 deletions examples/Voice/BasicVoice/Audio/BasicAudioPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,191 +8,117 @@
using Disqord.Voice;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Qommon.Metadata;

namespace BasicVoice;

/// <summary>
/// Represents a basic <see cref="AudioPlayer"/> implementation
/// that supports queueing audio sources and sends notifications
/// to a text channel.
/// </summary>
public class BasicAudioPlayer : AudioPlayer
// Basic player implementation that supports queueing and sends notifications to a text channel.
public class BasicAudioPlayer(
DiscordBotBase bot,
Snowflake notificationsChannelId,
IVoiceConnection connection) : AudioPlayer(connection)
{
/// <summary>
/// Gets the bot of this audio player.
/// </summary>
public DiscordBotBase Bot { get; }

/// <summary>
/// Gets the ID of the channel this audio player will send notifications to.
/// </summary>
public Snowflake NotificationsChannelId { get; }

// The lock is necessary to prevent the race condition
// between Enqueue() and OnSourceFinished().
private readonly object _queueLock = new();
private readonly Queue<AudioSource> _queue;
public DiscordBotBase Bot { get; } = bot;

public BasicAudioPlayer(
DiscordBotBase bot,
Snowflake notificationsChannelId,
IVoiceConnection connection)
: base(connection)
{
Bot = bot;
NotificationsChannelId = notificationsChannelId;
public Snowflake NotificationsChannelId { get; } = notificationsChannelId;

_queue = new();
}
public Song? CurrentSong { get; private set; }

/// <summary>
/// Invoked when this audio player is stopped.
/// </summary>
/// <param name="exception"> The exception that caused the stop or <see langword="null"/> if no exception occurred. </param>
/// <returns>
/// A <see cref="ValueTask"/> representing the work.
/// </returns>
protected override async ValueTask OnStopped(Exception? exception)
private readonly object _queueLock = new();
private readonly Queue<Song> _queue = new();

public bool Enqueue(Song track)
{
if (exception == null)
lock (_queueLock)
{
// If an exception occurred, we'll log it.
Bot.Logger.LogError(exception, "An exception occurred in the audio player for guild ID {GuildId}.", GuildId);

// Here you can add different handling for different exceptions that might occur
// but for VoiceConnectionException the logic should always be basically the same.
// VoiceConnectionException indicates that the connection object was rendered unusable
// because, for example, the bot was disconnected from the voice channel.
// We dispose of this audio player allowing for a new one to be created.
if (exception is VoiceConnectionException)
if (!TrySetSource(track.Source))
{
var playerService = Bot.Services.GetRequiredService<AudioPlayerService>();
await playerService.DisposePlayerAsync(GuildId);
_queue.Enqueue(track);
return true;
}

CurrentSong = track;
}

return false;
}

/// <summary>
/// Invoked when an audio source has finished playing.
/// Starts playing the next audio source if one is queued
/// and sends notifications to the channel with ID <see cref="NotificationsChannelId"/>.
/// </summary>
/// <param name="source"> The audio source that finished playing. </param>
/// <param name="wasReplaced"> <see langword="true"/> if the previous audio source was replaced with a new one. </param>
/// <returns>
/// A <see cref="ValueTask"/> representing the work.
/// </returns>
protected override ValueTask OnSourceFinished(AudioSource source, bool wasReplaced)
private async Task SendNotificationAsync(string content)
{
var nextSource = PlayNextSource();
// Yield to not delay playing the next audio track.
await Task.Yield();

// Not awaited so that we don't block the audio playback.
_ = SendNotificationAsync(source, wasReplaced, exception: null, nextSource);
return default;
try
{
await Bot.SendMessageAsync(NotificationsChannelId,
new LocalMessage().WithContent(content));
}
catch (Exception ex)
{
Bot.Logger.LogWarning(ex, "Failed to send notification for guild ID {GuildId}.", GuildId);
}
}

/// <summary>
/// Invoked when an <see cref="AudioSource"/> has errored,
/// i.e. thrown an exception.
/// </summary>
/// <param name="source"> The <see cref="AudioSource"/> that has errored. </param>
/// <param name="exception"> The exception that occurred. </param>
/// <returns>
/// A <see cref="ValueTask"/> representing the work.
/// </returns>
protected override ValueTask OnSourceErrored(AudioSource source, Exception exception)
protected override ValueTask OnSourceStarted(IAudioSource source)
{
// An error occurred in the audio source.
// We'll simply log it.
var title = source.GetMetadataOrDefault<string>(AudioMetadataKeys.Title, null);
Bot.Logger.LogError(exception, "An exception occurred in the audio source '{Title}' ({AudioSourceType}) "
+ "in the audio player for guild ID {GuildId}.", title ?? "unknown", source.GetType().Name, GuildId);

var nextSource = PlayNextSource();
var song = CurrentSong;
if (song != null)
{
_ = SendNotificationAsync($"Now playing {Markdown.Bold(song.Title)}.");
}

// Not awaited so that we don't block the audio playback.
_ = SendNotificationAsync(source, wasReplaced: false, exception, nextSource);
return default;
}

private AudioSource? PlayNextSource()
protected override ValueTask OnSourceFinished(IAudioSource source, bool wasReplaced)
{
lock (_queueLock)
var song = CurrentSong;
if (song != null && !wasReplaced)
{
if (_queue.TryDequeue(out var queuedSource))
{
// If there's a queued source we start playing it.
if (TrySetSource(queuedSource))
{
return queuedSource;
}
}
_ = SendNotificationAsync($"Finished playing {Markdown.Bold(song.Title)}.");
}

return null;
PlayNextTrack();
return default;
}

private async Task SendNotificationAsync(AudioSource finishedSource, bool wasReplaced, Exception? exception, AudioSource? nextSource)
protected override ValueTask OnSourceErrored(IAudioSource source, Exception exception)
{
// Yield, so the code below runs in background.
await Task.Yield();
var displayName = CurrentSong?.Title ?? source.GetType().Name;
Bot.Logger.LogError(exception, "An exception occurred in audio source '{Title}' for guild ID {GuildId}.", displayName, GuildId);

// Below we access metadata on the audio sources.
// This metadata is set in AudioModule.
string? notification = null;
if (finishedSource.TryGetMetadata<string>(AudioMetadataKeys.Title, out var title))
{
if (exception != null)
{
notification += $"An error occurred while playing {Markdown.Code(title)}.\n";
}
else
{
notification += $"{(wasReplaced ? "Skipped" : "Finished")} playing {Markdown.Code(title)}.\n";
}
}
_ = SendNotificationAsync($"An error occurred while playing {Markdown.Bold(displayName)}.");

if (nextSource != null && nextSource.TryGetMetadata(AudioMetadataKeys.Title, out title))
{
notification += $"Now playing {Markdown.Code(title)}.";
}
PlayNextTrack();
return default;
}

if (notification != null)
protected override async ValueTask OnStopped(Exception? exception)
{
if (exception != null)
{
try
{
var message = new LocalMessage().WithContent(notification.TrimStart());
await Bot.SendMessageAsync(NotificationsChannelId, message);
}
catch (Exception ex)
Bot.Logger.LogError(exception, "An exception occurred in the audio player for guild ID {GuildId}.", GuildId);

if (exception is VoiceConnectionException)
{
// If an exception occurred, we'll log it.
Bot.Logger.LogError(ex, "An exception occurred while sending the notification in the audio player for guild ID {GuildId}.", GuildId);
var playerService = Bot.Services.GetRequiredService<AudioPlayerService>();
await playerService.DisposePlayerAsync(GuildId);
}
}
}

/// <summary>
/// Enqueues the specified audio source.
/// </summary>
/// <param name="source"> The audio source to enqueue. </param>
/// <returns>
/// <see langword="true"/> if the source was enqueued;
/// <see langword="false"/> if the source is being played immediately.
/// </returns>
public bool Enqueue(AudioSource source)
private void PlayNextTrack()
{
lock (_queueLock)
{
if (!TrySetSource(source))
if (_queue.TryDequeue(out var next))
{
// If a source is already playing we enqueue the source.
_queue.Enqueue(source);
return true;
CurrentSong = next;
TrySetSource(next.Source);
}
else
{
CurrentSong = null;
}
}

return false;
}
}
Loading
Loading