Files
ytLlive/Services/Win32FullScreenDetector.cs
T

195 lines
7.1 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;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Interop;
namespace ytLive.Services;
/// <summary>A physical display: its capture-key index, geometry, and primary flag.</summary>
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}";
}
/// <summary>
/// Win32 detection: <c>GetForegroundWindow</c> + <c>DwmGetWindowAttribute</c>
/// (DWMWA_EXTENDED_FRAME_BOUNDS) + <c>MonitorFromWindow</c> + <c>GetMonitorInfo</c>.
/// 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 =
/// <c>EnumDisplayMonitors</c> 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.
/// </summary>
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<Win32Rect>()) == 0;
}
private static bool TryGetMonitorInfo(IntPtr monitor, out MonitorInfo info)
{
info = new MonitorInfo { CbSize = Marshal.SizeOf<MonitorInfo>() };
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<DisplayInfo> GetDisplays()
{
var result = new List<DisplayInfo>();
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<MonitorInfoEx>() };
if (!GetMonitorInfo(monitor, ref info)) return $"Display {monitor}";
var deviceName = info.SzDevice.TrimEnd('\0');
var dev = new DisplayDevice { Cb = Marshal.SizeOf<DisplayDevice>() };
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<IntPtr> EnumerateMonitors()
{
var handles = new List<IntPtr>();
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);
}