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; /// /// Non-modal floating preview window for a single participant. Shows the /// engine's most recent 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 with /// — 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. /// 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 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); } } } }