Refactor video playback implementation in PLib video library manager. Replace MediaPlayer.Controls with LibVLCSharp for improved performance and compatibility. Update VideoPlayerView and associated ViewModels to accommodate the new video control, ensuring seamless integration with the existing UI. Revise README.md to document changes and new player features.
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using LibVLCSharp.Shared;
|
||||
|
||||
namespace PLib.Desktop.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A video surface backed by LibVLC.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// VLC draws straight into a native child window that Avalonia creates for us, so no frame
|
||||
/// ever crosses into managed memory — playback costs what the decoder costs and nothing more.
|
||||
/// The price is airspace: this region is a separate window on top of the Avalonia surface, so
|
||||
/// nothing can be drawn over the picture. The media page keeps its controls beside the video
|
||||
/// rather than on it for exactly that reason.
|
||||
/// <para>
|
||||
/// The property surface deliberately mirrors what a media control is expected to expose —
|
||||
/// source, position, duration, volume — so the page binds to it the same way it would to any
|
||||
/// other player.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class VlcVideoView : NativeControlHost
|
||||
{
|
||||
public static readonly StyledProperty<Uri?> SourceProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, Uri?>(nameof(Source));
|
||||
|
||||
public static readonly StyledProperty<bool> AutoPlayProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(AutoPlay), defaultValue: true);
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> PositionProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Position));
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> DurationProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Duration));
|
||||
|
||||
public static readonly StyledProperty<bool> IsPlayingProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsPlaying));
|
||||
|
||||
public static readonly StyledProperty<bool> IsMutedProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsMuted));
|
||||
|
||||
/// <summary>Volume as a fraction; LibVLC works in percent and is converted on the way in.</summary>
|
||||
public static readonly StyledProperty<double> VolumeProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, double>(nameof(Volume), defaultValue: 0.8);
|
||||
|
||||
public static readonly StyledProperty<string?> LastErrorProperty =
|
||||
AvaloniaProperty.Register<VlcVideoView, string?>(nameof(LastError));
|
||||
|
||||
private MediaPlayer? _player;
|
||||
|
||||
/// <summary>
|
||||
/// True once VLC has been handed the native window. Playback cannot start before that,
|
||||
/// or VLC opens a top-level window of its own.
|
||||
/// </summary>
|
||||
private bool _surfaceReady;
|
||||
|
||||
public Uri? Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public bool AutoPlay
|
||||
{
|
||||
get => GetValue(AutoPlayProperty);
|
||||
set => SetValue(AutoPlayProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan Position
|
||||
{
|
||||
get => GetValue(PositionProperty);
|
||||
private set => SetValue(PositionProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan Duration
|
||||
{
|
||||
get => GetValue(DurationProperty);
|
||||
private set => SetValue(DurationProperty, value);
|
||||
}
|
||||
|
||||
public bool IsPlaying
|
||||
{
|
||||
get => GetValue(IsPlayingProperty);
|
||||
private set => SetValue(IsPlayingProperty, value);
|
||||
}
|
||||
|
||||
public bool IsMuted
|
||||
{
|
||||
get => GetValue(IsMutedProperty);
|
||||
set => SetValue(IsMutedProperty, value);
|
||||
}
|
||||
|
||||
public double Volume
|
||||
{
|
||||
get => GetValue(VolumeProperty);
|
||||
set => SetValue(VolumeProperty, value);
|
||||
}
|
||||
|
||||
public string? LastError
|
||||
{
|
||||
get => GetValue(LastErrorProperty);
|
||||
private set => SetValue(LastErrorProperty, value);
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
if (_player is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_player.Media is not null)
|
||||
{
|
||||
_player.Play();
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenCurrentSource();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pause() => _player?.SetPause(true);
|
||||
|
||||
public void Stop() => _player?.Stop();
|
||||
|
||||
public void Seek(TimeSpan position)
|
||||
{
|
||||
if (_player is { IsSeekable: true })
|
||||
{
|
||||
_player.Time = (long)position.TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
||||
{
|
||||
// Avalonia gives us an empty child window of the right platform kind; VLC renders
|
||||
// into it once we hand over the handle.
|
||||
var handle = base.CreateNativeControlCore(parent);
|
||||
|
||||
_player = new MediaPlayer(VlcRuntime.Shared);
|
||||
AttachPlayerEvents(_player);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_player.Hwnd = handle.Handle;
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_player.XWindow = (uint)handle.Handle;
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_player.NsObject = handle.Handle;
|
||||
}
|
||||
|
||||
_player.Mute = IsMuted;
|
||||
_player.Volume = ToVlcVolume(Volume);
|
||||
_surfaceReady = true;
|
||||
|
||||
if (AutoPlay)
|
||||
{
|
||||
OpenCurrentSource();
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||
{
|
||||
// Tear the player down before the window it draws into disappears.
|
||||
if (_player is { } player)
|
||||
{
|
||||
_player = null;
|
||||
_surfaceReady = false;
|
||||
|
||||
DetachPlayerEvents(player);
|
||||
player.Stop();
|
||||
player.Dispose();
|
||||
}
|
||||
|
||||
base.DestroyNativeControlCore(control);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (_player is not { } player)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (change.Property == SourceProperty)
|
||||
{
|
||||
OpenCurrentSource();
|
||||
}
|
||||
else if (change.Property == VolumeProperty)
|
||||
{
|
||||
player.Volume = ToVlcVolume(Volume);
|
||||
}
|
||||
else if (change.Property == IsMutedProperty)
|
||||
{
|
||||
player.Mute = IsMuted;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenCurrentSource()
|
||||
{
|
||||
if (!_surfaceReady || _player is not { } player || Source is not { } source)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LastError = null;
|
||||
|
||||
// The media object only has to survive the call: VLC takes its own reference.
|
||||
using var media = new Media(VlcRuntime.Shared, source);
|
||||
|
||||
if (!player.Play(media))
|
||||
{
|
||||
LastError = "VLC не смог открыть файл";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachPlayerEvents(MediaPlayer player)
|
||||
{
|
||||
player.TimeChanged += OnTimeChanged;
|
||||
player.LengthChanged += OnLengthChanged;
|
||||
player.Playing += OnPlaying;
|
||||
player.Paused += OnStoppedPlaying;
|
||||
player.Stopped += OnStoppedPlaying;
|
||||
player.EndReached += OnStoppedPlaying;
|
||||
player.EncounteredError += OnEncounteredError;
|
||||
}
|
||||
|
||||
private void DetachPlayerEvents(MediaPlayer player)
|
||||
{
|
||||
player.TimeChanged -= OnTimeChanged;
|
||||
player.LengthChanged -= OnLengthChanged;
|
||||
player.Playing -= OnPlaying;
|
||||
player.Paused -= OnStoppedPlaying;
|
||||
player.Stopped -= OnStoppedPlaying;
|
||||
player.EndReached -= OnStoppedPlaying;
|
||||
player.EncounteredError -= OnEncounteredError;
|
||||
}
|
||||
|
||||
// Every VLC event arrives on one of its own threads, so nothing here may touch an
|
||||
// Avalonia property directly.
|
||||
private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) =>
|
||||
Post(() => Position = TimeSpan.FromMilliseconds(Math.Max(0, e.Time)));
|
||||
|
||||
private void OnLengthChanged(object? sender, MediaPlayerLengthChangedEventArgs e) =>
|
||||
Post(() => Duration = TimeSpan.FromMilliseconds(Math.Max(0, e.Length)));
|
||||
|
||||
private void OnPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = true);
|
||||
|
||||
private void OnStoppedPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = false);
|
||||
|
||||
private void OnEncounteredError(object? sender, EventArgs e) =>
|
||||
Post(() => LastError = "VLC сообщил об ошибке воспроизведения");
|
||||
|
||||
private static void Post(Action action) => Dispatcher.UIThread.Post(action, DispatcherPriority.Background);
|
||||
|
||||
private static int ToVlcVolume(double volume) => (int)Math.Round(Math.Clamp(volume, 0, 1) * 100);
|
||||
}
|
||||
Reference in New Issue
Block a user