Screen backdrop (schema v5/v6) + five-scene catalog + webcam polish: live desktop/game capture as a permanent non-deletable bottom layer (Source.IsBackdrop), auto full-screen game detection (Win32FullScreenDetector) else primary display, GraphicsCapturePicker re-designation, refcounted shared ScreenCaptureManager, Live-only backdrop by policy (HasBackdrop + v5-v6 backfill + EnforceBackdropPolicy, checkbox gone), SceneCatalog (Starting/Live/BRB/Chat/Ending) with + button re-adding missing scenes, webcam mid-session transparent-container fix (GetPreviewBitmap propagation), Chat half-screen-area cap, 'Add Webcam' always opens the picker (SwapWebcamIdentityAsync, no silent resurrect), WindowsRuntimeMarshal frame-read + 5s-throttled errors, docs updated, tests (65 passing)

This commit is contained in:
2026-08-07 14:15:50 -07:00
parent e037ba027b
commit ad9b3e1e48
27 changed files with 2232 additions and 116 deletions
+194
View File
@@ -0,0 +1,194 @@
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);
}