using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Graphics; using Windows.Graphics.Capture; using Windows.Graphics.DirectX; using Windows.Graphics.DirectX.Direct3D11; using Windows.Graphics.Imaging; using ytLive.Helpers; namespace ytLive.Services; /// /// A live screen capture for one target key ("monitor:<n>" or /// "window:<hwnd>"). Owns the GraphicsCaptureItem, a free-threaded /// Direct3D11CaptureFramePool and the capture session; frames are converted on /// the capture worker thread from the GPU surface to a CPU BGRA VideoFrame. /// Known OS limits: DRM content captures as black frames; capture pauses while /// the app is minimized (the frame pool simply stops delivering frames). /// public sealed class ScreenCaptureFrameSource : IScreenCaptureSource { private readonly object _gate = new(); private GraphicsCaptureItem _item; private Direct3D11CaptureFramePool? _framePool; private GraphicsCaptureSession? _session; private SizeInt32 _poolSize; private bool _started; private bool _framePending; private DateTime _lastErrorLog = DateTime.MinValue; // The composition master frame (see ai.md "Resolution tiers"): the backdrop // is an input layer, so we never hold a CPU frame bigger than the master. private const int MaxBackdropWidth = 1920; private const int MaxBackdropHeight = 1080; // A failing conversion must not re-flood the log at frame rate. private static readonly TimeSpan ErrorLogThrottle = TimeSpan.FromSeconds(5); public string Key { get; } public event Action? FrameAvailable; internal ScreenCaptureFrameSource(string key, GraphicsCaptureItem item) { Key = key; _item = item; } /// Wraps an item chosen through the OS GraphicsCapturePicker. public static ScreenCaptureFrameSource CreateForPicker(string key, GraphicsCaptureItem item) => new(key, item); public static ScreenCaptureFrameSource CreateForMonitor(int monitorIndex) { var hmonitor = Win32FullScreenDetector.GetMonitorHandle(monitorIndex); if (hmonitor == IntPtr.Zero) throw new InvalidOperationException($"Monitor {monitorIndex} is not connected"); var item = CaptureInterop.CreateForMonitor(hmonitor); return new ScreenCaptureFrameSource($"monitor:{monitorIndex}", item); } public static ScreenCaptureFrameSource CreateForWindow(IntPtr hwnd) { var item = CaptureInterop.CreateForWindow(hwnd); return new ScreenCaptureFrameSource($"window:0x{hwnd.ToInt64():X}", item); } public Task StartAsync() { lock (_gate) { if (_started) return Task.CompletedTask; var item = _item; var device = Direct3D11Helper.CreateDevice(); var framePool = Direct3D11CaptureFramePool.CreateFreeThreaded( device, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, item.Size); _poolSize = item.Size; var session = framePool.CreateCaptureSession(item); framePool.FrameArrived += OnFrameArrived; session.StartCapture(); _framePool = framePool; _session = session; _started = true; } return Task.CompletedTask; } public Task StopAsync() { lock (_gate) { _started = false; _framePending = false; if (_framePool != null) _framePool.FrameArrived -= OnFrameArrived; _session?.Dispose(); _session = null; _framePool?.Dispose(); _framePool = null; } return Task.CompletedTask; } private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args) { var frame = sender.TryGetNextFrame(); if (frame == null) return; lock (_gate) { if (!_started) { frame.Dispose(); return; } } if (frame.ContentSize.Width != _poolSize.Width || frame.ContentSize.Height != _poolSize.Height) { sender.Recreate(Direct3D11Helper.CreateDevice(), DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, frame.ContentSize); _poolSize = frame.ContentSize; } if (_framePending) { frame.Dispose(); return; } _framePending = true; _ = ProcessFrameAsync(frame); } private async Task ProcessFrameAsync(Direct3D11CaptureFrame frame) { try { using (frame) using (var softwareBitmap = await SoftwareBitmap.CreateCopyFromSurfaceAsync( frame.Surface, BitmapAlphaMode.Ignore)) { FrameAvailable?.Invoke(CopyToVideoFrame(softwareBitmap)); } } catch (Exception ex) { var now = DateTime.UtcNow; if (now - _lastErrorLog >= ErrorLogThrottle) { _lastErrorLog = now; AppLog.Write($"ScreenCaptureFrameSource: frame conversion failed: {ex.Message}"); } } finally { _framePending = false; } } private static VideoFrame CopyToVideoFrame(SoftwareBitmap bitmap) { var sw = bitmap.PixelWidth; var sh = bitmap.PixelHeight; using var buffer = bitmap.LockBuffer(BitmapBufferAccessMode.Read); using var reference = buffer.CreateReference(); if (!WindowsRuntimeMarshal.TryGetDataUnsafe(reference, out var data, out var capacity)) throw new InvalidOperationException("Could not access the frame buffer."); var srcStride = sw * 4; var count = (int)Math.Min(capacity, (uint)(sh * srcStride)); // The master frame is 1920×1080; a larger monitor is scaled down here so // the CPU never holds a frame above the master (the rendered layer fills // the frame with UniformToFill regardless). if (sw > MaxBackdropWidth || sh > MaxBackdropHeight) { var scale = Math.Min(MaxBackdropWidth / (double)sw, MaxBackdropHeight / (double)sh); var dw = Math.Max(1, (int)(sw * scale)); var dh = Math.Max(1, (int)(sh * scale)); return new VideoFrame(dw, dh, DownscaleBgra(data, sw, sh, srcStride, dw, dh)); } var pixels = new byte[count]; Marshal.Copy(data, pixels, 0, pixels.Length); return new VideoFrame(sw, sh, pixels); } // Bilinear downscale to the master frame. Reads each source row pair through // Marshal.Copy (no unsafe), writing tightly packed BGRA output. private static byte[] DownscaleBgra(IntPtr src, int sw, int sh, int srcStride, int dw, int dh) { var row0 = new byte[srcStride]; var row1 = new byte[srcStride]; var dst = new byte[dw * dh * 4]; var xs = sw / (double)dw; var ys = sh / (double)dh; for (var y = 0; y < dh; y++) { var sy = Math.Min(sh - 1, (int)(y * ys)); var sy1 = Math.Min(sh - 1, sy + 1); var fy = (y * ys) - sy; Marshal.Copy(IntPtr.Add(src, sy * srcStride), row0, 0, srcStride); Marshal.Copy(IntPtr.Add(src, sy1 * srcStride), row1, 0, srcStride); var dRow = y * dw * 4; for (var x = 0; x < dw; x++) { var sx = Math.Min(sw - 1, (int)(x * xs)); var sx1 = Math.Min(sw - 1, sx + 1); var fx = (x * xs) - sx; for (var c = 0; c < 4; c++) { var i0 = sx * 4 + c; var i1 = sx1 * 4 + c; var top = row0[i0] + (row0[i1] - row0[i0]) * fx; var bottom = row1[i0] + (row1[i1] - row1[i0]) * fx; dst[dRow + x * 4 + c] = (byte)(top + (bottom - top) * fy); } } } return dst; } }