using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Windows.Interop; namespace ytLive.Services; /// A physical display: its capture-key index, geometry, and primary flag. public sealed record DisplayInfo(int Index, string Name, int Width, int Height, int X, int Y, bool IsPrimary) { public string Label => IsPrimary ? $"{Name} — {Width}×{Height} (primary)" : $"{Name} — {Width}×{Height}"; } /// /// Win32 detection: GetForegroundWindow + DwmGetWindowAttribute /// (DWMWA_EXTENDED_FRAME_BOUNDS) + MonitorFromWindow + GetMonitorInfo. /// A foreground window whose extended frame bounds cover an entire monitor is /// treated as a full-screen game/app on that monitor's index (monitor order = /// EnumDisplayMonitors enumeration order, the same order the capture /// factory uses to map index → HMONITOR). Windows of our own process are excluded. /// DRM-protected content captures as black frames — a documented OS limit. /// public sealed class Win32FullScreenDetector : IFullScreenDetector { private const int DwmwaExtendedFrameBounds = 9; private const uint MonitorDefaultToNearest = 2; private const uint MonitorInfoFPrimary = 0x00000001; public int? GetForegroundFullScreenMonitorIndex() { var hwnd = GetForegroundWindow(); if (hwnd == IntPtr.Zero || IsOwnWindow(hwnd)) return null; if (!TryGetExtendedFrameBounds(hwnd, out var bounds)) return null; var monitor = MonitorFromWindow(hwnd, MonitorDefaultToNearest); if (monitor == IntPtr.Zero || !TryGetMonitorInfo(monitor, out var info)) return null; var r = info.RcMonitor; var coversMonitor = bounds.Left <= r.Left && bounds.Top <= r.Top && bounds.Right >= r.Right && bounds.Bottom >= r.Bottom; if (!coversMonitor) return null; return MonitorIndex(monitor); } private static bool IsOwnWindow(IntPtr hwnd) { _ = GetWindowThreadProcessId(hwnd, out var pid); return pid == Environment.ProcessId; } private static bool TryGetExtendedFrameBounds(IntPtr hwnd, out Win32Rect bounds) { bounds = default; return DwmGetWindowAttribute(hwnd, DwmwaExtendedFrameBounds, ref bounds, Marshal.SizeOf()) == 0; } private static bool TryGetMonitorInfo(IntPtr monitor, out MonitorInfo info) { info = new MonitorInfo { CbSize = Marshal.SizeOf() }; return GetMonitorInfo(monitor, ref info); } private static int? MonitorIndex(IntPtr monitor) { var handles = EnumerateMonitors(); var index = handles.IndexOf(monitor); return index >= 0 ? index : null; } public int PrimaryMonitorIndex() => GetDisplays().FirstOrDefault(d => d.IsPrimary)?.Index ?? 0; public IReadOnlyList GetDisplays() { var result = new List(); var handles = EnumerateMonitors(); for (var i = 0; i < handles.Count; i++) { if (!TryGetMonitorInfo(handles[i], out var info)) continue; var name = GetDisplayName(handles[i]); result.Add(new DisplayInfo( i, name, info.RcMonitor.Right - info.RcMonitor.Left, info.RcMonitor.Bottom - info.RcMonitor.Top, info.RcMonitor.Left, info.RcMonitor.Top, (info.DwFlags & MonitorInfoFPrimary) != 0)); } return result; } private static string GetDisplayName(IntPtr monitor) { var info = new MonitorInfoEx { CbSize = Marshal.SizeOf() }; if (!GetMonitorInfo(monitor, ref info)) return $"Display {monitor}"; var deviceName = info.SzDevice.TrimEnd('\0'); var dev = new DisplayDevice { Cb = Marshal.SizeOf() }; if (!EnumDisplayDevices(deviceName, 0, ref dev, 0)) return deviceName; var friendly = dev.DeviceString.TrimEnd('\0'); return string.IsNullOrWhiteSpace(friendly) ? deviceName : friendly; } public static IntPtr GetMonitorHandle(int index) { var handles = EnumerateMonitors(); return index >= 0 && index < handles.Count ? handles[index] : IntPtr.Zero; } private static List EnumerateMonitors() { var handles = new List(); EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr hMonitor, IntPtr hdcMonitor, ref Win32Rect lprcMonitor, IntPtr dwData) => { handles.Add(hMonitor); return true; }, IntPtr.Zero); return handles; } [StructLayout(LayoutKind.Sequential)] private struct Win32Rect { public int Left; public int Top; public int Right; public int Bottom; } [StructLayout(LayoutKind.Sequential)] private struct MonitorInfo { public int CbSize; public Win32Rect RcMonitor; public Win32Rect RcWork; public uint DwFlags; } [StructLayout(LayoutKind.Sequential)] private struct MonitorInfoEx { public int CbSize; public Win32Rect RcMonitor; public Win32Rect RcWork; public uint DwFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string SzDevice; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct DisplayDevice { public int Cb; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString; public uint StateFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey; } [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, ref DisplayDevice lpDisplayDevice, uint dwFlags); [DllImport("user32.dll")] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("dwmapi.dll")] private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, ref Win32Rect pvAttribute, int cbAttribute); private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Win32Rect lprcMonitor, IntPtr dwData); }