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