diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
index dd1c347..5cf4c85 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
@@ -7,7 +7,7 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
{
- [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
+ [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[Inject] public required IBrowserViewportService BrowserViewportService { get; set; }
private bool _isMinimized = true;
@@ -16,21 +16,21 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private bool _isDesktop = true;
private Guid _viewportSubscriptionId;
- private bool IsLoaded => PlayerService.IsLoaded;
- private bool IsLoading => PlayerService.IsLoading;
- private bool IsStreaming => PlayerService.CanStartStreaming;
- private bool IsStreamingMode => PlayerService.IsStreamingMode;
- private bool IsPlaying => PlayerService.IsPlaying;
- private bool IsPaused => PlayerService.IsPaused;
- private double? Duration => PlayerService.Duration;
- private double Volume => PlayerService.Volume;
- private double LoadProgress => PlayerService.LoadProgress;
- private string? ErrorMessage => PlayerService.ErrorMessage;
+ private bool IsLoaded => PlayerService?.IsLoaded ?? false;
+ private bool IsLoading => PlayerService?.IsLoading ?? false;
+ private bool IsStreaming => PlayerService?.CanStartStreaming ?? false;
+ private bool IsStreamingMode => PlayerService?.IsStreamingMode ?? false;
+ private bool IsPlaying => PlayerService?.IsPlaying ?? false;
+ private bool IsPaused => PlayerService?.IsPaused ?? false;
+ private double? Duration => PlayerService?.Duration;
+ private double Volume => PlayerService?.Volume ?? 0;
+ private double LoadProgress => PlayerService?.LoadProgress ?? 0;
+ private string? ErrorMessage => PlayerService?.ErrorMessage;
///
/// Display time - shows seek position while dragging, otherwise current playback time.
///
- private double DisplayTime => _isSeeking ? _seekPosition : PlayerService.CurrentTime;
+ private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
///
/// Seek is enabled once track is loaded AND duration is known (from WAV header).
@@ -38,26 +38,16 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
///
private bool CanSeek => IsLoaded && Duration.HasValue && Duration.Value > 0;
- protected override async Task OnInitializedAsync()
+ protected override void OnParametersSet()
{
- await base.OnInitializedAsync();
- // Set up EventCallback for track selection
- PlayerService.OnTrackSelected = new EventCallback(this, Expand);
-
- // Store the original OnStateChanged callback set by the provider
- var originalOnStateChanged = PlayerService.OnStateChanged;
-
- // Set up a wrapper that calls both the original callback and our StateHasChanged
- PlayerService.OnStateChanged = new EventCallback(this, async () =>
+ // PlayerService is cascaded by AudioPlayerProvider; once it arrives,
+ // wire our track-selection handler. The provider owns OnStateChanged —
+ // we intentionally do NOT wrap or replace it. Re-renders propagate
+ // from the provider via the standard Blazor child render path.
+ if (PlayerService != null)
{
- // Invoke the original callback (AudioPlayerProvider's StateHasChanged)
- if (originalOnStateChanged.HasValue)
- {
- await originalOnStateChanged.Value.InvokeAsync();
- }
- // Also trigger our own re-render
- await InvokeAsync(StateHasChanged);
- });
+ PlayerService.OnTrackSelected = new EventCallback(this, Expand);
+ }
}
private async Task Expand()
@@ -76,16 +66,19 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private async Task TogglePlayPause()
{
+ if (PlayerService == null) return;
await PlayerService.TogglePlayPause();
}
private async Task Stop()
{
+ if (PlayerService == null) return;
await PlayerService.Stop();
}
private void OnSeekStart()
{
+ if (PlayerService == null) return;
_isSeeking = true;
_seekPosition = PlayerService.CurrentTime;
}
@@ -98,20 +91,22 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private async Task OnSeekEnd(double position)
{
+ if (PlayerService == null) return;
_isSeeking = false;
await PlayerService.Seek(position);
}
private async Task OnVolumeChange(double volume)
{
+ if (PlayerService == null) return;
await PlayerService.SetVolume(volume);
}
-
+
private void ClearError()
{
- PlayerService.ClearError();
+ PlayerService?.ClearError();
}
-
+
private void ToggleMinimized()
{
_isMinimized = !_isMinimized;
@@ -120,7 +115,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private async Task Close()
{
- if (PlayerService.IsLoaded)
+ if (PlayerService != null && PlayerService.IsLoaded)
{
await PlayerService.Unload();
}
diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs
index 6535f22..6689089 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs
@@ -7,7 +7,7 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
{
[Inject] public required AudioInteropService AudioInterop { get; set; }
- [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
+ [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[Parameter] public int BucketCount { get; set; } = 32;
@@ -15,46 +15,30 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
private double[] _spectrumData = Array.Empty();
private bool _isAnimating = false;
private string? _playerId;
- private EventCallback? _originalOnStateChanged;
- private bool IsVisible => PlayerService.IsPlaying || PlayerService.IsPaused || _isAnimating;
+ private bool IsVisible => (PlayerService?.IsPlaying ?? false) || (PlayerService?.IsPaused ?? false) || _isAnimating;
protected override void OnInitialized()
{
_spectrumData = new double[BucketCount];
+ }
- // Get the player ID from the service
- if (PlayerService is AudioPlayerService baseService)
+ protected override async Task OnParametersSetAsync()
+ {
+ // Provider re-renders cascade down to children and re-run OnParametersSet.
+ // Pick up the player id once the cascade arrives, then drive animation
+ // state from the parent's current IsPlaying — no callback wrapping needed.
+ if (_playerId == null && PlayerService is AudioPlayerService baseService)
{
_playerId = baseService.PlayerId;
}
- // Chain into the existing OnStateChanged callback to detect play/pause
- _originalOnStateChanged = PlayerService.OnStateChanged;
- PlayerService.OnStateChanged = new EventCallback(this, async () =>
- {
- // Call original callback first
- if (_originalOnStateChanged.HasValue)
- {
- await _originalOnStateChanged.Value.InvokeAsync();
- }
- // Then update our animation state
- await UpdateAnimationState();
- });
- }
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (firstRender)
- {
- // Initial check in case already playing
- await UpdateAnimationState();
- }
+ await UpdateAnimationState();
}
private async Task UpdateAnimationState()
{
- if (string.IsNullOrEmpty(_playerId)) return;
+ if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
var shouldAnimate = PlayerService.IsPlaying;
diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
index 2272bea..707fe41 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
@@ -17,25 +17,20 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
protected override void OnInitialized()
{
- // Create the service immediately (but don't initialize yet)
+ // Create the service immediately (but don't initialize yet).
+ // The base class lazily initializes on first track selection via
+ // EnsureInitializedAsync — that path is correct because audio contexts
+ // require a user gesture anyway. Initializing eagerly here causes 4+
+ // SignalR round-trips before any content is stable.
_audioPlayerService = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
- // Set up EventCallback to properly marshal UI updates back to UI thread
- // Use InvokeAsync to ensure proper Blazor render cycle triggering
+ // Provider is the SOLE owner of OnStateChanged. When the service fires,
+ // the provider re-renders, which cascades to its children automatically.
+ // Children must not wrap or replace this callback.
_audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
// OnTrackSelected will be set by individual child components that need it
}
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (firstRender && _audioPlayerService != null)
- {
- // Initialize the service after render when JavaScript is available
- await _audioPlayerService.InitializeAsync();
- StateHasChanged();
- }
- }
-
///
/// Dispose the player on unmount so the JS setInterval driving progress
/// callbacks no longer holds a DotNetObjectReference into a destroyed