cc1fa60a4d
Rename VolumeControls to VolumeZone; stack 24-bucket SpectrumVisualizer above volume slider; remove it from PlayerSeekZone. MudSlider stays as seek placeholder. Pin flex-shrink:0 on volume-zone; add Class param to VolumeZone for layout flexibility.
125 lines
3.8 KiB
C#
125 lines
3.8 KiB
C#
using DeepDrftPublic.Client.Services;
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
|
|
|
public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
|
{
|
|
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
|
|
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
|
|
|
[Parameter] public int BucketCount { get; set; } = 24;
|
|
|
|
private readonly string _instanceId = Guid.NewGuid().ToString();
|
|
private double[] _spectrumData = Array.Empty<double>();
|
|
private bool _isAnimating = false;
|
|
private string? _playerId;
|
|
private IStreamingPlayerService? _subscribedService;
|
|
|
|
private bool IsVisible => (PlayerService?.IsPlaying ?? false) || (PlayerService?.IsPaused ?? false) || _isAnimating;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_spectrumData = new double[BucketCount];
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
// The cascade is IsFixed, so the provider's re-renders do NOT re-run
|
|
// OnParametersSet here, and this component has no incoming parameters
|
|
// that change. Subscribe to the multicast StateChanged side-channel so
|
|
// animation state stays correct independent of parent re-renders —
|
|
// notably when the bar expands while a track is already playing.
|
|
if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
|
|
{
|
|
if (_subscribedService != null)
|
|
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
|
|
|
PlayerService.StateChanged += OnPlayerStateChanged;
|
|
_subscribedService = PlayerService;
|
|
}
|
|
|
|
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
|
{
|
|
_playerId = baseService.PlayerId;
|
|
}
|
|
|
|
await UpdateAnimationState();
|
|
}
|
|
|
|
private void OnPlayerStateChanged() => InvokeAsync(async () =>
|
|
{
|
|
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
|
{
|
|
_playerId = baseService.PlayerId;
|
|
}
|
|
await UpdateAnimationState();
|
|
});
|
|
|
|
private async Task UpdateAnimationState()
|
|
{
|
|
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
|
|
|
|
var shouldAnimate = PlayerService.IsPlaying;
|
|
|
|
if (shouldAnimate && !_isAnimating)
|
|
{
|
|
await StartAnimation();
|
|
}
|
|
else if (!shouldAnimate && _isAnimating)
|
|
{
|
|
await StopAnimation();
|
|
}
|
|
}
|
|
|
|
private async Task StartAnimation()
|
|
{
|
|
if (_isAnimating || string.IsNullOrEmpty(_playerId)) return;
|
|
|
|
_isAnimating = true;
|
|
await AudioInterop.StartSpectrumAnimationAsync(_playerId, _instanceId, OnSpectrumData);
|
|
}
|
|
|
|
private async Task StopAnimation()
|
|
{
|
|
if (!_isAnimating || string.IsNullOrEmpty(_playerId)) return;
|
|
|
|
_isAnimating = false;
|
|
await AudioInterop.StopSpectrumAnimationAsync(_playerId, _instanceId);
|
|
|
|
// Clear the display
|
|
Array.Clear(_spectrumData);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private Task OnSpectrumData(double[] data)
|
|
{
|
|
if (data.Length > 0)
|
|
{
|
|
_spectrumData = data;
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private double GetBarHeight(int index)
|
|
{
|
|
if (index >= _spectrumData.Length) return 0;
|
|
|
|
// Scale to 0-100 percentage, with minimum height for visual appeal
|
|
var value = _spectrumData[index];
|
|
return Math.Max(2, value * 100);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_subscribedService != null)
|
|
{
|
|
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
|
_subscribedService = null;
|
|
}
|
|
await StopAnimation();
|
|
}
|
|
}
|