275 lines
8.3 KiB
C#
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);
|
|
}
|