diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index 9c0dafd..e52e1f3 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -1,91 +1,80 @@ @if (_isMinimized) { -
+
+ Class="minimized-button" + OnClick="@ToggleMinimized"/>
} else { - @* Full-width outer container *@ -
- -
- @* Full Screen *@ -
-
-
+
+ +
+ + @* Desktop Layout *@ +
+
+
- - @if (!IsLoaded) - { - - } +
- +
- - @* Seek slider *@ - - -
+ +
+ +
+ +
- @* Mobile *@ -
-
-
+ @* Mobile Layout *@ +
+
+
- - - - @if (!IsLoaded) + @if (IsLoading) { }
- + +
- - @* Seek slider *@ + + Disabled="!IsLoaded"/>
-
+ @* Control Buttons - positioned absolutely like original *@ +
-
- - - - @if (!string.IsNullOrEmpty(ErrorMessage)) - { - - @ErrorMessage - - } - +
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { + + @ErrorMessage + + }
- @* Spacer div to maintain layout spacing *@ -
-} + @* Spacer to prevent content overlap *@ +
+} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index 9877e40..457cec1 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -1,4 +1,4 @@ -using DeepDrftWeb.Client.Services; +using DeepDrftWeb.Client.Services; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -7,10 +7,11 @@ namespace DeepDrftWeb.Client.Controls.AudioPlayerBar; public partial class AudioPlayerBar : ComponentBase { [CascadingParameter] public required IPlayerService PlayerService { get; set; } - [Parameter] public bool ShowLoadProgress { get; set; } = true; + private bool _isMinimized = true; private bool IsLoaded => PlayerService.IsLoaded; + private bool IsLoading => PlayerService.IsLoading; private bool IsPlaying => PlayerService.IsPlaying; private bool IsPaused => PlayerService.IsPaused; private double CurrentTime => PlayerService.CurrentTime; diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css index a6737bf..c7da143 100644 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css +++ b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css @@ -1,26 +1,24 @@ -/* AudioPlayerBar Component - Scoped Styles */ +/* Preserve key visual styles while simplifying layout */ -/* Outer container - full width, fixed to bottom */ -.deepdrft-player-outer-container { +/* Player outer container - fixed positioning */ +.player-outer-container { position: fixed; bottom: 0; left: 0; right: 0; - width: 100%; z-index: 1200; padding: 0; margin: 0; } -/* Inner container wrapper */ -.deepdrft-player-inner-container { +/* Player inner container */ +.player-inner-container { padding: 1rem; padding-bottom: 1.5rem; - margin: 0 auto; } -/* Player bar with rounded corners and semi-opaque background */ -.deepdrft-audio-player-bar { +/* Custom backdrop blur container */ +.player-backdrop { position: relative; background: var(--deepdrft-theme-background-gray); backdrop-filter: blur(15px); @@ -29,21 +27,21 @@ border: 1px solid var(--deepdrft-theme-primary); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 2px 10px var(--deepdrft-theme-secondary); - transition: all 0.3s ease; color: #ffffff; + transition: all 0.3s ease; overflow: hidden; margin-bottom: 1rem; } -/* Spacer div to maintain layout spacing */ -.deepdrft-player-spacer { - height: 140px; - width: 100%; - flex-shrink: 0; +/* Control buttons positioning */ +.player-controls { + position: absolute; + top: 0.5rem; + right: 0.5rem; } -/* Minimized floating dock */ -.deepdrft-minimized-player-dock { +/* Minimized floating dock with gradient */ +.minimized-dock { position: fixed; bottom: 60px; right: 60px; @@ -51,148 +49,83 @@ width: 60px; height: 60px; border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; + cursor: pointer; background: linear-gradient(135deg, var(--deepdrft-theme-primary) 0%, var(--deepdrft-theme-secondary) 50%, var(--deepdrft-theme-tertiary) 100% ); backdrop-filter: blur(15px); + -webkit-backdrop-filter: blur(15px); border: 2px solid var(--deepdrft-theme-secondary); box-shadow: 0 4px 20px var(--deepdrft-theme-primary), 0 2px 10px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; - cursor: pointer; } -.deepdrft-minimized-player-dock:hover { +.minimized-dock:hover { transform: scale(1.1); - box-shadow: 0 6px 25px rgba(var(--mud-palette-primary-rgb), 0.8), + box-shadow: 0 6px 25px rgba(var(--deepdrft-theme-primary), 0.8), 0 3px 15px rgba(0, 0, 0, 0.4); } -.deepdrft-minimized-player-button { - border-radius: 50%; +/* Minimized button styles */ +.minimized-button { + border-radius: 50% !important; background: transparent !important; - color: white; - transition: all 0.3s ease; + color: white !important; + transition: all 0.3s ease !important; box-shadow: none !important; border: none !important; width: 48px !important; height: 48px !important; } -/* Layout containers */ -.deepdrft-audio-player-content { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1.25rem; +/* Spacer to prevent content overlap */ +.player-spacer { + height: 140px; + width: 100%; + flex-shrink: 0; } -.deepdrft-audio-controls-section { - display: flex; - flex-direction: column; - gap: 0.25rem; +/* Essential layout adjustments only */ +.controls-left { min-width: 200px; } -.deepdrft-audio-buttons-row { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.deepdrft-audio-info-row { - display: flex; - align-items: center; - gap: 0.75rem; - margin-top: 0.25rem; - justify-items: center; -} - -.deepdrft-audio-volume-section { - display: flex; - align-items: center; - gap: 0.25rem; - width: 140px; -} - -.deepdrft-audio-minimize-section { - position: absolute; - top: 0.5rem; - right: 0.5rem; - display: flex; - align-items: center; - justify-content: center; -} - -/* Control elements */ -.deepdrft-audio-seek-slider { +.seekbar-flex { flex: 1; - margin-right: 0.5rem; } -.deepdrft-audio-volume-slider { - width: 100px; +.volume-right { + /*min-width: 140px;*/ } -.deepdrft-audio-time { - min-width: 120px; -} - -.deepdrft-audio-volume-icon { - margin-right: 4px; -} - -/* Mobile responsiveness */ +/* Mobile responsive adjustments */ @media (max-width: 768px) { - .deepdrft-minimized-player-dock { + .minimized-dock { bottom: 15px; right: 15px; width: 56px; height: 56px; } - .deepdrft-minimized-player-button { + .minimized-button { width: 44px !important; height: 44px !important; } - .deepdrft-player-inner-container { + .player-inner-container { padding: 0.75rem; padding-bottom: 1.25rem; } - .deepdrft-audio-player-bar { + .player-backdrop { border-radius: 1rem; margin-bottom: 1.25rem; } - .deepdrft-audio-player-content { - flex-direction: column; - gap: 0.5rem; - padding: 0.75rem 1rem; - } - - .deepdrft-audio-controls-section { - align-items: center; - width: 100%; - } - - .deepdrft-audio-seek-slider { - margin-right: 0; - width: 100%; - } - - .deepdrft-audio-volume-section { - width: 100%; - justify-content: center; - } - - .deepdrft-player-spacer { + .player-spacer { height: 160px; } } \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor deleted file mode 100644 index e52e1f3..0000000 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor +++ /dev/null @@ -1,103 +0,0 @@ -@if (_isMinimized) -{ -
- -
-} -else -{ -
- -
- - @* Desktop Layout *@ -
-
-
- - -
- -
- -
- -
- -
- -
-
- - @* Mobile Layout *@ -
-
-
- - @if (IsLoading) - { - - } -
- - -
- - -
- - @* Control Buttons - positioned absolutely like original *@ -
- - -
-
-
- - @if (!string.IsNullOrEmpty(ErrorMessage)) - { - - @ErrorMessage - - } -
- - @* Spacer to prevent content overlap *@ -
-} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.cs deleted file mode 100644 index 02cdd55..0000000 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.cs +++ /dev/null @@ -1,93 +0,0 @@ -using DeepDrftWeb.Client.Services; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace DeepDrftWeb.Client.Controls.AudioPlayerBar; - -public partial class AudioPlayerBar2 : ComponentBase -{ - [CascadingParameter] public required IPlayerService PlayerService { get; set; } - [Parameter] public bool ShowLoadProgress { get; set; } = true; - private bool _isMinimized = true; - - private bool IsLoaded => PlayerService.IsLoaded; - private bool IsLoading => PlayerService.IsLoading; - private bool IsPlaying => PlayerService.IsPlaying; - private bool IsPaused => PlayerService.IsPaused; - private double CurrentTime => PlayerService.CurrentTime; - private double? Duration => PlayerService.Duration; - private double Volume => PlayerService.Volume; - private double LoadProgress => PlayerService.LoadProgress; - private string? ErrorMessage => PlayerService.ErrorMessage; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - PlayerService.OnStateChanged += StateHasChanged; - PlayerService.OnTrackSelected += Expand; - } - - private async Task Expand() - { - if (_isMinimized) - { - _isMinimized = false; - StateHasChanged(); - } - } - private static string FormatTime(double seconds) - { - var timeSpan = TimeSpan.FromSeconds(seconds); - return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss"); - } - - private async Task TogglePlayPause() - { - await PlayerService.TogglePlayPause(); - } - - private async Task Stop() - { - await PlayerService.Stop(); - } - - private async Task OnSeek(double position) - { - await PlayerService.Seek(position); - } - - private async Task OnVolumeChange(double volume) - { - await PlayerService.SetVolume(volume); - } - - private void ClearError() - { - PlayerService.ClearError(); - } - - private void ToggleMinimized() - { - _isMinimized = !_isMinimized; - StateHasChanged(); - } - - private async Task Close() - { - if (PlayerService.IsLoaded) - { - await PlayerService.Unload(); - } - - if (!_isMinimized) - { - _isMinimized = true; - StateHasChanged(); - } - } - - private string GetPlayIcon() - { - return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; - } -} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.css b/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.css deleted file mode 100644 index c7da143..0000000 --- a/DeepDrftWeb.Client/Controls/AudioPlayerBar/AudioPlayerBar2.razor.css +++ /dev/null @@ -1,131 +0,0 @@ -/* Preserve key visual styles while simplifying layout */ - -/* Player outer container - fixed positioning */ -.player-outer-container { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 1200; - padding: 0; - margin: 0; -} - -/* Player inner container */ -.player-inner-container { - padding: 1rem; - padding-bottom: 1.5rem; -} - -/* Custom backdrop blur container */ -.player-backdrop { - position: relative; - background: var(--deepdrft-theme-background-gray); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border-radius: 1rem; - border: 1px solid var(--deepdrft-theme-primary); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), - 0 2px 10px var(--deepdrft-theme-secondary); - color: #ffffff; - transition: all 0.3s ease; - overflow: hidden; - margin-bottom: 1rem; -} - -/* Control buttons positioning */ -.player-controls { - position: absolute; - top: 0.5rem; - right: 0.5rem; -} - -/* Minimized floating dock with gradient */ -.minimized-dock { - position: fixed; - bottom: 60px; - right: 60px; - z-index: 1300; - width: 60px; - height: 60px; - border-radius: 50%; - cursor: pointer; - background: linear-gradient(135deg, - var(--deepdrft-theme-primary) 0%, - var(--deepdrft-theme-secondary) 50%, - var(--deepdrft-theme-tertiary) 100% - ); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border: 2px solid var(--deepdrft-theme-secondary); - box-shadow: 0 4px 20px var(--deepdrft-theme-primary), - 0 2px 10px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease; -} - -.minimized-dock:hover { - transform: scale(1.1); - box-shadow: 0 6px 25px rgba(var(--deepdrft-theme-primary), 0.8), - 0 3px 15px rgba(0, 0, 0, 0.4); -} - -/* Minimized button styles */ -.minimized-button { - border-radius: 50% !important; - background: transparent !important; - color: white !important; - transition: all 0.3s ease !important; - box-shadow: none !important; - border: none !important; - width: 48px !important; - height: 48px !important; -} - -/* Spacer to prevent content overlap */ -.player-spacer { - height: 140px; - width: 100%; - flex-shrink: 0; -} - -/* Essential layout adjustments only */ -.controls-left { - min-width: 200px; -} - -.seekbar-flex { - flex: 1; -} - -.volume-right { - /*min-width: 140px;*/ -} - -/* Mobile responsive adjustments */ -@media (max-width: 768px) { - .minimized-dock { - bottom: 15px; - right: 15px; - width: 56px; - height: 56px; - } - - .minimized-button { - width: 44px !important; - height: 44px !important; - } - - .player-inner-container { - padding: 0.75rem; - padding-bottom: 1.25rem; - } - - .player-backdrop { - border-radius: 1rem; - margin-bottom: 1.25rem; - } - - .player-spacer { - height: 160px; - } -} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor b/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor new file mode 100644 index 0000000..c7c9cd8 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor @@ -0,0 +1,3 @@ + + @ChildContent + \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor.cs new file mode 100644 index 0000000..1356683 --- /dev/null +++ b/DeepDrftWeb.Client/Controls/AudioPlayerProvider.razor.cs @@ -0,0 +1,31 @@ +using DeepDrftWeb.Client.Services; +using DeepDrftWeb.Client.Clients; +using Microsoft.AspNetCore.Components; + +namespace DeepDrftWeb.Client.Controls; + +public partial class AudioPlayerProvider : ComponentBase +{ + [Inject] public required AudioInteropService AudioInterop { get; set; } + [Inject] public required TrackMediaClient TrackMediaClient { get; set; } + + private AudioPlayerService? _audioPlayerService; + + [Parameter] public RenderFragment? ChildContent { get; set; } + + protected override void OnInitialized() + { + // Create the service immediately (but don't initialize yet) + _audioPlayerService = new AudioPlayerService(AudioInterop, TrackMediaClient); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && _audioPlayerService != null) + { + // Initialize the service after render when JavaScript is available + await _audioPlayerService.InitializeAsync(); + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor deleted file mode 100644 index 6292516..0000000 --- a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor +++ /dev/null @@ -1,3 +0,0 @@ - - @ChildContent - \ No newline at end of file diff --git a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs b/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs deleted file mode 100644 index 66c7bdf..0000000 --- a/DeepDrftWeb.Client/Controls/AudioPlayerService.razor.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DeepDrftWeb.Client.Services; -using Microsoft.AspNetCore.Components; -using DeepDrftModels.Entities; - -namespace DeepDrftWeb.Client.Controls; - -public partial class AudioPlayerService : ComponentBase -{ - [Inject] public required AudioPlaybackEngine AudioPlaybackEngine { get; set; } - - private readonly PlayerService _playerService = new(); - private IPlayerService PlayerService => _playerService; - - [Parameter] public RenderFragment? ChildContent { get; set; } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - // Initialize the PlayerService with the AudioPlaybackEngine now that it's available - await _playerService.InitializeAsync(AudioPlaybackEngine); - StateHasChanged(); - } - } -} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Layout/MainLayout.razor b/DeepDrftWeb.Client/Layout/MainLayout.razor index 1df49e9..f6cc905 100644 --- a/DeepDrftWeb.Client/Layout/MainLayout.razor +++ b/DeepDrftWeb.Client/Layout/MainLayout.razor @@ -7,7 +7,7 @@ - + @@ -20,9 +20,9 @@ @Body - + - + diff --git a/DeepDrftWeb.Client/Services/AudioPlaybackEngine.cs b/DeepDrftWeb.Client/Services/AudioPlaybackEngine.cs index 3469a38..ee420be 100644 --- a/DeepDrftWeb.Client/Services/AudioPlaybackEngine.cs +++ b/DeepDrftWeb.Client/Services/AudioPlaybackEngine.cs @@ -1,4 +1,7 @@ -using DeepDrftModels.Entities; +// DEPRECATED: This class has been merged into AudioPlayerService +// TODO: Remove after testing new implementation +/* +using DeepDrftModels.Entities; using DeepDrftWeb.Client.Clients; using NetBlocks.Models; @@ -357,4 +360,5 @@ public class AudioPlaybackEngine : IAsyncDisposable { await AudioInterop.DisposePlayerAsync(PlayerId); } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/DeepDrftWeb.Client/Services/AudioPlayerService.cs b/DeepDrftWeb.Client/Services/AudioPlayerService.cs new file mode 100644 index 0000000..b023972 --- /dev/null +++ b/DeepDrftWeb.Client/Services/AudioPlayerService.cs @@ -0,0 +1,400 @@ +using DeepDrftModels.Entities; +using DeepDrftWeb.Client.Clients; +using NetBlocks.Models; + +namespace DeepDrftWeb.Client.Services; + +public class AudioPlayerService : IPlayerService, IAsyncDisposable +{ + private readonly AudioInteropService _audioInterop; + private readonly TrackMediaClient _trackMediaClient; + + public string PlayerId { get; private set; } = Guid.NewGuid().ToString(); + + // State properties + public bool IsInitialized { get; private set; } = false; + public bool IsLoaded { get; private set; } = false; + public bool IsLoading { get; private set; } = false; + public bool IsPlaying { get; private set; } = false; + public bool IsPaused { get; private set; } = false; + public double CurrentTime { get; private set; } = 0; + public double? Duration { get; private set; } = null; + public double Volume { get; private set; } = 0.8; + public double LoadProgress { get; private set; } = 0; + public string? ErrorMessage { get; private set; } + + // Events + public event Action? OnStateChanged; + public event Events.EventAsync? OnTrackSelected; + + public AudioPlayerService(AudioInteropService audioInterop, TrackMediaClient trackMediaClient) + { + _audioInterop = audioInterop; + _trackMediaClient = trackMediaClient; + } + + public async Task InitializeAsync() + { + if (IsInitialized) return; + + try + { + var result = await _audioInterop.CreatePlayerAsync(PlayerId); + if (!result.Success) + { + ErrorMessage = $"Failed to initialize audio player: {result.Error}"; + NotifyStateChanged(); + return; + } + + await _audioInterop.SetOnProgressCallbackAsync(PlayerId, OnProgressCallback); + await _audioInterop.SetOnEndCallbackAsync(PlayerId, OnPlaybackEndCallback); + await _audioInterop.SetOnLoadProgressCallbackAsync(PlayerId, OnLoadProgressCallback); + + await _audioInterop.SetVolumeAsync(PlayerId, Volume); + + IsInitialized = true; + ErrorMessage = null; + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Failed to initialize audio player: {ex.Message}"; + NotifyStateChanged(); + } + } + + public async Task SelectTrack(TrackEntity track) + { + await EnsureInitializedAsync(); + + NotifyStateChanged(); + + if (OnTrackSelected != null) + await OnTrackSelected.Invoke(); + + await LoadTrack(track); + NotifyStateChanged(); + } + + private async Task LoadTrack(TrackEntity track) + { + try + { + if (IsLoading) return; + + if (IsPlaying || IsPaused) + { + await Unload(); + } + + // Reset state to indicate loading has started + ErrorMessage = null; + LoadProgress = 0; + IsLoaded = false; + IsLoading = true; + Duration = null; + CurrentTime = 0; + NotifyStateChanged(); + + var loadResult = await _audioInterop.InitializeBufferedPlayerAsync(PlayerId); + if (loadResult?.Success != true) + { + ErrorMessage = $"Failed to initialize audio buffer: {loadResult?.Error ?? "Unknown error"}"; + return; + } + + var mediaResult = await _trackMediaClient.GetTrackMedia(track.EntryKey); + if (!mediaResult.Success) + { + ErrorMessage = mediaResult.GetMessage(); + return; + } + + if (mediaResult.Value == null) + { + ErrorMessage = "No audio returned from server"; + return; + } + + TrackMediaResponse audio = mediaResult.Value; + await StreamAudio(audio); + } + catch (Exception ex) + { + ErrorMessage = $"Error loading audio: {ex.Message}"; + LoadProgress = 0; + IsLoaded = false; + } + finally + { + IsLoading = false; + NotifyStateChanged(); + } + } + + private async Task StreamAudio(TrackMediaResponse audio) + { + try + { + const int bufferSize = 32 * 1024; + long totalBytesRead = 0; + int currentBytes; + + do + { + var buffer = new byte[bufferSize]; + currentBytes = await audio.Stream.ReadAsync(buffer, 0, buffer.Length); + + if (currentBytes > 0) + { + totalBytesRead += currentBytes; + + if (currentBytes < bufferSize) + { + var trimmedBuffer = new byte[currentBytes]; + Array.Copy(buffer, trimmedBuffer, currentBytes); + buffer = trimmedBuffer; + } + + var appendResult = await _audioInterop.AppendAudioBlockAsync(PlayerId, buffer); + if (!appendResult.Success) + { + throw new Exception($"Failed to append audio block: {appendResult.Error}"); + } + + if (audio.ContentLength > 0) + { + LoadProgress = Math.Min(1.0, (double)totalBytesRead / audio.ContentLength); + NotifyStateChanged(); + } + } + } while (currentBytes > 0); + + var finalizeResult = await _audioInterop.FinalizeAudioBufferAsync(PlayerId); + if (!finalizeResult.Success) + { + throw new Exception($"Failed to finalize audio buffer: {finalizeResult.Error}"); + } + + Duration = finalizeResult.Duration; + LoadProgress = 1.0; + IsLoaded = true; + ErrorMessage = null; + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Error streaming audio: {ex.Message}"; + LoadProgress = 0; + IsLoaded = false; + NotifyStateChanged(); + throw; + } + } + + public async Task TogglePlayPause() + { + if (!IsLoaded) return; + + try + { + AudioOperationResult result; + + if (IsPlaying) + { + result = await _audioInterop.PauseAsync(PlayerId); + if (result.Success) + { + IsPlaying = false; + IsPaused = true; + } + } + else + { + result = await _audioInterop.PlayAsync(PlayerId); + if (result.Success) + { + IsPlaying = true; + IsPaused = false; + } + } + + if (!result.Success) + { + ErrorMessage = $"Playback error: {result.Error}"; + } + else + { + ErrorMessage = null; + } + + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Error controlling playback: {ex.Message}"; + NotifyStateChanged(); + } + } + + public async Task Stop() + { + if (!IsLoaded) return; + + try + { + var result = await _audioInterop.StopAsync(PlayerId); + if (result.Success) + { + IsPlaying = false; + IsPaused = false; + CurrentTime = 0; + ErrorMessage = null; + } + else + { + ErrorMessage = $"Stop error: {result.Error}"; + } + + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Error stopping playback: {ex.Message}"; + NotifyStateChanged(); + } + } + + public async Task Unload() + { + if (!IsLoaded) return; + + try + { + await Stop(); + var result = await _audioInterop.UnloadAsync(PlayerId); + if (result.Success) + { + IsPlaying = false; + IsPaused = false; + CurrentTime = 0; + Duration = null; + LoadProgress = 0; + IsLoaded = false; + ErrorMessage = null; + } + else + { + ErrorMessage = $"Unload error: {result.Error}"; + } + + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Error unloading track: {ex.Message}"; + NotifyStateChanged(); + } + } + + public async Task Seek(double position) + { + if (!IsLoaded) return; + + try + { + var result = await _audioInterop.SeekAsync(PlayerId, position); + if (result.Success) + { + CurrentTime = position; + ErrorMessage = null; + } + else + { + ErrorMessage = $"Seek error: {result.Error}"; + } + + NotifyStateChanged(); + } + catch (Exception ex) + { + ErrorMessage = $"Error seeking: {ex.Message}"; + NotifyStateChanged(); + } + } + + public async Task SetVolume(double volume) + { + Volume = volume; + + if (IsLoaded) + { + try + { + var result = await _audioInterop.SetVolumeAsync(PlayerId, volume); + if (!result.Success) + { + ErrorMessage = $"Volume error: {result.Error}"; + } + else + { + ErrorMessage = null; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error setting volume: {ex.Message}"; + } + } + + NotifyStateChanged(); + } + + public void ClearError() + { + ErrorMessage = null; + NotifyStateChanged(); + } + + private async Task OnProgressCallback(double currentTime) + { + CurrentTime = currentTime; + NotifyStateChanged(); + } + + private async Task OnPlaybackEndCallback() + { + IsPlaying = false; + IsPaused = false; + CurrentTime = 0; + NotifyStateChanged(); + } + + private async Task OnLoadProgressCallback(double progress) + { + LoadProgress = progress; + NotifyStateChanged(); + } + + private async Task EnsureInitializedAsync() + { + if (!IsInitialized) + { + await InitializeAsync(); + } + } + + private void NotifyStateChanged() + { + OnStateChanged?.Invoke(); + } + + public async ValueTask DisposeAsync() + { + if (IsInitialized) + { + await _audioInterop.DisposePlayerAsync(PlayerId); + } + } +} \ No newline at end of file diff --git a/DeepDrftWeb.Client/Services/IPlayerService.cs b/DeepDrftWeb.Client/Services/IPlayerService.cs index 651827e..56d183c 100644 --- a/DeepDrftWeb.Client/Services/IPlayerService.cs +++ b/DeepDrftWeb.Client/Services/IPlayerService.cs @@ -22,6 +22,7 @@ public interface IPlayerService event Events.EventAsync OnTrackSelected; // Control methods + Task InitializeAsync(); Task SelectTrack(TrackEntity track); Task Stop(); Task Unload(); diff --git a/DeepDrftWeb.Client/Services/PlayerService.cs b/DeepDrftWeb.Client/Services/PlayerService.cs index 2cec8e8..e045662 100644 --- a/DeepDrftWeb.Client/Services/PlayerService.cs +++ b/DeepDrftWeb.Client/Services/PlayerService.cs @@ -1,3 +1,6 @@ +// DEPRECATED: This class has been replaced by AudioPlayerService +// TODO: Remove after testing new implementation +/* using DeepDrftModels.Entities; using NetBlocks.Models; @@ -128,4 +131,5 @@ public class PlayerService : IPlayerService await InitializeAsync(_audioEngine); } } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/DeepDrftWeb.Client/Startup.cs b/DeepDrftWeb.Client/Startup.cs index d3b8971..919c212 100644 --- a/DeepDrftWeb.Client/Startup.cs +++ b/DeepDrftWeb.Client/Startup.cs @@ -29,6 +29,6 @@ public static class Startup }); services.AddScoped(); services.AddScoped(); - services.AddTransient(); + // AudioPlaybackEngine removed - functionality merged into AudioPlayerService } } \ No newline at end of file