using Avalonia; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Threading; using LibVLCSharp.Shared; namespace PLib.Desktop.Controls; /// /// A video surface backed by LibVLC. /// /// /// 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. /// /// 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. /// /// public sealed class VlcVideoView : NativeControlHost { public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); public static readonly StyledProperty AutoPlayProperty = AvaloniaProperty.Register(nameof(AutoPlay), defaultValue: true); public static readonly StyledProperty PositionProperty = AvaloniaProperty.Register(nameof(Position)); public static readonly StyledProperty DurationProperty = AvaloniaProperty.Register(nameof(Duration)); public static readonly StyledProperty IsPlayingProperty = AvaloniaProperty.Register(nameof(IsPlaying)); public static readonly StyledProperty IsMutedProperty = AvaloniaProperty.Register(nameof(IsMuted)); /// Volume as a fraction; LibVLC works in percent and is converted on the way in. public static readonly StyledProperty VolumeProperty = AvaloniaProperty.Register(nameof(Volume), defaultValue: 0.8); public static readonly StyledProperty LastErrorProperty = AvaloniaProperty.Register(nameof(LastError)); private MediaPlayer? _player; /// /// True once VLC has been handed the native window. Playback cannot start before that, /// or VLC opens a top-level window of its own. /// 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); }