Merge branch 'public-structural-fix' into dev
This commit is contained in:
@@ -7,7 +7,7 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
|||||||
|
|
||||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
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; }
|
[Inject] public required IBrowserViewportService BrowserViewportService { get; set; }
|
||||||
|
|
||||||
private bool _isMinimized = true;
|
private bool _isMinimized = true;
|
||||||
@@ -16,21 +16,21 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
private bool _isDesktop = true;
|
private bool _isDesktop = true;
|
||||||
private Guid _viewportSubscriptionId;
|
private Guid _viewportSubscriptionId;
|
||||||
|
|
||||||
private bool IsLoaded => PlayerService.IsLoaded;
|
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
|
||||||
private bool IsLoading => PlayerService.IsLoading;
|
private bool IsLoading => PlayerService?.IsLoading ?? false;
|
||||||
private bool IsStreaming => PlayerService.CanStartStreaming;
|
private bool IsStreaming => PlayerService?.CanStartStreaming ?? false;
|
||||||
private bool IsStreamingMode => PlayerService.IsStreamingMode;
|
private bool IsStreamingMode => PlayerService?.IsStreamingMode ?? false;
|
||||||
private bool IsPlaying => PlayerService.IsPlaying;
|
private bool IsPlaying => PlayerService?.IsPlaying ?? false;
|
||||||
private bool IsPaused => PlayerService.IsPaused;
|
private bool IsPaused => PlayerService?.IsPaused ?? false;
|
||||||
private double? Duration => PlayerService.Duration;
|
private double? Duration => PlayerService?.Duration;
|
||||||
private double Volume => PlayerService.Volume;
|
private double Volume => PlayerService?.Volume ?? 0;
|
||||||
private double LoadProgress => PlayerService.LoadProgress;
|
private double LoadProgress => PlayerService?.LoadProgress ?? 0;
|
||||||
private string? ErrorMessage => PlayerService.ErrorMessage;
|
private string? ErrorMessage => PlayerService?.ErrorMessage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Display time - shows seek position while dragging, otherwise current playback time.
|
/// Display time - shows seek position while dragging, otherwise current playback time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private double DisplayTime => _isSeeking ? _seekPosition : PlayerService.CurrentTime;
|
private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seek is enabled once track is loaded AND duration is known (from WAV header).
|
/// Seek is enabled once track is loaded AND duration is known (from WAV header).
|
||||||
@@ -38,26 +38,16 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool CanSeek => IsLoaded && Duration.HasValue && Duration.Value > 0;
|
private bool CanSeek => IsLoaded && Duration.HasValue && Duration.Value > 0;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
await base.OnInitializedAsync();
|
|
||||||
// Set up EventCallback for track selection
|
|
||||||
PlayerService.OnTrackSelected = new EventCallback(this, Expand);
|
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 () =>
|
|
||||||
{
|
|
||||||
// Invoke the original callback (AudioPlayerProvider's StateHasChanged)
|
|
||||||
if (originalOnStateChanged.HasValue)
|
|
||||||
{
|
|
||||||
await originalOnStateChanged.Value.InvokeAsync();
|
|
||||||
}
|
}
|
||||||
// Also trigger our own re-render
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Expand()
|
private async Task Expand()
|
||||||
@@ -76,16 +66,19 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private async Task TogglePlayPause()
|
private async Task TogglePlayPause()
|
||||||
{
|
{
|
||||||
|
if (PlayerService == null) return;
|
||||||
await PlayerService.TogglePlayPause();
|
await PlayerService.TogglePlayPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Stop()
|
private async Task Stop()
|
||||||
{
|
{
|
||||||
|
if (PlayerService == null) return;
|
||||||
await PlayerService.Stop();
|
await PlayerService.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSeekStart()
|
private void OnSeekStart()
|
||||||
{
|
{
|
||||||
|
if (PlayerService == null) return;
|
||||||
_isSeeking = true;
|
_isSeeking = true;
|
||||||
_seekPosition = PlayerService.CurrentTime;
|
_seekPosition = PlayerService.CurrentTime;
|
||||||
}
|
}
|
||||||
@@ -98,18 +91,20 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private async Task OnSeekEnd(double position)
|
private async Task OnSeekEnd(double position)
|
||||||
{
|
{
|
||||||
|
if (PlayerService == null) return;
|
||||||
_isSeeking = false;
|
_isSeeking = false;
|
||||||
await PlayerService.Seek(position);
|
await PlayerService.Seek(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnVolumeChange(double volume)
|
private async Task OnVolumeChange(double volume)
|
||||||
{
|
{
|
||||||
|
if (PlayerService == null) return;
|
||||||
await PlayerService.SetVolume(volume);
|
await PlayerService.SetVolume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearError()
|
private void ClearError()
|
||||||
{
|
{
|
||||||
PlayerService.ClearError();
|
PlayerService?.ClearError();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleMinimized()
|
private void ToggleMinimized()
|
||||||
@@ -120,7 +115,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private async Task Close()
|
private async Task Close()
|
||||||
{
|
{
|
||||||
if (PlayerService.IsLoaded)
|
if (PlayerService != null && PlayerService.IsLoaded)
|
||||||
{
|
{
|
||||||
await PlayerService.Unload();
|
await PlayerService.Unload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
{
|
{
|
||||||
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
[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;
|
[Parameter] public int BucketCount { get; set; } = 32;
|
||||||
|
|
||||||
@@ -15,46 +15,30 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
private double[] _spectrumData = Array.Empty<double>();
|
private double[] _spectrumData = Array.Empty<double>();
|
||||||
private bool _isAnimating = false;
|
private bool _isAnimating = false;
|
||||||
private string? _playerId;
|
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()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_spectrumData = new double[BucketCount];
|
_spectrumData = new double[BucketCount];
|
||||||
|
}
|
||||||
|
|
||||||
// Get the player ID from the service
|
protected override async Task OnParametersSetAsync()
|
||||||
if (PlayerService is AudioPlayerService baseService)
|
{
|
||||||
|
// 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;
|
_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();
|
await UpdateAnimationState();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
// Initial check in case already playing
|
|
||||||
await UpdateAnimationState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateAnimationState()
|
private async Task UpdateAnimationState()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_playerId)) return;
|
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
|
||||||
|
|
||||||
var shouldAnimate = PlayerService.IsPlaying;
|
var shouldAnimate = PlayerService.IsPlaying;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
<CascadingValue Value="@(_audioPlayerService)" IsFixed="true">
|
@* IsFixed="true": the StreamingAudioPlayerService instance is created once in OnInitialized
|
||||||
|
and is never replaced — the reference is stable for the lifetime of the component.
|
||||||
|
If instance swapping at runtime is ever needed, change IsFixed to false (adds subscription
|
||||||
|
overhead on every parent re-render, but allows children to see the new reference). *@
|
||||||
|
<CascadingValue Value="@(_audioPlayerService)" IsFixed="true">
|
||||||
@ChildContent
|
@ChildContent
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
@@ -17,25 +17,20 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
protected override void OnInitialized()
|
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);
|
_audioPlayerService = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
|
||||||
|
|
||||||
// Set up EventCallback to properly marshal UI updates back to UI thread
|
// Provider is the SOLE owner of OnStateChanged. When the service fires,
|
||||||
// Use InvokeAsync to ensure proper Blazor render cycle triggering
|
// 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));
|
_audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
|
||||||
// OnTrackSelected will be set by individual child components that need it
|
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose the player on unmount so the JS setInterval driving progress
|
/// Dispose the player on unmount so the JS setInterval driving progress
|
||||||
/// callbacks no longer holds a DotNetObjectReference into a destroyed
|
/// callbacks no longer holds a DotNetObjectReference into a destroyed
|
||||||
|
|||||||
Reference in New Issue
Block a user