Files
PLib/src/PLib.Desktop/Controls/VlcVideoView.cs
T

275 lines
8.3 KiB
C#

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);
}