Files
ytLlive/Services/ScreenCaptureFrameSource.cs
T

225 lines
8.0 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.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;
/// <summary>
/// A live screen capture for one target key ("monitor:&lt;n&gt;" or
/// "window:&lt;hwnd&gt;"). 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).
/// </summary>
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<VideoFrame>? FrameAvailable;
internal ScreenCaptureFrameSource(string key, GraphicsCaptureItem item)
{
Key = key;
_item = item;
}
/// <summary>Wraps an item chosen through the OS GraphicsCapturePicker.</summary>
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;
}
}