Compare commits

...

2 Commits

Author SHA1 Message Date
FrigaT
41a9d27005 Создание своего прогресса загрузки трека 2026-04-16 04:35:05 +03:00
FrigaT
35140b71b7 Доработан плеер 2026-04-16 04:11:04 +03:00
5 changed files with 326 additions and 200 deletions

View File

@@ -7,71 +7,96 @@
@inject ISnackbar Snackbar
@inject HttpClient Http
<MudPaper Class="pa-4" Elevation="0" Width="100%" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
<MudStack Row Gap="2" AlignItems="AlignItems.Center" Wrap="Wrap.Wrap">
<!-- Информация о треке -->
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
@if (!string.IsNullOrEmpty(_currentTrackCoverUrl))
{
<MudImage Src="@_currentTrackCoverUrl" Height="40" Width="40" Class="rounded" />
}
<MudText Typo="Typo.body1" Style="font-weight: 500;">@_currentTrackTitle</MudText>
</MudStack>
<MudPaper Class="pa-2" Elevation="0" Width="100%" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
<MudStack Row AlignItems="AlignItems.Center" Wrap="Wrap.Wrap">
<!-- Кнопки управления -->
<MudStack Row Gap="1">
<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" />
</MudStack>
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
@onmouseleave="() => { _isPlayHovered = false; }"
style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden; width: 50px; height: 50px;">
<!-- Ползунок прогресса -->
<MudItem 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))" />
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrackCoverUrl))
{
<MudImage Src="@AudioPlayerService.CurrentTrackCoverUrl.FormatCoverUrl(50, 50)" Height="50" Width="50" Class="rounded" Style="display: block;" />
}
<MudItem class="play-overlay"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: transparent; display: flex; align-items: center; justify-content: center; border-radius: 4px;">
<MudToggleIconButton Toggled="@AudioPlayerService.IsPlaying"
Icon="@Icons.Material.Filled.PlayArrow"
Color="@Color.Primary"
ToggledIcon="@Icons.Material.Filled.Pause"
ToggledColor="@Color.Primary"
ToggledChanged="OnPlayClick" />
</MudItem>
</MudItem>
<!-- Время и громкость -->
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">@_currentTime / @_totalTime</MudText>
<MudStack Row Gap="1" AlignItems="AlignItems.Center" Style="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))" />
<!-- Название и прогресс -->
<MudStack AlignItems="AlignItems.Stretch" Class="flex-grow-1" Style="height: 100%;" Spacing="0">
<MudStack Row AlignItems="AlignItems.Stretch" Class="flex-grow-1">
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTrackTitle</MudText>
<MudSpacer />
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTimeString / @AudioPlayerService.TotalTimeString</MudText>
</MudStack>
<MudSlider Value="@AudioPlayerService.CurrentProgress" Class="mt-n1" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => SeekTo(newValue))" Step="0.01" />
<MudProgressLinear Color="Color.Primary" Buffer="true" Value="@AudioPlayerService.CurrentProgress" BufferValue="@_bufferValue" Class="my-7" />
<TrackProgress Value="@AudioPlayerService.CurrentProgress"
Min="0"
Max="100"
Color="Color.Primary"
BufferValue="@_bufferValue" />
</MudStack>
<!-- Громкость -->
<MudItem @onmouseenter="() => _volumeIsOpen = true"
@onmouseleave="() => _volumeIsOpen = false"
@onwheel="OnVolumeHandleWheel"
Style="position: relative; display: flex; align-items: center;">
<MudIconButton Icon="@(AudioPlayerService.CurrentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)"
Size="Size.Small"
Color="Color.Default"
OnClick="ToggleMute" />
@* Попавер с минимальной шириной *@
<MudPopover Open="@_volumeIsOpen"
AnchorOrigin="Origin.TopCenter"
TransformOrigin="Origin.BottomCenter"
Fixed="true"
Class="pa-0 mt-n5"
Style="height:120px; width: 10px; background-color: transparent !important; overflow: visible !important;">
<MudProgressLinear Vertical="true" Color="Color.Primary" Size="Size.Medium" Value="@AudioPlayerService.CurrentVolume" />
</MudPopover>
</MudItem>
</MudStack>
</MudPaper>
<audio id="@_audioId" style="display: none;"></audio>
@code {
private const double _defaultVolume = 50;
private const double _volumeDefault = 50;
// Генерируем уникальный ID для аудиоэлемента, чтобы избежать конфликтов при множественных экземплярах
private string _audioId = $"audio_{Guid.NewGuid():N}";
private IJSObjectReference? _audioModule;
private IJSObjectReference? _audioElement;
private double _currentProgress;
private double _currentVolume = _defaultVolume;
private string _currentTime = "0:00";
private string _totalTime = "0:00";
private bool _isPlaying;
private Timer? _progressTimer;
private bool _isMuted;
private string? _currentAccessToken;
private string? _currentSharedPlaylistId;
private string? _currentTrackCoverUrl;
private string? _currentTrackTitle;
// Громкость
private bool _volumeIsOpen;
private double _volumeBeforeMute;
private double _bufferValue;
private bool _isPlayHovered;
protected override async Task OnInitializedAsync()
{
AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay;
AudioPlayerService.OnPlayRequested += OnPlay;
AudioPlayerService.OnPauseRequested += OnPause;
AudioPlayerService.OnStopRequested += OnStop;
AudioPlayerService.OnSeekRequested += OnSeek;
AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange;
AudioPlayerService.OnStateChanged += OnStateChanged;
await LoadSavedVolume();
await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация
AudioPlayerService.OnLoadAndPlayRequested += OnServiceLoadAndPlay;
AudioPlayerService.OnPlayRequested += OnServicePlay;
AudioPlayerService.OnPauseRequested += OnServicePause;
AudioPlayerService.OnSeekRequested += OnServiceSeek;
AudioPlayerService.OnVolumeChangeRequested += OnServiceVolumeChange;
AudioPlayerService.OnStateChanged += OnServiceStateChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -82,20 +107,6 @@
}
}
private void OnStateChanged()
{
_currentTrackTitle = AudioPlayerService.CurrentTrackTitle;
_currentTrackCoverUrl = AudioPlayerService.CurrentTrackCoverUrl?.FormatCoverUrl(40, 40);
InvokeAsync(StateHasChanged);
}
private async Task LoadSavedVolume()
{
var savedVolume = await PlayerStorage.GetVolumeAsync() ?? _defaultVolume;
_currentVolume = savedVolume;
await AudioPlayerService.SetVolumeAsync(savedVolume);
}
private async Task EnsureAudioModuleAsync()
{
if (_audioModule == null)
@@ -108,32 +119,21 @@
public async Task OnAudioEnded()
{
AudioPlayerService.NotifyTrackEnded();
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
StateHasChanged();
}
[JSInvokable]
public async Task OnTimeUpdate(double currentTime, double duration)
{
if (double.IsNaN(currentTime) || double.IsNaN(duration) || duration <= 0) return;
var progress = (currentTime / duration) * 100;
var currentTimeStr = FormatTime(currentTime);
var totalTimeStr = FormatTime(duration);
AudioPlayerService.UpdateProgress(progress, currentTimeStr, totalTimeStr);
_currentProgress = progress;
_currentTime = currentTimeStr;
_totalTime = totalTimeStr;
await InvokeAsync(StateHasChanged);
if (!double.IsNaN(duration) && !double.IsNaN(currentTime) && duration > 0)
{
AudioPlayerService.UpdateProgress(currentTime, duration);
}
}
[JSInvokable]
public async Task OnDurationReady(double duration)
public async Task OnDownloadProgress(double second)
{
if (duration <= 0) return;
_totalTime = FormatTime(duration);
await InvokeAsync(StateHasChanged);
_bufferValue = second / AudioPlayerService.TotalTime * 100;
}
private async Task<bool> CheckAuthAsync()
@@ -147,15 +147,16 @@
return true;
}
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
#region Обработка сервиса
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
//if (!await CheckAuthAsync()) return;
var tokens = await TokenStorage.GetTokensAsync();
_currentAccessToken = accessToken ?? tokens.token;
_currentSharedPlaylistId = sharedPlaylistId;
accessToken = tokens.token;
}
if (string.IsNullOrWhiteSpace(_currentAccessToken) && string.IsNullOrWhiteSpace(_currentSharedPlaylistId))
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
{
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
return;
@@ -163,51 +164,27 @@
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
await EnsureAudioModuleAsync();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, _currentAccessToken, _currentSharedPlaylistId);
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
}
private async Task OnPlay()
private async Task OnServicePlay()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
private async Task OnPause()
private async Task OnServicePause()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("pause");
_isPlaying = false;
StopProgressTimer();
StateHasChanged();
}
private async Task OnStop()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("stop");
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
StateHasChanged();
}
private async Task OnSeek(double percent)
private async Task OnServiceSeek(double time)
{
if (_audioElement == null) return;
try
{
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration))
{
var newTime = (percent / 100) * duration;
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
}
await _audioElement.InvokeVoidAsync("setCurrentTime", time);
}
catch (Exception ex)
{
@@ -215,15 +192,14 @@
}
}
private async Task OnVolumeChange(double volume)
private async Task OnServiceVolumeChange(double volume)
{
if (_audioElement == null) return;
if (volume == AudioPlayerService.CurrentVolume) return;
try
{
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
_isMuted = false;
_currentVolume = volume;
await PlayerStorage.SetVolumeAsync(volume);
StateHasChanged();
}
catch (Exception ex)
@@ -232,17 +208,21 @@
}
}
private async Task TogglePlayPause()
private void OnServiceStateChanged()
{
if (_isPlaying)
await AudioPlayerService.PauseAsync();
else
await AudioPlayerService.PlayAsync();
InvokeAsync(StateHasChanged);
}
#endregion
private async Task Stop()
private async Task OnVolumeHandleWheel(WheelEventArgs e)
{
await AudioPlayerService.StopAsync();
// Изменяем громкость на 5 единиц за один тик колесика
double step = 5;
double newVolume = e.DeltaY < 0
? Math.Min(AudioPlayerService.CurrentVolume + step, 100)
: Math.Max(AudioPlayerService.CurrentVolume - step, 0);
await ChangeVolume(newVolume);
}
private async Task SeekTo(double value)
@@ -257,57 +237,30 @@
private async Task ToggleMute()
{
_isMuted = !_isMuted;
var newVolume = _isMuted ? 0 : _currentVolume;
await AudioPlayerService.SetVolumeAsync(newVolume);
if (AudioPlayerService.CurrentVolume > 0)
{
_volumeBeforeMute = AudioPlayerService.CurrentVolume;
await AudioPlayerService.SetVolumeAsync(0);
}
private void StartProgressTimer()
else
{
StopProgressTimer();
_progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500);
}
private void StopProgressTimer() => _progressTimer?.Dispose();
private async Task UpdateProgress()
{
if (_audioElement == 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))
{
var progress = (current / duration) * 100;
var currentTime = FormatTime(current);
var totalTime = FormatTime(duration);
AudioPlayerService.UpdateProgress(progress, currentTime, totalTime);
_currentProgress = progress;
_currentTime = currentTime;
_totalTime = totalTime;
await InvokeAsync(StateHasChanged);
}
}
catch (Exception ex)
{
Console.WriteLine($"UpdateProgress error: {ex.Message}");
await AudioPlayerService.SetVolumeAsync(_volumeBeforeMute);
_volumeBeforeMute = 0;
}
}
private string FormatTime(double seconds)
private async Task OnPlayClick()
{
var total = (int)seconds;
var mins = total / 60;
var secs = total % 60;
return $"{mins}:{secs:D2}";
if (AudioPlayerService.IsPlaying)
await AudioPlayerService.PauseAsync();
else
await AudioPlayerService.PlayAsync();
}
public async ValueTask DisposeAsync()
{
try
{
StopProgressTimer();
if (_audioElement != null)
await _audioElement.DisposeAsync();
if (_audioModule != null)

View File

@@ -0,0 +1,137 @@
<div class="track-progress-container @ColorClass" @onwheel="HandleWheel">
@* Фоновая дорожка *@
<div class="progress-base-track">
@* Буфер *@
<div class="progress-buffer-bar" style="width: @(CalculatePercentage(BufferValue))%"></div>
@* Активная шкала (заполнение) *@
<div class="progress-active-bar" style="width: @(CalculatePercentage(Value))%"></div>
</div>
@* Ползунок *@
<input type="range"
min="@Min.ToString(System.Globalization.CultureInfo.InvariantCulture)"
max="@Max.ToString(System.Globalization.CultureInfo.InvariantCulture)"
step="0.01"
value="@Value.ToString(System.Globalization.CultureInfo.InvariantCulture)"
@oninput="OnInput"
class="progress-input" />
</div>
<style>
.track-progress-container {
position: relative;
width: 100%;
height: 12px;
display: flex;
align-items: center;
--track-color: var(--mud-palette-primary); /* Дефолт */
}
/* Привязка цветов MudBlazor */
.track-color-primary { --track-color: var(--mud-palette-primary); }
.track-color-secondary { --track-color: var(--mud-palette-secondary); }
.track-color-info { --track-color: var(--mud-palette-info); }
.track-color-success { --track-color: var(--mud-palette-success); }
.track-color-warning { --track-color: var(--mud-palette-warning); }
.track-color-error { --track-color: var(--mud-palette-error); }
.progress-base-track {
position: absolute;
width: 100%;
height: 4px;
background: var(--mud-palette-action-disabled-background);
border-radius: 4px;
overflow: hidden;
}
.progress-buffer-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: var(--mud-palette-action-default-hover);
transition: width 0.3s ease;
z-index: 1;
}
.progress-active-bar {
position: absolute;
height: 100%;
left: 0;
top: 0;
background: var(--track-color);
border-radius: 4px;
}
.progress-input {
position: absolute;
width: 100%;
background: transparent;
-webkit-appearance: none;
z-index: 5;
margin: 0;
}
/* Стили шарика (Thumb) */
.progress-input::-webkit-slider-thumb {
-webkit-appearance: none;
height: 14px;
width: 14px;
border-radius: 50%;
background: var(--track-color);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
box-shadow: var(--mud-elevation-2);
border: 2px solid var(--mud-palette-surface);
}
.progress-input::-moz-range-thumb {
height: 14px;
width: 14px;
border-radius: 50%;
background: var(--track-color);
cursor: pointer;
opacity: 0;
border: 2px solid var(--mud-palette-surface);
}
.track-progress-container:hover .progress-input::-webkit-slider-thumb { opacity: 1; }
.track-progress-container:hover .progress-input::-moz-range-thumb { opacity: 1; }
.progress-input:focus { outline: none; }
</style>
@code {
[Parameter] public double Value { get; set; }
[Parameter] public double BufferValue { get; set; }
[Parameter] public double Min { get; set; } = 0;
[Parameter] public double Max { get; set; } = 100;
[Parameter] public Color Color { get; set; } = Color.Primary;
[Parameter] public EventCallback<double> ValueChanged { get; set; }
// Генерируем CSS класс на основе перечисления Color
private string ColorClass => $"track-color-{Color.ToString().ToLower()}";
private async Task OnInput(ChangeEventArgs e)
{
if (double.TryParse(e.Value?.ToString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var newValue))
{
await ValueChanged.InvokeAsync(newValue);
}
}
private async Task HandleWheel(WheelEventArgs e)
{
double range = Max - Min;
double step = range * 0.02;
var newValue = e.DeltaY < 0 ? Math.Min(Value + step, Max) : Math.Max(Value - step, Min);
await ValueChanged.InvokeAsync(newValue);
}
private double CalculatePercentage(double val)
{
if (Max <= Min) return 0;
return ((Math.Clamp(val, Min, Max) - Min) / (Max - Min)) * 100;
}
}

View File

@@ -10,15 +10,18 @@ public class AudioPlayerService : IAudioPlayerService
private readonly TokenStorage _tokenStorage;
private readonly ISnackbar _snackbar;
private readonly HttpClient _http;
private readonly PlayerStorage _playerStorage;
private string? _currentTrackId;
private string? _currentTrackTitle;
private string? _currentTrackCoverUrl;
private bool _isPlaying;
private double _currentVolume = 70;
private double _currentVolume = 50;
private double _currentProgress;
private string _currentTime = "0:00";
private string _totalTime = "0:00";
private double _currentTime = 0;
private double _totalTime = 0;
private string _currentTimeString = "0:00";
private string _totalTimeString = "0:00";
public string? CurrentTrackId => _currentTrackId;
public string? CurrentTrackTitle => _currentTrackTitle;
@@ -34,21 +37,42 @@ public class AudioPlayerService : IAudioPlayerService
}
}
public double CurrentProgress => _currentProgress;
public string CurrentTime => _currentTime;
public string TotalTime => _totalTime;
public double CurrentTime => _currentTime;
public double TotalTime => _totalTime;
public string CurrentTimeString => _currentTimeString;
public string TotalTimeString => _totalTimeString;
public event Action? OnStateChanged;
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient)
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient, PlayerStorage playerStorage)
{
_tokenStorage = tokenStorage;
_snackbar = snackbar;
_http = httpClient;
_playerStorage = playerStorage;
_ = LoadVolume();
}
private async Task LoadVolume()
{
var savedVolume = await _playerStorage.GetVolumeAsync();
if (savedVolume != null)
{
_currentVolume = savedVolume.Value;
}
}
// Внешние команды (вызываются из компонентов)
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, string? title = null, string? coverUrl = null)
{
if (_currentTrackId == trackId)
{
await PlayAsync();
return;
}
// Если accessToken не передан, пытаемся получить его из хранилища
if (string.IsNullOrWhiteSpace(accessToken))
{
@@ -101,19 +125,10 @@ public class AudioPlayerService : IAudioPlayerService
OnPauseRequested?.Invoke();
}
public async Task StopAsync()
{
_isPlaying = false;
_currentTrackId = null;
_currentProgress = 0;
_currentTime = "0:00";
OnStateChanged?.Invoke();
OnStopRequested?.Invoke();
}
public async Task SeekToAsync(double percent)
{
OnSeekRequested?.Invoke(percent);
var newTime = (percent / 100) * _totalTime;
OnSeekRequested?.Invoke(newTime);
}
public async Task SetVolumeAsync(double volume)
@@ -121,13 +136,13 @@ public class AudioPlayerService : IAudioPlayerService
_currentVolume = volume;
OnStateChanged?.Invoke();
OnVolumeChangeRequested?.Invoke(volume);
await _playerStorage.SetVolumeAsync(volume);
}
// События для связи с реальным AudioPlayer компонентом
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
public event Func<Task>? OnPlayRequested;
public event Func<Task>? OnPauseRequested;
public event Func<Task>? OnStopRequested;
public event Func<double, Task>? OnSeekRequested;
public event Func<double, Task>? OnVolumeChangeRequested;
@@ -144,11 +159,14 @@ public class AudioPlayerService : IAudioPlayerService
OnStateChanged?.Invoke();
}
public void UpdateProgress(double progress, string currentTime, string totalTime)
public void UpdateProgress(double currentTime, double totalTime)
{
var progress = (currentTime / totalTime) * 100;
_currentProgress = progress;
_currentTime = currentTime;
_currentTimeString = FormatDuration(currentTime);
_totalTime = totalTime;
_totalTimeString = FormatDuration(totalTime);
OnStateChanged?.Invoke();
}
@@ -157,7 +175,10 @@ public class AudioPlayerService : IAudioPlayerService
_isPlaying = false;
_currentTrackId = null;
_currentProgress = 0;
_currentTime = "0:00";
_currentTime = 0;
_currentTimeString = "0:00";
_totalTime = 0;
_currentTimeString = "0:00";
OnStateChanged?.Invoke();
}
@@ -183,4 +204,11 @@ public class AudioPlayerService : IAudioPlayerService
}
return null;
}
private string FormatDuration(double seconds)
{
var mins = (int)(seconds / 60);
var secs = (int)(seconds % 60);
return $"{mins}:{secs:D2}";
}
}

View File

@@ -19,11 +19,17 @@ public interface IAudioPlayerService
/// <summary>Прогресс воспроизведения в процентах (0100).</summary>
double CurrentProgress { get; }
/// <summary>Текущее время в секундах.</summary>
double CurrentTime { get; }
/// <summary>Общая длительность в секундах</summary>
double TotalTime { get; }
/// <summary>Отформатированное текущее время (мм:сс).</summary>
string CurrentTime { get; }
string CurrentTimeString { get; }
/// <summary>Отформатированная общая длительность (мм:сс).</summary>
string TotalTime { get; }
string TotalTimeString { get; }
/// <summary>Отформатированное название текущего трека.</summary>
string? CurrentTrackTitle { get; }
@@ -47,9 +53,6 @@ public interface IAudioPlayerService
/// <summary>Поставить на паузу.</summary>
Task PauseAsync();
/// <summary>Остановить воспроизведение и выгрузить трек.</summary>
Task StopAsync();
/// <summary>Перемотать на указанный процент (0100).</summary>
Task SeekToAsync(double percent);
@@ -76,10 +79,7 @@ public interface IAudioPlayerService
/// <summary>Запрос на паузу.</summary>
event Func<Task>? OnPauseRequested;
/// <summary>Запрос на остановку и выгрузку трека.</summary>
event Func<Task>? OnStopRequested;
/// <summary>Запрос на перемотку (процент 0100).</summary>
/// <summary>Запрос на перемотку (секунды).</summary>
event Func<double, Task>? OnSeekRequested;
/// <summary>Запрос на изменение громкости (0100).</summary>
@@ -94,7 +94,7 @@ public interface IAudioPlayerService
void SetCurrentTrack(string? trackId);
/// <summary>Обновить прогресс и отображаемое время.</summary>
void UpdateProgress(double progress, string currentTime, string totalTime);
void UpdateProgress(double currentTime, double totalTime);
/// <summary>Уведомить об окончании трека.</summary>
void NotifyTrackEnded();

View File

@@ -26,14 +26,13 @@
const stop = () => { audio.pause(); audio.currentTime = 0; };
const setVolume = (volume) => { audio.volume = toNumber(volume); };
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
const getDuration = () => durationReady ? durationValue : 0;
const getCurrentTime = () => toNumber(audio.currentTime);
audio.addEventListener('loadedmetadata', () => {
const current = toNumber(audio.currentTime);
durationValue = toNumber(audio.duration);
durationReady = durationValue > 0;
if (dotNetHelper && durationReady) {
dotNetHelper.invokeMethodAsync('OnDurationReady', durationValue);
dotNetHelper.invokeMethodAsync('OnTimeUpdate', current, durationValue);
}
});
@@ -50,6 +49,15 @@
}
});
audio.addEventListener('progress', () => {
if (dotNetHelper) {
if (audio.buffered.length > 0 && audio.duration) {
const bufferedEnd = toNumber(audio.buffered.end(audio.buffered.length - 1));
dotNetHelper.invokeMethodAsync('OnDownloadProgress', bufferedEnd);
}
}
});
// Возвращаем все методы, которые будут вызываться из C#
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, getDuration, getCurrentTime };
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime };
}