Files
PlaylistShared/PlaylistShared.Pwa/Components/AudioPlayer.razor

303 lines
10 KiB
Plaintext

@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Authorization
@namespace PlaylistShared.Pwa.Components
@inject HttpClient Http
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<div style="display: flex; gap: 8px;">
<MudIconButton Icon="@(_isPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
Size="Size.Medium"
Color="Color.Primary"
OnClick="TogglePlayPause" />
<MudIconButton Icon="@Icons.Material.Filled.Stop"
Size="Size.Medium"
Color="Color.Default"
OnClick="Stop" />
</div>
<div style="flex-grow: 1; min-width: 150px;">
<MudSlider @bind-Value="_currentProgress"
@bind-Value:event="oninput"
Min="0"
Max="100"
Size="Size.Small"
ValueChanged="@((double newValue) => SeekTo(newValue))" />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<MudText Typo="Typo.body2">@_currentTime / @_totalTime</MudText>
<div style="display: flex; align-items: center; gap: 8px; width: 120px;">
<MudIconButton Icon="@(_currentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)"
Size="Size.Small"
Color="Color.Default"
OnClick="ToggleMute" />
<MudSlider @bind-Value="_currentVolume"
@bind-Value:event="oninput"
Min="0"
Max="100"
Size="Size.Small"
ValueChanged="@((double newValue) => ChangeVolume(newValue))" />
</div>
</div>
</div>
</MudPaper>
<audio id="@_audioId" style="display: none;"></audio>
@code {
private string _audioId = $"audio_{Guid.NewGuid():N}";
private IJSObjectReference? _audioModule;
private IJSObjectReference? _audioElement;
private double _currentProgress;
private double _currentVolume = 70;
private string _currentTime = "0:00";
private string _totalTime = "0:00";
private bool _isPlaying;
private Timer? _progressTimer;
private bool _isMuted;
[Inject] protected IJSRuntime JS { get; set; } = null!;
[Inject] private TokenStorage TokenStorage { get; set; } = null!;
[Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!;
[Inject] private ISnackbar Snackbar { get; set; } = null!;
/// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary>
[Parameter] public bool RequireAuth { get; set; } = true;
/// <summary>ID расшаренного плейлиста.</summary>
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
/// <summary>Событие при завершении трека.</summary>
[Parameter] public EventCallback OnTrackEnded { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await EnsureAudioModuleAsync();
}
}
[JSInvokable]
public async Task OnAudioEnded()
{
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
if (OnTrackEnded.HasDelegate)
await OnTrackEnded.InvokeAsync();
StateHasChanged();
}
[JSInvokable]
public async Task OnTimeUpdate(double currentTime, double duration)
{
// Защита от некорректных значений
if (double.IsNaN(currentTime) || double.IsNaN(duration) || double.IsInfinity(currentTime) || double.IsInfinity(duration))
return;
if (duration <= 0) return;
_currentProgress = (currentTime / duration) * 100;
_currentTime = FormatTime(currentTime);
// Длительность не обновляем здесь
await InvokeAsync(StateHasChanged);
}
[JSInvokable]
public async Task OnDurationReady(double duration)
{
if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) return;
_totalTime = FormatTime(duration);
await InvokeAsync(StateHasChanged);
}
private async Task EnsureAudioModuleAsync()
{
if (_audioModule == null)
_audioModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/AudioPlayer.js");
if (_audioElement == null)
_audioElement = await _audioModule.InvokeAsync<IJSObjectReference>("init", _audioId, DotNetObjectReference.Create(this));
}
private async Task<bool> CheckAuthAsync()
{
if (!RequireAuth) return true;
var authState = await AuthProvider.GetAuthenticationStateAsync();
var isAuthenticated = authState.User.Identity?.IsAuthenticated == true;
if (!isAuthenticated)
{
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
return false;
}
return true;
}
public async Task LoadAndPlayAsync(string trackId)
{
if (!await CheckAuthAsync()) return;
var tokens = await TokenStorage.GetTokensAsync();
var accessToken = tokens.token;
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId))
{
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
return;
}
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
await EnsureAudioModuleAsync();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, SharedPlaylistId);
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
public async Task PlayAsync()
{
if (!await CheckAuthAsync()) return;
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
public async Task PauseAsync()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("pause");
_isPlaying = false;
StopProgressTimer();
StateHasChanged();
}
public async Task StopAsync()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("stop");
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
StateHasChanged();
}
private async Task TogglePlayPause()
{
if (_isPlaying)
await PauseAsync();
else
await PlayAsync();
}
private async Task Stop()
{
await StopAsync();
}
private async Task SeekTo(double value)
{
if (_audioElement == null) return;
try
{
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration))
{
var newTime = (value / 100) * duration;
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
}
}
catch (Exception ex)
{
Console.WriteLine($"SeekTo error: {ex.Message}");
}
}
private async Task ChangeVolume(double value)
{
if (_audioElement == null) return;
try
{
var volume = value / 100;
await _audioElement.InvokeVoidAsync("setVolume", volume);
_isMuted = false;
StateHasChanged();
}
catch (Exception ex)
{
Console.WriteLine($"ChangeVolume error: {ex.Message}");
}
}
private async Task ToggleMute()
{
if (_audioElement == null) return;
_isMuted = !_isMuted;
var newVolume = _isMuted ? 0 : (_currentVolume / 100);
await _audioElement.InvokeVoidAsync("setVolume", newVolume);
StateHasChanged();
}
private void StartProgressTimer()
{
StopProgressTimer();
_progressTimer = new Timer(async _ =>
{
await UpdateProgress();
}, null, 0, 500);
}
private void StopProgressTimer() => _progressTimer?.Dispose();
private async Task UpdateProgress()
{
if (_audioElement == null)
{
Console.WriteLine("UpdateProgress: _audioElement is null");
return;
}
try
{
var current = await _audioElement.InvokeAsync<double>("getCurrentTime");
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current))
{
_currentProgress = (current / duration) * 100;
_currentTime = FormatTime(current);
_totalTime = FormatTime(duration);
await InvokeAsync(StateHasChanged);
}
}
catch (Exception ex)
{
Console.WriteLine($"UpdateProgress error: {ex.Message}");
}
}
private string FormatTime(double seconds)
{
var total = (int)seconds;
var mins = total / 60;
var secs = total % 60;
return $"{mins}:{secs:D2}";
}
public async ValueTask DisposeAsync()
{
try
{
StopProgressTimer();
if (_audioElement != null)
await _audioElement.DisposeAsync();
if (_audioModule != null)
await _audioModule.DisposeAsync();
}
catch { }
}
}