Files
PLib/src/PLib.Desktop/Views/VideoPlayerView.axaml.cs
T

127 lines
4.4 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using MediaPlayer.Controls;
using PLib.Desktop.ViewModels;
namespace PLib.Desktop.Views;
/// <summary>
/// Transport controls for the media page.
/// </summary>
/// <remarks>
/// Playback state lives on <see cref="GpuMediaPlayer"/> itself — position, duration and
/// whether it is playing are all its own properties, and seeking is a method call. Mirroring
/// that into the view model would buy nothing but a second copy to keep in sync, so this
/// code-behind wires the buttons straight to the control. Everything the page knows about
/// the video — title, path, the commands around it — stays in the view model.
/// </remarks>
public sealed partial class VideoPlayerView : UserControl
{
/// <summary>True while the user is dragging the seek bar, so playback must not fight them.</summary>
private bool _isScrubbing;
public VideoPlayerView()
{
InitializeComponent();
PlayPauseButton.Click += OnPlayPause;
MuteButton.Click += OnToggleMute;
// Tunnelled: the Slider's own handlers mark these as handled on the way back up.
Seek.AddHandler(PointerPressedEvent, OnScrubStarted, RoutingStrategies.Tunnel);
Seek.AddHandler(PointerReleasedEvent, OnScrubFinished, RoutingStrategies.Tunnel);
VolumeSlider.PropertyChanged += OnVolumeChanged;
Player.GetObservable(GpuMediaPlayer.PositionProperty).Subscribe(OnPositionChanged);
Player.GetObservable(GpuMediaPlayer.DurationProperty).Subscribe(OnDurationChanged);
Player.GetObservable(GpuMediaPlayer.IsPlayingProperty).Subscribe(OnIsPlayingChanged);
Player.GetObservable(GpuMediaPlayer.LastErrorProperty).Subscribe(OnErrorChanged);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
// Leaving the page has to stop the decoder; nothing else will.
Player.Stop();
}
private void OnPlayPause(object? sender, RoutedEventArgs e)
{
if (Player.IsPlaying)
{
Player.Pause();
}
else
{
Player.Play();
}
}
private void OnToggleMute(object? sender, RoutedEventArgs e) => Player.IsMuted = !Player.IsMuted;
private void OnScrubStarted(object? sender, PointerPressedEventArgs e) => _isScrubbing = true;
private void OnScrubFinished(object? sender, PointerReleasedEventArgs e)
{
_isScrubbing = false;
Player.Seek(TimeSpan.FromSeconds(Seek.Value));
}
private void OnVolumeChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property != RangeBase.ValueProperty)
{
return;
}
Player.Volume = VolumeSlider.Value;
MuteIcon.Kind = VolumeIconFor(VolumeSlider.Value, Player.IsMuted);
}
private void OnPositionChanged(TimeSpan position)
{
PositionText.Text = DisplayText.Duration(position);
if (!_isScrubbing)
{
Seek.Value = position.TotalSeconds;
}
}
private void OnDurationChanged(TimeSpan duration)
{
DurationText.Text = DisplayText.Duration(duration);
// A zero maximum would pin the thumb to the left and swallow every seek.
Seek.Maximum = duration > TimeSpan.Zero ? duration.TotalSeconds : 1;
Seek.IsEnabled = duration > TimeSpan.Zero;
}
private void OnIsPlayingChanged(bool isPlaying) =>
PlayPauseIcon.Kind = isPlaying
? Material.Icons.MaterialIconKind.Pause
: Material.Icons.MaterialIconKind.Play;
private void OnErrorChanged(string? error)
{
ErrorText.Text = string.IsNullOrWhiteSpace(error)
? null
: $"Не удалось воспроизвести файл: {error}";
ErrorText.IsVisible = !string.IsNullOrWhiteSpace(error);
}
private static Material.Icons.MaterialIconKind VolumeIconFor(double volume, bool isMuted) =>
isMuted || volume <= 0.001
? Material.Icons.MaterialIconKind.VolumeOff
: volume < 0.5
? Material.Icons.MaterialIconKind.VolumeMedium
: Material.Icons.MaterialIconKind.VolumeHigh;
}