Refactor VlcVideoView for enhanced video playback performance and user interaction. Update README.md to clarify the new control's functionality, including frame handling and buffer management. Modify VideoPlayerView.axaml for improved layout and error message display. Enable unsafe code blocks in project settings to optimize frame copying process.
This commit is contained in:
@@ -78,20 +78,19 @@ dotnet test
|
|||||||
только по «Сохранить», так что отмена не оставляет следов. Пересканирование запускается
|
только по «Сохранить», так что отмена не оставляет следов. Пересканирование запускается
|
||||||
только если изменилось то, что влияет на состав библиотеки, — смена темы или ширины кадра
|
только если изменилось то, что влияет на состав библиотеки, — смена темы или ширины кадра
|
||||||
его не вызывает.
|
его не вызывает.
|
||||||
- **Плеер — свой контрол `VlcVideoView` поверх LibVLCSharp.** Avalonia создаёт нативное
|
- **Плеер — свой контрол `VlcVideoView` поверх LibVLCSharp.** VLC декодирует в память,
|
||||||
дочернее окно, VLC рисует прямо в него: ни один кадр не проходит через управляемую память.
|
которую мы ему выдаём, а рисуем кадр сами: видео остаётся обычным контролом Avalonia —
|
||||||
Расплата — airspace: поверх видео ничего нарисовать нельзя, поэтому контролы и сообщения
|
участвует в hit-тесте, принимает жесты, поверх него можно класть что угодно. Цена — одно
|
||||||
об ошибках живут рядом с картинкой, а не на ней. Транспорт (позиция, длительность,
|
копирование на показанный кадр, и на 4K оно становится основной стоимостью воспроизведения.
|
||||||
play/pause) — свойства самого контрола, поэтому им управляет code-behind страницы;
|
Буферов два: VLC декодирует в один, пока мы читаем другой; на `Display` они меняются
|
||||||
дублировать это состояние во вьюмодель значило бы держать вторую копию и синхронизировать её.
|
местами под коротким локом. Транспорт (позиция, длительность, play/pause) — свойства самого
|
||||||
Закрытие страницы обнуляет `OpenedVideo`, вью уходит из дерева, `DestroyNativeControlCore`
|
контрола, поэтому им управляет code-behind страницы; дублировать это состояние во вьюмодель
|
||||||
гасит плеер.
|
значило бы держать вторую копию и синхронизировать её. Закрытие страницы обнуляет
|
||||||
По той же причине в полноэкранном режиме остаётся тонкая полоса управления внизу:
|
`OpenedVideo`, вью уходит из дерева, и плеер гасится вместе с буферами.
|
||||||
всплывающего оверлея поверх видео нативная поверхность не допускает, а движение мыши над
|
До этого пробовали два готовых пути. `MediaPlayer.Controls`: декодер работал, но кадры до
|
||||||
ней до Avalonia не доходит — автоскрытию не на что реагировать.
|
экрана не доходили — чёрный экран и на GPU-, и на CPU-пути, при полностью рабочем в
|
||||||
Готовый `MediaPlayer.Controls` пробовали до этого: декодер работал, но кадры до экрана
|
приложении `OpenGlControlBase`. Нативное окно VLC через `NativeControlHost`: картинка
|
||||||
не доходили — чёрный экран и на GPU-, и на CPU-пути, при полностью рабочем в приложении
|
появилась, но окно поверх поверхности Avalonia не пропускает ни клик, ни оверлей.
|
||||||
`OpenGlControlBase`. Свой контрол ни от чьей версии Avalonia не зависит.
|
|
||||||
- **Кэш превью самовосстанавливается.** Диск — ключ `sha256(путь|размер|mtime)`, память — LRU
|
- **Кэш превью самовосстанавливается.** Диск — ключ `sha256(путь|размер|mtime)`, память — LRU
|
||||||
на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически
|
на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически
|
||||||
на месте (`IThumbnailGenerator.IsAvailable`), и перерисовывает удалённые; после полного
|
на месте (`IThumbnailGenerator.IsAvailable`), и перерисовывает удалённые; после полного
|
||||||
|
|||||||
@@ -1,279 +1,463 @@
|
|||||||
using Avalonia;
|
using System.Runtime.InteropServices;
|
||||||
using Avalonia.Controls;
|
using System.Text;
|
||||||
using Avalonia.Platform;
|
using Avalonia;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Controls;
|
||||||
using LibVLCSharp.Shared;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
namespace PLib.Desktop.Controls;
|
using Avalonia.Platform;
|
||||||
|
using Avalonia.Threading;
|
||||||
/// <summary>
|
using LibVLCSharp.Shared;
|
||||||
/// A video surface backed by LibVLC.
|
|
||||||
/// </summary>
|
namespace PLib.Desktop.Controls;
|
||||||
/// <remarks>
|
|
||||||
/// VLC draws straight into a native child window that Avalonia creates for us, so no frame
|
/// <summary>
|
||||||
/// ever crosses into managed memory — playback costs what the decoder costs and nothing more.
|
/// A video surface backed by LibVLC that draws through Avalonia.
|
||||||
/// The price is airspace: this region is a separate window on top of the Avalonia surface, so
|
/// </summary>
|
||||||
/// nothing can be drawn over the picture. The media page keeps its controls beside the video
|
/// <remarks>
|
||||||
/// rather than on it for exactly that reason.
|
/// VLC decodes into memory we hand it and we paint the result ourselves, so the video is an
|
||||||
/// <para>
|
/// ordinary control: it takes part in hit testing, gestures reach it, and anything can be
|
||||||
/// The property surface deliberately mirrors what a media control is expected to expose —
|
/// drawn on top. The alternative — letting VLC render into a native child window — costs
|
||||||
/// source, position, duration, volume — so the page binds to it the same way it would to any
|
/// nothing per frame but puts the picture in its own window above the Avalonia surface,
|
||||||
/// other player.
|
/// where no click and no overlay can reach it.
|
||||||
/// </para>
|
/// <para>
|
||||||
/// </remarks>
|
/// The price paid here is one copy per displayed frame. At 1080p that is a few megabytes;
|
||||||
public sealed class VlcVideoView : NativeControlHost
|
/// at 4K it becomes the dominant cost of playback.
|
||||||
{
|
/// </para>
|
||||||
public static readonly StyledProperty<Uri?> SourceProperty =
|
/// </remarks>
|
||||||
AvaloniaProperty.Register<VlcVideoView, Uri?>(nameof(Source));
|
public sealed class VlcVideoView : Control
|
||||||
|
{
|
||||||
public static readonly StyledProperty<bool> AutoPlayProperty =
|
public static readonly StyledProperty<Uri?> SourceProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(AutoPlay), defaultValue: true);
|
AvaloniaProperty.Register<VlcVideoView, Uri?>(nameof(Source));
|
||||||
|
|
||||||
public static readonly StyledProperty<TimeSpan> PositionProperty =
|
public static readonly StyledProperty<bool> AutoPlayProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Position));
|
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(AutoPlay), defaultValue: true);
|
||||||
|
|
||||||
public static readonly StyledProperty<TimeSpan> DurationProperty =
|
public static readonly StyledProperty<TimeSpan> PositionProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Duration));
|
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Position));
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsPlayingProperty =
|
public static readonly StyledProperty<TimeSpan> DurationProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsPlaying));
|
AvaloniaProperty.Register<VlcVideoView, TimeSpan>(nameof(Duration));
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsMutedProperty =
|
public static readonly StyledProperty<bool> IsPlayingProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsMuted));
|
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsPlaying));
|
||||||
|
|
||||||
/// <summary>Volume as a fraction; LibVLC works in percent and is converted on the way in.</summary>
|
public static readonly StyledProperty<bool> IsMutedProperty =
|
||||||
public static readonly StyledProperty<double> VolumeProperty =
|
AvaloniaProperty.Register<VlcVideoView, bool>(nameof(IsMuted));
|
||||||
AvaloniaProperty.Register<VlcVideoView, double>(nameof(Volume), defaultValue: 0.8);
|
|
||||||
|
/// <summary>Volume as a fraction; LibVLC works in percent and is converted on the way in.</summary>
|
||||||
public static readonly StyledProperty<string?> LastErrorProperty =
|
public static readonly StyledProperty<double> VolumeProperty =
|
||||||
AvaloniaProperty.Register<VlcVideoView, string?>(nameof(LastError));
|
AvaloniaProperty.Register<VlcVideoView, double>(nameof(Volume), defaultValue: 0.8);
|
||||||
|
|
||||||
private MediaPlayer? _player;
|
public static readonly StyledProperty<string?> LastErrorProperty =
|
||||||
|
AvaloniaProperty.Register<VlcVideoView, string?>(nameof(LastError));
|
||||||
/// <summary>
|
|
||||||
/// True once VLC has been handed the native window. Playback cannot start before that,
|
/// <summary>VLC's 32-bit packed BGRX; matches <see cref="PixelFormat.Bgra8888"/> byte for byte.</summary>
|
||||||
/// or VLC opens a top-level window of its own.
|
private const string Chroma = "RV32";
|
||||||
/// </summary>
|
|
||||||
private bool _surfaceReady;
|
/// <summary>
|
||||||
|
/// Guards the frame buffers and the bitmap. Held only for a pointer swap on the decoder
|
||||||
public Uri? Source
|
/// side and for the copy on the render side, never across a call into VLC.
|
||||||
{
|
/// </summary>
|
||||||
get => GetValue(SourceProperty);
|
private readonly Lock _gate = new();
|
||||||
set => SetValue(SourceProperty, value);
|
|
||||||
}
|
// Kept in fields because they are handed to native code: a local delegate would be
|
||||||
|
// collected while VLC still holds the function pointer.
|
||||||
public bool AutoPlay
|
private readonly MediaPlayer.LibVLCVideoFormatCb _formatCallback;
|
||||||
{
|
private readonly MediaPlayer.LibVLCVideoCleanupCb _cleanupCallback;
|
||||||
get => GetValue(AutoPlayProperty);
|
private readonly MediaPlayer.LibVLCVideoLockCb _lockCallback;
|
||||||
set => SetValue(AutoPlayProperty, value);
|
private readonly MediaPlayer.LibVLCVideoDisplayCb _displayCallback;
|
||||||
}
|
|
||||||
|
private MediaPlayer? _player;
|
||||||
public TimeSpan Position
|
private WriteableBitmap? _bitmap;
|
||||||
{
|
|
||||||
get => GetValue(PositionProperty);
|
/// <summary>The buffer VLC is decoding into right now.</summary>
|
||||||
private set => SetValue(PositionProperty, value);
|
private IntPtr _back;
|
||||||
}
|
|
||||||
|
/// <summary>The most recently completed frame, safe to read while VLC fills the other one.</summary>
|
||||||
public TimeSpan Duration
|
private IntPtr _front;
|
||||||
{
|
|
||||||
get => GetValue(DurationProperty);
|
private int _width;
|
||||||
private set => SetValue(DurationProperty, value);
|
private int _height;
|
||||||
}
|
private int _stride;
|
||||||
|
private bool _hasNewFrame;
|
||||||
public bool IsPlaying
|
|
||||||
{
|
public VlcVideoView()
|
||||||
get => GetValue(IsPlayingProperty);
|
{
|
||||||
private set => SetValue(IsPlayingProperty, value);
|
_formatCallback = OnFormat;
|
||||||
}
|
_cleanupCallback = OnCleanup;
|
||||||
|
_lockCallback = OnLock;
|
||||||
public bool IsMuted
|
_displayCallback = OnDisplay;
|
||||||
{
|
}
|
||||||
get => GetValue(IsMutedProperty);
|
|
||||||
set => SetValue(IsMutedProperty, value);
|
public Uri? Source
|
||||||
}
|
{
|
||||||
|
get => GetValue(SourceProperty);
|
||||||
public double Volume
|
set => SetValue(SourceProperty, value);
|
||||||
{
|
}
|
||||||
get => GetValue(VolumeProperty);
|
|
||||||
set => SetValue(VolumeProperty, value);
|
public bool AutoPlay
|
||||||
}
|
{
|
||||||
|
get => GetValue(AutoPlayProperty);
|
||||||
public string? LastError
|
set => SetValue(AutoPlayProperty, value);
|
||||||
{
|
}
|
||||||
get => GetValue(LastErrorProperty);
|
|
||||||
private set => SetValue(LastErrorProperty, value);
|
public TimeSpan Position
|
||||||
}
|
{
|
||||||
|
get => GetValue(PositionProperty);
|
||||||
public void Play()
|
private set => SetValue(PositionProperty, value);
|
||||||
{
|
}
|
||||||
if (_player is null)
|
|
||||||
{
|
public TimeSpan Duration
|
||||||
return;
|
{
|
||||||
}
|
get => GetValue(DurationProperty);
|
||||||
|
private set => SetValue(DurationProperty, value);
|
||||||
if (_player.Media is not null)
|
}
|
||||||
{
|
|
||||||
_player.Play();
|
public bool IsPlaying
|
||||||
}
|
{
|
||||||
else
|
get => GetValue(IsPlayingProperty);
|
||||||
{
|
private set => SetValue(IsPlayingProperty, value);
|
||||||
OpenCurrentSource();
|
}
|
||||||
}
|
|
||||||
}
|
public bool IsMuted
|
||||||
|
{
|
||||||
public void Pause() => _player?.SetPause(true);
|
get => GetValue(IsMutedProperty);
|
||||||
|
set => SetValue(IsMutedProperty, value);
|
||||||
public void Stop() => _player?.Stop();
|
}
|
||||||
|
|
||||||
public void Seek(TimeSpan position)
|
public double Volume
|
||||||
{
|
{
|
||||||
if (_player is { IsSeekable: true })
|
get => GetValue(VolumeProperty);
|
||||||
{
|
set => SetValue(VolumeProperty, value);
|
||||||
_player.Time = (long)position.TotalMilliseconds;
|
}
|
||||||
}
|
|
||||||
}
|
public string? LastError
|
||||||
|
{
|
||||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
get => GetValue(LastErrorProperty);
|
||||||
{
|
private set => SetValue(LastErrorProperty, value);
|
||||||
// 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);
|
public void Play()
|
||||||
|
{
|
||||||
_player = new MediaPlayer(VlcRuntime.Shared);
|
if (_player?.Media is not null)
|
||||||
AttachPlayerEvents(_player);
|
{
|
||||||
|
_player.Play();
|
||||||
if (OperatingSystem.IsWindows())
|
}
|
||||||
{
|
else
|
||||||
_player.Hwnd = handle.Handle;
|
{
|
||||||
}
|
OpenCurrentSource();
|
||||||
else if (OperatingSystem.IsLinux())
|
}
|
||||||
{
|
}
|
||||||
_player.XWindow = (uint)handle.Handle;
|
|
||||||
}
|
public void Pause() => _player?.SetPause(true);
|
||||||
else if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
public void Stop() => _player?.Stop();
|
||||||
_player.NsObject = handle.Handle;
|
|
||||||
}
|
public void Seek(TimeSpan position)
|
||||||
|
{
|
||||||
// VLC grabs mouse and keyboard on its own window by default, which swallows every
|
if (_player is { IsSeekable: true } player)
|
||||||
// gesture before Avalonia can see it. We do not need DVD menus, so hand input back.
|
{
|
||||||
_player.EnableMouseInput = false;
|
player.Time = (long)position.TotalMilliseconds;
|
||||||
_player.EnableKeyInput = false;
|
}
|
||||||
|
}
|
||||||
_player.Mute = IsMuted;
|
|
||||||
_player.Volume = ToVlcVolume(Volume);
|
public override void Render(DrawingContext context)
|
||||||
_surfaceReady = true;
|
{
|
||||||
|
Rect source;
|
||||||
if (AutoPlay)
|
WriteableBitmap bitmap;
|
||||||
{
|
|
||||||
OpenCurrentSource();
|
lock (_gate)
|
||||||
}
|
{
|
||||||
|
if (_width <= 0 || _height <= 0 || _front == IntPtr.Zero)
|
||||||
return handle;
|
{
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
|
||||||
{
|
EnsureBitmap();
|
||||||
// Tear the player down before the window it draws into disappears.
|
bitmap = _bitmap!;
|
||||||
if (_player is { } player)
|
|
||||||
{
|
if (_hasNewFrame)
|
||||||
_player = null;
|
{
|
||||||
_surfaceReady = false;
|
using var locked = bitmap.Lock();
|
||||||
|
CopyFrame(_front, _stride, locked.Address, locked.RowBytes, _height);
|
||||||
DetachPlayerEvents(player);
|
_hasNewFrame = false;
|
||||||
player.Stop();
|
}
|
||||||
player.Dispose();
|
|
||||||
}
|
source = new Rect(0, 0, _width, _height);
|
||||||
|
}
|
||||||
base.DestroyNativeControlCore(control);
|
|
||||||
}
|
context.DrawImage(bitmap, source, FitInto(source.Size, Bounds.Size));
|
||||||
|
}
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
||||||
{
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
base.OnPropertyChanged(change);
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
if (_player is not { } player)
|
|
||||||
{
|
_player = new MediaPlayer(VlcRuntime.Shared);
|
||||||
return;
|
AttachPlayerEvents(_player);
|
||||||
}
|
|
||||||
|
_player.SetVideoFormatCallbacks(_formatCallback, _cleanupCallback);
|
||||||
if (change.Property == SourceProperty)
|
_player.SetVideoCallbacks(_lockCallback, null, _displayCallback);
|
||||||
{
|
|
||||||
OpenCurrentSource();
|
_player.Mute = IsMuted;
|
||||||
}
|
_player.Volume = ToVlcVolume(Volume);
|
||||||
else if (change.Property == VolumeProperty)
|
|
||||||
{
|
if (AutoPlay)
|
||||||
player.Volume = ToVlcVolume(Volume);
|
{
|
||||||
}
|
OpenCurrentSource();
|
||||||
else if (change.Property == IsMutedProperty)
|
}
|
||||||
{
|
}
|
||||||
player.Mute = IsMuted;
|
|
||||||
}
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
}
|
{
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
private void OpenCurrentSource()
|
|
||||||
{
|
if (_player is { } player)
|
||||||
if (!_surfaceReady || _player is not { } player || Source is not { } source)
|
{
|
||||||
{
|
_player = null;
|
||||||
return;
|
|
||||||
}
|
DetachPlayerEvents(player);
|
||||||
|
|
||||||
try
|
// Stop before releasing anything: the callbacks must not fire into freed memory.
|
||||||
{
|
player.Stop();
|
||||||
LastError = null;
|
player.Dispose();
|
||||||
|
}
|
||||||
// The media object only has to survive the call: VLC takes its own reference.
|
|
||||||
using var media = new Media(VlcRuntime.Shared, source);
|
lock (_gate)
|
||||||
|
{
|
||||||
if (!player.Play(media))
|
ReleaseBuffers();
|
||||||
{
|
_bitmap?.Dispose();
|
||||||
LastError = "VLC не смог открыть файл";
|
_bitmap = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
LastError = ex.Message;
|
{
|
||||||
}
|
base.OnPropertyChanged(change);
|
||||||
}
|
|
||||||
|
if (_player is not { } player)
|
||||||
private void AttachPlayerEvents(MediaPlayer player)
|
{
|
||||||
{
|
return;
|
||||||
player.TimeChanged += OnTimeChanged;
|
}
|
||||||
player.LengthChanged += OnLengthChanged;
|
|
||||||
player.Playing += OnPlaying;
|
if (change.Property == SourceProperty)
|
||||||
player.Paused += OnStoppedPlaying;
|
{
|
||||||
player.Stopped += OnStoppedPlaying;
|
OpenCurrentSource();
|
||||||
player.EndReached += OnStoppedPlaying;
|
}
|
||||||
player.EncounteredError += OnEncounteredError;
|
else if (change.Property == VolumeProperty)
|
||||||
}
|
{
|
||||||
|
player.Volume = ToVlcVolume(Volume);
|
||||||
private void DetachPlayerEvents(MediaPlayer player)
|
}
|
||||||
{
|
else if (change.Property == IsMutedProperty)
|
||||||
player.TimeChanged -= OnTimeChanged;
|
{
|
||||||
player.LengthChanged -= OnLengthChanged;
|
player.Mute = IsMuted;
|
||||||
player.Playing -= OnPlaying;
|
}
|
||||||
player.Paused -= OnStoppedPlaying;
|
}
|
||||||
player.Stopped -= OnStoppedPlaying;
|
|
||||||
player.EndReached -= OnStoppedPlaying;
|
private void OpenCurrentSource()
|
||||||
player.EncounteredError -= OnEncounteredError;
|
{
|
||||||
}
|
if (_player is not { } player || Source is not { } source)
|
||||||
|
{
|
||||||
// Every VLC event arrives on one of its own threads, so nothing here may touch an
|
return;
|
||||||
// Avalonia property directly.
|
}
|
||||||
private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) =>
|
|
||||||
Post(() => Position = TimeSpan.FromMilliseconds(Math.Max(0, e.Time)));
|
try
|
||||||
|
{
|
||||||
private void OnLengthChanged(object? sender, MediaPlayerLengthChangedEventArgs e) =>
|
LastError = null;
|
||||||
Post(() => Duration = TimeSpan.FromMilliseconds(Math.Max(0, e.Length)));
|
|
||||||
|
// The media object only has to survive the call: VLC takes its own reference.
|
||||||
private void OnPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = true);
|
using var media = new Media(VlcRuntime.Shared, source);
|
||||||
|
|
||||||
private void OnStoppedPlaying(object? sender, EventArgs e) => Post(() => IsPlaying = false);
|
if (!player.Play(media))
|
||||||
|
{
|
||||||
private void OnEncounteredError(object? sender, EventArgs e) =>
|
LastError = "VLC не смог открыть файл";
|
||||||
Post(() => LastError = "VLC сообщил об ошибке воспроизведения");
|
}
|
||||||
|
}
|
||||||
private static void Post(Action action) => Dispatcher.UIThread.Post(action, DispatcherPriority.Background);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
private static int ToVlcVolume(double volume) => (int)Math.Round(Math.Clamp(volume, 0, 1) * 100);
|
LastError = ex.Message;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================= VLC video callbacks =======================
|
||||||
|
// All of these run on VLC's own threads. Nothing here may touch an Avalonia property
|
||||||
|
// directly, and nothing may block for long: the decoder is waiting.
|
||||||
|
|
||||||
|
private uint OnFormat(
|
||||||
|
ref IntPtr opaque,
|
||||||
|
IntPtr chroma,
|
||||||
|
ref uint width,
|
||||||
|
ref uint height,
|
||||||
|
ref uint pitches,
|
||||||
|
ref uint lines)
|
||||||
|
{
|
||||||
|
var frameWidth = (int)width;
|
||||||
|
var frameHeight = (int)height;
|
||||||
|
var stride = frameWidth * 4;
|
||||||
|
|
||||||
|
Marshal.Copy(Encoding.ASCII.GetBytes(Chroma), 0, chroma, 4);
|
||||||
|
pitches = (uint)stride;
|
||||||
|
lines = (uint)frameHeight;
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ReleaseBuffers();
|
||||||
|
|
||||||
|
_width = frameWidth;
|
||||||
|
_height = frameHeight;
|
||||||
|
_stride = stride;
|
||||||
|
|
||||||
|
var size = stride * frameHeight;
|
||||||
|
_back = Marshal.AllocHGlobal(size);
|
||||||
|
_front = Marshal.AllocHGlobal(size);
|
||||||
|
_hasNewFrame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One buffer set; VLC hands it back through the lock callback.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCleanup(ref IntPtr opaque)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ReleaseBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr OnLock(IntPtr opaque, IntPtr planes)
|
||||||
|
{
|
||||||
|
Marshal.WriteIntPtr(planes, 0, _back);
|
||||||
|
return _back;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisplay(IntPtr opaque, IntPtr picture)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
// Swap rather than copy: the finished frame becomes readable and VLC carries on
|
||||||
|
// decoding into the buffer we were showing.
|
||||||
|
(_front, _back) = (_back, _front);
|
||||||
|
_hasNewFrame = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================= Player events =======================
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 сообщил об ошибке воспроизведения");
|
||||||
|
|
||||||
|
// ======================= Helpers =======================
|
||||||
|
|
||||||
|
/// <summary>Must be called with <see cref="_gate"/> held.</summary>
|
||||||
|
private void EnsureBitmap()
|
||||||
|
{
|
||||||
|
if (_bitmap is not null &&
|
||||||
|
_bitmap.PixelSize.Width == _width &&
|
||||||
|
_bitmap.PixelSize.Height == _height)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bitmap?.Dispose();
|
||||||
|
_bitmap = new WriteableBitmap(
|
||||||
|
new PixelSize(_width, _height),
|
||||||
|
new Vector(96, 96),
|
||||||
|
PixelFormat.Bgra8888,
|
||||||
|
AlphaFormat.Opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Must be called with <see cref="_gate"/> held.</summary>
|
||||||
|
private void ReleaseBuffers()
|
||||||
|
{
|
||||||
|
if (_back != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(_back);
|
||||||
|
_back = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_front != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(_front);
|
||||||
|
_front = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
_width = 0;
|
||||||
|
_height = 0;
|
||||||
|
_stride = 0;
|
||||||
|
_hasNewFrame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void CopyFrame(IntPtr source, int sourceStride, IntPtr target, int targetStride, int rows)
|
||||||
|
{
|
||||||
|
var rowBytes = Math.Min(sourceStride, targetStride);
|
||||||
|
|
||||||
|
if (sourceStride == targetStride)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy((void*)source, (void*)target, (long)targetStride * rows, (long)rowBytes * rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var row = 0; row < rows; row++)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(
|
||||||
|
(void*)(source + (row * sourceStride)),
|
||||||
|
(void*)(target + (row * targetStride)),
|
||||||
|
targetStride,
|
||||||
|
rowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Largest rectangle of the source aspect ratio that fits, centred.</summary>
|
||||||
|
private static Rect FitInto(Size source, Size available)
|
||||||
|
{
|
||||||
|
if (source.Width <= 0 || source.Height <= 0 || available.Width <= 0 || available.Height <= 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scale = Math.Min(available.Width / source.Width, available.Height / source.Height);
|
||||||
|
var width = source.Width * scale;
|
||||||
|
var height = source.Height * scale;
|
||||||
|
|
||||||
|
return new Rect((available.Width - width) / 2, (available.Height - height) / 2, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<!-- The video control copies decoded frames between unmanaged buffers and the bitmap;
|
||||||
|
a managed round trip would double the work on every frame. -->
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -61,26 +61,26 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ======================= Video ======================= -->
|
<!-- ======================= Video ======================= -->
|
||||||
<!-- VLC paints into a native child window, which sits above the Avalonia surface, so
|
<Panel Grid.Row="1" Name="VideoArea" Background="Black">
|
||||||
the error message lives in its own row instead of on top of the picture. -->
|
<controls:VlcVideoView Name="Player"
|
||||||
<Grid Grid.Row="1" RowDefinitions="*,Auto">
|
Source="{Binding Source}"
|
||||||
<Panel Grid.Row="0" Name="VideoArea" Background="Black">
|
AutoPlay="True"
|
||||||
<controls:VlcVideoView Name="Player"
|
Volume="0.8" />
|
||||||
Source="{Binding Source}"
|
|
||||||
AutoPlay="True"
|
|
||||||
Volume="0.8" />
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Border Grid.Row="1"
|
<Border Name="ErrorBar"
|
||||||
Name="ErrorBar"
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MaxWidth="460"
|
||||||
|
CornerRadius="10"
|
||||||
Background="{DynamicResource SurfaceBrush}"
|
Background="{DynamicResource SurfaceBrush}"
|
||||||
Padding="16,10"
|
Padding="16,12"
|
||||||
IsVisible="False">
|
IsVisible="False">
|
||||||
<TextBlock Name="ErrorText"
|
<TextBlock Name="ErrorText"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
|
TextAlignment="Center"
|
||||||
Foreground="{DynamicResource TextSecondaryBrush}" />
|
Foreground="{DynamicResource TextSecondaryBrush}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Panel>
|
||||||
|
|
||||||
<!-- ======================= Transport ======================= -->
|
<!-- ======================= Transport ======================= -->
|
||||||
<Border Grid.Row="2" Classes="panelFooter" Padding="16,10">
|
<Border Grid.Row="2" Classes="panelFooter" Padding="16,10">
|
||||||
|
|||||||
Reference in New Issue
Block a user