using System; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using LANCommander.Launcher.Helpers; namespace LANCommander.Launcher.Controls; /// /// Plays a video file muted and looping, rendering frames directly to the /// Avalonia render surface. The output preserves the source video's aspect /// ratio (uniform stretch, centred within the control bounds). /// public class InlineVideoPlayer : Control, IDisposable, ICarouselPlaybackItem { public static readonly StyledProperty VideoPathProperty = AvaloniaProperty.Register(nameof(VideoPath)); public string? VideoPath { get => GetValue(VideoPathProperty); set => SetValue(VideoPathProperty, value); } private VideoFrameRenderer? _renderer; private bool _isAttached; private bool _disposed; /// /// Whether the carousel currently considers this item visible. Playback only /// runs while both visible and attached, so off-screen videos don't decode. /// private bool _isActive; /// Current playback position in milliseconds. public long CurrentTimeMs => _renderer?.Player?.Time ?? 0; static InlineVideoPlayer() { AffectsRender(VideoPathProperty); } // ── Layout ─────────────────────────────────────────────────────────── protected override Size MeasureOverride(Size availableSize) => availableSize; // ── Lifecycle ──────────────────────────────────────────────────────── protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == VideoPathProperty) { StopPlayback(); var path = change.GetNewValue(); if (!string.IsNullOrEmpty(path) && _isAttached && _isActive) StartPlayback(path); } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _isAttached = true; // Playback is started by the carousel via SetCarouselActive once it has // determined which items are on-screen. if (_isActive && !string.IsNullOrEmpty(VideoPath) && _renderer == null) StartPlayback(VideoPath); } // ── Carousel visibility ────────────────────────────────────────────── /// /// Called by the carousel to indicate whether this item is on-screen. /// Visible items play (creating the renderer lazily); off-screen items pause /// so they stop decoding frames and consuming CPU. /// public void SetCarouselActive(bool active) { if (_isActive == active) return; _isActive = active; if (!_isAttached || _disposed) return; if (active) { if (_renderer == null) { if (!string.IsNullOrEmpty(VideoPath)) StartPlayback(VideoPath); } else { _renderer.Player?.SetPause(false); } } else { _renderer?.Player?.SetPause(true); } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _isAttached = false; StopPlayback(); base.OnDetachedFromVisualTree(e); } // ── Public transport controls ──────────────────────────────────────── /// Pauses playback without disposing the renderer. public void Pause() { _renderer?.Player?.SetPause(true); } /// Seeks to and resumes muted playback. public void ResumeAt(long timeMs) { if (_renderer?.Player is not { } player) return; player.Time = timeMs; player.SetPause(false); } // ── Playback lifecycle ─────────────────────────────────────────────── private void StartPlayback(string path) { try { // Constrain render to carousel slot size while preserving aspect ratio. _renderer = new VideoFrameRenderer(maxWidth: 384, maxHeight: 216); _renderer.FrameReady += InvalidateVisual; _renderer.Play(path, muted: true, loop: true); } catch { // LibVLC initialisation may fail — control remains blank. } } private void StopPlayback() { if (_renderer != null) { _renderer.FrameReady -= InvalidateVisual; _renderer.Dispose(); _renderer = null; } } // ── Rendering ──────────────────────────────────────────────────────── public override void Render(DrawingContext context) { if (_renderer?.Bitmap is not { } bmp) return; var srcW = (double)bmp.PixelSize.Width; var srcH = (double)bmp.PixelSize.Height; var dstW = Bounds.Width; var dstH = Bounds.Height; if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) return; // Uniform stretch: scale to fit, preserving aspect ratio, centred. var scale = Math.Min(dstW / srcW, dstH / srcH); var scaledW = srcW * scale; var scaledH = srcH * scale; var offsetX = (dstW - scaledW) / 2; var offsetY = (dstH - scaledH) / 2; context.DrawImage( bmp, new Rect(0, 0, srcW, srcH), new Rect(offsetX, offsetY, scaledW, scaledH)); } // ── IDisposable ────────────────────────────────────────────────────── public void Dispose() { if (!_disposed) { _disposed = true; StopPlayback(); } } }