diff --git a/Console/Audio/AudioSender.cs b/Console/Audio/AudioSender.cs index 2725e8f..4b02b5f 100644 --- a/Console/Audio/AudioSender.cs +++ b/Console/Audio/AudioSender.cs @@ -1,134 +1,116 @@ using System.Threading.Channels; -using OpenTK.Audio.OpenAL; +using PortAudioSharp; namespace Console.Audio; -internal class AudioSender(int sourceId, ALFormat targetFormat) : IAsyncDisposable +internal class AudioSender(float volume, PlayState initialState) : IAsyncDisposable { - private readonly Channel _queue = Channel.CreateBounded(150); + private readonly Channel> _queue = Channel.CreateBounded>( + 500 + ); public readonly int SampleRate = 48000; public readonly int Channels = 2; - private readonly int[] _buffers = AL.GenBuffers(50); - private bool _clearBuffer = false; + public PlayState State { get; set; } = initialState; + public float Volume { get; set; } = volume; + public TimeSpan CurrentTime { get; private set; } = default; + public TaskCompletionSource WaitForEmptyBuffer { get; set; } = new(); + private PortAudioSharp.Stream? _stream; public void ClearBuffer() { - _clearBuffer = true; while (_queue.Reader.TryRead(out var next)) { next.Dispose(); } } - public async ValueTask Add(PcmPacket data) => await _queue.Writer.WriteAsync(data); + public async ValueTask Add(PcmPacket data) => await _queue.Writer.WriteAsync(data); - private async ValueTask ClearBufferAL(CancellationToken token) + public unsafe void StartSending(CancellationToken token = default) { - AL.GetSource(sourceId, ALGetSourcei.SourceState, out int initialState); + PortAudio.Initialize(); + + // Define a callback delegate for audio processing + StreamCallbackResult callback( + IntPtr input, + IntPtr output, + UInt32 frameCount, + ref StreamCallbackTimeInfo timeInfo, + StreamCallbackFlags statusFlags, + IntPtr userData + ) + { + var sizeInBytes = (int)frameCount * 2; - AL.SourceStop(sourceId); - AL.GetSource(sourceId, ALGetSourcei.BuffersQueued, out int queuedCount); + if (token.IsCancellationRequested) + return StreamCallbackResult.Abort; - if (queuedCount > 0) - { - int[] bufferIds = new int[queuedCount]; - AL.SourceUnqueueBuffers(sourceId, queuedCount, bufferIds); - foreach (var buffer in bufferIds) + if (State == PlayState.Paused) { - using var next = await _queue.Reader.ReadAsync(token); - AL.BufferData(buffer, targetFormat, next.Data, SampleRate); - AL.SourceQueueBuffer(sourceId, buffer); + var spanUnmanagedBuffer = new Span(output.ToPointer(), sizeInBytes); + spanUnmanagedBuffer.Clear(); + return StreamCallbackResult.Continue; } - } - _clearBuffer = false; - - if ((ALSourceState)initialState == ALSourceState.Playing) - { - AL.SourcePlay(sourceId); - } + if (State == PlayState.Stopped) + return StreamCallbackResult.Abort; - if ((ALSourceState)initialState == ALSourceState.Paused) - { - AL.SourcePlay(sourceId); - AL.SourcePause(sourceId); - } - } + if (_queue.Reader.TryRead(out var nextBuffer)) + { + using var buffer = nextBuffer; + var spanUnmanagedBuffer = new Span(output.ToPointer(), sizeInBytes); + var source = buffer.Data[..sizeInBytes]; - public async Task StartSending(CancellationToken token = default) - { - var fillBuffers = await _queue - .Reader.ReadAllAsync(token) - .Take(10) - .ToListAsync(cancellationToken: token); + for (var i = 0; i < source.Length; i++) + { + source[i] = (short)(source[i] * Volume); + } - for (int i = 0; i < fillBuffers.Count; i++) - { - using var item = fillBuffers[i]; - AL.BufferData(_buffers[i], targetFormat, item.Data, SampleRate); - AL.SourceQueueBuffer(sourceId, _buffers[i]); + source.CopyTo(spanUnmanagedBuffer); + CurrentTime = buffer.Time; + return StreamCallbackResult.Continue; + } + else + { + WaitForEmptyBuffer.TrySetResult(); + var spanUnmanagedBuffer = new Span(output.ToPointer(), sizeInBytes); + spanUnmanagedBuffer.Clear(); + return StreamCallbackResult.Continue; + } } - var _ = Task.Run( - async () => - { - try - { - while (!token.IsCancellationRequested) - { - if (_clearBuffer) - { - await ClearBufferAL(token); - continue; - } - - AL.GetSource( - sourceId, - ALGetSourcei.BuffersProcessed, - out int releasedCount - ); - - if (releasedCount > 0) - { - int[] bufferIds = new int[releasedCount]; - AL.SourceUnqueueBuffers(sourceId, releasedCount, bufferIds); - foreach (var buffer in bufferIds) - { - using var next = await _queue.Reader.ReadAsync(token); - AL.BufferData(buffer, targetFormat, next.Data, SampleRate); - AL.SourceQueueBuffer(sourceId, buffer); - } - } - - AL.GetSource(sourceId, ALGetSourcei.SourceState, out int stateInt); - - if ((ALSourceState)stateInt == ALSourceState.Stopped) - { - AL.SourcePlay(sourceId); - } - - await Task.Delay(100); - } - } - finally - { - await ClearBufferAL(token); - } - }, - token + StreamParameters param = new(); + var deviceIndex = PortAudio.DefaultOutputDevice; + var info = PortAudio.GetDeviceInfo(deviceIndex); + param.device = PortAudio.DefaultOutputDevice; + param.channelCount = Channels; + param.sampleFormat = SampleFormat.Int16; + param.suggestedLatency = info.defaultLowOutputLatency; + param.hostApiSpecificStreamInfo = IntPtr.Zero; + + _stream = new PortAudioSharp.Stream( + inParams: null, + outParams: param, + streamFlags: StreamFlags.ClipOff, + sampleRate: SampleRate, + framesPerBuffer: 960, //TODO This should not be hardcoded maybe? + callback: callback, + userData: IntPtr.Zero ); - AL.SourcePlay(sourceId); + _stream.Start(); } public ValueTask DisposeAsync() { ClearBuffer(); - AL.SourceStop(sourceId); - AL.GetSource(sourceId, ALGetSourcei.BuffersProcessed, out int releasedCount); - int[] bufferIds = new int[releasedCount]; - AL.SourceUnqueueBuffers(sourceId, releasedCount, bufferIds); - AL.DeleteBuffers(bufferIds); + State = PlayState.Stopped; + try + { + _stream?.Stop(); + _stream?.Dispose(); + } + catch { } return ValueTask.CompletedTask; } } diff --git a/Console/Audio/Containers/Matroska/Matroska.cs b/Console/Audio/Containers/Matroska/Matroska.cs index eace730..72e3cb2 100644 --- a/Console/Audio/Containers/Matroska/Matroska.cs +++ b/Console/Audio/Containers/Matroska/Matroska.cs @@ -43,7 +43,6 @@ public void Dispose() _ebmlReader.Dispose(); _inputStream.Dispose(); _memoryOwner.Dispose(); - CurrentTime = default; TotalTime = default; } @@ -52,11 +51,9 @@ public async ValueTask DisposeAsync() await _ebmlReader.DisposeAsync(); await _inputStream.DisposeAsync(); _memoryOwner.Dispose(); - CurrentTime = default; TotalTime = default; } - public TimeSpan CurrentTime { get; private set; } = TimeSpan.Zero; public TimeSpan TotalTime { get; private set; } public async Task AddFrames(CancellationToken cancellationToken) @@ -154,16 +151,7 @@ await WriteBlock( } } - private static void ShortsToBytes(ReadOnlySpan input, Span output) - { - for (int i = 0; i < input.Length; i++) - { - output[i * 2] = (byte)input[i]; - output[i * 2 + 1] = (byte)(input[i] >> 8); - } - } - - private async ValueTask AddOpusPacket(ReadOnlyMemory data) + private async ValueTask AddOpusPacket(ReadOnlyMemory data, TimeSpan time) { var frames = OpusPacketInfo.GetNumFrames(data.Span); var samplePerFrame = OpusPacketInfo.GetNumSamplesPerFrame(data.Span, _sender.SampleRate); @@ -171,23 +159,8 @@ private async ValueTask AddOpusPacket(ReadOnlyMemory data) var pcmSize = frameSize * _sender.Channels; var pcm = ArrayPool.Shared.Rent(pcmSize); - var pcmBytes = ArrayPool.Shared.Rent(pcmSize * 2); - - try - { - _decoder.Decode(data.Span, pcm.AsSpan()[..pcmSize], frameSize); - ShortsToBytes(pcm.AsSpan()[..pcmSize], pcmBytes.AsSpan()[..(pcmSize * 2)]); - await _sender.Add(new PcmPacket(pcmBytes, pcmSize * 2)); - } - catch - { - ArrayPool.Shared.Return(pcmBytes); - throw; - } - finally - { - ArrayPool.Shared.Return(pcm); - } + _decoder.Decode(data.Span, pcm.AsSpan()[..pcmSize], frameSize); + await _sender.Add(new PcmPacket(pcm, pcmSize, time)); } private async ValueTask WriteBlock( @@ -209,13 +182,13 @@ CancellationToken cancellationToken var memory = _memoryOwner.Memory[..size]; var block = await _ebmlReader.GetSimpleBlock(memory, cancellationToken); - CurrentTime = time + TimeSpan.FromMilliseconds(block.Timestamp); + var currentTime = time + TimeSpan.FromMilliseconds(block.Timestamp); - if (CurrentTime.TotalMilliseconds < _seekTime) + if (currentTime.TotalMilliseconds < _seekTime) return; foreach (var frame in block.GetFrames()) - await AddOpusPacket(frame); + await AddOpusPacket(frame, currentTime); return; } diff --git a/Console/Audio/PcmPacket.cs b/Console/Audio/PcmPacket.cs index 1580623..f8f16e7 100644 --- a/Console/Audio/PcmPacket.cs +++ b/Console/Audio/PcmPacket.cs @@ -2,11 +2,13 @@ namespace Console.Audio; -internal struct PcmPacket(byte[] Data, int Lenght) : IDisposable +internal readonly struct PcmPacket(T[] Data, int Lenght, TimeSpan Time) : IDisposable + where T : struct { - private readonly byte[] _data = Data; - public readonly ReadOnlySpan Data => _data.AsSpan()[..Lenght]; + private readonly T[] _data = Data; + public readonly Span Data => _data.AsSpan()[..Lenght]; public int Lenght { get; } = Lenght; + public TimeSpan Time { get; } = Time; - public readonly void Dispose() => ArrayPool.Shared.Return(_data); + public readonly void Dispose() => ArrayPool.Shared.Return(_data); } diff --git a/Console/Audio/PlayState.cs b/Console/Audio/PlayState.cs new file mode 100644 index 0000000..a1f8b2c --- /dev/null +++ b/Console/Audio/PlayState.cs @@ -0,0 +1,8 @@ +namespace Console.Audio; + +public enum PlayState +{ + Playing, + Paused, + Stopped +} diff --git a/Console/Audio/PlayerController.cs b/Console/Audio/PlayerController.cs index 585a0b8..8d09270 100644 --- a/Console/Audio/PlayerController.cs +++ b/Console/Audio/PlayerController.cs @@ -2,7 +2,6 @@ using Console.Audio.DownloadHandlers; using Nito.AsyncEx; using Nito.Disposables.Internals; -using OpenTK.Audio.OpenAL; using YoutubeExplode; using YoutubeExplode.Search; using YoutubeExplode.Videos; @@ -12,17 +11,11 @@ namespace Console.Audio; public class PlayerController : IAsyncDisposable { private readonly AsyncLock _lock = new(); - + private readonly YoutubeClient _youtubeClient; private float _volume = 0.5f; - + private PlayState _state = PlayState.Stopped; private List _queue = []; private int _currentSongIndex = 0; - - private readonly ALDevice _device; - private readonly ALContext _context; - private readonly int _sourceId; - private readonly ALFormat _targetFormat; - private readonly YoutubeClient _youtubeClient; private Matroska? _matroskaPlayerBuffer = null; private AudioSender? _audioSender = null; private CancellationTokenSource _currentSongTokenSource = new(); @@ -32,6 +25,16 @@ public class PlayerController : IAsyncDisposable public event Action>? QueueChanged; //Maybe emit state to show a loading spinner public event Action? OnFinish; + public PlayState State + { + get { return _state; } + set + { + _state = value; + if (_audioSender is not null) + _audioSender.State = (PlayState)value; + } + } public int Volume { get { return (int)(_volume * 100); } @@ -41,13 +44,13 @@ public int Volume return; _volume = value / 100f; - AL.Source(_sourceId, ALSourcef.Gain, _volume); + + if (_audioSender is not null) + _audioSender.Volume = _volume; } } - - public TimeSpan? Time => _matroskaPlayerBuffer?.CurrentTime; + public TimeSpan? Time => _audioSender?.CurrentTime; public TimeSpan? TotalTime => _matroskaPlayerBuffer?.TotalTime ?? Song?.Duration; - public ALSourceState? State => SourceState(); public IVideo? Song => _queue.ElementAtOrDefault(_currentSongIndex); public IReadOnlyCollection Songs => _queue; public LoopState LoopState { get; set; } @@ -55,20 +58,6 @@ public int Volume public PlayerController(YoutubeClient youtubeClient) { _youtubeClient = youtubeClient; - _device = ALC.OpenDevice(Environment.GetEnvironmentVariable("DeviceName")); - _context = ALC.CreateContext(_device, new ALContextAttributes()); - ALC.MakeContextCurrent(_context); - - var error = ALC.GetError(_device); - // Check for any errors - if (error != AlcError.NoError) - { - throw new Exception($"Error code: {error}"); - } - - _sourceId = AL.GenSource(); - _targetFormat = ALFormat.Stereo16; - AL.Source(_sourceId, ALSourcef.Gain, _volume); } public async ValueTask DisposeAsync() @@ -81,9 +70,6 @@ public async ValueTask DisposeAsync() _disposed = true; await StopAsync().ConfigureAwait(false); - ALC.DestroyContext(_context); - ALC.CloseDevice(_device); - AL.DeleteSource(_sourceId); if (_matroskaPlayerBuffer is not null) { @@ -121,7 +107,7 @@ public async Task> GetRecommendationsAsync() => private void ResetState() { - AL.SourceStop(_sourceId); + State = PlayState.Stopped; _audioSender?.ClearBuffer(); _currentSongTokenSource.Cancel(); } @@ -204,12 +190,6 @@ public async Task SetAsync(ISearchResult item, CancellationToken cancellationTok QueueChanged?.Invoke(_queue); } - private ALSourceState SourceState() - { - AL.GetSource(_sourceId, ALGetSourcei.SourceState, out int stateInt); - return (ALSourceState)stateInt; - } - public async Task PlayAsync() { using var _l = await _lock.LockAsync(); @@ -217,15 +197,15 @@ public async Task PlayAsync() if (Song is null) return; - if (SourceState() == ALSourceState.Playing) + if (State == PlayState.Playing) { return; } - if (SourceState() == ALSourceState.Paused) + if (State == PlayState.Paused) { StateChanged?.Invoke(); - AL.SourcePlay(_sourceId); + State = PlayState.Playing; return; } @@ -236,8 +216,8 @@ public async Task PlayAsync() await _matroskaPlayerBuffer.DisposeAsync(); _currentSongTokenSource = new CancellationTokenSource(); - - _audioSender = new AudioSender(_sourceId, _targetFormat); + _audioSender = new AudioSender(_volume, _state); + State = PlayState.Playing; try { @@ -249,8 +229,11 @@ public async Task PlayAsync() _matroskaPlayerBuffer.OnFinish += async () => { + _audioSender.WaitForEmptyBuffer = new(); + await _audioSender.WaitForEmptyBuffer.Task; _currentSongTokenSource.Cancel(); await _audioSender.DisposeAsync(); + State = PlayState.Stopped; OnFinish?.Invoke(); }; @@ -266,6 +249,7 @@ public async Task PlayAsync() //This could happen if the video is too old and there is no opus support _currentSongTokenSource.Cancel(); await _audioSender.DisposeAsync(); + State = PlayState.Stopped; OnFinish?.Invoke(); } } @@ -286,7 +270,7 @@ public async Task SkipAsync(bool bypassLoop = false) _currentSongIndex = 0; _audioSender?.ClearBuffer(); - AL.SourceStop(_sourceId); + State = PlayState.Stopped; } } @@ -298,7 +282,8 @@ public async Task GoBackAsync() { if (_currentSongIndex > 0) _currentSongIndex--; - AL.SourceStop(_sourceId); + State = PlayState.Stopped; + _audioSender?.ClearBuffer(); } } @@ -306,12 +291,12 @@ public async Task GoBackAsync() public async Task PauseAsync() { using var _ = await _lock.LockAsync(); - AL.SourcePause(_sourceId); + State = PlayState.Paused; } public async Task StopAsync() { using var _ = await _lock.LockAsync(); - AL.SourceStop(_sourceId); + State = PlayState.Stopped; } } diff --git a/Console/Commands/MainCommand.cs b/Console/Commands/MainCommand.cs index 611283a..fa9ca47 100644 --- a/Console/Commands/MainCommand.cs +++ b/Console/Commands/MainCommand.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using CliFx; +using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Console.Audio; @@ -69,7 +68,7 @@ public async ValueTask ExecuteAsync(IConsole console) X = Pos.Right(queueWin), Y = Pos.Bottom(searchWin), Width = Dim.Fill(), - Height = Dim.Fill() - 8, + Height = Dim.Fill()! - 8, ColorScheme = customColors }; diff --git a/Console/Console.csproj b/Console/Console.csproj index 596e937..be5cdf9 100644 --- a/Console/Console.csproj +++ b/Console/Console.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + true @@ -17,11 +18,10 @@ - - + - + diff --git a/Console/Database/DbContext.cs b/Console/Database/DbContext.cs index 184c92d..f5ce765 100644 --- a/Console/Database/DbContext.cs +++ b/Console/Database/DbContext.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using YoutubeExplode.Playlists; -using static Terminal.Gui.SpinnerStyle; namespace Console.Database; diff --git a/Console/Database/LocalPlaylist.cs b/Console/Database/LocalPlaylist.cs index 2b747c6..626c788 100644 --- a/Console/Database/LocalPlaylist.cs +++ b/Console/Database/LocalPlaylist.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using YoutubeExplode.Playlists; namespace Console.Database; diff --git a/Console/Database/LocalPlaylistSong.cs b/Console/Database/LocalPlaylistSong.cs index a860def..53343d2 100644 --- a/Console/Database/LocalPlaylistSong.cs +++ b/Console/Database/LocalPlaylistSong.cs @@ -1,6 +1,4 @@ -using YoutubeExplode.Videos; - -namespace Console.Database; +namespace Console.Database; internal class LocalPlaylistSong { diff --git a/Console/Migrations/20240803220027_InitialCreate.cs b/Console/Migrations/20240803220027_InitialCreate.cs index 7bae1ce..c84e3de 100644 --- a/Console/Migrations/20240803220027_InitialCreate.cs +++ b/Console/Migrations/20240803220027_InitialCreate.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -15,7 +14,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Playlists", columns: table => new { - PlaylistId = table.Column(type: "INTEGER", nullable: false) + PlaylistId = table + .Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), Name = table.Column(type: "TEXT", nullable: false), Description = table.Column(type: "TEXT", nullable: false) @@ -23,7 +23,8 @@ protected override void Up(MigrationBuilder migrationBuilder) constraints: table => { table.PrimaryKey("PK_Playlists", x => x.PlaylistId); - }); + } + ); migrationBuilder.CreateTable( name: "Songs", @@ -39,7 +40,8 @@ protected override void Up(MigrationBuilder migrationBuilder) constraints: table => { table.PrimaryKey("PK_Songs", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "PlaylistSongs", @@ -56,32 +58,33 @@ protected override void Up(MigrationBuilder migrationBuilder) column: x => x.PlaylistId, principalTable: "Playlists", principalColumn: "PlaylistId", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "FK_PlaylistSongs_Songs_SongId", column: x => x.SongId, principalTable: "Songs", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateIndex( name: "IX_PlaylistSongs_SongId", table: "PlaylistSongs", - column: "SongId"); + column: "SongId" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "PlaylistSongs"); + migrationBuilder.DropTable(name: "PlaylistSongs"); - migrationBuilder.DropTable( - name: "Playlists"); + migrationBuilder.DropTable(name: "Playlists"); - migrationBuilder.DropTable( - name: "Songs"); + migrationBuilder.DropTable(name: "Songs"); } } } diff --git a/Console/StatusBarFactory.cs b/Console/StatusBarFactory.cs index 68fce73..4e80fba 100644 --- a/Console/StatusBarFactory.cs +++ b/Console/StatusBarFactory.cs @@ -16,7 +16,14 @@ PlayerController playerController public StatusBar Create() => new( [ - new Shortcut(Key.Esc, "Exit", () => { }), + new Shortcut( + Key.Esc, + "Exit", + () => + { + Application.RequestStop(); + } + ), new Shortcut(Key.Q.WithCtrl, "Search", searchView.SetFocus), new Shortcut( Key.L.WithCtrl, diff --git a/Console/Utils.cs b/Console/Utils.cs index 83fd5e2..7c7e3b1 100644 --- a/Console/Utils.cs +++ b/Console/Utils.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using OpenTK.Audio.OpenAL; +using PortAudioSharp; using Terminal.Gui; namespace Console; @@ -10,18 +9,7 @@ public static class Utils public static void ConfigurePlatformDependencies() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - OpenALLibraryNameContainer.OverridePath = "libopenal.so"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - OpenALLibraryNameContainer.OverridePath = "libopenal.dylib"; - } - else - { - OpenALLibraryNameContainer.OverridePath = "soft_oal.dll"; - } + PortAudio.LoadNativeLibrary(); } public static string? ShowInputDialog(string title, string prompt, ColorScheme colorScheme) diff --git a/Console/Views/PlayerView.cs b/Console/Views/PlayerView.cs index 84b08c6..ecaa23e 100644 --- a/Console/Views/PlayerView.cs +++ b/Console/Views/PlayerView.cs @@ -1,6 +1,5 @@ using Console.Audio; using Console.Extensions; -using OpenTK.Audio.OpenAL; using Terminal.Gui; namespace Console.Views; @@ -19,7 +18,7 @@ public void ShowPlayer() win.RemoveAll(); ResetTitle(); - var baseContainer = new View { Height = Dim.Auto(), Width = Dim.Auto(), }; + var baseContainer = new View { Height = Dim.Auto(), Width = Dim.Fill(), }; var backButton = new Button { @@ -156,10 +155,7 @@ async Task NextSong(bool bypassLoop) playPauseButton.Accept += async (_, args) => { - if (player.State is null) - return; - - if (player.State == ALSourceState.Playing) + if (player.State == PlayState.Playing) { playPauseButton.Text = "play"; diff --git a/Tests/PlayerTests.cs b/Tests/PlayerTests.cs index 490e413..f520f92 100644 --- a/Tests/PlayerTests.cs +++ b/Tests/PlayerTests.cs @@ -1,7 +1,6 @@ using Console; using Console.Audio; using FluentAssertions; -using OpenTK.Audio.OpenAL; using YoutubeExplode; namespace Tests; @@ -30,7 +29,7 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=f8mL0_4GeV0") await _player.PlayAsync(); await finishTask.Task; - _player.State.Should().Be(ALSourceState.Stopped); + _player.State.Should().Be(PlayState.Stopped); _player.Song.Should().Be(video); } @@ -47,7 +46,7 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=f8mL0_4GeV0") await _player.PlayAsync(); await _player.SkipAsync(); - _player.State.Should().Be(ALSourceState.Initial); + _player.State.Should().Be(PlayState.Stopped); _player.Song.Should().Be(null); } @@ -63,7 +62,7 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=ZKzmyGKWFjU") await Task.Delay(5000); await _player.PauseAsync(); - _player.State.Should().Be(ALSourceState.Paused); + _player.State.Should().Be(PlayState.Paused); _player.Song.Should().Be(video); } @@ -79,7 +78,7 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=ZKzmyGKWFjU") await Task.Delay(5000); await _player.StopAsync(); - _player.State.Should().Be(ALSourceState.Stopped); + _player.State.Should().Be(PlayState.Stopped); _player.Song.Should().Be(video); } @@ -87,7 +86,10 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=ZKzmyGKWFjU") public async Task I_can_set_another_song_while_playing() { var finishTask = new TaskCompletionSource(); - _player.OnFinish += finishTask.SetResult; + _player.OnFinish += () => + { + finishTask.TrySetResult(); + }; var video = ( await _player.SearchAsync("https://www.youtube.com/watch?v=ZKzmyGKWFjU") @@ -98,12 +100,11 @@ await _player.SearchAsync("https://www.youtube.com/watch?v=f8mL0_4GeV0") await _player.SetAsync(video); await _player.PlayAsync(); - await Task.Delay(5000); await _player.SetAsync(video2); await _player.PlayAsync(); await finishTask.Task; - _player.State.Should().Be(ALSourceState.Stopped); + _player.State.Should().Be(PlayState.Stopped); _player.Song.Should().Be(video2); }