dragon-iso/src/TeamsISO.App/PreviewWindow.xaml.cs

95 lines
3.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using TeamsISO.Engine.Controller;
using TeamsISO.Engine.Pipeline;
namespace TeamsISO.App;
/// <summary>
/// Non-modal floating preview window for a single participant. Shows the
/// engine's most recent <see cref="ProcessedFrame"/> at a higher refresh
/// rate (~20Hz) than the participants DataGrid thumbnails (1Hz). Multi-
/// monitor friendly: operator drags it to a second display, leaves the
/// main TeamsISO window on the primary.
///
/// Uses <see cref="WriteableBitmap"/> with <see cref="WriteableBitmap.WritePixels(Int32Rect, IntPtr, int, int)"/>
/// — the engine produces full-resolution BGRA frames so we can write them
/// straight into the bitmap without scaling. WPF's Image control with
/// Stretch=Uniform handles aspect-correct fit to the window size.
/// </summary>
public partial class PreviewWindow : Window
{
private readonly IIsoController _controller;
private readonly Guid _participantId;
private readonly DispatcherTimer _refreshTimer;
private WriteableBitmap? _bitmap;
private int _lastWidth;
private int _lastHeight;
public PreviewWindow(IIsoController controller, Guid participantId, string displayName)
{
InitializeComponent();
_controller = controller;
_participantId = participantId;
TitleText.Text = displayName;
_refreshTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
{
// 50ms = 20Hz. High enough for a smooth-feeling preview without
// hogging the dispatcher; still cheap because each refresh is just
// a memcpy from the engine's last frame into our pinned BackBuffer.
Interval = TimeSpan.FromMilliseconds(50),
};
_refreshTimer.Tick += OnTick;
Loaded += (_, _) => _refreshTimer.Start();
Closed += (_, _) =>
{
_refreshTimer.Stop();
_refreshTimer.Tick -= OnTick;
};
}
private void OnClose(object sender, RoutedEventArgs e) => Close();
private void OnTick(object? sender, EventArgs e)
{
ProcessedFrame? frame;
try { frame = _controller.GetLatestProcessedFrame(_participantId); }
catch { return; }
if (frame is null || frame.Pixels.IsEmpty || frame.Width <= 0 || frame.Height <= 0) return;
if (frame.Pixels.Length < frame.Width * frame.Height * 4) return;
// (Re)allocate the WriteableBitmap when the source resolution changes.
// FrameProcessor normalizes to a configured target so this happens at
// most once per session, but we still defend against switches.
if (_bitmap is null || frame.Width != _lastWidth || frame.Height != _lastHeight)
{
_bitmap = new WriteableBitmap(
frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null);
PreviewImage.Source = _bitmap;
_lastWidth = frame.Width;
_lastHeight = frame.Height;
ResolutionText.Text = $"{frame.Width}×{frame.Height}";
}
// WritePixels takes a buffer + stride + rect. Stride = width * 4 for
// BGRA. We slice the ProcessedFrame's ReadOnlyMemory<byte> via .Span
// and use the IntPtr overload via MemoryMarshal — but the
// byte-array overload is simpler and the compiler picks the right
// ToArray-free path because the engine already allocates a fresh
// array per frame.
unsafe
{
fixed (byte* p = frame.Pixels.Span)
{
_bitmap.WritePixels(
new Int32Rect(0, 0, frame.Width, frame.Height),
(IntPtr)p,
frame.Pixels.Length,
frame.Width * 4);
}
}
}
}